Real-time Bluetooth Low Energy beacon visualization on WLED LED strips
Project description
BLE2WLED - Bluetooth Beacon to WLED Visualizer
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:
- Receives BLE beacon data via MQTT (beacon ID and signal strength/RSSI)
- Converts signal strength to distance using RF propagation modeling
- Maps distance to colors on a gradient (yellow when near → red when far)
- Adds visual identity with hash-based hue shifts for each beacon
- Implements graceful fade-out for beacons that disappear
- Renders motion trails as beacons move along the LED strip
- 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.
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
- Copy the template configuration file:
cp .env.example .env
- Edit
.envwith 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
- 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.examplefile should be committed to version control - Create a
.envfile 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 strengthsnapshot(): 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 updatebroker(str): MQTT broker hostname or IPtopic(str): MQTT topic to subscribe toport(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 IPled_count(int): Total number of LEDsport(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 IPled_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 dBmtx_power(int, default=-59): Transmit power of beacon in dBmn(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 metersnear(float, default=0.5): Distance threshold for "near" in metersfar(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 brightnesslife=0.5: 50% brightnesslife=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 trailfade_factor(float, 0-1): Brightness decay per segment0.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
- Install WLED firmware on your ESP32/Arduino device
- Ensure UDP realtime protocol is enabled in WLED settings
- 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 WLEDpaho-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:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- 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_intervalto process fewer updates - Reduce
trail_lengthto lower computation - Check network latency to MQTT/WLED devices
Beacons stuck or not fading
- Adjust
timeout_secondsif beacons disconnect intermittently - Increase
fade_out_secondsfor longer fade effect
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ble2wled-1.0.0.tar.gz.
File metadata
- Download URL: ble2wled-1.0.0.tar.gz
- Upload date:
- Size: 76.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96a276e0766adeae8836c97a8035d39af5e95781bb1ffb63ef6d01a26a751ff2
|
|
| MD5 |
245e90eb5547487a3fa0d5216d98b470
|
|
| BLAKE2b-256 |
aca17d3d94feb102fc3bce9cf1f0c58004b8bb98adfdaf44513279f6a5563353
|
File details
Details for the file ble2wled-1.0.0-py3-none-any.whl.
File metadata
- Download URL: ble2wled-1.0.0-py3-none-any.whl
- Upload date:
- Size: 31.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92191688d23dbd1dd39bc3a994c0be93414896b5f1b2e12ee8f4d5537f519e82
|
|
| MD5 |
453d4df0796514e0867e791ed65df0a1
|
|
| BLAKE2b-256 |
e809d647eb10f88559c1263474aaf85f955f53b4c06df27efe42925f72b716e3
|