Add overlay toggle to calibration dialog, fix serial reconnect on edge test

Add a 💡 button in the calibration modal header (CSS mode only) that
toggles the LED overlay visualization. Auto-stops overlay on modal close
if started from the dialog. Checks and reflects current overlay status
on modal open.

Fix serial devices creating a new connection on every edge test toggle,
which triggered Arduino bootloader resets. Now reuses the cached idle
client for all device types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 17:22:15 +03:00
parent 67a15776b2
commit a6253e8d96
4 changed files with 70 additions and 24 deletions

View File

@@ -641,32 +641,15 @@ class ProcessorManager:
pixels = [(0, 0, 0)] * ds.led_count pixels = [(0, 0, 0)] * ds.led_count
await self._send_pixels_to_device(device_id, pixels) await self._send_pixels_to_device(device_id, pixels)
def _is_serial_device(self, device_id: str) -> bool:
"""Check if a device uses a serial (COM) connection."""
ds = self._devices.get(device_id)
return ds is not None and ds.device_type not in ("wled",)
async def _send_pixels_to_device(self, device_id: str, pixels) -> None: async def _send_pixels_to_device(self, device_id: str, pixels) -> None:
"""Send pixels to a device. """Send pixels to a device via cached idle client.
Serial devices: temporary connection (open, send, close). Reuses a cached connection to avoid repeated serial reconnections
WLED devices: cached idle client. (which trigger Arduino bootloader reset on Adalight devices).
""" """
ds = self._devices[device_id]
try: try:
if self._is_serial_device(device_id): client = await self._get_idle_client(device_id)
client = create_led_client( await client.send_pixels(pixels)
ds.device_type, ds.device_url,
led_count=ds.led_count, baud_rate=ds.baud_rate,
)
try:
await client.connect()
await client.send_pixels(pixels)
finally:
await client.close()
else:
client = await self._get_idle_client(device_id)
await client.send_pixels(pixels)
except Exception as e: except Exception as e:
logger.error(f"Failed to send pixels to {device_id}: {e}") logger.error(f"Failed to send pixels to {device_id}: {e}")

View File

@@ -120,7 +120,7 @@ import {
showCalibration, closeCalibrationModal, forceCloseCalibrationModal, saveCalibration, showCalibration, closeCalibrationModal, forceCloseCalibrationModal, saveCalibration,
updateOffsetSkipLock, updateCalibrationPreview, updateOffsetSkipLock, updateCalibrationPreview,
setStartPosition, toggleEdgeInputs, toggleDirection, toggleTestEdge, setStartPosition, toggleEdgeInputs, toggleDirection, toggleTestEdge,
showCSSCalibration, showCSSCalibration, toggleCalibrationOverlay,
} from './features/calibration.js'; } from './features/calibration.js';
// Layer 6: tabs // Layer 6: tabs
@@ -345,6 +345,7 @@ Object.assign(window, {
toggleDirection, toggleDirection,
toggleTestEdge, toggleTestEdge,
showCSSCalibration, showCSSCalibration,
toggleCalibrationOverlay,
// tabs // tabs
switchTab, switchTab,

View File

@@ -9,6 +9,7 @@ import { API_BASE, getHeaders, fetchWithAuth } from '../core/api.js';
import { showToast } from '../core/ui.js'; import { showToast } from '../core/ui.js';
import { Modal } from '../core/modal.js'; import { Modal } from '../core/modal.js';
import { closeTutorial, startCalibrationTutorial } from './tutorials.js'; import { closeTutorial, startCalibrationTutorial } from './tutorials.js';
import { startCSSOverlay, stopCSSOverlay } from './color-strips.js';
/* ── CalibrationModal subclass ────────────────────────────────── */ /* ── CalibrationModal subclass ────────────────────────────────── */
@@ -37,6 +38,11 @@ class CalibrationModal extends Modal {
onForceClose() { onForceClose() {
closeTutorial(); closeTutorial();
if (_isCSS()) { if (_isCSS()) {
const cssId = document.getElementById('calibration-css-id')?.value;
if (_overlayStartedHere && cssId) {
stopCSSOverlay(cssId);
_overlayStartedHere = false;
}
_clearCSSTestMode(); _clearCSSTestMode();
document.getElementById('calibration-css-id').value = ''; document.getElementById('calibration-css-id').value = '';
const testGroup = document.getElementById('calibration-css-test-group'); const testGroup = document.getElementById('calibration-css-test-group');
@@ -55,6 +61,7 @@ const calibModal = new CalibrationModal();
let _dragRaf = null; let _dragRaf = null;
let _previewRaf = null; let _previewRaf = null;
let _overlayStartedHere = false;
/* ── Helpers ──────────────────────────────────────────────────── */ /* ── Helpers ──────────────────────────────────────────────────── */
@@ -83,6 +90,50 @@ async function _clearCSSTestMode() {
} }
} }
function _setOverlayBtnActive(active) {
const btn = document.getElementById('calibration-overlay-btn');
if (!btn) return;
if (active) {
btn.style.background = 'var(--primary-color)';
btn.style.color = 'white';
} else {
btn.style.background = '';
btn.style.color = '';
}
}
async function _checkOverlayStatus(cssId) {
try {
const resp = await fetchWithAuth(`/color-strip-sources/${cssId}/overlay/status`);
if (resp.ok) {
const data = await resp.json();
_setOverlayBtnActive(data.active);
}
} catch { /* ignore */ }
}
export async function toggleCalibrationOverlay() {
const cssId = document.getElementById('calibration-css-id')?.value;
if (!cssId) return;
try {
const resp = await fetchWithAuth(`/color-strip-sources/${cssId}/overlay/status`);
if (!resp.ok) return;
const { active } = await resp.json();
if (active) {
await stopCSSOverlay(cssId);
_setOverlayBtnActive(false);
_overlayStartedHere = false;
} else {
await startCSSOverlay(cssId);
_setOverlayBtnActive(true);
_overlayStartedHere = true;
}
} catch (err) {
if (err.isAuth) return;
console.error('Failed to toggle calibration overlay:', err);
}
}
/* ── Public API (exported names unchanged) ────────────────────── */ /* ── Public API (exported names unchanged) ────────────────────── */
export async function showCalibration(deviceId) { export async function showCalibration(deviceId) {
@@ -114,6 +165,8 @@ export async function showCalibration(deviceId) {
document.getElementById('calibration-device-id').value = device.id; document.getElementById('calibration-device-id').value = device.id;
document.getElementById('cal-device-led-count-inline').textContent = device.led_count; document.getElementById('cal-device-led-count-inline').textContent = device.led_count;
document.getElementById('cal-css-led-count-group').style.display = 'none'; document.getElementById('cal-css-led-count-group').style.display = 'none';
document.getElementById('calibration-overlay-btn').style.display = 'none';
document.getElementById('calibration-tutorial-btn').style.marginLeft = '';
document.getElementById('cal-start-position').value = calibration.start_position; document.getElementById('cal-start-position').value = calibration.start_position;
document.getElementById('cal-layout').value = calibration.layout; document.getElementById('cal-layout').value = calibration.layout;
@@ -267,6 +320,14 @@ export async function showCSSCalibration(cssId) {
calibModal.snapshot(); calibModal.snapshot();
calibModal.open(); calibModal.open();
// Show overlay toggle and check current status
_overlayStartedHere = false;
const overlayBtn = document.getElementById('calibration-overlay-btn');
overlayBtn.style.display = '';
document.getElementById('calibration-tutorial-btn').style.marginLeft = '0';
_setOverlayBtnActive(false);
_checkOverlayStatus(cssId);
initSpanDrag(); initSpanDrag();
requestAnimationFrame(() => renderCalibrationCanvas()); requestAnimationFrame(() => renderCalibrationCanvas());

View File

@@ -3,7 +3,8 @@
<div class="modal-content" style="max-width: 700px;"> <div class="modal-content" style="max-width: 700px;">
<div class="modal-header"> <div class="modal-header">
<h2 id="calibration-modal-title" data-i18n="calibration.title">📐 LED Calibration</h2> <h2 id="calibration-modal-title" data-i18n="calibration.title">📐 LED Calibration</h2>
<button class="tutorial-trigger-btn" onclick="startCalibrationTutorial()" data-i18n-title="calibration.tutorial.start" title="Start tutorial">?</button> <button id="calibration-overlay-btn" class="tutorial-trigger-btn" onclick="toggleCalibrationOverlay()" data-i18n-title="overlay.button.show" title="Show overlay visualization" style="display:none">&#x1F4A1;</button>
<button id="calibration-tutorial-btn" class="tutorial-trigger-btn" onclick="startCalibrationTutorial()" data-i18n-title="calibration.tutorial.start" title="Start tutorial">?</button>
<button class="modal-close-btn" onclick="closeCalibrationModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button> <button class="modal-close-btn" onclick="closeCalibrationModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">