Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
1d644af
Pi5 Support - Planning: Add library research and implementation plan
jantman Oct 25, 2025
8bfa76c
Pi5 Support - 1.1: Define LED backend abstraction interface
jantman Oct 25, 2025
6550574
Pi5 Support - 1.2: Implement rpi_ws281x backend wrapper
jantman Oct 25, 2025
cb81bbe
Pi5 Support - 1.3: Add backend factory and registry system
jantman Oct 25, 2025
e1f9a5b
Pi5 Support - 1.4: Update StripSegment for backend abstraction
jantman Oct 25, 2025
f494a73
Pi5 Support - 1.5: Integrate backend factory into EffectRunner
jantman Oct 25, 2025
5e4ac9e
Pi5 Support - 1.6: Add comprehensive backend unit tests and fix segme…
jantman Oct 25, 2025
e5fc576
Pi5 Support - 1.6: Add missing rpi_ws281x backend test file
jantman Oct 26, 2025
29b2578
Pi5 Support - 1.7: Update versioneer from 0.28 to 0.29
jantman Oct 26, 2025
112af04
Pi5 Support - Milestone 2: Backend Selection Configuration
jantman Oct 26, 2025
73661ce
Pi5 Support - 3.1: Document Adafruit backend requirements
jantman Oct 26, 2025
d5b4a85
Pi5 Support - 3.2: Implement Adafruit NeoPixel SPI backend
jantman Oct 26, 2025
90933fd
Pi5 Support - 3.3: Register Adafruit backend in factory
jantman Oct 26, 2025
aa67a21
Pi5 Support - 3.4: Update UI to show Adafruit backend option
jantman Oct 26, 2025
e49c70d
Pi5 Support - 3.6: Add comprehensive unit tests for Adafruit backend
jantman Oct 26, 2025
4552545
Pi5 Support - 3.8: Fix test for rpi_ws281x backend name update
jantman Oct 26, 2025
a9956a7
Pi5 Support: Update pi5.md with completion status for Milestones 1-3
jantman Oct 26, 2025
1ad6227
Pi5 Support - 4.1: Add comprehensive user documentation for Pi5 support
jantman Oct 26, 2025
cc2b1d6
Pi5 Support - 4.2: Update README and changelog
jantman Oct 27, 2025
d81a6e2
Pi5 Support - 4.3: Enhance setup wizard for backend selection
jantman Oct 27, 2025
9820570
Pi5 Support: Update pi5.md with Task 4.3 completion status
jantman Oct 27, 2025
90fb273
Pi5 Support - 4.4: Enhance logging and diagnostics
jantman Oct 27, 2025
9a62a8e
Pi5 Support: Update pi5.md with Task 4.4 completion status
jantman Oct 27, 2025
9db65b9
Pi5 Support - 4.7: Add Adafruit backend dependencies to setup.py
jantman Oct 27, 2025
3c1b9ee
Pi5 Support: Update pi5.md with Task 4.7 completion status
jantman Oct 27, 2025
c6c55fb
Pi5 Support: Add Milestone 5 for backend-aware wizard enhancement
jantman Oct 27, 2025
d02f16c
Bugfix: Fix get_available_backends() returning wrong data structure
jantman Oct 27, 2025
4beedf6
Bugfix: Fix settings migration creating empty backend config
jantman Oct 27, 2025
87251ee
Debug: Add logging to on_settings_save to diagnose settings not persi…
jantman Oct 27, 2025
d0630f1
Debug: Also log when get_settings_defaults() is called
jantman Oct 27, 2025
5595a61
Pi5 Support - 4.9: Fix settings not persisting when saved
jantman Oct 27, 2025
6686e11
Fix SimpleApiPlugin is_api_protected warning
jantman Oct 27, 2025
be01aa1
Pi5 Support - 4.9b: Fix settings not being saved - add settings templ…
jantman Oct 27, 2025
435fbb3
Revert incorrect template config change - auto-discovery is per-type
jantman Oct 27, 2025
58acd68
Pi5 Support - 4.9c: Fix settings viewmodel initialization errors
jantman Oct 27, 2025
3017d35
Pi5 Support - 4.9d: Fix visibility bindings for backend-specific fields
jantman Oct 27, 2025
7ec0903
Pi5 Support - 4.9e: Fix process join error during settings save
jantman Oct 27, 2025
a0019af
Remove debug logging from settings methods
jantman Oct 27, 2025
155ad51
Pi5 Support - 4.9: Update documentation with bug fixes from user testing
jantman Oct 27, 2025
506baaa
Add Milestone 6: Replace SPI Backend with PWM Backend
jantman Oct 28, 2025
845c716
Pi5 Support - 6.1: Implement Adafruit PWM backend for Pi 5
jantman Oct 28, 2025
e3b2c0d
Pi5 Support - 6.2: Register PWM backend and remove SPI backend
jantman Oct 28, 2025
20ee379
Pi5 Support - 6.3: Update default settings for PWM backend
jantman Oct 28, 2025
0812ddb
Pi5 Support - 6.4: Update UI for PWM backend GPIO pin configuration
jantman Oct 28, 2025
fabef3e
Pi5 Support - 6: Replace SPI backend with PWM backend
jantman Nov 1, 2025
e602f4c
Fix pixel order constants to use tuples instead of module attributes
jantman Nov 3, 2025
41e2651
Fix multiprocessing context error for Python 3.13 compatibility
jantman Nov 3, 2025
8e1088e
Use explicit fork context for all multiprocessing objects
jantman Nov 3, 2025
76fdd3f
Add setBrightness() compatibility alias to PWM backend
jantman Nov 3, 2025
bb22f90
Add camelCase compatibility aliases to PWM backend for rpi_ws281x API
jantman Nov 3, 2025
5b2f739
Add comprehensive debug logging for LED state changes and effect trig…
jantman Nov 4, 2025
c501791
Fix LED transition from heating to printing effect when temperature t…
jantman Nov 8, 2025
df0eb52
Fix KeyError when turning lights off - handle 'blank' mode specially
jantman Nov 8, 2025
28e898b
Docs: Mark Milestone 6 (PWM Backend) as complete with bug fixes
jantman Nov 15, 2025
3d91d77
Wizard Enhancement - 5.1 & 5.2: Backend-aware wizard with Pi 5 support
jantman Nov 15, 2025
3292e03
Wizard Enhancement - 5.3: Update UI to show backend-specific tests
jantman Nov 15, 2025
a8d58b0
Wizard Enhancement - 5.5: Add comprehensive tests for backend-aware w…
jantman Nov 15, 2025
d200ce9
Docs: Mark Milestone 5 (Backend-Aware Wizard) as complete
jantman Nov 15, 2025
320480b
Fix critical wizard bug and improve backend code quality
jantman Nov 15, 2025
6db8635
Docs: Consolidate verbose Pi 5 documentation into concise additions
jantman Nov 15, 2025
4c10fdf
Fix lgpio notification file creation permissions issue on OctoPrint
jantman Nov 27, 2025
e332285
Fix tests: add pin validation and proper mocking via conftest
jantman Nov 27, 2025
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
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ Add some RGB LEDs to your 3D printer for a quick status update!

