ble2wled.animation - Animation Loop

Beacon animation and trail rendering.

This module handles animation of beacons moving across LED strips and rendering motion trails for visual effects.

Example

Animate beacons with motion trails:

from ble2wled.animation import BeaconRunner, add_trail
from ble2wled.colors import ble_beacon_to_rgb

runner = BeaconRunner(led_count=60)
leds = [[0, 0, 0] for _ in range(60)]

# Get next position for beacon and add trail
pos = runner.next_position('beacon_1')
color = ble_beacon_to_rgb('beacon_1', rssi=-50, life=0.8)
add_trail(leds, pos, color, trail_length=10, fade_factor=0.75)
class ble2wled.animation.BeaconRunner(led_count: int)[source]

Bases: object

Manages animation positions for beacons moving along LED strip.

Each beacon gets its own position on the LED strip. Calling next_position() increments the position, creating a moving effect across the strip.

led_count

Total number of LEDs in the strip.

Type:

int

positions

Current position of each beacon.

Type:

dict

__init__(led_count: int)[source]

Initialize beacon runner.

Parameters:

led_count (int) – Total number of LEDs in the strip.

Example

Create a beacon runner for a 60-LED strip:

runner = BeaconRunner(led_count=60)
next_position(beacon_id: str) int[source]

Get next position for a beacon.

Increments position and wraps around at LED count. Maintains consistent position increment per beacon across calls.

Parameters:

beacon_id (str) – Unique beacon identifier.

Returns:

Next position index (0 to led_count-1).

Return type:

int

Example

Get beacon positions for animation:

runner = BeaconRunner(led_count=60)
pos1 = runner.next_position('beacon_1')  # Returns 0
pos1 = runner.next_position('beacon_1')  # Returns 1
pos2 = runner.next_position('beacon_2')  # Returns 0
ble2wled.animation.add_trail(leds: List[List[int]], position: int, color: Tuple[int, int, int], trail_length: int, fade_factor: float) None[source]

Add motion trail to LED array.

Creates a fading trail effect behind the beacon by rendering progressively dimmer segments behind the main position. Uses additive blending to combine trail colors with existing LED values.

Trail Rendering:
  • LED at position gets full color

  • Each previous LED gets color * fade_factor^distance

  • Colors are additively blended (clamped at 255)

Parameters:
  • leds (list) – LED color array (list of [R, G, B] with values 0-255).

  • position (int) – Current beacon position index.

  • color (tuple) – RGB color tuple (R, G, B) with values 0-255.

  • trail_length (int) – Number of LEDs in the trail (including main position).

  • fade_factor (float) – Brightness decay per segment (0.0 to 1.0). 0.75 means each trailing segment is 75% brightness of previous.

Raises:

ValueError – If trail_length is negative or fade_factor outside 0-1.

Example

Add a red trail to the LED array:

leds = [[0, 0, 0] for _ in range(60)]
add_trail(leds, position=10, color=(255, 0, 0),
         trail_length=8, fade_factor=0.75)
# Now leds[10] has max red, leds[9] has 75% red, etc.

Overview

The animation module provides the main animation loop and LED rendering logic. It:

  • Processes beacon state into LED colors

  • Renders motion trails

  • Handles fade-out animations

  • Updates WLED device at regular intervals

Key Function

run_wled_beacons() is the main entry point for running beacon visualization.

Quick Example

from ble2wled.config import Config
from ble2wled.states import BeaconState
from ble2wled.mqtt import EspresenseBeaconListener
from ble2wled.wled import WLEDUDPController
from ble2wled.animation import run_wled_beacons

# Load configuration
config = Config('.env')

# Create beacon state and MQTT listener
beacon_state = BeaconState()
mqtt_listener = EspresenseBeaconListener(
    beacon_state,
    broker=config.mqtt_broker,
    location=config.mqtt_location
)
mqtt_listener.start()

# Create WLED controller
controller = WLEDUDPController(config.wled_host, config.led_count)

# Run animation loop (blocking)
run_wled_beacons(
    controller,
    led_count=config.led_count,
    beacon_state=beacon_state
)

Functions

run_wled_beacons()

Main animation loop for beacon visualization.

Parameters:

  • controller - WLED device controller (WLEDHTTPController or WLEDUDPController)

  • led_count (int) - Total number of LEDs

  • beacon_state (BeaconState) - Beacon state tracker

  • update_interval (float, default 0.05) - Update interval in seconds (50ms = 20fps)

  • trail_length (int, default 8) - Length of motion trail in LEDs

  • fade_factor (float, default 0.75) - Trail fade factor (0.1-1.0)

Behavior:

The function runs a continuous animation loop:

  1. Read beacon state - Get active and fading beacons

  2. Initialize LED array - Create array of [R, G, B] for each LED

  3. Render trails - For each beacon, render motion trail on LEDs

  4. Apply fade-out - Reduce brightness for beacons in timeout

  5. Update device - Send LED array to WLED controller

  6. Cleanup - Remove expired beacons

  7. Sleep - Wait update_interval before next iteration

Raises:

  • KeyboardInterrupt - When user presses Ctrl+C

Example:

from ble2wled.animation import run_wled_beacons
from ble2wled.wled import WLEDUDPController
from ble2wled.states import BeaconState

state = BeaconState()
controller = WLEDUDPController('192.168.1.100', 60)

try:
    run_wled_beacons(
        controller,
        led_count=60,
        beacon_state=state,
        update_interval=0.05,      # 20fps
        trail_length=10,           # 10-LED trail
        fade_factor=0.75           # Dim trail
    )
except KeyboardInterrupt:
    print("Animation stopped")

Animation Parameters

Update Interval

Controls animation smoothness and CPU usage.

Values:

  • 0.02 - 50fps (very smooth, high CPU)

  • 0.05 - 20fps (good balance, recommended)

  • 0.1 - 10fps (lower CPU, less smooth)

  • 0.2 - 5fps (low CPU, clearly animated)

Example:

# Fast animation
run_wled_beacons(controller, ..., update_interval=0.02)

# Smooth default
run_wled_beacons(controller, ..., update_interval=0.05)

# CPU efficient
run_wled_beacons(controller, ..., update_interval=0.1)

Trail Length

Number of LEDs the motion trail extends backward from beacon position.

Values:

  • 4 - Short trails (just position)

  • 8 - Medium trails (default)

  • 15 - Long trails (motion effect)

  • 30 - Very long trails (significant motion history)

Example:

# Short trail
run_wled_beacons(controller, ..., trail_length=4)

# Medium trail (default)
run_wled_beacons(controller, ..., trail_length=8)

# Long trail (motion effect)
run_wled_beacons(controller, ..., trail_length=15)

Fade Factor

How much each trail segment fades compared to the previous.

Values:

  • 0.1 - Fast fade (dim trails)

  • 0.5 - Medium fade

  • 0.75 - Default fade

  • 0.9 - Slow fade (bright trails)

Behavior:

For a trail with trail_length=8 and fade_factor=0.75:

  • Position 0 (current): brightness 1.0

  • Position 1: brightness 0.75

  • Position 2: brightness 0.56 (0.75²)

  • Position 3: brightness 0.42 (0.75³)

  • … and so on

Example:

# Dim trails
run_wled_beacons(controller, ..., fade_factor=0.5)

# Medium trails (default)
run_wled_beacons(controller, ..., fade_factor=0.75)

# Bright trails
run_wled_beacons(controller, ..., fade_factor=0.9)

Examples

Basic Animation Loop

from ble2wled.animation import run_wled_beacons
from ble2wled.states import BeaconState
from ble2wled.wled import WLEDUDPController
from ble2wled.simulator import MockBeaconGenerator

# Setup
state = BeaconState()
controller = WLEDUDPController('192.168.1.100', 60)

# Generate mock beacons for testing
generator = MockBeaconGenerator(state, 3, 60)
generator.start()

# Run animation
try:
    run_wled_beacons(controller, 60, state)
finally:
    generator.stop()

With MQTT

from ble2wled.animation import run_wled_beacons
from ble2wled.states import BeaconState
from ble2wled.mqtt import EspresenseBeaconListener
from ble2wled.wled import WLEDUDPController

# Setup
state = BeaconState()
listener = EspresenseBeaconListener(state, 'localhost', 'living_room')
controller = WLEDUDPController('192.168.1.100', 60)

# Start listener
listener.start()

# Run animation
try:
    run_wled_beacons(
        controller,
        led_count=60,
        beacon_state=state,
        update_interval=0.05,
        trail_length=8,
        fade_factor=0.75
    )
finally:
    listener.stop()

With Custom Parameters

from ble2wled.animation import run_wled_beacons
from ble2wled.config import Config
from ble2wled.states import BeaconState
from ble2wled.wled import WLEDUDPController

# Load configuration
config = Config('.env')

# Create components
state = BeaconState(
    timeout_seconds=config.beacon_timeout_seconds,
    fade_out_seconds=config.beacon_fade_out_seconds
)
controller = WLEDUDPController(config.wled_host, config.led_count)

# Run with configuration values
run_wled_beacons(
    controller,
    led_count=config.led_count,
    beacon_state=state,
    update_interval=config.animation_update_interval,
    trail_length=config.animation_trail_length,
    fade_factor=config.animation_fade_factor
)

Performance Considerations

For smooth 20fps animation on 60 LEDs:

run_wled_beacons(
    controller,
    led_count=60,
    beacon_state=state,
    update_interval=0.05        # 20fps
)

For CPU-constrained systems:

run_wled_beacons(
    controller,
    led_count=60,
    beacon_state=state,
    update_interval=0.1,        # 10fps
    trail_length=4,             # Shorter trail
    fade_factor=0.5             # Faster fade
)

For high-end systems with more LEDs:

run_wled_beacons(
    controller,
    led_count=300,              # More LEDs
    beacon_state=state,
    update_interval=0.02,       # 50fps
    trail_length=15,            # Longer trail
    fade_factor=0.9             # Slower fade
)

Debugging Animation

To debug animation issues, capture LED data before sending:

from ble2wled.animation import run_wled_beacons
from ble2wled.wled import WLEDUDPController

class DebugController(WLEDUDPController):
    def update(self, leds):
        # Print first 10 LEDs
        print(f"LEDs: {leds[:10]}")
        super().update(leds)

controller = DebugController('192.168.1.100', 60)
run_wled_beacons(controller, 60, state)

See Also