Skip to content

OpenKNX/OFM-NeoPixel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OFM-NeoPixel

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.


Table of Contents


Overview

OFM-NeoPixel provides a three-layer architecture for managing addressable LED strips:

  1. PhysicalStrip - Hardware abstraction for individual LED strips
  2. VirtualStrip - Logical composition of multiple physical strips
  3. 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).

What Makes This Library Different?

  • 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)

Key Features

Effect System (NEW)

  • 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

Hardware Layer

  • 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

Software Layer

  • 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

Integration

  • OpenKNX module interface
  • Console command system for configuration and testing
  • GroupObject support (planned)
  • Real-time performance monitoring

Quick Start

Basic Example

#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();
}

Multi-Strip Example with Effects

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();
}

Console Configuration Example

# 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

Architecture

System Overview

┌─────────────────────────────────────────────────────────┐
│                    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

Data Flow with Global Power Management

┌──────────────────────────────────────────────────────────┐
│ 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                     │
└──────────────────────────────────────────────────────────┘

Memory Layout

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

ColorOrder Architecture

Design Philosophy: Unified RGB interface with per-strip hardware adaptation.

Overview

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]
└──────────────┘

Supported ColorOrders

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]

Data Flow Example

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

Key Design Principles

  1. VirtualStrip is ColorOrder-agnostic

    • Always stores RGB/RGBW internally
    • No color conversion in VirtualStrip layer
    • Simplifies effect development
  2. PhysicalStrip handles hardware differences

    • Converts RGB → hardware ColorOrder
    • Each PhysicalStrip can have different ColorOrder
    • Conversion happens once per frame
  3. Hardware drivers are byte-oriented

    • Receive already-converted bytes
    • No color logic in hardware layer
    • Protocol-specific formatting only (APA102 brightness, etc.)

Mixed ColorOrders in ONE VirtualStrip

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!

Setting ColorOrder

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=BGR

VirtualStrip 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 internally

Troubleshooting ColorOrder

Problem: Wrong colors (e.g., RED shows as GREEN)

Solution:

  1. Check hardware datasheet - Verify actual ColorOrder

    • WS2812B: Usually GRB (but some clones are RGB!)
    • APA102: Usually BGR
    • SK6812: Usually GRB or GRBW
  2. 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
  3. 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);

Performance Impact

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)

Installation

PlatformIO

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=1

Resource Limits:

The library includes configurable limits to prevent memory exhaustion:

  • NEOPIXEL_MAX_PHYSICAL_STRIPS (default: 6) - Maximum physical LED strips
  • NEOPIXEL_MAX_VIRTUAL_STRIPS (default: 12) - Maximum virtual strips
  • NEOPIXEL_MAX_SEGMENTS (default: 16) - Maximum segments
  • NEOPIXEL_ENFORCE_LIMITS (default: 1) - Enable/disable limit enforcement

These limits are used for vector pre-allocation and optional runtime enforcement.

Arduino IDE

  1. Download the repository as ZIP
  2. Sketch -> Include Library -> Add .ZIP Library
  3. Select the downloaded ZIP file

OpenKNX Project

  1. Clone into your OpenKNX project's lib directory:
cd lib
git clone https://github.com/OpenKNX/OFM-NeoPixel.git
  1. Add to your main.cpp:
#ifdef NEOPIXEL_MODULE
    #include "NeoPixel.h"
#endif

void setup() {
    // ...
    #ifdef NEOPIXEL_MODULE
        openknx.addModule(13, neoPixelModule);
    #endif
    // ...
}
  1. Define in platformio.ini:
build_flags =
    -DNEOPIXEL_MODULE
    ; Optional: Enable tests and benchmarks
    ; -DOPENKNX_NEOPIXEL_TESTS
    ; -DOPENKNX_NEOPIXEL_BENCHMARK

Hardware Support

Supported Microcontrollers

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

Supported LED Protocols

1-Wire Protocols (Single Data Line)

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

SPI Protocols (Clock + Data Lines)

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

Wiring

1-Wire (WS2812B, SK6812, etc.)

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

SPI (APA102, WS2801)

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.