![rainbow effect](/assets/rainbow.gif)

A highly configurable yet easy to use plugin for attaching WS2811, WS2812 and SK6812 or LEDs to your Raspberry Pi for a printer status update!
A highly configurable yet easy to use plugin for attaching WS2811, WS2812 and SK6812 LEDs to your Raspberry Pi (including Raspberry Pi 5!) for a printer status update!

With lots of options effects and integrations to choose from, you can customise the plugin to do things _exactly_ as you want them.

Most prominent features include:

- **Raspberry Pi 5 support** with multiple LED control backends to choose from
- Printer status effects
- Tracking heating, printing and cooling progress
- Intercepting M150 commands & controlling with @ commands
Expand All @@ -36,11 +37,33 @@ You can take a look at the [documentation](https://cp2004.gitbook.io/ws281x-led-
Setting up the plugin couldn't be easier! There are 3 main steps, with configuration made easy with the setup wizard.

- Wiring your LEDs
- Configuring SPI
- Choosing and configuring an LED control backend (rpi_ws281x for Pi 3/4, Adafruit CircuitPython for Pi 5)
- Configuring plugin settings

Follow the detailed [setup guide](https://cp2004.gitbook.io/ws281x-led-status/guides/setup-guide-1) in the documentation to get up and running.

**Note for Raspberry Pi 5 users:** This plugin now supports Pi 5 using the Adafruit CircuitPython NeoPixel (PWM) backend. See the documentation for setup instructions specific to Pi 5.

## Raspberry Pi 5 Support

This plugin now supports **all Raspberry Pi models including Pi 5** through a flexible LED backend system:

- **Pi 1-4, Zero**: Use the `rpi_ws281x` backend (default, fully backward compatible)
- **Pi 5**: Use the `Adafruit CircuitPython NeoPixel (PWM)` backend

**Key features:**
- Multiple backend support with easy selection in plugin settings
- Pi 5 backend supports any GPIO pin (not limited to specific pins)
- No special OS configuration required for Pi 5 (no SPI setup needed)
- All plugin features work identically with both backends
- Automatic dependency installation via OctoPrint Plugin Manager

**Quick setup for Pi 5:**
1. Install plugin normally through Plugin Manager
2. In plugin settings, select "Adafruit CircuitPython NeoPixel (PWM)" backend
3. Configure your GPIO pin (default: 18) and pixel order (usually GRB)
4. Restart OctoPrint and you're ready!

## Getting help

Please read the [Get Help Guide](https://cp2004.gitbook.io/ws281x-led-status/guides/get-help-guide) as well as the [rest of the documentation](https://cp2004.gitbook.io/ws281x-led-status/), to see if your question has been answered there. Still got questions? Get in touch:
Expand Down
6 changes: 5 additions & 1 deletion docs/guides/setup-guide-1/spi-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ description: >-

# SPI Setup

The plugin uses the Raspberry Pi's SPI interface to push data to the LED strip, rather than PWM since it doesn't need to be run as root to use SPI.
{% hint style="warning" %}
**Raspberry Pi 5 users:** If you're using the Adafruit CircuitPython NeoPixel (PWM) backend, **you can skip this entire page!** The PWM backend requires no special OS configuration. Simply select your backend and GPIO pin in plugin settings.
{% endhint %}

The plugin's `rpi_ws281x` backend (for Pi 1-4) uses the Raspberry Pi's SPI interface to push data to the LED strip, rather than PWM since it doesn't need to be run as root to use SPI.

As a result of this, there are a couple of OS level configuration items that need to be handled. Luckily for you, the plugin makes this very easy for you to do by providing a UI to run the commands.

Expand Down
17 changes: 14 additions & 3 deletions docs/guides/setup-guide-1/supported-hardware.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,21 @@ I have had good results with a 74ACHT125 level shifter, which is recommended by

### Raspberry Pi

All models of Raspberry Pi are supported currently, however for new models I will have to wait for upstream support from the rpi-ws281x library first. This page will be updated if this happens!
**All models of Raspberry Pi are supported**, including:
- Raspberry Pi 1, 2, 3, 4
- Raspberry Pi Zero, Zero 2
- **Raspberry Pi 5** (new!)

This also means that no other devices than a Raspberry Pi are supported. There are no alternative libraries for WS281x LED control (for Python) that could enable this, so there is nothing that can be done. Sorry!
The plugin now uses a flexible LED backend system to support all hardware versions:
- **Pi 1-4, Zero**: Uses the `rpi_ws281x` backend by default
- **Pi 5**: Uses the `Adafruit CircuitPython NeoPixel (PWM)` backend

The plugin **will not load** if it is not running on a Raspberry Pi, even if it does install.
You can select your backend in the plugin settings during initial setup. Both backends support all plugin features identically.

{% hint style="info" %}
For Raspberry Pi 5 users: Select the "Adafruit CircuitPython NeoPixel (PWM)" backend in plugin settings. No special OS configuration is required - just select your GPIO pin and you're ready to go!
{% endhint %}

**Note:** Only Raspberry Pi devices are supported. The plugin **will not load** if it is not running on a Raspberry Pi, even if it does install.

## Got the necessary hardware? Wire it up!
99 changes: 95 additions & 4 deletions octoprint_ws281x_led_status/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@
import re
import time

# Create a fork context for all multiprocessing objects
# This ensures consistent context across Queue and Process for Python 3.13+ compatibility
mp_context = multiprocessing.get_context('fork')

# noinspection PyPackageRequirements
import octoprint.plugin
from octoprint.events import Events, all_events
from octoprint.util.version import is_octoprint_compatible

from octoprint_ws281x_led_status import api, constants, settings, triggers, util, wizard
from octoprint_ws281x_led_status.backend.factory import (
get_available_backends,
get_backend_diagnostics,
)
from octoprint_ws281x_led_status.constants import AtCommands, DeprecatedAtCommands
from octoprint_ws281x_led_status.runner import EffectRunner
from octoprint_ws281x_led_status.util import RestartableTimer
Expand Down Expand Up @@ -47,7 +55,7 @@ def __init__(self):
self.wizard = wizard.PluginWizard(PI_MODEL)

self.current_effect_process = None # type: multiprocessing.Process
self.effect_queue = multiprocessing.Queue()
self.effect_queue = mp_context.Queue()

self.custom_triggers = triggers.Trigger(self.effect_queue)

Expand Down Expand Up @@ -110,6 +118,9 @@ def get_assets(self):

# Startup plugin
def on_startup(self, host, port):
# Log backend diagnostics on startup
self._log_backend_diagnostics()

self.custom_triggers.process_settings(
self._settings.get(["custom"], merged=True)
)
Expand Down Expand Up @@ -166,6 +177,8 @@ def get_template_vars(self):
"progress_names": constants.PROGRESS_EFFECTS.keys(),
"pi_model": PI_MODEL,
"strip_types": constants.STRIP_TYPES,
"backends": get_available_backends(),
"backend_recommendation": self.wizard.get_backend_recommendation(),
"timezone": util.get_timezone(),
"version": self._plugin_version,
"is_docker": os.path.exists(os.path.join("/bin", "s6-svscanctl"))
Expand Down Expand Up @@ -210,12 +223,60 @@ def on_api_command(self, command, data):
def on_api_get(self, request):
return self.api.on_api_get(request=request)

def is_api_protected(self):
# Require authentication for all API commands
return True

# Websocket communication
def _send_ui_msg(self, msg_type, payload):
self._plugin_manager.send_plugin_message(
"ws281x_led_status", {"type": msg_type, "payload": payload}
)

def _log_backend_diagnostics(self):
"""Log available backends and their status on startup for diagnostic purposes"""
self._logger.info("=== LED Backend Diagnostics ===")

diagnostics = get_backend_diagnostics()

if not diagnostics:
self._logger.warning("No LED backends registered!")
return

for backend_name, info in diagnostics.items():
status = "✓ Available" if info["available"] else "✗ Unavailable"
self._logger.info(
f" {backend_name} ({info['display_name']}): {status}"
)

if not info["available"]:
self._logger.info(f" Reason: {info['availability_reason']}")

self._logger.debug(f" Class: {info['class']}")
self._logger.debug(f" Description: {info['description']}")

# Log currently configured backend
configured_backend = self._settings.get(["backend", "type"], merged=True)
self._logger.info(f"Configured backend: {configured_backend}")

# Warn if configured backend is not available
if configured_backend in diagnostics:
if not diagnostics[configured_backend]["available"]:
self._logger.warning(
f"WARNING: Configured backend '{configured_backend}' is not available! "
f"LED strip will fail to initialize."
)
self._logger.warning(
f" Reason: {diagnostics[configured_backend]['availability_reason']}"
)
else:
self._logger.error(
f"ERROR: Configured backend '{configured_backend}' is not registered!"
)

self._logger.info("=== End Backend Diagnostics ===")


# Event Handler plugin
def on_event(self, event, payload):
if event == Events.PRINT_DONE:
Expand Down Expand Up @@ -285,13 +346,14 @@ def start_effect_process(self):
if self.current_effect_process and not self.current_effect_process.is_alive():
self.stop_effect_process()
# Start effect runner here
self.current_effect_process = multiprocessing.Process(
self.current_effect_process = mp_context.Process(
target=EffectRunner,
name="WS281x LED Status Effect Process",
kwargs={
"debug": self._settings.get_boolean(["features", "debug_logging"]),
"queue": self.effect_queue,
"strip_settings": self._settings.get(["strip"], merged=True),
"backend_settings": self._settings.get(["backend"], merged=True),
"effect_settings": self._settings.get(["effects"], merged=True),
"features_settings": self._settings.get(["features"], merged=True),
"previous_state": self.current_state,
Expand All @@ -316,7 +378,9 @@ def stop_effect_process(self):
if self.current_effect_process is not None:
if self.current_effect_process.is_alive():
self.effect_queue.put(constants.KILL_MSG)
self.current_effect_process.join()
# Only join if the process was actually started
if self.current_effect_process._popen is not None:
self.current_effect_process.join()

self._logger.info("WS281x LED Status runner stopped")

Expand Down Expand Up @@ -551,12 +615,21 @@ def process_gcode_q(
else:
if self.heating:
# Currently heating, now stopping - go back to last event
self._logger.info(
f"[STATE] Heating stopped by gcode: {gcode or cmd}"
)
self.heating = False
if self._printer.is_printing():
# If printing, go back to print progress immediately
self._logger.info(
f"[STATE] Transitioning from heating to print progress (current: {self.current_progress}%)"
)
self.on_print_progress(progress=self.current_progress)
else:
# Otherwise go back to the previous effect
self._logger.info(
f"[STATE] Heating stopped, returning to previous effect: {self.previous_event or 'none'}"
)
self.process_previous_event()

self.custom_triggers.on_gcode_command(gcode, cmd)
Expand Down Expand Up @@ -609,8 +682,23 @@ def abort():

# Stop if current is above target
if current_temp > target:
self._logger.info(
f"[STATE] Heating complete: {heater} reached {current_temp}°C (target: {target}°C)"
)
self.heating = False
return abort()
if self._printer.is_printing():
# If printing, go back to print progress immediately
self._logger.info(
f"[STATE] Transitioning from heating to print progress (current: {self.current_progress}%)"
)
self.on_print_progress(progress=self.current_progress)
else:
# Otherwise go back to the previous effect
self._logger.info(
f"[STATE] Heating complete, returning to previous effect: {self.previous_event or 'none'}"
)
self.process_previous_event()
return parsed_temps

self.update_effect(
{
Expand Down Expand Up @@ -852,3 +940,6 @@ def __plugin_load__():
"octoprint.comm.protocol.temperatures.received": __plugin_implementation__.temperatures_received,
"octoprint.comm.protocol.atcommand.sending": __plugin_implementation__.process_at_command,
}

from . import _version
__version__ = _version.get_versions()['version']
Loading