Skip to content

Effects Architecture

This document provides a comprehensive overview of the Light Effects Framework architecture, including design decisions, implementation details, and lifecycle management.

Table of Contents

High-Level Overview

The Light Effects Framework is built on a layered architecture that separates concerns and provides a clean abstraction for effect management:

┌─────────────────────────────────────────────────────┐
│              Application Layer                       │
│  (User code, business logic, effect selection)      │
└────────────────┬────────────────────────────────────┘
                 │
┌────────────────▼────────────────────────────────────┐
│              Effects API Layer                       │
│   • Conductor (orchestration + dynamic lights)      │
│   • EffectPulse (sends waveforms via Device)        │
│   • FrameEffect → Colorloop, Rainbow, Flicker,     │
│     Aurora, Progress, Sunrise/Sunset (frame-based)  │
│   • EffectRegistry (discovery + filtering)          │
│   • LIFXEffect (base class)                         │
└──────┬─────────────────────────────┬────────────────┘
       │ (EffectPulse)               │ (FrameEffect)
┌──────▼──────────────────┐  ┌───────▼───────────────────┐
│  Device Layer            │  │  Animation Layer           │
│  (lifx.devices)          │  │  (lifx.animation)          │
│  • set_waveform()        │  │  • Animator (direct UDP)   │
│  • set_color()           │  │  • PacketGenerator         │
└──────┬──────────────────┘  │  • FrameBuffer              │
       │                      └───────┬───────────────────┘
       │                              │
┌──────▼──────────────────────────────▼───────────────┐
│           Network / Protocol Layers                  │
│   • UDP transport, binary serialization             │
└─────────────────────────────────────────────────────┘

Key Principles

  1. Zero Dependencies: Uses only Python stdlib and existing lifx-async components
  2. State Preservation: Automatically captures and restores device state
  3. Type Safety: Full type hints with strict Pyright validation
  4. Async/Await: Native asyncio for concurrent operations
  5. Extensibility: Abstract base class for custom effects

Component Architecture

Module Structure

src/lifx/effects/
├── __init__.py              # Public API exports
├── base.py                  # LIFXEffect abstract base class
├── frame_effect.py          # FrameEffect base class + FrameContext
├── conductor.py             # Conductor orchestrator
├── pulse.py                 # EffectPulse implementation
├── colorloop.py             # EffectColorloop (FrameEffect) implementation
├── rainbow.py               # EffectRainbow (FrameEffect) implementation
├── flicker.py               # EffectFlicker (FrameEffect) implementation
├── aurora.py                # EffectAurora (FrameEffect) implementation
├── progress.py              # EffectProgress (FrameEffect, multizone only)
├── sunrise.py               # EffectSunrise + EffectSunset (FrameEffect, matrix only)
├── registry.py              # EffectRegistry, DeviceType, DeviceSupport, EffectInfo
├── models.py                # PreState, RunningEffect dataclasses
├── state_manager.py         # DeviceStateManager for state capture/restore
└── const.py                 # Effect constants

Component Responsibilities

Conductor (conductor.py)

Purpose: Central orchestrator managing effect lifecycle across multiple devices.

Responsibilities:

  • Track running effects per device (serial → RunningEffect mapping)
  • Capture device state before effects
  • Power on devices if needed
  • Execute effects via effect.async_perform()
  • Restore device state after effects complete
  • Handle concurrent effects on different devices
  • Provide thread-safe state management with asyncio.Lock()

Key Data Structures:

class Conductor:
    _running: dict[str, RunningEffect]  # serial → RunningEffect
    _lock: asyncio.Lock                 # Thread-safe access

LIFXEffect Base Class (base.py)

Purpose: Abstract base for all effect implementations.

Responsibilities:

  • Define effect interface (abstract async_play() method)
  • Handle power-on logic in async_perform()
  • Provide startup color via from_poweroff_hsbk()
  • Keep inherit_prestate() for compatibility (superseded; no longer consulted)
  • Store conductor reference and participants

Key Methods:

class LIFXEffect(ABC):
    async def async_perform(participants):  # Setup + call async_play()
        ...

    @abstractmethod
    async def async_play():                 # Effect logic (override)
        ...

    async def from_poweroff_hsbk(light):    # Startup color (override)
        ...

    def inherit_prestate(other):            # Superseded; not consulted
        ...

FrameEffect Base Class (frame_effect.py)

Purpose: Abstract base for effects that generate color frames at a fixed FPS.

Responsibilities:

  • Define frame generation interface (abstract generate_frame() method)
  • Run frame loop with timing control (fps, duration, stop_event)
  • Create FrameContext per device per frame with timing and layout info
  • Convert user-friendly HSBK colors to protocol tuples
  • Dispatch frames via Animators (set by Conductor)
  • Provide async_setup() hook for pre-loop initialization

Key Data:

class FrameEffect(LIFXEffect):
    _fps: float                    # Frames per second
    _duration: float | None        # Run time limit (None = infinite)
    _stop_event: asyncio.Event     # Stop signal
    _animators: list[Animator]     # Set by Conductor before play

Relationship to LIFXEffect: FrameEffect extends LIFXEffect and implements async_play() as a frame loop. Subclasses implement generate_frame() instead.

Effect Implementations

EffectPulse (pulse.py):

  • Extends LIFXEffect directly
  • Sends one waveform to each participant, which the light then runs itself
  • Five modes with different timing and waveforms
  • Intelligent color selection based on mode
  • Auto-completion after configured cycles

EffectColorloop (colorloop.py):

  • Extends FrameEffect (not LIFXEffect directly)
  • Implements continuous hue rotation via frame generation
  • Time-based hue calculation with configurable FPS
  • Randomized direction and saturation
  • Runs indefinitely until stopped
  • Supports state inheritance for seamless transitions
  • Works across all device types (Light, MultiZoneLight, MatrixLight)

EffectRainbow (rainbow.py):

  • Extends FrameEffect
  • Spreads full 360-degree rainbow across all pixels, scrolling over time
  • Configurable period, brightness, saturation, and inter-device spread
  • Best on multizone strips and matrix lights

EffectFlicker (flicker.py):

  • Renamed from EffectFlame, which remains as a deprecated alias, because "Flame" is the name of the firmware effect that matrix lights run themselves
  • Extends FrameEffect
  • Layered sine waves produce organic flicker (no random state)
  • Warm color range: hue 0-40, high saturation, configurable kelvin range
  • Matrix enhancement: vertical brightness falloff (bottom rows hotter)
  • Works on all color device types

EffectAurora (aurora.py):

  • Extends FrameEffect
  • Palette interpolation with shortest-path hue wrapping
  • Sine wave brightness modulation creates flowing "curtain" bands
  • Matrix enhancement: vertical gradient (brightest in middle rows)
  • Configurable palette, speed, brightness, and inter-device spread

EffectProgress (progress.py):

  • Extends FrameEffect
  • Animated progress bar with traveling bright spot
  • Mutable position attribute for real-time updates
  • Supports single color or gradient foreground
  • Multizone only (has_multizone required)

EffectSunrise + EffectSunset (sunrise.py):

  • Both extend FrameEffect
  • Duration-based (finite) — complete automatically
  • Shared _sun_frame() helper: radial model centered at configurable origin
  • Five color phases: night → dawn → golden hour → morning → daylight
  • origin parameter: "bottom" (rectangular tiles) or "center" (Ceiling lights)
  • Sunrise: restore_on_complete=False (stays at daylight)
  • Sunset: optionally powers off lights after last frame
  • Matrix only (has_matrix required)

EffectRegistry (registry.py):

  • Central discovery mechanism for effect compatibility
  • DeviceType enum: LIGHT, MULTIZONE, MATRIX
  • DeviceSupport enum: RECOMMENDED, COMPATIBLE, NOT_SUPPORTED
  • EffectInfo frozen dataclass with name, class, description, support map
  • get_effect_registry() returns lazily-initialized default registry with all software effects

Data Models (models.py)

PreState:

Stores device state before effect:

@dataclass
class PreState:
    power: bool                     # Power state (on/off)
    color: HSBK                     # Current color
    zone_colors: list[HSBK] | None  # Multizone colors (if applicable)
    tile_colors: list[list[HSBK]] | None  # Every tile's colours (matrix lights)
    stored_colors: dict[str, list[HSBK] | None] | None  # Ceiling/Mirror components

RunningEffect:

Associates effect with its pre-state:

@dataclass
class RunningEffect:
    effect: LIFXEffect    # Effect instance
    prestate: PreState    # Captured state

Effect Lifecycle

The effect lifecycle consists of five distinct phases:

1. Initialization

User creates effect instance with desired parameters:

effect = EffectPulse(mode='blink', cycles=5)

What happens:

  • Effect object created with parameters stored
  • No network activity yet
  • No conductor association yet

2. State Capture

