Add configurable baud rate for Adalight with dynamic FPS hint

Baud rate is now a first-class device field, passed through the full
stack: Device model → API schemas → routes → ProcessorManager →
AdalightClient. The frontend shows a baud rate dropdown (115200–2M)
for Adalight devices in both Add Device and Settings modals, with a
live "Max FPS ≈ N" hint computed from LED count and baud rate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 16:35:41 +03:00
parent 1612c04c90
commit afb20f2dac
13 changed files with 160 additions and 38 deletions

View File

@@ -1,6 +1,6 @@
"""Adalight serial LED client — sends pixel data over serial using the Adalight protocol."""
import time
import asyncio
from datetime import datetime
from typing import List, Optional, Tuple
@@ -59,14 +59,16 @@ def _build_adalight_header(led_count: int) -> bytes:
class AdalightClient(LEDClient):
"""LED client for Arduino Adalight serial devices."""
def __init__(self, url: str, led_count: int = 0, **kwargs):
def __init__(self, url: str, led_count: int = 0, baud_rate: int = None, **kwargs):
"""Initialize Adalight client.
Args:
url: Serial port string, e.g. "COM3" or "COM3:230400"
led_count: Number of LEDs on the strip (required for Adalight header)
baud_rate: Override baud rate (if None, parsed from url or default 115200)
"""
self._port, self._baud_rate = parse_adalight_url(url)
self._port, url_baud = parse_adalight_url(url)
self._baud_rate = baud_rate or url_baud
self._led_count = led_count
self._serial = None
self._connected = False
@@ -82,13 +84,11 @@ class AdalightClient(LEDClient):
import serial
try:
self._serial = serial.Serial(
port=self._port,
baudrate=self._baud_rate,
timeout=1,
self._serial = await asyncio.to_thread(
serial.Serial, port=self._port, baudrate=self._baud_rate, timeout=1
)
# Wait for Arduino to finish bootloader reset
time.sleep(ARDUINO_RESET_DELAY)
# Wait for Arduino to finish bootloader reset (non-blocking)
await asyncio.sleep(ARDUINO_RESET_DELAY)
self._connected = True
logger.info(
f"Adalight connected: {self._port} @ {self._baud_rate} baud "
@@ -119,13 +119,13 @@ class AdalightClient(LEDClient):
pixels: List[Tuple[int, int, int]],
brightness: int = 255,
) -> bool:
"""Send pixel data over serial using Adalight protocol."""
"""Send pixel data over serial using Adalight protocol (non-blocking)."""
if not self.is_connected:
return False
try:
frame = self._build_frame(pixels, brightness)
self._serial.write(frame)
await asyncio.to_thread(self._serial.write, frame)
return True
except Exception as e:
logger.error(f"Adalight send_pixels error: {e}")
@@ -133,22 +133,8 @@ class AdalightClient(LEDClient):
@property
def supports_fast_send(self) -> bool:
return True
def send_pixels_fast(
self,
pixels: List[Tuple[int, int, int]],
brightness: int = 255,
) -> None:
"""Synchronous fire-and-forget serial send."""
if not self.is_connected:
return
try:
frame = self._build_frame(pixels, brightness)
self._serial.write(frame)
except Exception as e:
logger.error(f"Adalight send_pixels_fast error: {e}")
# Serial write is blocking — use async send_pixels path instead
return False
def _build_frame(self, pixels: List[Tuple[int, int, int]], brightness: int) -> bytes:
"""Build a complete Adalight frame: header + brightness-scaled RGB data."""

View File

@@ -30,8 +30,9 @@ class AdalightDeviceProvider(LEDDeviceProvider):
from wled_controller.core.adalight_client import AdalightClient
led_count = kwargs.pop("led_count", 0)
baud_rate = kwargs.pop("baud_rate", None)
kwargs.pop("use_ddp", None) # Not applicable for serial
return AdalightClient(url, led_count=led_count, **kwargs)
return AdalightClient(url, led_count=led_count, baud_rate=baud_rate, **kwargs)
async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
from wled_controller.core.adalight_client import AdalightClient

View File

@@ -132,6 +132,7 @@ class DeviceState:
led_count: int
calibration: CalibrationConfig
device_type: str = "wled"
baud_rate: Optional[int] = None
health: DeviceHealth = field(default_factory=DeviceHealth)
health_task: Optional[asyncio.Task] = None
# Calibration test mode (works independently of target processing)
@@ -222,6 +223,7 @@ class ProcessorManager:
led_count: int,
calibration: Optional[CalibrationConfig] = None,
device_type: str = "wled",
baud_rate: Optional[int] = None,
):
"""Register a device for health monitoring.
@@ -231,6 +233,7 @@ class ProcessorManager:
led_count: Number of LEDs
calibration: Calibration config (creates default if None)
device_type: LED device type (e.g. "wled")
baud_rate: Serial baud rate (for adalight devices)
"""
if device_id in self._devices:
raise ValueError(f"Device {device_id} already registered")
@@ -244,6 +247,7 @@ class ProcessorManager:
led_count=led_count,
calibration=calibration,
device_type=device_type,
baud_rate=baud_rate,
)
self._devices[device_id] = state
@@ -277,7 +281,7 @@ class ProcessorManager:
del self._devices[device_id]
logger.info(f"Unregistered device {device_id}")
def update_device_info(self, device_id: str, device_url: Optional[str] = None, led_count: Optional[int] = None):
def update_device_info(self, device_id: str, device_url: Optional[str] = None, led_count: Optional[int] = None, baud_rate: Optional[int] = None):
"""Update device connection info."""
if device_id not in self._devices:
raise ValueError(f"Device {device_id} not found")
@@ -287,6 +291,8 @@ class ProcessorManager:
ds.device_url = device_url
if led_count is not None:
ds.led_count = led_count
if baud_rate is not None:
ds.baud_rate = baud_rate
def update_calibration(self, device_id: str, calibration: CalibrationConfig):
"""Update calibration for a device.
@@ -527,14 +533,16 @@ class ProcessorManager:
# Resolve stream settings
self._resolve_stream_settings(state)
# Determine device type from device state
# Determine device type and baud rate from device state
device_type = "wled"
baud_rate = None
if state.device_id in self._devices:
device_type = self._devices[state.device_id].device_type
baud_rate = self._devices[state.device_id].baud_rate
# Connect to LED device via factory
try:
state.led_client = create_led_client(device_type, state.device_url, use_ddp=True, led_count=state.led_count)
state.led_client = create_led_client(device_type, state.device_url, use_ddp=True, led_count=state.led_count, baud_rate=baud_rate)
await state.led_client.connect()
logger.info(f"Target {target_id} connected to {device_type} device ({state.led_count} LEDs)")
@@ -885,7 +893,7 @@ class ProcessorManager:
if active_client:
await active_client.send_pixels(pixels)
else:
async with create_led_client(ds.device_type, ds.device_url, use_ddp=True, led_count=ds.led_count) as client:
async with create_led_client(ds.device_type, ds.device_url, use_ddp=True, led_count=ds.led_count, baud_rate=ds.baud_rate) as client:
await client.send_pixels(pixels)
except Exception as e:
logger.error(f"Failed to send test pixels for {device_id}: {e}")
@@ -905,7 +913,7 @@ class ProcessorManager:
if active_client:
await active_client.send_pixels(pixels)
else:
async with create_led_client(ds.device_type, ds.device_url, use_ddp=True, led_count=ds.led_count) as client:
async with create_led_client(ds.device_type, ds.device_url, use_ddp=True, led_count=ds.led_count, baud_rate=ds.baud_rate) as client:
await client.send_pixels(pixels)
except Exception as e:
logger.error(f"Failed to clear pixels for {device_id}: {e}")

View File

@@ -35,6 +35,7 @@ class WLEDDeviceProvider(LEDDeviceProvider):
def create_client(self, url: str, **kwargs) -> LEDClient:
from wled_controller.core.wled_client import WLEDClient
kwargs.pop("led_count", None)
kwargs.pop("baud_rate", None)
return WLEDClient(url, **kwargs)
async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth: