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:
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "ТВ в Гостиной",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user