Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9a17734
Servo Control Plugin added
and-who Feb 1, 2026
7cbfa7c
Servo Controll dependency test
and-who Feb 1, 2026
a484268
Servo Controll dependencies added
and-who Feb 1, 2026
9b06fb6
Servo Controll dependencies test
and-who Feb 1, 2026
ad1efc9
Servo Controll dependencies test
and-who Feb 1, 2026
147147a
Servo Controll extended control
and-who Feb 1, 2026
5be16f8
Servo Control enable Pin if moved again
and-who Feb 1, 2026
eeea237
Servo Control moved Driver to separate Utils
and-who Feb 1, 2026
d993f96
Servo Control move asyncron
and-who Feb 1, 2026
7aee95e
Servo Control move asyncron
and-who Feb 1, 2026
0248b5a
Servo Control move image invertition
and-who Feb 1, 2026
a500ce9
Servo Control move image invertition
and-who Feb 1, 2026
22613ef
Servo Control Test Image
and-who Feb 1, 2026
33bf990
InkyPi Listen also on ipv6
and-who Feb 3, 2026
03d9930
Skip move when Angle reached
and-who Feb 6, 2026
eb51e3a
invert setting
and-who Feb 6, 2026
cca3aed
invert setting
and-who Feb 6, 2026
f701395
testimage text color
and-who Feb 6, 2026
a69eaec
test image handling
and-who Feb 6, 2026
84c53f6
Angle Config changed
and-who Feb 6, 2026
2e68b57
Angle Config changed
and-who Feb 6, 2026
66451d0
Angle Config changed
and-who Feb 6, 2026
6f9828f
Angle Config changed
and-who Feb 6, 2026
79d6f0e
Install Setup
and-who Feb 8, 2026
5980ca8
Update Settings
and-who Feb 8, 2026
06835f2
Settins Style
and-who Feb 8, 2026
f7640f7
API Example
and-who Feb 8, 2026
310ea39
HTML Element Style
and-who Feb 8, 2026
fe3427a
HTML Element Style
and-who Feb 8, 2026
91ba62f
Updated docs
and-who Feb 8, 2026
5c16757
More robust End Position
and-who Feb 8, 2026
6cf4e2d
serve dual binding
and-who Feb 8, 2026
3f933ce
fixed button styling
and-who Feb 14, 2026
418c115
added local mock suport
and-who Feb 14, 2026
95dfa97
minor fixes
and-who Feb 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,35 @@ To install InkyPi, follow these steps:
```
3. Run the installation script with sudo:
```bash
sudo bash install/install.sh [-W <waveshare device model>]
sudo bash install/install.sh [-W <waveshare device model>] [-S]
```
Option:
Options:

* -W \<waveshare device model\> - specify this parameter **ONLY** if installing for a Waveshare display. After the -W option specify the Waveshare device model e.g. epd7in3f.
* -S (optional) - include this flag to enable servo control support. If not specified, servo support is disabled.

e.g. for Inky displays use:
Examples:

For Inky displays without servo:
```bash
sudo bash install/install.sh
```

and for [Waveshare displays](#waveshare-display-support) use:
For Inky displays with servo support:
```bash
sudo bash install/install.sh -S
```

For [Waveshare displays](#waveshare-display-support) without servo:
```bash
sudo bash install/install.sh -W epd7in3f
```

For [Waveshare displays](#waveshare-display-support) with servo support:
```bash
sudo bash install/install.sh -W epd7in3f -S
```


After the installation is complete, the script will prompt you to reboot your Raspberry Pi. Once rebooted, the display will update to show the InkyPi splash screen.

Expand Down
1 change: 1 addition & 0 deletions docs/community.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ A collection of 3D models, custom builds, and frames contributed by the communit
| 7.3" Inky Impression | 3D Printable bezel for IKEA Rodalm Frame | [3D model](https://makerworld.com/en/models/1242875-inky-impression-bezel-for-ikea-rodalm#profileId-1263722) | [scotthsieh0503](https://github.com/scotthsieh0503) |
| 7.3" Inky Impression (2025 Edition) | 3D Printable frame with stand | [3D model](https://makerworld.com/en/models/1482862-inky-impression-2025-edition-7-3-frame#profileId-1548643) | [JeremyCottontail](https://github.com/JeremyCottontail) |
| 7.3" Inky Impression (2025 Edition) | 3D Printable bezel for IKEA Rodalm Frame | [3D model](https://makerworld.com/en/models/1457388-inky-impression-2025-pim773-ikea-rodlam-mount) | [Maxweeje](https://makerworld.com/en/@Maxweeje)
| 7.3" Inky Impression | 3D Printable Rotatable Stand | [3D model](https://www.thingiverse.com/thing:7290592) | [Andreas Wolf](https://github.com/and-who) |
| 5.7" Inky Impression | Lego Stand | [Instructions](https://github.com/pikesley/impression-clock?tab=readme-ov-file#making-the-stand) | [pikesley](https://github.com/pikesley) |
| 13.3" Inky Impression | 3D Printable Frame and Stand | [3D model](https://makerworld.com/en/models/1618040-inky-impression-pimoroni-13-3-frame) | [Phil Schaffer](https://makerworld.com/en/@pjschaffer) |
3 changes: 2 additions & 1 deletion install/config_base/device.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"orientation": "horizontal",
"inverted_image": false,
"scheduler_sleep_time": 60,
"startup": true
"startup": true,
"servo_enabled": false
}
85 changes: 75 additions & 10 deletions install/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
# Description: This script automates the installation of InkyPI and creation of
# the InkyPI service.
#
# Usage: ./install.sh [-W <waveshare_device>]
# Usage: ./install.sh [-W <waveshare_device>] [-S]
# -W <waveshare_device> (optional) Install for a Waveshare device,
# specifying the device model type, e.g. epd7in3e.
#
# If not specified then the Pimoroni Inky display
# is assumed.
# -S (optional) Install servo control support and enable PWM overlay.
# If not specified, servo support is disabled.
# =============================================================================

# Formatting stuff
Expand Down Expand Up @@ -48,13 +50,23 @@ PIP_REQUIREMENTS_FILE="$SCRIPT_DIR/requirements.txt"
WS_TYPE=""
WS_REQUIREMENTS_FILE="$SCRIPT_DIR/ws-requirements.txt"

# Parse the arguments, looking for the -W option.
#
# Additional requirements for Servo support.
#
# empty means no servo support required, otherwise install servo support
SERVO_ENABLED=""
SERVO_REQUIREMENTS_FILE="$SCRIPT_DIR/servo-requirements.txt"

# Parse the arguments, looking for the -W and -S options.
parse_arguments() {
while getopts ":W:" opt; do
while getopts ":W:S" opt; do
case $opt in
W) WS_TYPE=$OPTARG
echo "Optional parameter WS is set for Waveshare support. Screen type is: $WS_TYPE"
;;
S) SERVO_ENABLED="true"
echo "Optional parameter S is set for Servo control support."
;;
\?) echo "Invalid option: -$OPTARG." >&2
exit 1
;;
Expand Down Expand Up @@ -140,6 +152,28 @@ enable_interfaces(){
fi
}

enable_pwm_overlay(){
# Only enable PWM overlay if servo support is requested
if [[ -z "$SERVO_ENABLED" ]]; then
return
fi

echo "Enabling PWM overlay for servo control"
local CONFIG_PRIMARY="/boot/firmware/config.txt"
local CONFIG_FALLBACK="/boot/config.txt"

for cfg in "$CONFIG_PRIMARY" "$CONFIG_FALLBACK"; do
if [ -f "$cfg" ]; then
if ! grep -E -q '^[[:space:]]*dtoverlay=pwm-2chan' "$cfg"; then
sed -i '/^dtparam=spi=on/a dtoverlay=pwm-2chan' "$cfg"
echo_success "\tPWM overlay enabled (pwm-2chan) in $cfg"
else
echo_success "\tPWM overlay already enabled in $cfg"
fi
fi
done
}

show_loader() {
local pid=$!
local delay=0.1
Expand Down Expand Up @@ -192,6 +226,11 @@ install_debian_dependencies() {
fi
}

install_servo_dependencies() {
echo "Installing servo dependencies (libgpiod and tools)."
sudo apt-get install -y gpiod python3-libgpiod libgpiod-dev > /dev/null
}

setup_zramswap_service() {
echo "Enabling and starting zramswap service."
sudo apt-get install -y zram-tools > /dev/null
Expand Down Expand Up @@ -219,6 +258,13 @@ create_venv(){
show_loader "\tInstalling additional Waveshare python dependencies. "
fi

# do additional dependencies for Servo support.
if [[ -n "$SERVO_ENABLED" ]]; then
echo "Adding additional dependencies for servo control to the python virtual environment. "
$VENV_PATH/bin/python -m pip install -r $SERVO_REQUIREMENTS_FILE > servo_pip_install.log &
show_loader "\tInstalling additional Servo python dependencies. "
fi

}

install_app_service() {
Expand Down Expand Up @@ -254,12 +300,12 @@ install_config() {
}

#
# Update the device.json file with the supplied Waveshare parameter (if set).
# Update the device.json file with the supplied Waveshare and Servo parameters (if set).
#
update_config() {
local DEVICE_JSON="$CONFIG_DIR/device.json"

if [[ -n "$WS_TYPE" ]]; then
local DEVICE_JSON="$CONFIG_DIR/device.json"

if grep -q '"display_type":' "$DEVICE_JSON"; then
# Update existing display_type value
sed -i "s/\"display_type\": \".*\"/\"display_type\": \"$WS_TYPE\"/" "$DEVICE_JSON"
Expand All @@ -273,8 +319,22 @@ update_config() {
echo "}" >> "$DEVICE_JSON" # Add trailing }
echo "Added display_type: $WS_TYPE"
fi
else
echo "Config not updated as WS_TYPE flag is not set"
fi

if [[ -n "$SERVO_ENABLED" ]]; then
if grep -q '"servo_enabled":' "$DEVICE_JSON"; then
# Update existing servo_enabled value
sed -i 's/"servo_enabled": false/"servo_enabled": true/' "$DEVICE_JSON"
echo "Updated servo_enabled to: true"
else
# Append servo_enabled safely, ensuring proper comma placement
if grep -q '}$' "$DEVICE_JSON"; then
sed -i '$s/}/,/' "$DEVICE_JSON" # Replace last } with a comma
fi
echo " \"servo_enabled\": true" >> "$DEVICE_JSON"
echo "}" >> "$DEVICE_JSON" # Add trailing }
echo "Added servo_enabled: true"
Comment on lines +330 to +336
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.

update_config() attempts to append servo_enabled by replacing the last } with , and then echoing a new key and another }. This produces invalid JSON (a standalone comma line / duplicated closing brace) for existing installs that don't already have servo_enabled. Consider using jq/python to edit JSON safely, or insert the new key before the final } while ensuring the previous property line gets the comma.

Copilot uses AI. Check for mistakes.
fi
fi
}

Expand Down Expand Up @@ -364,7 +424,12 @@ if [[ -n "$WS_TYPE" ]]; then
fetch_waveshare_driver
fi
enable_interfaces
enable_pwm_overlay
install_debian_dependencies
# Only install servo dependencies if servo support is requested
if [[ -n "$SERVO_ENABLED" ]]; then
install_servo_dependencies
fi
# check OS version for Bookworm to setup zramswap
if [[ $(get_os_version) = "12" ]] ; then
echo "OS version is Bookworm - setting up zramswap"
Expand All @@ -378,8 +443,8 @@ install_cli
create_venv
install_executable
install_config
# update the config file with additional WS if defined.
if [[ -n "$WS_TYPE" ]]; then
# update the config file with additional WS or Servo if defined.
if [[ -n "$WS_TYPE" ]] || [[ -n "$SERVO_ENABLED" ]]; then
update_config
fi
install_app_service
Expand Down
2 changes: 1 addition & 1 deletion install/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ psutil==7.0.0
cysystemd==2.0.1
waitress==3.0.2
feedparser==6.0.11
astral>=3.1
astral>=3.1
4 changes: 4 additions & 0 deletions install/servo-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
gpiozero==2.0.1
gpiod>=2.0.0
lgpio==0.2.2.0
RPi.GPIO==0.7.1
11 changes: 8 additions & 3 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,19 @@ def get_config(self, key=None, default={}):
return self.config

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}

Comment on lines 69 to 83
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.
# Build ordered list
ordered = []
Expand Down
5 changes: 4 additions & 1 deletion src/config/device_dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
480
],
"orientation": "horizontal",
"servo_enabled": true,
"plugin_order": [
"calendar",
"ai_image",
Expand Down Expand Up @@ -45,5 +46,7 @@
"image_hash": null,
"refresh_type": null,
"plugin_id": null
}
},
"inverted_image": false,
"current_servo_angle": 45
}
8 changes: 7 additions & 1 deletion src/display/display_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ def display_image(self, image, image_settings=[]):

if not hasattr(self, "display"):
raise ValueError("No valid display instance initialized.")

# If no Image provided, skip rendering
if image is None:
logger.info("No image provided, skipping rendering")
return

# Save the image
logger.info(f"Saving image to {self.device_config.current_image_file}")
Expand All @@ -77,7 +82,8 @@ def display_image(self, image, image_settings=[]):
# Resize and adjust orientation
image = change_orientation(image, self.device_config.get_config("orientation"))
image = resize_image(image, self.device_config.get_resolution(), image_settings)
if self.device_config.get_config("inverted_image"): image = image.rotate(180)
invert_setting = self.device_config.get_config("inverted_image")
if str(invert_setting).lower() in ("1", "true", "yes", "on"): image = image.rotate(180)
image = apply_image_enhancement(image, self.device_config.get_config("image_settings"))

# Pass to the concrete instance to render to the device.
Expand Down
2 changes: 1 addition & 1 deletion src/inkypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,6 @@
except:
pass # Ignore if we can't get the IP

serve(app, host="0.0.0.0", port=PORT, threads=1)
serve(app, listen="0.0.0.0:80 [::]:80", threads=1)
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 Waitress serve() call is now hard-coded to bind to port 80, which ignores the PORT variable (dev mode sets PORT = 8080). This will break --dev runs (binding privileged port 80) and makes the logged URL misleading. Use PORT when constructing the listen address (or revert to host/port args) so dev/prod behave as configured.

Suggested change
serve(app, listen="0.0.0.0:80 [::]:80", threads=1)
serve(app, listen=f"0.0.0.0:{PORT} [::]:{PORT}", threads=1)

Copilot uses AI. Check for mistakes.
finally:
refresh_task.stop()
22 changes: 22 additions & 0 deletions src/plugins/servo_control/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Servo Control Plugin

This Plugin provides control 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 Inversion saved in device_config.
Mainly this Plugin is intended to be used together with the Rotating Image Frame to create a physical frame rotation when displaying images.
But it could also be used for controlling other Mechanics (e.g. Windscreen Wipers, Blinds, etc.).

## Rotating Image Frame Example

https://www.thingiverse.com/thing:7290592

### Parts Needed
- Raspberry Pi Zero 2 W
- Screen (tested with Pimoron Inky Impression - 7.3” Spectra 6 Edition)
- Powercable for Raspberry Pi
- Servo Motor (e.g. SG90)
- 18 Jumper Cables
- Rotating Frame Assembly 3D Print e.g. from Thingiverse (https://www.thingiverse.com/thing:7290592)
- Bearing (e.g. 8 mm x 22 mm x 7 mm)
- Frame (Ikea Roedalm - 200 mm x 150 mm)


Binary file added src/plugins/servo_control/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/plugins/servo_control/plugin-info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"display_name": "Servo Control",
"id": "servo_control",
"class": "ServoControl",
"repository": ""
}
Loading