Power Considerations

  • 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

Console Commands

Overview

All commands start with neo prefix. Use neo help to see available commands.

Core Commands

Information

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)

Update Control

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 mode

Update 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)

Physical Strip Commands

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 details

Virtual Strip Commands

neo 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 attachments

Segment Commands

neo 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 details

Effect Commands

neo 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 brightness

Testing Commands

Available 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 test

Benchmark Commands

Available 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 performance

API Reference

NeoPixel Module

class 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 instance

NeoPixelManager

class 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;
};

PhysicalStrip

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;
};

VirtualStrip

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();
};

Segment

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)
};

Effect System

// 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);
}

Examples

Example 1: Single Strip with Rainbow Effect

#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();
}

Example 2: Multi-Strip Composition

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);
}

Example 3: Dynamic Control

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();
    }
}

Example 4: RGBW Strip with White Channel

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();
}

Example 5: Performance Monitoring

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();
    }
}

Performance

Benchmarks (RP2040 @ 133 MHz)

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

Memory Usage

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

Troubleshooting

Q: LEDs show wrong colors

A: Check color order setting. Try different ColorOrder values (RGB, GRB, BGR, RGBW, GRBW).

auto virt = neoPixelModule.addVirtualStrip(64, ColorOrder::RGB);  // Try different orders

Q: LEDs flicker or show random colors

A:

  1. Add 470Ω resistor on data line
  2. Add 1000µF capacitor between VCC and GND
  3. Keep data wire short (<30cm)
  4. Ensure proper power supply

Q: First LED always wrong color

A: Common with WS2812B. Add a dummy LED at the start or use a level shifter.

Q: Performance issues / low FPS

A:

  1. Reduce update interval: neo speed slow
  2. Reduce LED count or segment count
  3. Use simpler effects
  4. Check neo perf for diagnostics

Q: Strip not updating

A:

  1. Check if auto-update is enabled: neo auto on
  2. Call neoPixelModule.updateAll() manually
  3. Verify virtual strip attachments: neo virt list
  4. Check segment configuration: neo seg list

Q: Out of memory errors

A: Reduce resource limits in platformio.ini:

build_flags =
    -DNEOPIXEL_MAX_PHYSICAL_STRIPS=4
    -DNEOPIXEL_MAX_VIRTUAL_STRIPS=2
    -DNEOPIXEL_MAX_SEGMENTS=8

Q: How do I control LEDs from KNX?

A: Not yet implemented. Planned for future release via GroupObjects.

Q: My power supply is getting hot / voltage drops

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.


Power Management & Current Limiting

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.

How It Works

1. Current Calculation

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)

2. Automatic Brightness Scaling

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 ✓

3. Architecture Flow

┌─────────────────┐
│  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()      │
└─────────────────┘

LED Current Profiles

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)

Configuration

Global Power Limit (Recommended)

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);
}

Custom LED Profile

For non-standard LEDs with different current draws:

LedCurrentProfile customProfile(18, 22, 18, 0);  // R, G, B, W in mA
mgr->getPowerManager()->setLedProfile(customProfile);

Per-Strip Manual Limiting (Advanced)

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();

Monitoring Power Consumption

Real-Time Current/Power Display

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);
}

Via Console

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

Practical Examples

Example 1: Small Installation (100 LEDs)

// 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

Example 2: Large Installation (500 LEDs)

// 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

Example 3: Multiple Power Supplies

// 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);

Brightness Interaction (Important!)

Segment Brightness vs. Power Limiting:

The library applies brightness in this order:

  1. Segment Brightness (segment->setBrightness(128)) - Applied when Effect writes pixels
  2. 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.

Important Notes

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

Performance Impact

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.


Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Submit a pull request

Development Guidelines

  • Follow existing code style
  • Add documentation for new features
  • Test on target hardware (RP2040/RP2350/ESP32)
  • Update relevant documentation files

License

GNU General Public License v3.0

See LICENSE file for details.


Credits

Author: Erkan Çolak
Project: OpenKNX
Repository: https://github.com/OpenKNX/OFM-NeoPixel

Special thanks to the OpenKNX community and contributors.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •