Version: 0.0.2
Platform: OpenKNX (RP2040, RP2350, ESP32-S3)
License: GNU GPL v3.0
Author: Erkan Çolak
A high-performance, hardware-optimized LED control library for addressable RGB/RGBW strips on OpenKNX devices with self-describing effects and stateless architecture.
- Overview
- Key Features
- Quick Start
- Architecture
- Installation
- Hardware Support
- Console Commands
- API Reference
- Examples
- Performance
- Power Management & Current Limiting
- Troubleshooting
- Contributing
- License
- Credits
OFM-NeoPixel provides a three-layer architecture for managing addressable LED strips:
- PhysicalStrip - Hardware abstraction for individual LED strips
- VirtualStrip - Logical composition of multiple physical strips
- Segment - Effect zones with independent animations
This design enables complex LED configurations with minimal CPU overhead through hardware acceleration (PIO/DMA on RP2040, RMT on ESP32).
- Hardware Accelerated: Zero CPU overhead during LED updates (DMA/PIO/RMT)
- Stateless Effects: 96% memory savings - single effect instance for all segments
- Self-Describing Effects: Auto-generated UI, console commands, and documentation
- Multi-Strip Composition: Combine multiple physical strips into one logical strip
- Platform Optimized: RP2040 (PIO/DMA), RP2350 (PIO/DMA), ESP32-S3 (RMT)
- Parameter Introspection API: Effects describe their own parameters
- Auto-Generated UI: Console and web UI generate automatically
- 12 Parameter Types: UINT8, BOOL, COLOR_RGB, PERCENT, ENUM, etc.
- Zero Code Changes: Add new effects without modifying Segment/Console/UI
- Type-Safe: ParameterType enum for validation
- Multiple strip support (up to 7 on RP2040, 11 on RP2350, 7 on ESP32-S3)
- Protocol support: WS2812, WS2812B, WS2813, WS2815, SK6812, APA102, WS2801
- Automatic driver selection based on platform and protocol
- DMA transfers for zero-CPU overhead (RP2040/RP2350)
- RMT hardware acceleration (ESP32-S3)
- SPI support for APA102/WS2801 strips
- Virtual strip abstraction with automatic offset calculation
- Segment-based effect system
- Integrated effects: Solid, Rainbow, Pride2015, Confetti, Juggle, BPM, Cylon, Wipe
- Per-segment brightness control
- Color order abstraction (RGB, GRB, BGR, RGBW, GRBW)
- Performance tracking and statistics
- OpenKNX module interface
- Console command system for configuration and testing
- GroupObject support (planned)
- Real-time performance monitoring
#include "OpenKNX.h"
#include "NeoPixel.h"
void setup() {
openknx.init(0);
openknx.addModule(13, neoPixelModule);
openknx.setup();
NeoPixelManager* neopixel_manager = neoPixelModule.getManager();
if(neopixel_manager)
{
// Create a physical strip (GPIO 9, 64 LEDs, WS2812B, RGB)
auto strip = neopixel_manager->addStrip(22, 64, LedProtocol::WS2812B, ColorOrder::RGB);
// Initialize the physical strip
strip->init();
// Update the strip
neopixel_manager->updateAll();
}
}
void loop() {
openknx.loop();
}void setup() {
openknx.init(0);
openknx.addModule(13, neoPixelModule);
openknx.setup();
NeoPixelManager* npxmgr = neoPixelModule.getManager();
if(npxmgr)
{
// Add three physical strips (Default Order is RGB)
auto strip0 = npxmgr->addStrip(22, 64, LedProtocol::WS2812B, ColorOrder::RGB);
auto strip1 = npxmgr->addStrip(7, 64, LedProtocol::WS2812B, ColorOrder::RGB);
auto strip2 = npxmgr->addSpiStrip(9, 8, 40, LedProtocol::APA102, ColorOrder::RGB);
// Initialize the physical strips
if(strip0) strip0->init();
if(strip1) strip1->init();
if(strip2) strip2->init();
// ONE VirtualStrip for all (168 LEDs total: 64+64+40)
auto virt0 = npxmgr->addVirtualStrip(168, ColorOrder::RGB); // Default RGB, PhysicalStrips handle conversion
// Attach all physical strips
npxmgr->attachPhysicalToVirtual(virt0, strip0, 0); // Offset 0-63
npxmgr->attachPhysicalToVirtual(virt0, strip1, 64); // Offset 64-127
npxmgr->attachPhysicalToVirtual(virt0, strip2, 128); // Offset 128-167
// ONE segment for all LEDs
Segment* seg0 = npxmgr->addSegment(virt0, 0, 167); // All 168 LEDs
// Set the getSolid effect to the segment0
seg0->setEffect(EffectPool::getSolid());
seg0->setPrimaryColor(50, 0, 0, 255); // Dark red
// Enable auto-update
npxmgr->updateAll();
}# Add physical strips
neo phys add 9 8 # GPIO 9: 8 LEDs
neo phys add 22 64 # GPIO 22: 64 LEDs
# Create virtual strip
neo virt add 72 # 72 LEDs total
# Attach physical strips to virtual
neo virt attach 0 0 # Attach PhysStrip[0] to VirtStrip[0] at offset 0
neo virt attach 0 1 # Attach PhysStrip[1] to VirtStrip[0] at offset 8
# Create segments (effect zones)
neo seg add 0 0 35 # Segment[0]: LEDs 0-35 in VirtStrip[0]
neo seg add 0 36 71 # Segment[1]: LEDs 36-71 in VirtStrip[0]
# Assign effects
neo effect 0 1 # Rainbow on Segment[0]
neo effect 1 6 # Cylon on Segment[1]
neo brightness 1 200 # 78% brightness
# Start rendering
neo auto on # Auto-update at 20 FPS
# Check performance
neo perf # Show CPU usage and frame rate┌─────────────────────────────────────────────────────────┐
│ NeoPixel Module │
│ (OpenKNX Integration Layer) │
│ - Console commands (with parameter API) │
│ - GroupObject handling (planned) │
│ - Lifecycle management │
└────────────────────────┬────────────────────────────────┘
|
┌────────────────────────▼────────────────────────────────┐
│ NeoPixelManager │
│ - Physical strip lifecycle │
│ - Virtual strip composition │
│ - Segment orchestration │
│ - Effect update scheduling │
│ - GLOBAL Power Management (NEW!) │
│ └─> PowerManager: Current limiting across ALL strips │
└────┬────────────┬─────────────┬─────────────────────────┘
| | |
┌────▼───────┐ ┌─▼────────┐ ┌─▼────────┐
│ Physical │ │ Virtual │ │ Segment │
│ Strip │ │ Strip │ │ + State │
│+ColorOrder │ │(RGB only)│ │ +Config │
└────┬───────┘ └──────────┘ └─────┬────┘
| |
┌────▼───────────────┐ ┌─────▼─────────────────┐
│ IHardwareDriver │ │ Effect (Singleton) │
│ - PIO (RP2040) │ │ - Stateless │
│ - RMT (ESP32) │ │ - Parameter API │
│ - SPI (All) │ │ - Self-describing │
└────────────────────┘ └───────────────────────┘
Effect System (NEW):
┌──────────────────────────────────────────────────────┐
│ Effect Pool (Singletons, ~80 bytes) │
│ Solid │ Rainbow │ BPM │ Pride │ ... (10 effects) │
└────┬─────────┬──────┬─────┬──────────────────────────┘
│ │ │ │
└─────────┴──────┴─────┴────► Shared by 100 segments
= 8 bytes per effect
vs 800+ bytes with state
┌──────────────────────────────────────────────────────────┐
│ PHASE 1: Effect Updates │
│ Effect.update() -> Segment.setPixel() -> VirtualStrip │
│ Calculates ideal pixel colors (RGB/RGBW) │
└───────────────────────┬──────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ PHASE 2: GLOBAL POWER MANAGEMENT (NEW!) │
│ NeoPixelManager::updateAll() │
│ ├─ Calculate total current across ALL VirtualStrips │
│ ├─ PowerManager: Sum(I_strip1 + I_strip2 + ...) │
│ ├─ If total > limit: globalScale = limit / total │
│ └─ Scale ALL VirtualStrip buffers: pixel *= globalScale │
└───────────────────────┬──────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ PHASE 3: Sync Virtual→Physical │
│ VirtualStrip.syncToPhysical() │
│ Copy SCALED buffer to PhysicalStrips with ColorOrder │
│ conversion (RGB→GRB/BGR/etc.) │
└───────────────────────┬──────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ PHASE 4: Hardware Transfer │
│ PhysicalStrip.show() → DMA/PIO/RMT/SPI │
│ Non-blocking hardware transfer to GPIO │
└───────────────────────┬──────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ LED Hardware: Displays scaled, safe output │
│ Power consumption ≤ configured limit │
└──────────────────────────────────────────────────────────┘
System Components RAM Usage
────────────────────────────────────────────
NeoPixelManager ~200 bytes
PhysicalStrip (per strip) ~150 bytes + LED buffer
├─ LED buffer (RGB) N × 3 bytes
└─ DMA buffer (if enabled) N × 4 bytes
VirtualStrip ~70 bytes + LED buffer
└─ LED buffer N × bytesPerLed
Segment (per segment) ~180 bytes
Effect instances (shared) ~200 bytes total
Example: 3 strips (100+64+8 LEDs), 1 virtual (172 LEDs), 3 segments
Total RAM: ~3.2 KB
Design Philosophy: Unified RGB interface with per-strip hardware adaptation.
The ColorOrder system provides automatic color byte reordering for different LED hardware:
┌──────────────┐
│ Application │ Always uses logical RGB(W) colors
│ (Effects) │ Example: RED = RGB(255, 0, 0)
└──────┬───────┘
│ Always RGB/RGBW
▼
┌──────────────┐
│ VirtualStrip │ Stores pixels in RGB/RGBW format
│ Buffer │ [R, G, B] or [R, G, B, W]
└──────┬───────┘
│ syncToPhysical() sends RGB
▼
┌──────────────┐
│PhysicalStrip │ Converts RGB → Hardware ColorOrder
│ │ Example GRB: RGB(255,0,0) → [0,255,0]
│ ColorOrder: │ Example BGR: RGB(255,0,0) → [0,0,255]
│ GRB / BGR │
└──────┬───────┘
│ Hardware-ordered bytes
▼
┌──────────────┐
│ Hardware │ Receives native byte order
│ Driver (PIO/ │ WS2812B reads: [G, R, B]
│ RMT / SPI) │ APA102 reads: [Brightness, B, G, R]
└──────────────┘
| ColorOrder | LED Chips | Byte Mapping | Example (RED) |
|---|---|---|---|
RGB |
WS2811, some clones | [R, G, B] | [255, 0, 0] |
RBG |
Rare variants | [R, B, G] | [255, 0, 0] |
GRB |
WS2812, WS2812B | [G, R, B] | [0, 255, 0] |
GBR |
Rare variants | [G, B, R] | [0, 0, 255] |
BGR |
APA102, WS2801 | [B, G, R] | [0, 0, 255] |
BRG |
Rare variants | [B, R, G] | [0, 255, 0] |
RGBW |
SK6812 | [R, G, B, W] | [255, 0, 0, 0] |
GRBW |
SK6812 variants | [G, R, B, W] | [0, 255, 0, 0] |
Scenario: WS2812B (GRB hardware) showing RED
1. Application: segment->setPrimaryColor(255, 0, 0, 0); // Logical RGB
2. Effect: Reads config.primaryRGBW = 0xFF000000
Calls segment->setPixel(i, 255, 0, 0, 0);
3. Segment: Applies brightness
Calls virtualStrip->setPixel(idx, 255, 0, 0);
4. VirtualStrip: Stores in buffer: [255, 0, 0] (Always RGB!)
Calls syncToPhysical()
5. PhysicalStrip: Reads ColorOrder = GRB
Converts: byte0=G(0), byte1=R(255), byte2=B(0)
Calls driver->setPixel(i, 0, 255, 0);
6. PIO Driver: Writes bytes to GPIO: [0, 255, 0]
7. WS2812B LED: Interprets as: G=0, R=255, B=0 → RED
-
VirtualStrip is ColorOrder-agnostic
- Always stores RGB/RGBW internally
- No color conversion in VirtualStrip layer
- Simplifies effect development
-
PhysicalStrip handles hardware differences
- Converts RGB → hardware ColorOrder
- Each PhysicalStrip can have different ColorOrder
- Conversion happens once per frame
-
Hardware drivers are byte-oriented
- Receive already-converted bytes
- No color logic in hardware layer
- Protocol-specific formatting only (APA102 brightness, etc.)
This is the killer feature: Combine strips with different ColorOrders seamlessly!
// Example: WS2812B (GRB) + APA102 (BGR) in one logical strip
auto strip0 = mgr->addStrip(22, 64, LedProtocol::WS2812B, ColorOrder::GRB);
auto strip1 = mgr->addSpiStrip(9, 8, 40, LedProtocol::APA102, ColorOrder::BGR);
// Combine into ONE VirtualStrip
auto virt = mgr->addVirtualStrip(104, ColorOrder::RGB); // Always RGB!
mgr->attachPhysicalToVirtual(virt, strip0, 0); // WS2812B at 0-63
mgr->attachPhysicalToVirtual(virt, strip1, 64); // APA102 at 64-103
// ONE segment, ONE effect across BOTH strips with DIFFERENT hardware!
auto seg = mgr->addSegment(virt, 0, 103);
seg->setEffect(EffectPool::getRainbow());
seg->setPrimaryColor(255, 0, 0, 255); // RED on both strips ✅Result: Both strips show the same logical colors despite different hardware!
Per PhysicalStrip (Recommended):
// 1-Wire strips
auto strip = mgr->addStrip(pin, count, protocol, ColorOrder::GRB);
// SPI strips
auto strip = mgr->addSpiStrip(mosi, sck, count, protocol, ColorOrder::BGR);
// Console
neo phys add 9 64 2 1 # GPIO 9, 64 LEDs, WS2812B, ColorOrder=GRB
neo spi add 8 9 40 5 4 # MOSI=8, SCK=9, 40 LEDs, APA102, ColorOrder=BGRVirtualStrip ColorOrder (Legacy/Ignored):
VirtualStrip has a ColorOrder parameter for backward compatibility, but it's not used for color conversion. VirtualStrip always stores RGB/RGBW internally.
// This parameter is ignored for color conversion
auto virt = mgr->addVirtualStrip(100, ColorOrder::RGB); // Always RGB internallyProblem: Wrong colors (e.g., RED shows as GREEN)
Solution:
-
Check hardware datasheet - Verify actual ColorOrder
- WS2812B: Usually GRB (but some clones are RGB!)
- APA102: Usually BGR
- SK6812: Usually GRB or GRBW
-
Test all combinations:
// Try each ColorOrder until colors match ColorOrder::RGB // If this works, your LEDs are RGB-native ColorOrder::GRB // Most WS2812B ColorOrder::BGR // Most APA102
-
Console test:
neo color 0 50 0 0 # Should show RED neo color 0 0 50 0 # Should show GREEN neo color 0 0 0 50 # Should show BLUE
Example: Your hardware tested as RGB-native (unusual but valid):
auto strip0 = mgr->addStrip(22, 64, LedProtocol::WS2812B, ColorOrder::RGB);
auto strip2 = mgr->addSpiStrip(9, 8, 40, LedProtocol::APA102, ColorOrder::RGB);ColorOrder conversion has negligible performance impact:
- When: Once per frame during
syncToPhysical() - Where: Simple switch-case byte reordering
- Cost: ~3 CPU cycles per LED (~0.01ms for 100 LEDs)
- DMA/PIO: Still zero-CPU overhead during GPIO transmission
Benchmark (100 LEDs, RP2040 @ 133MHz):
- No ColorOrder: 0.08ms
- With ColorOrder: 0.09ms (+0.01ms)
- DMA transfer: 0.00ms (zero CPU)
Add to platformio.ini:
[env:your_board]
lib_deps =
https://github.com/OpenKNX/OFM-NeoPixel.git
build_flags =
-DNEOPIXEL_MODULE
; Optional: Configure resource limits
; -DNEOPIXEL_MAX_PHYSICAL_STRIPS=12
; -DNEOPIXEL_MAX_VIRTUAL_STRIPS=6
; -DNEOPIXEL_MAX_SEGMENTS=32
; -DNEOPIXEL_ENFORCE_LIMITS=1Resource Limits:
The library includes configurable limits to prevent memory exhaustion:
NEOPIXEL_MAX_PHYSICAL_STRIPS(default: 6) - Maximum physical LED stripsNEOPIXEL_MAX_VIRTUAL_STRIPS(default: 12) - Maximum virtual stripsNEOPIXEL_MAX_SEGMENTS(default: 16) - Maximum segmentsNEOPIXEL_ENFORCE_LIMITS(default: 1) - Enable/disable limit enforcement
These limits are used for vector pre-allocation and optional runtime enforcement.
- Download the repository as ZIP
- Sketch -> Include Library -> Add .ZIP Library
- Select the downloaded ZIP file
- Clone into your OpenKNX project's
libdirectory:
cd lib
git clone https://github.com/OpenKNX/OFM-NeoPixel.git- Add to your
main.cpp:
#ifdef NEOPIXEL_MODULE
#include "NeoPixel.h"
#endif
void setup() {
// ...
#ifdef NEOPIXEL_MODULE
openknx.addModule(13, neoPixelModule);
#endif
// ...
}- Define in
platformio.ini:
build_flags =
-DNEOPIXEL_MODULE
; Optional: Enable tests and benchmarks
; -DOPENKNX_NEOPIXEL_TESTS
; -DOPENKNX_NEOPIXEL_BENCHMARK| Platform | Architecture | PIO/RMT Channels | Max Strips | Status |
|---|---|---|---|---|
| RP2040 | ARM Cortex-M0+ | 8 PIO (7 usable) | 7 + 2 SPI | Tested |
| RP2350 | ARM Cortex-M33 | 12 PIO (11 usable) | 11 + 2 SPI | Tested |
| ESP32-S3 | Xtensa LX7 | 4 RMT (3 usable) | 7 + 2 SPI | Not Tested |
| Protocol | Voltage | Colors | Speed | Color Order | Notes |
|---|---|---|---|---|---|
| WS2812 | 5V | RGB | 800kHz | GRB | Original |
| WS2812B | 5V | RGB | 800kHz | GRB | Most common |
| WS2813 | 5V | RGB | 800kHz | GRB | Data backup line |
| WS2815 | 12V | RGB | 800kHz | GRB | High voltage |
| WS2811 | 12V | RGB | 400kHz | RGB | Slower timing |
| SK6812 | 5V/12V | RGBW | 800kHz | GRBW | 4-channel |
| SK6805 | 5V | RGBW | 800kHz | GRBW | 4-channel |
| WS2814 | 12V | RGBW | 800kHz | GRBW | 4-channel |
| TM1814 | 12V | RGBW | 800kHz | GRBW | 4-channel |
| GS8208 | 12V | RGB | 800kHz | GRB | High voltage |
| Protocol | Voltage | Colors | Max Speed | Features |
|---|---|---|---|---|
| APA102 | 5V | RGB + Brightness | 20 MHz | Per-LED brightness |
| SK9822 | 5V | RGB + Brightness | 15 MHz | APA102 clone |
| WS2801 | 5V | RGB | 25 MHz | Simple SPI |
| LPD8806 | 5V | RGB (7-bit) | 20 MHz | Legacy |
RP2040/ESP32 LED Strip
─────────────────────────────
GPIO Pin ────────► DIN
GND ────────► GND
5V/12V ────────► VCC
Important: A 3.3V-to-5V level shifter is highly recommended for reliable operation. While some setups work without it (especially with short wires <30cm), LED strips expect 5V logic levels. The RP2040/RP2350/ESP32 output 3.3V, which may cause:
- Random flickering or glitches
- First LED showing wrong colors
- Unreliable data transmission with longer strips
RP2040/ESP32 LED Strip
─────────────────────────────
SPI MOSI ────────► DI/SDI
SPI SCK ────────► CI/CLK
GND ────────► GND
5V ────────► VCC
Important: A 3.3V-to-5V level shifter is highly recommended for both MOSI and SCK lines. APA102/WS2801 strips expect 5V logic levels for reliable high-speed SPI communication.
-
Current Draw:
- WS2812B: ~60mA per LED at full white
- SK6812 RGBW: ~80mA per LED at full white
- APA102: ~60mA per LED at full brightness
-
Power Supply:
- Use external 5V power supply for >10 LEDs
- Calculate total current: (LED count × 60mA) + 20% safety margin
- Example: 100 LEDs × 60mA = 6A minimum power supply
-
Wiring Best Practices:
- Add 1000µF capacitor between VCC and GND near LED strip
- Use 470Ω resistor on data line for signal protection
- Keep data wire short (<30cm) or use level shifter
- Use proper wire gauge for power (18-22 AWG for <3A, 16 AWG for >3A)
-
Level Shifter:
- Highly recommended for 3.3V microcontrollers (RP2040/RP2350/ESP32)
- Prevents flickering, glitches, and unreliable operation
- Required for long cable runs (>30cm)
- See wiring section above for recommended ICs and tutorials
All commands start with neo prefix. Use neo help to see available commands.
neo # Show module info and strip count
neo list # List all strips, virtual strips, and segments
neo info # Detailed system information
neo perf # Performance statistics (FPS, CPU usage)neo update # Manual update (send buffer to LEDs)
neo clear # Clear all LEDs (turn off)
neo speed <ms> # Set update interval (e.g., 'neo speed 50' = 20 FPS)
neo auto <on|off> # Enable/disable auto-update modeUpdate Speed Presets:
neo speed slow # 10 FPS (100ms)
neo speed normal # 20 FPS (50ms) - default
neo speed fast # 30 FPS (33ms)
neo speed max # 50 FPS (20ms)
neo speed extreme # 80 FPS (12ms)
neo speed ludicrous # 120 FPS (4ms)neo phys add <pin> <count> [protocol]
# Add physical strip
# Examples:
neo phys add 9 64 # GPIO 9, 64 LEDs, WS2812B (default)
neo phys add 22 100 SK6812 # GPIO 22, 100 LEDs, SK6812 RGBW
neo phys add 5 50 APA102 # GPIO 5, 50 LEDs, APA102 (SPI)
neo phys del <index>
# Delete physical strip by index
neo phys del 0
neo phys list
# List all physical strips with detailsneo virt add <count> [colorOrder]
# Create virtual strip
# Examples:
neo virt add 72 # 72 LEDs, GRB (default)
neo virt add 100 RGB # 100 LEDs, RGB order
neo virt add 150 RGBW # 150 LEDs, RGBW (4-channel)
neo virt del <index>
# Delete virtual strip
neo virt del 0
neo virt attach <virtIndex> <physIndex> [offset]
# Attach physical strip to virtual strip
# Offset is auto-calculated if omitted
neo virt attach 0 0 # Auto offset (0)
neo virt attach 0 1 # Auto offset (after strip 0)
neo virt attach 0 2 50 # Manual offset at LED 50
neo virt detach <virtIndex> <physIndex>
# Detach physical strip from virtual strip
neo virt detach 0 1
neo virt list
# List all virtual strips with attachmentsneo seg add <virtIndex> <startLed> <endLed>
# Create segment in virtual strip
# Examples:
neo seg add 0 0 35 # Segment 0: LEDs 0-35
neo seg add 0 36 71 # Segment 1: LEDs 36-71
neo seg add 0 10 20 # Segment 2: LEDs 10-20 (overlap OK)
neo seg del <index>
# Delete segment
neo seg del 0
neo seg list
# List all segments with detailsneo effects
# List all available effects with IDs and parameter counts
neo effect <segIndex> <effectId>
# Assign effect to segment (with default parameters)
# Effect IDs:
# 0 = Solid Color
# 1 = Wipe (Direction)
# 2 = Rainbow (Speed, Delta)
# 3 = Pride2015 (no parameters)
# 4 = Confetti (FadeSpeed, Saturation)
# 5 = Juggle (NumDots, FadeSpeed)
# 6 = BPM (BPM, Hue)
# 7 = Cylon (Speed, Hue, EyeSize, FadeAmount)
# │ 8 = RGBW Test ( Rotating RGBW test pattern)
# 9 = GarageDoor (special effect)
# Examples:
neo effect 0 0 # Solid color on Segment 0
neo effect 1 2 # Rainbow on Segment 1
neo effect 2 7 # Cylon on Segment 2
neo effect config <segIndex>
# Show all effect parameters for segment
# Displays parameter names, types, and current values
neo effect config 0 # Show parameters for Segment 0
neo effect config <segIndex> get <paramIndex>
# Get specific parameter value
neo effect config 0 get 0 # Get parameter 0 (e.g., Speed)
neo effect config 1 get 1 # Get parameter 1 (e.g., Hue)
neo effect config <segIndex> set <paramIndex> <value>
# Set specific parameter value
# Examples:
neo effect config 0 set 0 150 # Set Speed to 150
neo effect config 1 set 1 128 # Set Hue to 128
neo effect config 2 set 2 8 # Set EyeSize to 8
neo garage <segIndex> <phase>
# Control GarageDoor effect (ID 8)
# Phases: 0=OPENING, 1=RUNWAY, 2=COMPLETED, 3=STOPPED
neo garage 0 0 # Start opening animation
neo garage 0 1 # Runway lights
neo garage 0 2 # Completed (green)
neo garage 0 3 # Stop/pause
neo color <segIndex> <r> <g> <b> [w]
# Set primary color for segment
neo color 0 255 0 0 # Red
neo color 1 0 255 0 # Green
neo color 2 0 0 255 128 # Blue + 50% white (RGBW)
neo brightness <segIndex> <value>
# Set software brightness (0-255, all LED types)
neo brightness 0 128 # 50% brightness
neo brightness 1 255 # 100% brightness
neo hwbrightness <segIndex> <value>
# Set hardware brightness (0-255, APA102/SK9822 only)
# Does not reduce color depth like software brightness
neo hwbrightness 0 200 # 78% hardware brightnessAvailable when compiled with -DOPENKNX_NEOPIXEL_TESTS:
neo test anim start # Start animation test
neo test anim stop # Stop animation test
neo test simple start # Start simple test (color cycle)
neo test simple stop # Stop simple testAvailable when compiled with -DOPENKNX_NEOPIXEL_BENCHMARK:
neo benchmark led <count> # Benchmark LED count scaling
neo benchmark update # Benchmark update performance
neo benchmark driver # Test different driver types
neo benchmark effect # Compare effect performanceclass NeoPixel : public OpenKNX::Module {
public:
// Strip Management
PhysicalStrip* addStrip(uint8_t pin, uint16_t ledCount,
LedProtocol protocol = LedProtocol::WS2812B);
VirtualStrip* addVirtualStrip(uint16_t ledCount,
ColorOrder order = ColorOrder::GRB);
Segment* addSegment(uint8_t virtStripIndex,
uint16_t startLed, uint16_t endLed);
// Update Control
void updateAll();
void clearAll();
void setAutoUpdate(bool enable);
void setUpdateSpeed(UpdateSpeed speed);
void setUpdateInterval(uint32_t intervalMs);
// Access
NeoPixelManager* getManager();
};
extern NeoPixel neoPixelModule; // Global instanceclass NeoPixelManager {
public:
// Physical Strip Management
PhysicalStrip* addPhysicalStrip(uint8_t pin, uint16_t ledCount,
LedProtocol protocol);
bool removePhysicalStrip(uint8_t index);
PhysicalStrip* getPhysicalStrip(uint8_t index);
uint8_t getPhysicalStripCount() const;
// Virtual Strip Management
VirtualStrip* addVirtualStrip(uint16_t ledCount, ColorOrder order);
bool removeVirtualStrip(uint8_t index);
VirtualStrip* getVirtualStrip(uint8_t index);
uint8_t getVirtualStripCount() const;
// Segment Management
Segment* addSegment(uint8_t virtStripIndex,
uint16_t startLed, uint16_t endLed);
bool removeSegment(uint8_t index);
Segment* getSegment(uint8_t index);
uint8_t getSegmentCount() const;
// Update Control
void updateAll();
void updateSegments(uint32_t deltaTime);
void clearAll();
// Performance
ManagerStats getStats() const;
};class PhysicalStrip {
public:
// Construction
PhysicalStrip(uint8_t pin, uint16_t ledCount, LedProtocol protocol);
// Pixel Control
void setPixel(uint16_t index, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0);
void setPixel(uint16_t index, uint32_t color);
void fill(uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0);
void clear();
// Display Control
void show();
void setBrightness(uint8_t brightness);
// Properties
uint16_t getLedCount() const;
LedProtocol getProtocol() const;
uint8_t getPin() const;
uint8_t getBytesPerLed() const;
// Buffer Access
uint8_t* getBuffer();
const uint8_t* getBuffer() const;
// Driver
IHardwareDriver* getDriver() const;
};class VirtualStrip {
public:
// Construction
VirtualStrip(uint16_t ledCount, ColorOrder order = ColorOrder::GRB);
// Physical Strip Management
bool attachPhysical(PhysicalStrip* physical, uint16_t offset = UINT16_MAX);
bool detachPhysical(PhysicalStrip* physical);
// Pixel API
void setPixel(uint16_t index, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0);
void fill(uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0);
void clear();
// Buffer Access
uint8_t* getBuffer();
// Sync & Transfer
void mapToPhysical();
};class Segment {
public:
// Construction
Segment(VirtualStrip* strip, uint16_t startLed, uint16_t endLed);
// Properties
uint16_t getStartLed() const;
uint16_t getEndLed() const;
uint16_t getLedCount() const;
VirtualStrip* getVirtualStrip() const;
// Effect Management
void setEffect(uint8_t effectId);
void setEffect(Effect* effect);
Effect* getEffect() const;
// State Machine
void update(uint32_t deltaTime);
void pause();
void resume();
void stop();
bool isRunning() const;
// Configuration
void setColor(uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0);
void setBrightness(uint8_t brightness);
LedConfig& getConfig();
LedState& getState(); // Access state (for effects)
};// Base Effect Interface
class Effect {
public:
virtual void update(Segment* segment, uint32_t deltaTime) = 0;
virtual void reset() {}
virtual const char* getName();
virtual bool isDone(const Segment* segment) const;
};
// Built-in Effects (in EffectPool namespace)
namespace EffectPool {
extern EffectSolid solidEffect; // ID: 0
extern EffectRainbow rainbowEffect; // ID: 1
extern EffectPride pridEffect; // ID: 2
extern EffectConfetti confettiEffect;// ID: 3
extern EffectJuggle juggleEffect; // ID: 4
extern EffectBPM bpmEffect; // ID: 5
extern EffectCylon cylonEffect; // ID: 6
extern EffectWipe wipeEffect; // ID: 7
Effect* getEffect(uint8_t id);
}#include "OpenKNX.h"
#include "NeoPixel.h"
void setup() {
openknx.init(0);
openknx.addModule(13, neoPixelModule);
openknx.setup();
// Create physical strip
auto strip = neoPixelModule.addStrip(9, 64, LedProtocol::WS2812B);
// Create virtual strip and attach physical
auto virt = neoPixelModule.addVirtualStrip(64);
virt->attachPhysical(strip);
// Create segment covering all LEDs
auto segment = neoPixelModule.addSegment(0, 0, 63);
segment->setEffect(1); // Rainbow effect
segment->setBrightness(128);
// Enable auto-update
neoPixelModule.setAutoUpdate(true);
neoPixelModule.setUpdateSpeed(UpdateSpeed::NORMAL);
}
void loop() {
openknx.loop();
}void setup() {
openknx.init(0);
openknx.addModule(13, neoPixelModule);
openknx.setup();
// Create three physical strips
auto strip1 = neoPixelModule.addStrip(9, 100, LedProtocol::WS2812B);
auto strip2 = neoPixelModule.addStrip(10, 64, LedProtocol::WS2812B);
auto strip3 = neoPixelModule.addStrip(11, 8, LedProtocol::SK6812); // RGBW
// Create virtual strip (total 172 LEDs)
auto virt = neoPixelModule.addVirtualStrip(172);
// Attach physical strips (auto offset calculation)
virt->attachPhysical(strip1); // Offset: 0
virt->attachPhysical(strip2); // Offset: 100
virt->attachPhysical(strip3); // Offset: 164
// Create segments with different effects
auto seg1 = neoPixelModule.addSegment(0, 0, 99); // Strip 1
auto seg2 = neoPixelModule.addSegment(0, 100, 163); // Strip 2
auto seg3 = neoPixelModule.addSegment(0, 164, 171); // Strip 3
seg1->setEffect(1); // Rainbow
seg2->setEffect(6); // Cylon
seg3->setEffect(0); // Solid white
seg3->setColor(0, 0, 0, 255); // Pure white on W channel
// Enable auto-update
neoPixelModule.setAutoUpdate(true);
neoPixelModule.setUpdateSpeed(UpdateSpeed::FAST);
}uint32_t lastChange = 0;
uint8_t currentEffect = 0;
void loop() {
openknx.loop();
// Change effect every 10 seconds
if (millis() - lastChange > 10000) {
auto segment = neoPixelModule.getManager()->getSegment(0);
if (segment) {
currentEffect = (currentEffect + 1) % 8; // Cycle through 8 effects
segment->setEffect(currentEffect);
// Random brightness
uint8_t brightness = random(64, 255);
segment->setBrightness(brightness);
logInfoP("Changed to effect %d, brightness %d", currentEffect, brightness);
}
lastChange = millis();
}
}void setup() {
openknx.init(0);
openknx.addModule(13, neoPixelModule);
openknx.setup();
// SK6812 RGBW strip
auto strip = neoPixelModule.addStrip(9, 30, LedProtocol::SK6812);
auto virt = neoPixelModule.addVirtualStrip(30, ColorOrder::GRBW);
virt->attachPhysical(strip);
// Create three segments
auto seg1 = neoPixelModule.addSegment(0, 0, 9); // First 10 LEDs
auto seg2 = neoPixelModule.addSegment(0, 10, 19); // Middle 10 LEDs
auto seg3 = neoPixelModule.addSegment(0, 20, 29); // Last 10 LEDs
// Segment 1: Pure RGB color (no white)
seg1->setEffect(0); // Solid
seg1->setColor(255, 0, 0, 0); // Red
// Segment 2: RGB + White
seg2->setEffect(0);
seg2->setColor(0, 255, 0, 128); // Green + 50% white
// Segment 3: Pure white
seg3->setEffect(0);
seg3->setColor(0, 0, 0, 255); // White channel only
// Update once
neoPixelModule.updateAll();
}uint32_t lastPrint = 0;
void loop() {
openknx.loop();
// Print performance stats every 5 seconds
if (millis() - lastPrint > 5000) {
auto stats = neoPixelModule.getManager()->getStats();
logInfoP("Performance:");
logInfoP(" FPS: %.1f", stats.fps);
logInfoP(" Update time: %lu µs", stats.avgUpdateTime);
logInfoP(" CPU: %.2f%%", stats.cpuPercent);
logInfoP(" Strips: %d physical, %d virtual",
stats.physicalStripCount, stats.virtualStripCount);
logInfoP(" Segments: %d", stats.segmentCount);
lastPrint = millis();
}
}| Operation | Time | Notes |
|---|---|---|
| Update 64 LEDs (WS2812B) | ~22 µs | Effect calculation |
| DMA Transfer (64 LEDs) | ~2 µs | Non-blocking |
| Total CPU @ 30 FPS | 0.07% | Hardware accelerated |
Component RAM Usage
──────────────────────────────────────────
NeoPixelManager ~200 bytes
PhysicalStrip (per strip) ~150 bytes
+ LED buffer (64 LEDs RGB) 192 bytes
VirtualStrip (72 LEDs) ~70 bytes
+ LED buffer 216 bytes
Segment (per segment) ~180 bytes
Effect instances (shared) ~200 bytes total
──────────────────────────────────────────
Example Config (2 strips, 1 virtual, 2 segments)
Total RAM ~1.4 KB
A: Check color order setting. Try different ColorOrder values (RGB, GRB, BGR, RGBW, GRBW).
auto virt = neoPixelModule.addVirtualStrip(64, ColorOrder::RGB); // Try different ordersA:
- Add 470Ω resistor on data line
- Add 1000µF capacitor between VCC and GND
- Keep data wire short (<30cm)
- Ensure proper power supply
A: Common with WS2812B. Add a dummy LED at the start or use a level shifter.
A:
- Reduce update interval:
neo speed slow - Reduce LED count or segment count
- Use simpler effects
- Check
neo perffor diagnostics
A:
- Check if auto-update is enabled:
neo auto on - Call
neoPixelModule.updateAll()manually - Verify virtual strip attachments:
neo virt list - Check segment configuration:
neo seg list
A: Reduce resource limits in platformio.ini:
build_flags =
-DNEOPIXEL_MAX_PHYSICAL_STRIPS=4
-DNEOPIXEL_MAX_VIRTUAL_STRIPS=2
-DNEOPIXEL_MAX_SEGMENTS=8A: Not yet implemented. Planned for future release via GroupObjects.
A: Enable power management to limit current draw:
mgr->setMaxCurrent(3000); // Limit to 3A
mgr->getPowerManager()->setLedProfile(LedProfiles::WS2812B);See the Power Management section below for details.
IMPORTANT: High-power LED installations can exceed your power supply's capacity, causing:
- Voltage drops (LEDs flicker or change color)
- Overheating power supplies
- Damaged hardware or fire hazards
- Unexpected behavior (brownouts, resets)
OFM-NeoPixel includes software-based current limiting to prevent these issues.
Each LED color channel draws current proportional to its brightness:
Current(channel) = MaxCurrent(channel) × (Brightness / 255)
Current(LED) = Current(R) + Current(G) + Current(B) + Current(W)
Example: WS2812B at full white (R=255, G=255, B=255)
Current = (20mA × 255/255) + (20mA × 255/255) + (20mA × 255/255)
= 20mA + 20mA + 20mA
= 60mA per LED
For 100 LEDs at full brightness: 100 × 60mA = 6000mA (6A)
When total current exceeds the configured limit, all pixel values are proportionally reduced:
Scale = MaxCurrent / CalculatedCurrent
ScaledBrightness = OriginalBrightness × Scale
Example: 100 LEDs drawing 6A with 5A limit
Scale = 5000mA / 6000mA = 0.833 (83.3%)
All pixel values multiplied by 0.833
Result: Max current = 5A ✓
┌─────────────────┐
│ Effect Update │ -> Calculates ideal pixel colors
└────────┬────────┘
↓
┌─────────────────┐
│ Segment Buffer │ -> Stores RGB/RGBW values (0-255)
└────────┬────────┘
↓
┌─────────────────┐
│ PowerManager │ -> Calculates total current
│ ├─ Calculate │ Sum all pixel currents
│ ├─ Compare │ vs. MaxCurrent limit
│ └─ Scale │ Reduce if exceeded
└────────┬────────┘
↓
┌─────────────────┐
│ Physical Strip │ -> Send scaled values to LEDs
│ show() │
└─────────────────┘
Different LED types have different current consumption:
| LED Type | R (mA) | G (mA) | B (mA) | W (mA) | Total @ White |
|---|---|---|---|---|---|
| WS2812B | 20 | 20 | 20 | - | 60mA |
| SK6812 RGBW | 20 | 20 | 20 | 20 | 80mA |
| APA102 | 15 | 15 | 15 | - | 45mA |
Predefined Profiles:
LedProfiles::WS2812B // 20mA per channel
LedProfiles::SK6812_RGBW // 20mA per channel + W
LedProfiles::APA102 // 15mA per channel
LedProfiles::CONSERVATIVE // 20mA all channels (safe default)Set at manager level - applies to ALL strips:
void setup() {
NeoPixelManager* mgr = neoPixelModule.getManager();
// Set 5A maximum (5V × 5A = 25W max)
mgr->setMaxCurrent(5000);
// Choose LED profile
mgr->getPowerManager()->setLedProfile(LedProfiles::WS2812B);
// Enable/disable at runtime
mgr->setPowerManagementEnabled(true);
}For non-standard LEDs with different current draws:
LedCurrentProfile customProfile(18, 22, 18, 0); // R, G, B, W in mA
mgr->getPowerManager()->setLedProfile(customProfile);For fine-grained control over individual strips:
PowerManager pm(3000); // 3A limit
pm.setLedProfile(LedProfiles::SK6812_RGBW);
// Before show()
pm.applyCurrentLimit(strip->getBuffer(), strip->getLedCount(), 4); // 4 = RGBW
strip->show();void loop() {
// Get estimated power consumption
float watts = mgr->getTotalPowerWatts();
// Calculate current at 5V
float amps = watts / 5.0f;
Serial.printf("Power: %.2fW (%.2fA @ 5V)\n", watts, amps);
}neo power status # Show current consumption
neo power limit 3000 # Set 3A limit
neo power profile ws2812b # Set LED profile
neo power on/off # Enable/disable limiting// 100 WS2812B LEDs
// Max: 100 × 60mA = 6A
// Available: 5A USB power supply
mgr->setMaxCurrent(4500); // Leave 10% safety margin (5A × 0.9)
mgr->getPowerManager()->setLedProfile(LedProfiles::WS2812B);
// Auto-scales: Full white → 75% brightness// 500 SK6812 RGBW LEDs
// Max: 500 × 80mA = 40A
// Available: 15A @ 5V power supply
mgr->setMaxCurrent(14000); // 15A - 1A safety margin
mgr->getPowerManager()->setLedProfile(LedProfiles::SK6812_RGBW);
// Auto-scales: Full white → 35% brightness// Split into two independent managers
NeoPixelManager mgr1, mgr2;
// Power supply 1: 5A
mgr1.setMaxCurrent(5000);
auto strip1 = mgr1.addStrip(22, 100, WS2812B);
// Power supply 2: 3A
mgr2.setMaxCurrent(3000);
auto strip2 = mgr2.addStrip(23, 50, WS2812B);Segment Brightness vs. Power Limiting:
The library applies brightness in this order:
- Segment Brightness (
segment->setBrightness(128)) - Applied when Effect writes pixels - Power Limiting - Applied globally in
NeoPixelManager::updateAll()
Example:
segment->setBrightness(128); // 50% brightness
segment->setColor(255, 255, 255); // White
// Actual values in buffer: (127, 127, 127)
// Power calculation sees: 127 × 3 × 20mA = 7.6mA per LED (not 60mA!)Result: Power consumption shown in neo power reflects actual current after segment brightness is applied.
Why this is correct:
- Segment brightness is user intent ("I want 50% brightness")
- User wants reduced power consumption in this case
- Power limiting protects against unintentional overload
- Shown power values = real hardware consumption ✓
Note: Hardware brightness (APA102/SK9822) is separately tracked and included in power calculation.
Software limiting does NOT replace proper hardware design!
You MUST still:
- Use adequate power supply (calculate: LEDs × 60mA minimum)
- Use proper gauge wiring (5V: 18AWG for <2m, 16AWG for >2m)
- Inject power every 50-100 LEDs for long strips
- Add 1000µF capacitor near LED strip power input
- Add 470Ω resistor on data line
- Use fuse/circuit breaker for safety
Software limiting:
- Prevents overload from code/effects
- Protects against unexpected full-brightness scenarios
- Allows headroom for animations/transitions
- Does NOT protect against hardware shorts
- Does NOT compensate for inadequate wiring
- Does NOT eliminate need for proper PSU sizing
Current calculation overhead:
- Per pixel: ~5-10 CPU cycles
- 100 LEDs: ~0.05ms on RP2040 @ 133MHz
- 500 LEDs: ~0.25ms on RP2040 @ 133MHz
Recommendation: Enable power management always - the safety benefit far outweighs the minimal performance cost.
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
- Follow existing code style
- Add documentation for new features
- Test on target hardware (RP2040/RP2350/ESP32)
- Update relevant documentation files
GNU General Public License v3.0
See LICENSE file for details.
Author: Erkan Çolak
Project: OpenKNX
Repository: https://github.com/OpenKNX/OFM-NeoPixel
Special thanks to the OpenKNX community and contributors.