Conductor starts effect and captures current device state:

await conductor.start(effect, [light1, light2])

What happens:

For each light:
  1. If the light already runs an effect, on this or any other Conductor,
     take it out of that run with no restore (newest wins); the old run's
     other lights carry on
  2. Check if prestate can be inherited from that effect
  3. If not, capture new prestate:
     a. Get power state (get_power)
     b. Get current color (get_color)
     c. Get zone colors if multizone (get_color_zones or get_extended_color_zones)
  4. Store in RunningEffect and register in conductor._running

Timing: <1 second per device (mostly network I/O)

Special Cases:

  • Prestate Inheritance: A participant taken over from a running effect always reuses the replaced run's original PreState, so a later stop restores what was there before any effect
  • Multizone Devices: Uses extended messages if supported, falls back to standard messages
  • Powered-off Devices: All state is still captured (including zone colors that may be inaccurate)

3. Animator Creation (FrameEffect Only)

For FrameEffect subclasses, the Conductor creates Animators before the frame loop:

if isinstance(effect, FrameEffect):
    animators = await self._create_animators(effect, participants)
    effect._animators = animators
    await effect.async_setup(participants)

What happens:

For each participant:
  1. Borrow the light's own Animator: await light.animator.prepare()
     (the first prepare queries tile info or zone count; later ones do not)
  2. Draw through a writer on that Animator with the effect's own canvas
     and duration_ms = int(1500 / effect.fps), so transitions overlap:
     - MirrorLight → one 25x1 ring that wraps, scattered to both rings
     - every other light → the device's own canvas
  3. Call effect.async_setup(participants) for pre-loop initialization

Note: Animators use direct UDP, so no device connection is needed after preparation. Each light owns one Animator, so an effect and a direct frame sender such as LedFx share one writer and one ack gate. Stopping an effect releases its writers; the light's Animator stays open.

4. Power-On (Optional)

If effect.power_on == True, devices are powered on:

async def async_perform(self, participants):
    if self.power_on:
        for light in self.participants:
            power_level = await light.get_power()
            if power_level == 0:
                if not isinstance(light, MatrixLight):
                    startup_color = await self.from_poweroff_hsbk(light)
                    await light.set_color(startup_color, duration=0)
                await light.set_power(True, duration=0.3)

What happens:

For each powered-off light:
  1. Matrix lights (including Ceiling and Mirror) skip to step 3: no colour
     is written, so neither light component's stored colours change
  2. Get startup colour from from_poweroff_hsbk() and set it (duration=0)
  3. Power on with 0.3s fade (duration=0.3)

Timing: 0.3 seconds per powered-off device

5. Effect Execution

Effect logic runs via async_play():

await effect.async_play()

What happens:

  • LIFXEffect subclasses: Custom effect logic using device methods (e.g., set_waveform())
  • FrameEffect subclasses: Frame loop calling generate_frame() per device per frame, dispatching via Animators

Timing:

  • EffectPulse: period * cycles seconds
  • EffectColorloop: Runs indefinitely at calculated FPS

6. State Restoration & Cleanup

Conductor restores devices to pre-effect state:

await conductor.stop([light1, light2])

What happens:

For each light:
  1. Restore multizone colors (if applicable):
     - Use extended messages if supported
     - Use standard messages with apply=NO_APPLY, then apply=APPLY
     - Wait 0.3s for device processing
  2. Restore color:
     - set_color(prestate.color, duration=0)
     - Wait 0.3s
  3. Restore power:
     - set_power(prestate.power, duration=0)

Timing: 0.6-1.0 seconds per device (includes delays)

Special Cases:

  • Multizone devices get zones restored first
  • 0.3s delays ensure device processing completes
  • Errors are logged but don't stop other devices

State Management

State Storage

The conductor maintains a registry of running effects:

_running: dict[str, RunningEffect]

Key: Device serial number (12-digit hex string) Value: RunningEffect containing effect instance and captured PreState

State Capture Details

Power State

Integer power level captured via get_power():

power_level = await light.get_power()  # Returns int (0 or 65535)
is_on = power_level > 0

Color State

HSBK color captured via get_color():

color, power, _ = await light.get_color()  # power is int (0 or 65535)

Returns:

  • color: HSBK (hue, saturation, brightness, kelvin)
  • power: Power level as integer (0 for off, 65535 for on)
  • label: Device label

Multizone State

For MultiZoneLight devices, zone colors are captured:

if isinstance(light, MultiZoneLight):
    # Get all zones using the convenience method
    # Automatically uses the best method based on capabilities
    zone_colors = await light.get_all_color_zones()

Extended Multizone:

  • Single message retrieves all zones
  • Returns list[HSBK] with all zone colors
  • More efficient, used when available

Standard Multizone:

  • Retrieves zones in batches of 8
  • Multiple messages required for >8 zones
  • Used as fallback for older devices

Matrix State

For MatrixLight devices (Tile, Candle, Path, Ceiling and Mirror), every tile's colours are captured with get_all_tile_colors(), so the light gets back the whole tile it showed, not one colour. If the read fails, restoration falls back to the single captured colour.

Ceiling and Mirror lights also record both light components' stored colours. Restoring the tile and power, and any tile read while frames play, can change them; the restore puts the recorded colours back last, so an effect never changes what a component turns back on to.

State Restoration Details

Matrix Restoration

Each captured tile is written back with set_matrix_colors(tile_index, colours, duration=0), instead of the single colour, before power is restored.

Multizone Restoration

Zones are restored before color and power:

Extended Multizone:

await light.set_extended_color_zones(
    zone_index=0,
    colors=zone_colors,
    duration=0.0,
    apply=MultiZoneExtendedApplicationRequest.APPLY
)

Single message restores all zones.

Standard Multizone:

for i, color in enumerate(zone_colors):
    is_last = (i == len(zone_colors) - 1)
    apply = APPLY if is_last else NO_APPLY

    await light.set_color_zones(
        start=i, end=i,
        color=color,
        duration=0.0,
        apply=apply
    )

Multiple messages with apply logic:

  • NO_APPLY (0): Update buffer, don't display
  • APPLY (1): Update buffer and display (used on last zone)

This ensures atomic update visible only when all zones are set.

Timing Delays

Critical 0.3-second delays ensure device processing:

# After multizone restoration
await asyncio.sleep(0.3)

# After color restoration
await asyncio.sleep(0.3)

# No delay after power (last operation)

Without these delays, subsequent operations may arrive before device finishes processing, causing state corruption.

Prestate Inheritance

When a new effect replaces a running one on a light or light component, on any Conductor, the new run inherits the replaced run's original prior state, so a later stop restores what was there before any effect rather than a frame captured mid-effect.

inherit_prestate() is superseded: the Conductor no longer consults it, and overriding it has no effect. It stays on LIFXEffect for compatibility.

Concurrency Model

Thread Safety

The conductor uses an asyncio.Lock() for thread-safe state management:

async def start(self, effect, participants):
    async with self._lock:
        # Critical section: state capture and registration
        for light in participants:
            prestate = await self._capture_prestate(light)
            self._running[light.serial] = RunningEffect(effect, prestate)

    # Effect execution happens outside lock (concurrent)
    await effect.async_perform(participants)

Why lock is needed:

  • Prevents race conditions when starting/stopping effects concurrently
  • Protects _running dictionary modifications
  • Ensures atomic state capture and registration

Why effect execution is outside lock:

  • Allows multiple effects to run concurrently on different devices
  • Effect logic doesn't modify conductor state
  • Prevents blocking other operations during long-running effects

Concurrent Device Operations

Effects use asyncio.gather() for concurrent device operations:

# Apply waveform to all devices concurrently
tasks = [
    light.set_waveform(color, period, cycles, waveform)
    for light in self.participants
]
await asyncio.gather(*tasks)

Benefits:

  • Multiple devices updated in parallel
  • Network latency overlaps
  • Total time ≈ single device time (not N × device time)

Background Response Dispatcher

Each DeviceConnection has a background receiver task that routes responses:

# In DeviceConnection
async def _response_receiver(self):
    while self._running:
        packet = await self._receive_packet()
        # Route by sequence number to waiting coroutine
        self._pending[seq_num].set_result(packet)

Implications for effects:

  • Multiple concurrent requests on same device are supported
  • Responses are correctly matched even with concurrent operations
  • No additional coordination needed in effect code

Effect Concurrency Patterns

Pattern 1: Sequential Effects on Same Devices

# Effect 1 completes before Effect 2 starts
await conductor.start(effect1, lights)
await asyncio.sleep(duration1)
await conductor.start(effect2, lights)  # Captures new state

State is automatically restored between effects.

Pattern 2: Concurrent Effects on Different Devices

# Different devices, completely independent
await conductor.start(effect1, group1)
await conductor.start(effect2, group2)
# Both run concurrently

No locking needed - different devices, different state.

Pattern 3: Replacing Running Effect

# Start effect1
await conductor.start(effect1, lights)
await asyncio.sleep(5)

# Replace with effect2
await conductor.start(effect2, lights)  # Prestate may be inherited

If effect2.inherit_prestate(effect1) returns True, no restoration happens.

Device Type Handling

Device Capabilities Detection

The effects framework adapts to device capabilities automatically:

# Check if multizone
if isinstance(light, MultiZoneLight):
    # Capture zone colors
    zone_colors = await light.get_color_zones(...)

# Check if extended multizone supported
if light.capabilities and light.capabilities.has_extended_multizone:
    # Use efficient extended messages
    await light.set_extended_color_zones(...)

Device-Specific Behavior

Color Lights (Light)

  • Full HSBK color support
  • All effect parameters apply
  • No special handling needed

Multizone Lights (MultiZoneLight)

  • State Capture: Zone colors captured using extended or standard messages
  • State Restoration: All zones restored with proper apply logic
  • Effect Behavior: Entire device pulses/cycles together (zones not individually controlled)
  • Timing: 0.3s delay after zone restoration

Matrix Lights (MatrixLight)

  • FrameEffect support: Full 2D canvas via FrameContext.canvas_width / canvas_height
  • Spatial effects: Flicker (vertical gradient), Aurora (vertical brightness), Sunrise/Sunset (radial wavefront)
  • Canvas mapping: Multi-tile devices get a unified canvas based on tile positions
  • Mirror: Each ring, front and back, is a 25x1 canvas running clockwise from the ring origin (top centre by default, set with MirrorLight.ring_origin), viewed from the front, with FrameContext.wraps set. The back ring's zones run anticlockwise, so its slot takes them in reverse, level with the front, and both rings turn the same way. The origin is read when a writer is created, so a change applies from the next effect start. A whole-light frame effect runs as two ring participants that share the Mirror's one device_index and simulation, so both rings show the same frame and the two chipless positions stay dark. An effect on mirror.front or mirror.back draws on that ring alone. Each ring is a slot on the light's Animator, which scatters the ring's frame to its buffer positions and sends the whole tile in one Set64. The Mirror's own send_frame() still exposes the raw 4x13 tile.

HEV Lights (HevLight)

  • Treated like standard color lights
  • HEV cycle not affected by effects
  • Effects don't interfere with HEV functionality

Infrared Lights (InfraredLight)

  • Treated like standard color lights
  • Infrared LED not affected by color effects
  • Effects only control visible light

Monochrome/White Lights

  • Color parameters ignored: Hue and saturation have no effect
  • Brightness works: Effects can still toggle/fade brightness
  • Kelvin preserved: Temperature setting maintained
  • Recommendation: Limited usefulness (only brightness changes visible)

Design Decisions

Why Conductor Pattern?

Decision: Central conductor manages all effect lifecycle instead of effects managing themselves.

Rationale:

  1. Centralized State: Single source of truth for what's running where
  2. Consistent State Management: All effects get same capture/restore logic
  3. Concurrency Control: Single lock protects all state modifications
  4. User Simplicity: Users don't manage state manually

Alternative Considered: Effects self-manage state

Rejected because: Would require each effect to duplicate state logic, higher chance of bugs

Why Abstract Base Class?

Decision: LIFXEffect is abstract with required async_play() override.

Rationale:

  1. Type Safety: Enforces effect interface at type-check time
  2. Code Reuse: Common setup logic in async_perform()
  3. Extensibility: Users can create custom effects easily
  4. Consistency: All effects follow same pattern

Why Two-Phase Effect Execution?

Decision: async_perform() calls async_play() instead of single method.

Rationale:

  1. Separation of Concerns: Setup logic separate from effect logic
  2. User Simplicity: Users only override async_play(), setup is automatic
  3. Consistency: All effects get same power-on behavior
  4. Flexibility: Base class can add more setup steps without breaking subclasses

Why No Rate Limiting?

Decision: Effects don't implement rate limiting.

Rationale:

  1. Simplicity: Keeps core library simple and focused
  2. Flexibility: Applications have different rate limit needs
  3. Transparency: Users see actual device behavior
  4. Consistency: Matches lifx-async philosophy (no hidden rate limiting)

Recommendation: Applications should implement rate limiting if sending many concurrent requests.

Why 0.3-Second Delays?

Decision: Fixed 0.3-second delays between state restoration operations.

Rationale:

  1. Device Processing Time: LIFX devices need time to process commands
  2. Empirical Testing: 0.3s works reliably across all device types
  3. State Integrity: Prevents race conditions and state corruption
  4. Trade-off: Slightly slower restoration but guaranteed correctness

Alternative Considered: No delays, faster restoration

Rejected because: Causes state corruption and unpredictable behavior

Why Prestate Inheritance?

Decision: Optional optimization via inherit_prestate() method.

Rationale:

  1. Performance: Eliminates unnecessary state reset
  2. User Experience: No visible flash between compatible effects
  3. Opt-in: Only used when effect explicitly enables it
  4. Safe Default: Returns False unless overridden

Use Cases:

  • ColorLoop → ColorLoop: Seamless transition
  • Pulse → Pulse: Could enable but currently disabled
  • Different types: Should not inherit (different visual intent)

Why FrameEffect Pattern?

Decision: Separate frame generation from frame delivery via FrameEffect + animation module.

Rationale:

  1. Device Agnostic: Effects work across all device types (Light, MultiZoneLight, MatrixLight) without device-specific code
  2. Clean Separation: Effect authors implement generate_frame() returning HSBK colors; animation module handles packet construction, tile mapping, and UDP delivery
  3. Spatial Awareness: FrameContext provides pixel_count, canvas_width, canvas_height and wraps, enabling 2D effects (fire, rain) on matrix devices and seamless patterns on ring canvases
  4. Performance: Direct UDP via prebaked packet templates, no connection overhead
  5. Smooth Transitions: duration_ms parameter tells firmware to interpolate between frames

Alternative Considered: Keep all effects using device methods (set_color(), set_color_zones())

Rejected because: Required device-specific branching in every effect, couldn't leverage the animation module's performance, and made spatial effects impractical.

Integration Points

With Device Layer

LIFXEffect subclasses (e.g., EffectPulse) use standard device methods:

  • get_power(), set_power() (state capture/restore, power-on)
  • get_color(), set_color() (state capture/restore)
  • set_waveform() (EffectPulse sends one waveform per participant)
  • get_color_zones(), set_color_zones() (MultiZoneLight state)
  • get_extended_color_zones(), set_extended_color_zones() (MultiZoneLight state)

With Animation Layer

FrameEffect subclasses (e.g., EffectColorloop) use the animation module for frame delivery:

  • light.animator: the one Animator each light owns: SetColor packets for a single light, SetExtendedColorZones for a strip, Set64 with canvas mapping for a matrix
  • animator.send_frame() — synchronous frame dispatch (no async overhead)
  • duration_ms: firmware-level interpolation between frames, set per writer

The Conductor borrows each light's Animator in start() and releases it in stop().

With Network Layer

State capture/restore relies on existing lazy connection and concurrent request support:

  • Lazy connections open on first request and are reused
  • Requests are serialized via lock to prevent response mixing
  • FrameEffect frame delivery bypasses connections (direct UDP via Animator)

With Protocol Layer

Effects use existing protocol structures:

  • HSBK for color representation + HSBK.as_tuple() for protocol conversion
  • LightWaveform enum for waveform types (EffectPulse)
  • MultiZoneApplicationRequest for zone apply logic (state restore)
  • Auto-generated packet classes

Performance Characteristics

Memory Usage

  • Conductor: ~10KB base + running effects
  • Per Effect: ~1KB per device + effect-specific state
  • PreState: ~200 bytes per device (~100 bytes + zone colors)

CPU Usage

  • Minimal: Async I/O bound, not CPU bound
  • Concurrency: Multiple devices don't increase CPU significantly
  • Background Tasks: Requests serialized per connection, concurrent across devices

Network Traffic

State Capture

  • Power: 1 request per device
  • Color: 1 request per device
  • Multizone: 1 request (extended) or N/8 requests (standard)
  • Total: 3-4 packets per device

Effect Execution

  • Pulse: 1 waveform packet per device (via device connection)
  • FrameEffect (e.g., ColorLoop): 1 frame per device per FPS tick (via direct UDP Animator)

State Restoration

  • Multizone: 1 request (extended) or N requests (standard)
  • Color: 1 request per device
  • Power: 1 request per device
  • Total: 2-3 packets per device (or N+2 for standard multizone)

Scalability

  • Tested: 10+ devices
  • Expected: 50+ devices in production
  • Limitation: Network capacity and device response time
  • Recommendation: For 50+ devices, consider grouping effects or staggering start times

See Also