Skip to main content

Real-time Bluetooth Low Energy beacon visualization on WLED LED strips

Project description

BLE2WLED - Bluetooth Beacon to WLED Visualizer

CI Codecov

A Python package that detects Bluetooth Low Energy (BLE) beacons via MQTT and visualizes them in real-time on WLED LED strips using dynamic colors and motion trails.

Read the full documentation on https://ble2wled.tobotec.ch.

Overview

BLE2WLED bridges the gap between Bluetooth beacon detection and visual LED effects. It:

  1. Receives BLE beacon data via MQTT (beacon ID and signal strength/RSSI)
  2. Converts signal strength to distance using RF propagation modeling
  3. Maps distance to colors on a gradient (yellow when near → red when far)
  4. Adds visual identity with hash-based hue shifts for each beacon
  5. Implements graceful fade-out for beacons that disappear
  6. Renders motion trails as beacons move along the LED strip
  7. Outputs to WLED via HTTP or UDP (DRGB protocol) for smooth real-time animation

Prerequisites

ESPresense & MQTT Broker

You need a working MQTT broker that receives BLE beacon data in JSON format. Each message should contain a unique beacon ID and its RSSI value.
Ideally use ESPresense firmware on ESP32 devices for BLE scanning and MQTT publishing to a mosquito broker (running on HomeAssistant for example).

WLED Device

You need a WLED controlled LED strip. Use an ESP8266 or ESP32 running the WLED firmware (code tested with v0.15.0).

Changelog

See CHANGELOG.md for recent changes.

Installation

From Source

# Clone the repository
git clone https://github.com/tobus3000/ble2wled.git
cd ble2wled

# Install in development mode
pip install -e ".[dev]"

From PyPI

pip install ble2wled

Quick Start

from ble2wled import BeaconState, BeaconMQTTListener, WLEDUDPController, run_wled_beacons

# Initialize beacon state tracking
beacon_state = BeaconState(
    timeout_seconds=6.0,      # How long before a beacon times out
    fade_out_seconds=4.0      # How long to fade out after timeout
)

# Set up MQTT listener for beacon data
mqtt_listener = BeaconMQTTListener(
    beacon_state,
    broker="localhost",
    topic="espresense/devices",
    port=1883
)
mqtt_listener.start()

# Create WLED controller
controller = WLEDUDPController(
    host="wled.local",
    led_count=60,
    port=21324  # Default DRGB port
)

# Run the main animation loop
run_wled_beacons(
    controller,
    led_count=60,
    beacon_state=beacon_state,
    update_interval=0.2,  # 200ms updates
    trail_length=10,      # Number of LEDs in trail
    fade_factor=0.75      # Brightness decay per trail segment
)

Configuration via Environment Variables

BLE2WLED can be configured using environment variables or a .env file. This is the recommended way to configure the application without modifying code.

Setup Configuration

  1. Copy the template configuration file:
cp .env.example .env
  1. Edit .env with your settings:
# WLED LED Strip
WLED_HOST=wled.local
LED_COUNT=60

# MQTT Broker
MQTT_BROKER=localhost
MQTT_LOCATION=balkon
MQTT_PORT=1883
MQTT_USERNAME=espresense
MQTT_PASSWORD=your_secure_password

# Beacon Detection
BEACON_TIMEOUT_SECONDS=6.0
BEACON_FADE_OUT_SECONDS=4.0

# Animation
UPDATE_INTERVAL=0.2
TRAIL_LENGTH=10
FADE_FACTOR=0.75

# Output
OUTPUT_MODE=udp
UDP_PORT=21324
HTTP_TIMEOUT=1

# Logging
LOG_LEVEL=INFO
  1. Run the application:
python -m ble2wled

The application will automatically load .env from the current working directory.

Available Configuration Options

Environment Variable Default Description
WLED_HOST wled.local WLED device hostname or IP address
LED_COUNT 60 Number of LEDs in the strip (must be > 0)
MQTT_BROKER localhost MQTT broker hostname or IP
MQTT_LOCATION balkon Location identifier for beacon filtering (espresense topic)
MQTT_PORT 1883 MQTT broker port
MQTT_USERNAME None MQTT username for authentication (optional)
MQTT_PASSWORD None MQTT password for authentication (optional)
BEACON_TIMEOUT_SECONDS 6.0 Seconds before beacon timeout
BEACON_FADE_OUT_SECONDS 4.0 Seconds to fade out after timeout
UPDATE_INTERVAL 0.2 Animation update interval in seconds
TRAIL_LENGTH 10 Number of LEDs in motion trail
FADE_FACTOR 0.75 Trail brightness decay (0 < x ≤ 1)
OUTPUT_MODE udp Output protocol: udp or http
UDP_PORT 21324 UDP port for DRGB protocol
HTTP_TIMEOUT 1 HTTP request timeout in seconds
LOG_LEVEL INFO Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL

Note on .env file

  • The .env.example file should be committed to version control
  • Create a .env file locally with your settings (it's git-ignored)
  • Each deployment can have different settings without code changes

Core Components

BeaconState

Manages the state of detected beacons with automatic timeout and fade-out.

Parameters:

  • timeout_seconds (float, default=5.0): Duration before a beacon is considered timed out (no updates received)
  • fade_out_seconds (float, default=3.0): Duration to fade out the beacon after timeout

Methods:

  • update(beacon_id: str, rssi: int): Update beacon with signal strength
  • snapshot(): Get current active beacons with their RSSI and life values (0-1)
state = BeaconState(timeout_seconds=6.0, fade_out_seconds=4.0)
state.update("beacon_1", -50)
beacons = state.snapshot()  # {"beacon_1": (-50, 1.0)}

BeaconRunner

Manages position animation for beacons moving along the LED strip.

Parameters:

  • led_count (int): Total number of LEDs in the strip

Methods:

  • next_position(beacon_id: str): Get next position for a beacon (increments and wraps around)
runner = BeaconRunner(led_count=60)
pos = runner.next_position("beacon_1")  # Returns 0, then 1, 2, ... 59, 0, ...

BeaconMQTTListener

Connects to an MQTT broker and listens for beacon updates.

Parameters:

  • state (BeaconState): The beacon state object to update
  • broker (str): MQTT broker hostname or IP
  • topic (str): MQTT topic to subscribe to
  • port (int, default=1883): MQTT broker port

Expected MQTT message format:

{
  "beacon_id": "device_123",
  "rssi": -50
}

Methods:

  • start(): Start listening in background thread
listener = BeaconMQTTListener(state, "mqtt.example.com", "ble/beacons")
listener.start()

WLEDUDPController

Sends LED updates to WLED via UDP (DRGB protocol) - recommended for real-time performance.

Parameters:

  • host (str): WLED device hostname or IP
  • led_count (int): Total number of LEDs
  • port (int, default=21324): DRGB protocol port

Methods:

  • update(leds): Send LED color array (list of [R, G, B] values 0-255)
controller = WLEDUDPController("wled.local", 60)
leds = [[255, 0, 0], [0, 255, 0], ...]  # 60 RGB values
controller.update(leds)

WLEDHTTPController

Sends LED updates to WLED via HTTP - useful for debugging but slower than UDP.

Parameters:

  • host (str): WLED device hostname or IP
  • led_count (int): Total number of LEDs

Methods:

  • update(leds): Send LED color array via HTTP POST
controller = WLEDHTTPController("wled.local", 60)
controller.update(leds)

Color Mapping

Distance Estimation

RSSI (Received Signal Strength Indicator) is converted to distance using the free-space path loss model:

$$d = 10^{\frac{P_t - \text{RSSI}}{10n}}$$

Where:

  • $P_t$ = Transmit power (default: -59 dBm)
  • $n$ = Path loss exponent (default: 2.0)

Parameters:

  • rssi (int): Signal strength in dBm
  • tx_power (int, default=-59): Transmit power of beacon in dBm
  • n (float, default=2.0): Path loss exponent (2.0 = free space)
distance = estimate_distance_from_rssi(rssi=-50)

Gradient Coloring

Beacons are colored on a yellow→red gradient based on distance:

  • Near (< 0.5m): Yellow (RGB: 255, 255, 0)
  • Mid (0.5m - 5m): Yellow→Red transition
  • Far (> 10m): Red (RGB: 255, 0, 0)

Parameters:

  • distance (float): Distance in meters
  • near (float, default=0.5): Distance threshold for "near" in meters
  • far (float, default=10.0): Distance threshold for "far" in meters
r, g, b = gradient_color(distance=2.5)

Beacon Identity via Hue Shift

Each beacon receives a unique hue offset (0-8%) based on its hash, ensuring different beacons have distinct colors even at the same distance.

Life-Based Brightness

The life parameter (0-1) modulates the final brightness:

  • life=1.0: Full brightness
  • life=0.5: 50% brightness
  • life=0.0: Invisible (faded out)
r, g, b = ble_beacon_to_rgb("beacon_id", rssi=-50, life=0.75)

Trail Rendering

Motion trails are rendered behind beacons to create smooth animation:

Parameters:

  • trail_length (int): Number of LEDs in the trail
  • fade_factor (float, 0-1): Brightness decay per segment
    • 0.75: Each trailing segment is 75% brightness of the previous
    • Results in smooth fade effect
add_trail(leds, position=10, color=(255, 0, 0), trail_length=8, fade_factor=0.75)

Code Quality

This project uses Ruff for linting and code formatting with Black style enforcement.

Ruff Configuration

The project is configured to enforce:

  • Black style formatting (88-character line length)
  • PEP 8 compliance via pycodestyle
  • Import sorting via isort integration
  • Code quality checks via flake8-bugbear
  • Modern Python best practices via pyupgrade

Running Linting & Formatting

# Check for linting issues
ruff check src/ tests/

# Check formatting (dry-run)
ruff format src/ tests/ --check

# Auto-format all code
ruff format src/ tests/

# Run all checks and format
bash lint.sh

Running Tests

# Run all tests
pytest

# Run with coverage
pytest --cov=ble2wled tests/

# Run specific test file
pytest tests/test_beacon_state.py

# Verbose output
pytest -v

MQTT Configuration

Set up an MQTT broker to forward BLE beacon data. Expected message format:

{
  "beacon_id": "unique_identifier",
  "rssi": -45
}

Example with mosquitto:

# Publish test beacon
mosquitto_pub -t "ble/beacons" -m '{"beacon_id": "test_beacon", "rssi": -50}'

WLED Setup

  1. Install WLED firmware on your ESP32/Arduino device
  2. Ensure UDP realtime protocol is enabled in WLED settings
  3. Configure your WLED host/IP in the controller

Configuration Parameters

Beacon Timeout & Fade-Out

  • Shorter timeout (2-3s): Beacons disappear quickly when signal is lost

  • Longer timeout (5-10s): Beacons persist longer despite intermittent drops

  • Shorter fade-out (1-2s): Quick disappearance

  • Longer fade-out (3-5s): Smooth, lingering effect

Update Interval

  • Faster (0.1-0.2s): Smooth motion but higher CPU/network usage
  • Slower (0.5-1.0s): Lower resource usage, choppier animation

Trail Parameters

  • Longer trail (8-15 LEDs): More pronounced motion effect

  • Shorter trail (2-5 LEDs): Tighter beacon visualization

  • Higher fade_factor (0.8-0.9): Brighter trails, less decay

  • Lower fade_factor (0.5-0.7): Darker trails, faster fade

Dependencies

  • requests >= 2.25.0 - HTTP communication with WLED
  • paho-mqtt >= 1.6.0 - MQTT client for beacon reception

License

MIT License - See LICENSE file for details

Build Changelog

We use git-cliff to generate changelogs from conventional commit messages.
Run the following command to generate/update CHANGELOG.md:

Generate changelog since last tag:

git-cliff --latest -o CHANGELOG.md

For a new release, you can also use:

git-cliff --tag v1.0.0 -o CHANGELOG.md

Contributing

Contributions welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Submit a pull request

Troubleshooting

Beacons not appearing

  • Check MQTT broker is running and accessible
  • Verify beacon messages match expected JSON format
  • Confirm MQTT topic matches the listener configuration

LEDs not updating

  • Test WLED connectivity: ping wled.local
  • Verify UDP port 21324 is open (or configured port)
  • Check WLED device has realtime protocol enabled

Jerky animation

  • Increase update_interval to process fewer updates
  • Reduce trail_length to lower computation
  • Check network latency to MQTT/WLED devices

Beacons stuck or not fading

  • Adjust timeout_seconds if beacons disconnect intermittently
  • Increase fade_out_seconds for longer fade effect

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ble2wled-1.0.1.tar.gz (80.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ble2wled-1.0.1-py3-none-any.whl (31.4 kB view details)

Uploaded Python 3

File details

Details for the file ble2wled-1.0.1.tar.gz.

File metadata

  • Download URL: ble2wled-1.0.1.tar.gz
  • Upload date:
  • Size: 80.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for ble2wled-1.0.1.tar.gz
Algorithm Hash digest
SHA256 24e7e019531233fdab0840a54ea7e3c4beb9041937a14882963672f14ab0a1be
MD5 e034c4cac7e1c08e5caf33cae9509919
BLAKE2b-256 f3cf375ce7c38453f0036116656b743c0c8640e69b88775e350284f0400a7be4

See more details on using hashes here.

Provenance

The following attestation bundles were made for ble2wled-1.0.1.tar.gz:

Publisher: release.yml on tobus3000/ble2wled

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ble2wled-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: ble2wled-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 31.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for ble2wled-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 eae17430556f76c837d176b8d48c8cfd2c293e0e35db84fc7fe38ce63c9480c7
MD5 98405f8b8b982c61a0270234726579c3
BLAKE2b-256 8929f9e7a0be11d9ddbeb8d0ab8c632a77abd6cbc14f94c3ae33dadf205f129d

See more details on using hashes here.

Provenance

The following attestation bundles were made for ble2wled-1.0.1-py3-none-any.whl:

Publisher: release.yml on tobus3000/ble2wled

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page