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

@@ -49,6 +49,7 @@ def _device_to_response(device) -> DeviceResponse:
device_type=device.device_type, device_type=device.device_type,
led_count=device.led_count, led_count=device.led_count,
enabled=device.enabled, enabled=device.enabled,
baud_rate=device.baud_rate,
capabilities=sorted(get_device_capabilities(device.device_type)), capabilities=sorted(get_device_capabilities(device.device_type)),
calibration=CalibrationSchema(**calibration_to_dict(device.calibration)), calibration=CalibrationSchema(**calibration_to_dict(device.calibration)),
created_at=device.created_at, created_at=device.created_at,
@@ -115,6 +116,7 @@ async def create_device(
url=device_data.url, url=device_data.url,
led_count=led_count, led_count=led_count,
device_type=device_type, device_type=device_type,
baud_rate=device_data.baud_rate,
) )
# Register in processor manager for health monitoring # Register in processor manager for health monitoring
@@ -124,6 +126,7 @@ async def create_device(
led_count=device.led_count, led_count=device.led_count,
calibration=device.calibration, calibration=device.calibration,
device_type=device.device_type, device_type=device.device_type,
baud_rate=device.baud_rate,
) )
return _device_to_response(device) return _device_to_response(device)
@@ -229,6 +232,7 @@ async def update_device(
url=update_data.url, url=update_data.url,
enabled=update_data.enabled, enabled=update_data.enabled,
led_count=update_data.led_count, led_count=update_data.led_count,
baud_rate=update_data.baud_rate,
) )
# Sync connection info in processor manager # Sync connection info in processor manager
@@ -237,6 +241,7 @@ async def update_device(
device_id, device_id,
device_url=update_data.url, device_url=update_data.url,
led_count=update_data.led_count, led_count=update_data.led_count,
baud_rate=update_data.baud_rate,
) )
except ValueError: except ValueError:
pass pass

View File

@@ -13,6 +13,7 @@ class DeviceCreate(BaseModel):
url: str = Field(description="Device URL (e.g., http://192.168.1.100 or COM3)") url: str = Field(description="Device URL (e.g., http://192.168.1.100 or COM3)")
device_type: str = Field(default="wled", description="LED device type (e.g., wled, adalight)") device_type: str = Field(default="wled", description="LED device type (e.g., wled, adalight)")
led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (required for adalight)") led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (required for adalight)")
baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)")
class DeviceUpdate(BaseModel): class DeviceUpdate(BaseModel):
@@ -22,6 +23,7 @@ class DeviceUpdate(BaseModel):
url: Optional[str] = Field(None, description="Device URL or serial port") url: Optional[str] = Field(None, description="Device URL or serial port")
enabled: Optional[bool] = Field(None, description="Whether device is enabled") enabled: Optional[bool] = Field(None, description="Whether device is enabled")
led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (for devices with manual_led_count capability)") led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (for devices with manual_led_count capability)")
baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)")
class Calibration(BaseModel): class Calibration(BaseModel):
@@ -87,6 +89,7 @@ class DeviceResponse(BaseModel):
device_type: str = Field(default="wled", description="LED device type") device_type: str = Field(default="wled", description="LED device type")
led_count: int = Field(description="Total number of LEDs") led_count: int = Field(description="Total number of LEDs")
enabled: bool = Field(description="Whether device is enabled") enabled: bool = Field(description="Whether device is enabled")
baud_rate: Optional[int] = Field(None, description="Serial baud rate")
capabilities: List[str] = Field(default_factory=list, description="Device type capabilities") capabilities: List[str] = Field(default_factory=list, description="Device type capabilities")
calibration: Optional[Calibration] = Field(None, description="Calibration configuration") calibration: Optional[Calibration] = Field(None, description="Calibration configuration")
created_at: datetime = Field(description="Creation timestamp") created_at: datetime = Field(description="Creation timestamp")

View File

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

View File

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

View File

@@ -155,6 +155,7 @@ async def lifespan(app: FastAPI):
led_count=device.led_count, led_count=device.led_count,
calibration=device.calibration, calibration=device.calibration,
device_type=device.device_type, device_type=device.device_type,
baud_rate=device.baud_rate,
) )
logger.info(f"Registered device: {device.name} ({device.id})") logger.info(f"Registered device: {device.name} ({device.id})")
except Exception as e: except Exception as e:

View File

@@ -763,11 +763,27 @@ async function showSettings(deviceId) {
ledCountGroup.style.display = 'none'; ledCountGroup.style.display = 'none';
} }
// Show baud rate field for adalight devices
const baudRateGroup = document.getElementById('settings-baud-rate-group');
if (isAdalight) {
baudRateGroup.style.display = '';
const baudSelect = document.getElementById('settings-baud-rate');
if (device.baud_rate) {
baudSelect.value = String(device.baud_rate);
} else {
baudSelect.value = '115200';
}
updateSettingsBaudFpsHint();
} else {
baudRateGroup.style.display = 'none';
}
// Snapshot initial values for dirty checking // Snapshot initial values for dirty checking
settingsInitialValues = { settingsInitialValues = {
name: device.name, name: device.name,
url: device.url, url: device.url,
led_count: String(device.led_count || ''), led_count: String(device.led_count || ''),
baud_rate: String(device.baud_rate || '115200'),
device_type: device.device_type, device_type: device.device_type,
capabilities: caps, capabilities: caps,
state_check_interval: '30', state_check_interval: '30',
@@ -838,12 +854,16 @@ async function saveDeviceSettings() {
} }
try { try {
// Update device info (name, url, optionally led_count) // Update device info (name, url, optionally led_count, baud_rate)
const body = { name, url }; const body = { name, url };
const ledCountInput = document.getElementById('settings-led-count'); const ledCountInput = document.getElementById('settings-led-count');
if ((settingsInitialValues.capabilities || []).includes('manual_led_count') && ledCountInput.value) { if ((settingsInitialValues.capabilities || []).includes('manual_led_count') && ledCountInput.value) {
body.led_count = parseInt(ledCountInput.value, 10); body.led_count = parseInt(ledCountInput.value, 10);
} }
if (settingsInitialValues.device_type === 'adalight') {
const baudVal = document.getElementById('settings-baud-rate').value;
if (baudVal) body.baud_rate = parseInt(baudVal, 10);
}
const deviceResponse = await fetch(`${API_BASE}/devices/${deviceId}`, { const deviceResponse = await fetch(`${API_BASE}/devices/${deviceId}`, {
method: 'PUT', method: 'PUT',
headers: getHeaders(), headers: getHeaders(),
@@ -930,12 +950,15 @@ function onDeviceTypeChanged() {
const ledCountGroup = document.getElementById('device-led-count-group'); const ledCountGroup = document.getElementById('device-led-count-group');
const discoverySection = document.getElementById('discovery-section'); const discoverySection = document.getElementById('discovery-section');
const baudRateGroup = document.getElementById('device-baud-rate-group');
if (deviceType === 'adalight') { if (deviceType === 'adalight') {
urlGroup.style.display = 'none'; urlGroup.style.display = 'none';
urlInput.removeAttribute('required'); urlInput.removeAttribute('required');
serialGroup.style.display = ''; serialGroup.style.display = '';
serialSelect.setAttribute('required', ''); serialSelect.setAttribute('required', '');
ledCountGroup.style.display = ''; ledCountGroup.style.display = '';
baudRateGroup.style.display = '';
// Hide discovery list — serial port dropdown replaces it // Hide discovery list — serial port dropdown replaces it
if (discoverySection) discoverySection.style.display = 'none'; if (discoverySection) discoverySection.style.display = 'none';
// Populate from cache or show placeholder (lazy-load on focus) // Populate from cache or show placeholder (lazy-load on focus)
@@ -949,12 +972,14 @@ function onDeviceTypeChanged() {
opt.disabled = true; opt.disabled = true;
serialSelect.appendChild(opt); serialSelect.appendChild(opt);
} }
updateBaudFpsHint();
} else { } else {
urlGroup.style.display = ''; urlGroup.style.display = '';
urlInput.setAttribute('required', ''); urlInput.setAttribute('required', '');
serialGroup.style.display = 'none'; serialGroup.style.display = 'none';
serialSelect.removeAttribute('required'); serialSelect.removeAttribute('required');
ledCountGroup.style.display = 'none'; ledCountGroup.style.display = 'none';
baudRateGroup.style.display = 'none';
// Show cached results or trigger scan for WLED // Show cached results or trigger scan for WLED
if (deviceType in _discoveryCache) { if (deviceType in _discoveryCache) {
_renderDiscoveryList(); _renderDiscoveryList();
@@ -964,6 +989,36 @@ function onDeviceTypeChanged() {
} }
} }
function _computeMaxFps(baudRate, ledCount) {
if (!baudRate || !ledCount || ledCount < 1) return null;
const bitsPerFrame = (ledCount * 3 + 6) * 10;
return Math.floor(baudRate / bitsPerFrame);
}
function _renderFpsHint(hintEl, baudRate, ledCount) {
const fps = _computeMaxFps(baudRate, ledCount);
if (fps !== null) {
hintEl.textContent = `Max FPS ≈ ${fps}`;
hintEl.style.display = '';
} else {
hintEl.style.display = 'none';
}
}
function updateBaudFpsHint() {
const hintEl = document.getElementById('baud-fps-hint');
const baudRate = parseInt(document.getElementById('device-baud-rate').value, 10);
const ledCount = parseInt(document.getElementById('device-led-count').value, 10);
_renderFpsHint(hintEl, baudRate, ledCount);
}
function updateSettingsBaudFpsHint() {
const hintEl = document.getElementById('settings-baud-fps-hint');
const baudRate = parseInt(document.getElementById('settings-baud-rate').value, 10);
const ledCount = parseInt(document.getElementById('settings-led-count').value, 10);
_renderFpsHint(hintEl, baudRate, ledCount);
}
function _renderDiscoveryList() { function _renderDiscoveryList() {
const selectedType = document.getElementById('device-type').value; const selectedType = document.getElementById('device-type').value;
const devices = _discoveryCache[selectedType]; const devices = _discoveryCache[selectedType];
@@ -1226,6 +1281,10 @@ async function handleAddDevice(event) {
if (ledCountInput && ledCountInput.value) { if (ledCountInput && ledCountInput.value) {
body.led_count = parseInt(ledCountInput.value, 10); body.led_count = parseInt(ledCountInput.value, 10);
} }
const baudRateSelect = document.getElementById('device-baud-rate');
if (deviceType === 'adalight' && baudRateSelect && baudRateSelect.value) {
body.baud_rate = parseInt(baudRateSelect.value, 10);
}
const lastTemplateId = localStorage.getItem('lastCaptureTemplateId'); const lastTemplateId = localStorage.getItem('lastCaptureTemplateId');
if (lastTemplateId) { if (lastTemplateId) {
body.capture_template_id = lastTemplateId; body.capture_template_id = lastTemplateId;

View File

@@ -28,7 +28,7 @@
🔑 <span data-i18n="auth.login">Login</span> 🔑 <span data-i18n="auth.login">Login</span>
</button> </button>
<button id="logout-btn" class="btn btn-danger" onclick="logout()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;"> <button id="logout-btn" class="btn btn-danger" onclick="logout()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;">
🚪 <span data-i18n="auth.logout">Logout</span> 🚪
</button> </button>
</div> </div>
</header> </header>
@@ -260,7 +260,25 @@
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button> <button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div> </div>
<small class="input-hint" style="display:none" data-i18n="device.led_count_manual.hint">Number of LEDs on the strip (must match your Arduino sketch)</small> <small class="input-hint" style="display:none" data-i18n="device.led_count_manual.hint">Number of LEDs on the strip (must match your Arduino sketch)</small>
<input type="number" id="settings-led-count" min="1" max="10000"> <input type="number" id="settings-led-count" min="1" max="10000" oninput="updateSettingsBaudFpsHint()">
</div>
<div class="form-group" id="settings-baud-rate-group" style="display: none;">
<div class="label-row">
<label for="settings-baud-rate" data-i18n="device.baud_rate">Baud Rate:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="device.baud_rate.hint">Serial communication speed. Higher = more FPS but requires matching Arduino sketch.</small>
<select id="settings-baud-rate" onchange="updateSettingsBaudFpsHint()">
<option value="115200">115200</option>
<option value="230400">230400</option>
<option value="460800">460800</option>
<option value="500000">500000</option>
<option value="921600">921600</option>
<option value="1000000">1000000</option>
<option value="1500000">1500000</option>
<option value="2000000">2000000</option>
</select>
<small id="settings-baud-fps-hint" class="fps-hint" style="display:none"></small>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -641,7 +659,25 @@
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button> <button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div> </div>
<small class="input-hint" style="display:none" data-i18n="device.led_count_manual.hint">Number of LEDs on the strip (must match your Arduino sketch)</small> <small class="input-hint" style="display:none" data-i18n="device.led_count_manual.hint">Number of LEDs on the strip (must match your Arduino sketch)</small>
<input type="number" id="device-led-count" min="1" max="10000" placeholder="60"> <input type="number" id="device-led-count" min="1" max="10000" placeholder="60" oninput="updateBaudFpsHint()">
</div>
<div class="form-group" id="device-baud-rate-group" style="display: none;">
<div class="label-row">
<label for="device-baud-rate" data-i18n="device.baud_rate">Baud Rate:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="device.baud_rate.hint">Serial communication speed. Higher = more FPS but requires matching Arduino sketch.</small>
<select id="device-baud-rate" onchange="updateBaudFpsHint()">
<option value="115200">115200</option>
<option value="230400">230400</option>
<option value="460800">460800</option>
<option value="500000">500000</option>
<option value="921600">921600</option>
<option value="1000000">1000000</option>
<option value="1500000">1500000</option>
<option value="2000000">2000000</option>
</select>
<small id="baud-fps-hint" class="fps-hint" style="display:none"></small>
</div> </div>
<div id="add-device-error" class="error-message" style="display: none;"></div> <div id="add-device-error" class="error-message" style="display: none;"></div>
</form> </form>

View File

@@ -112,6 +112,8 @@
"device.serial_port.hint": "Select the COM port of the Adalight device", "device.serial_port.hint": "Select the COM port of the Adalight device",
"device.serial_port.none": "No serial ports found", "device.serial_port.none": "No serial ports found",
"device.led_count_manual.hint": "Number of LEDs on the strip (must match your Arduino sketch)", "device.led_count_manual.hint": "Number of LEDs on the strip (must match your Arduino sketch)",
"device.baud_rate": "Baud Rate:",
"device.baud_rate.hint": "Serial communication speed. Higher = more FPS but requires matching Arduino sketch.",
"device.url.hint": "IP address or hostname of the device (e.g. http://192.168.1.100)", "device.url.hint": "IP address or hostname of the device (e.g. http://192.168.1.100)",
"device.name": "Device Name:", "device.name": "Device Name:",
"device.name.placeholder": "Living Room TV", "device.name.placeholder": "Living Room TV",

View File

@@ -112,6 +112,8 @@
"device.serial_port.hint": "Выберите COM порт устройства Adalight", "device.serial_port.hint": "Выберите COM порт устройства Adalight",
"device.serial_port.none": "Серийные порты не найдены", "device.serial_port.none": "Серийные порты не найдены",
"device.led_count_manual.hint": "Количество светодиодов на ленте (должно совпадать с вашим скетчем Arduino)", "device.led_count_manual.hint": "Количество светодиодов на ленте (должно совпадать с вашим скетчем Arduino)",
"device.baud_rate": "Скорость порта:",
"device.baud_rate.hint": "Скорость серийного соединения. Выше = больше FPS, но требует соответствия скетчу Arduino.",
"device.url.hint": "IP адрес или имя хоста устройства (напр. http://192.168.1.100)", "device.url.hint": "IP адрес или имя хоста устройства (напр. http://192.168.1.100)",
"device.name": "Имя Устройства:", "device.name": "Имя Устройства:",
"device.name.placeholder": "ТВ в Гостиной", "device.name.placeholder": "ТВ в Гостиной",

View File

@@ -1160,6 +1160,13 @@ input:-webkit-autofill:focus {
font-size: 0.85rem; font-size: 0.85rem;
} }
.fps-hint {
display: block;
margin-top: 4px;
font-size: 0.82rem;
color: var(--info-color, #2196F3);
}
.slider-row { .slider-row {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -32,6 +32,7 @@ class Device:
led_count: int, led_count: int,
enabled: bool = True, enabled: bool = True,
device_type: str = "wled", device_type: str = "wled",
baud_rate: Optional[int] = None,
calibration: Optional[CalibrationConfig] = None, calibration: Optional[CalibrationConfig] = None,
created_at: Optional[datetime] = None, created_at: Optional[datetime] = None,
updated_at: Optional[datetime] = None, updated_at: Optional[datetime] = None,
@@ -42,13 +43,14 @@ class Device:
self.led_count = led_count self.led_count = led_count
self.enabled = enabled self.enabled = enabled
self.device_type = device_type self.device_type = device_type
self.baud_rate = baud_rate
self.calibration = calibration or create_default_calibration(led_count) self.calibration = calibration or create_default_calibration(led_count)
self.created_at = created_at or datetime.utcnow() self.created_at = created_at or datetime.utcnow()
self.updated_at = updated_at or datetime.utcnow() self.updated_at = updated_at or datetime.utcnow()
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""Convert device to dictionary.""" """Convert device to dictionary."""
return { d = {
"id": self.id, "id": self.id,
"name": self.name, "name": self.name,
"url": self.url, "url": self.url,
@@ -59,6 +61,9 @@ class Device:
"created_at": self.created_at.isoformat(), "created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(), "updated_at": self.updated_at.isoformat(),
} }
if self.baud_rate is not None:
d["baud_rate"] = self.baud_rate
return d
@classmethod @classmethod
def from_dict(cls, data: dict) -> "Device": def from_dict(cls, data: dict) -> "Device":
@@ -81,6 +86,7 @@ class Device:
led_count=data["led_count"], led_count=data["led_count"],
enabled=data.get("enabled", True), enabled=data.get("enabled", True),
device_type=data.get("device_type", "wled"), device_type=data.get("device_type", "wled"),
baud_rate=data.get("baud_rate"),
calibration=calibration, calibration=calibration,
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())), created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())), updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
@@ -165,6 +171,7 @@ class DeviceStore:
url: str, url: str,
led_count: int, led_count: int,
device_type: str = "wled", device_type: str = "wled",
baud_rate: Optional[int] = None,
calibration: Optional[CalibrationConfig] = None, calibration: Optional[CalibrationConfig] = None,
) -> Device: ) -> Device:
"""Create a new device.""" """Create a new device."""
@@ -176,6 +183,7 @@ class DeviceStore:
url=url, url=url,
led_count=led_count, led_count=led_count,
device_type=device_type, device_type=device_type,
baud_rate=baud_rate,
calibration=calibration, calibration=calibration,
) )
@@ -200,6 +208,7 @@ class DeviceStore:
url: Optional[str] = None, url: Optional[str] = None,
led_count: Optional[int] = None, led_count: Optional[int] = None,
enabled: Optional[bool] = None, enabled: Optional[bool] = None,
baud_rate: Optional[int] = None,
calibration: Optional[CalibrationConfig] = None, calibration: Optional[CalibrationConfig] = None,
) -> Device: ) -> Device:
"""Update device.""" """Update device."""
@@ -216,6 +225,8 @@ class DeviceStore:
device.calibration = create_default_calibration(led_count) device.calibration = create_default_calibration(led_count)
if enabled is not None: if enabled is not None:
device.enabled = enabled device.enabled = enabled
if baud_rate is not None:
device.baud_rate = baud_rate
if calibration is not None: if calibration is not None:
if calibration.get_total_leds() != device.led_count: if calibration.get_total_leds() != device.led_count:
raise ValueError( raise ValueError(