Skip to content

Comments

Servo Control Feature (Rotating Frame)#598

Open
and-who wants to merge 35 commits intofatihak:mainfrom
and-who:servo-control-plugin
Open

Servo Control Feature (Rotating Frame)#598
and-who wants to merge 35 commits intofatihak:mainfrom
and-who:servo-control-plugin

Conversation

@and-who
Copy link

@and-who and-who commented Feb 14, 2026

Description
The Servo Control plugin & utils adds physical servo motor control to InkyPi, enabling dynamic rotation of e.g. a display frame to match image orientation (e.g. https://www.thingiverse.com/thing:7290592). This is particularly useful for creating rotating picture frames that automatically adjust between landscape and portrait modes. (Could also be used to control other features)

💬: First I wanted to make a separate third Party Plugin but since it needs Dependencies and integration at the Install Steps I choose to do it directly in the Repo

Core System Enhancements:

  • Plugin Filtering: Added servo_enabled config flag to automatically disable servo_control plugin when servo support isn't installed
  • Null Image Handling: Plugins can now return None to skip image rendering (useful for control-only operations like servo movement)
  • Image Inversion Robustness: More resilient string-to-boolean conversion for inverted_image setting
  • Device Configuration: Added servo_enabled: false to base device config
  • Improved generic Styling: more styling of general Elements then specific styling

Possible Future Enhancments
Auto rotate in other Plugins.
We could store the Default Values for Portrait/Landscape, then if you e.g. upload an Image, you could detect which AspectRatio the Image has and auto rotate the Frame.

Copilot AI review requested due to automatic review settings February 14, 2026 12:27
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-party servo motor support to InkyPi (for rotating frames / orientation control), along with a new servo_control plugin, install-time opt-in dependencies, and a few core-system robustness updates to support control-only plugins.

Changes:

  • Introduces a new servo_control plugin and ServoDriver utility (kernel PWM sysfs / libgpiod backends).
  • Adds servo_enabled config flag and installer -S option to opt-in servo dependencies and PWM overlay.
  • Improves rendering pipeline robustness (allow plugins to return None; safer inverted_image coercion) and broadens generic UI element styling.

Reviewed changes

Copilot reviewed 16 out of 19 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/utils/servo_utils.py New servo hardware driver (sysfs PWM / libgpiod) and movement logic.
src/static/styles/main.css Adds more generic element styling and consolidates text color behavior.
src/static/images/current_image.png Adds a committed “current image” asset (likely runtime output path).
src/refresh_task.py Allows plugins to return None to skip display updates.
src/plugins/servo_control/settings.html New settings UI for servo control + device-config side effects + API example.
src/plugins/servo_control/servo_control.py New plugin implementing servo movement, optional test image, and device config updates.
src/plugins/servo_control/plugin-info.json Registers the new plugin.
src/plugins/servo_control/icon.png Adds plugin icon asset.
src/plugins/servo_control/README.md Documents the plugin and a rotating-frame build example.
src/inkypi.py Changes Waitress binding configuration.
src/display/display_manager.py Skips rendering on None and makes inverted_image parsing more robust.
src/config/device_dev.json Adds servo-related fields and updates dev config values/state.
src/config.py Filters servo_control based on servo_enabled.
install/servo-requirements.txt Adds optional Python requirements for servo support.
install/requirements.txt Minor formatting/line adjustment.
install/install.sh Adds -S flag, PWM overlay enablement, and servo dependency installation.
install/config_base/device.json Adds servo_enabled: false default to base config.
docs/community.md Adds a community link for a rotatable stand model.
README.md Documents installer -S flag and provides examples.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 69 to 83
def get_plugins(self):
"""Returns the list of plugin configurations, sorted by custom order if set."""
"""Returns the list of plugin configurations, sorted by custom order if set.
Disables servo_control plugin if servo_enabled is false in config."""
plugin_order = self.config.get('plugin_order', [])
servo_enabled = self.config.get('servo_enabled', False)

# Filter out servo_control plugin if servo is not enabled
filtered_plugins = [p for p in self.plugins_list if not (p['id'] == 'servo_control' and not servo_enabled)]

if not plugin_order:
return self.plugins_list
return filtered_plugins

# Create a dict for quick lookup
plugins_dict = {p['id']: p for p in self.plugins_list}
plugins_dict = {p['id']: p for p in filtered_plugins}

Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The servo_enabled filtering only applies to get_plugins() (UI ordering and load_plugins()), but get_plugin() still returns servo_control even when disabled. This can lead to runtime errors if a playlist or API call references servo_control while servo_enabled is false (plugin config exists, but it was never registered/loaded). Consider enforcing the same flag in get_plugin() and/or preventing refresh actions from selecting disabled plugins (e.g., filtering playlist plugin instances on load).

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +65
# Update image inversion if specified
invert_value = str(invert_setting).lower() in ("1", "true", "yes", "on")
device_config.update_value("inverted_image", invert_value, write=True)
logger.info(f"Updated inverted_image to {invert_value}")
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inverted_image is always overwritten in device config even when the checkbox is unchecked and therefore absent from the submitted form data (it becomes None and gets coerced to False). This makes it impossible to leave the existing device setting unchanged from this plugin. Only update device_config.inverted_image when the setting is explicitly present, or add a "keep current" option / hidden field pattern so false is explicit.

Suggested change
# Update image inversion if specified
invert_value = str(invert_setting).lower() in ("1", "true", "yes", "on")
device_config.update_value("inverted_image", invert_value, write=True)
logger.info(f"Updated inverted_image to {invert_value}")
# Update image inversion only if explicitly specified in settings
if 'inverted_image' in settings:
invert_value = str(invert_setting).lower() in ("1", "true", "yes", "on")
device_config.update_value("inverted_image", invert_value, write=True)
logger.info(f"Updated inverted_image to {invert_value}")

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,212 @@
import logging
from display.mock_display import MockDisplay
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MockDisplay is imported but never used in this module. Remove the unused import to avoid lint/runtime overhead and keep dependencies clear.

Suggested change
from display.mock_display import MockDisplay

Copilot uses AI. Check for mistakes.
Comment on lines 45 to 51
"refresh_time": "2026-02-14T12:22:33.752604+00:00",
"image_hash": "bd2481643abcf6ae451ee72e1f33f704367d4d21b7f60b1273dd14e57061a6c4",
"refresh_type": "Manual Update",
"plugin_id": "servo_control"
},
"inverted_image": true,
"current_servo_angle": 45
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

device_dev.json now contains runtime state (refresh timestamps/hash, inverted_image: true, current_servo_angle). This makes dev mode start from a non-default state and creates noisy diffs over time. Consider keeping refresh_info fields null and omitting/zeroing transient fields so the file remains a stable baseline config.

Suggested change
"refresh_time": "2026-02-14T12:22:33.752604+00:00",
"image_hash": "bd2481643abcf6ae451ee72e1f33f704367d4d21b7f60b1273dd14e57061a6c4",
"refresh_type": "Manual Update",
"plugin_id": "servo_control"
},
"inverted_image": true,
"current_servo_angle": 45
"refresh_time": null,
"image_hash": null,
"refresh_type": null,
"plugin_id": null
},
"inverted_image": false,
"current_servo_angle": 0

Copilot uses AI. Check for mistakes.
- Powercable for Raspberry Pi
- Servo Motor (e.g. SG90)
- 18 Jumper Cables
- Rotating Frame Assambly 3D Print e.g. from Thingiverse (https://www.thingiverse.com/thing:7290592)
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling: "Assambly" → "Assembly".

Suggested change
- Rotating Frame Assambly 3D Print e.g. from Thingiverse (https://www.thingiverse.com/thing:7290592)
- Rotating Frame Assembly 3D Print e.g. from Thingiverse (https://www.thingiverse.com/thing:7290592)

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +129
// Update speed display
document.getElementById('servo-speed').addEventListener('input', function() {
document.getElementById('speed-display').textContent = this.value + 'ms';
updateApiExample();
});
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script updates #speed-display on servo speed changes, but no element with id="speed-display" exists in the template. This will throw a JS error and can break the settings page. Either add the missing element (e.g., a span next to the input) or remove/update the JS references.

Copilot uses AI. Check for mistakes.
Comment on lines 65 to 67
self.current_gpio_pin = None
self._cleanup_gpiod()
self._cleanup_pwm_sysfs()
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configure() sets self.current_gpio_pin = None before calling _cleanup_pwm_sysfs(). _cleanup_pwm_sysfs() uses current_gpio_pin to determine which PWM channel to unexport, so this can leave the previously-exported PWM channel active when switching GPIO pins. Capture the previous pin (or call cleanup before clearing current_gpio_pin) so cleanup can unexport correctly.

Suggested change
self.current_gpio_pin = None
self._cleanup_gpiod()
self._cleanup_pwm_sysfs()
self._cleanup_gpiod()
self._cleanup_pwm_sysfs()
self.current_gpio_pin = None

Copilot uses AI. Check for mistakes.
Comment on lines 101 to 105
for angle in range(int(current_angle), int(target_angle), step):
pulse_us = self._angle_to_pulse_us(angle)
self._pwm_sysfs_set_pulse_us(pulse_us)
logger.info(f"new Angle: {angle}° new Pulse: {pulse_us}us")
time.sleep(speed_ms / 1000)
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-degree movement logs are emitted at INFO level inside the movement loop. For large angle changes this can spam logs and slow down movement on slower storage/SD cards. Consider demoting these to DEBUG or logging only the start/end (and maybe every N steps).

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,22 @@
# Servo Control Plugin

This Plugin provides controll of a Servo motor connected to your Raspberry Pi via a configurable GPIO pin.
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling: "controll" → "control".

Suggested change
This Plugin provides controll of a Servo motor connected to your Raspberry Pi via a configurable GPIO pin.
This Plugin provides control of a Servo motor connected to your Raspberry Pi via a configurable GPIO pin.

Copilot uses AI. Check for mistakes.
# Servo Control Plugin

This Plugin provides controll of a Servo motor connected to your Raspberry Pi via a configurable GPIO pin.
You can set the Target Angle and optional the orientation and Image Invertion saved in device_config.
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling: "Invertion" → "Inversion".

Suggested change
You can set the Target Angle and optional the orientation and Image Invertion saved in device_config.
You can set the Target Angle and optional the orientation and Image Inversion saved in device_config.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant