Animation None option, FPS min 1, serial COM lifecycle fixes

- Replace animation Enable checkbox with None option in effect selector;
  show effect description tooltip; disable speed slider when None selected
- Allow target FPS range 1-90 (was 10-90) across UI and backend validation
- Scope serial COM connections to target lifetime (no idle caching);
  use temporary connections for power-off/test mode
- Fix serial black frame on stop: flush after write, delay after task
  cancel to prevent race with in-flight thread pool write

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 04:33:56 +03:00
parent 8a0730d91b
commit ee52e2d98f
15 changed files with 126 additions and 71 deletions

View File

@@ -203,6 +203,14 @@
font-size: 0.85rem;
}
.field-desc {
display: block;
margin: 4px 0 0 0;
color: #888;
font-size: 0.82rem;
font-style: italic;
}
.fps-hint {
display: block;
margin-top: 4px;

View File

@@ -89,7 +89,7 @@ import {
// Layer 5: color-strip sources
import {
showCSSEditor, closeCSSEditorModal, forceCSSEditorClose, saveCSSEditor, deleteColorStrip,
onCSSTypeChange, colorCycleAddColor, colorCycleRemoveColor,
onCSSTypeChange, onAnimationTypeChange, colorCycleAddColor, colorCycleRemoveColor,
} from './features/color-strips.js';
// Layer 5: calibration
@@ -274,6 +274,7 @@ Object.assign(window, {
saveCSSEditor,
deleteColorStrip,
onCSSTypeChange,
onAnimationTypeChange,
colorCycleAddColor,
colorCycleRemoveColor,

View File

@@ -27,7 +27,6 @@ class CSSEditorModal extends Modal {
frame_interpolation: document.getElementById('css-editor-frame-interpolation').checked,
led_count: document.getElementById('css-editor-led-count').value,
gradient_stops: type === 'gradient' ? JSON.stringify(_gradientStops) : '[]',
animation_enabled: document.getElementById('css-editor-animation-enabled').checked,
animation_type: document.getElementById('css-editor-animation-type').value,
animation_speed: document.getElementById('css-editor-animation-speed').value,
cycle_speed: document.getElementById('css-editor-cycle-speed').value,
@@ -50,9 +49,10 @@ export function onCSSTypeChange() {
// Animation section — shown for static/gradient only (color_cycle is always animating)
const animSection = document.getElementById('css-editor-animation-section');
const animTypeSelect = document.getElementById('css-editor-animation-type');
const noneOpt = `<option value="none">${t('color_strip.animation.type.none')}</option>`;
if (type === 'static') {
animSection.style.display = '';
animTypeSelect.innerHTML =
animTypeSelect.innerHTML = noneOpt +
`<option value="breathing">${t('color_strip.animation.type.breathing')}</option>` +
`<option value="strobe">${t('color_strip.animation.type.strobe')}</option>` +
`<option value="sparkle">${t('color_strip.animation.type.sparkle')}</option>` +
@@ -61,7 +61,7 @@ export function onCSSTypeChange() {
`<option value="rainbow_fade">${t('color_strip.animation.type.rainbow_fade')}</option>`;
} else if (type === 'gradient') {
animSection.style.display = '';
animTypeSelect.innerHTML =
animTypeSelect.innerHTML = noneOpt +
`<option value="breathing">${t('color_strip.animation.type.breathing')}</option>` +
`<option value="gradient_shift">${t('color_strip.animation.type.gradient_shift')}</option>` +
`<option value="wave">${t('color_strip.animation.type.wave')}</option>` +
@@ -73,6 +73,7 @@ export function onCSSTypeChange() {
} else {
animSection.style.display = 'none';
}
_syncAnimationSpeedState();
if (type === 'gradient') {
requestAnimationFrame(() => gradientRenderAll());
@@ -80,22 +81,41 @@ export function onCSSTypeChange() {
}
function _getAnimationPayload() {
const type = document.getElementById('css-editor-animation-type').value;
return {
enabled: document.getElementById('css-editor-animation-enabled').checked,
type: document.getElementById('css-editor-animation-type').value,
enabled: type !== 'none',
type: type !== 'none' ? type : 'breathing',
speed: parseFloat(document.getElementById('css-editor-animation-speed').value),
};
}
function _loadAnimationState(anim) {
document.getElementById('css-editor-animation-enabled').checked = !!(anim && anim.enabled);
const speedEl = document.getElementById('css-editor-animation-speed');
speedEl.value = (anim && anim.speed != null) ? anim.speed : 1.0;
document.getElementById('css-editor-animation-speed-val').textContent =
parseFloat(speedEl.value).toFixed(1);
// Set type after onCSSTypeChange() has populated the dropdown
if (anim && anim.type) {
if (anim && anim.enabled && anim.type) {
document.getElementById('css-editor-animation-type').value = anim.type;
} else {
document.getElementById('css-editor-animation-type').value = 'none';
}
_syncAnimationSpeedState();
}
export function onAnimationTypeChange() {
_syncAnimationSpeedState();
}
function _syncAnimationSpeedState() {
const type = document.getElementById('css-editor-animation-type').value;
const isNone = type === 'none';
document.getElementById('css-editor-animation-speed').disabled = isNone;
const descEl = document.getElementById('css-editor-animation-type-desc');
if (descEl) {
const desc = t('color_strip.animation.type.' + type + '.desc') || '';
descEl.textContent = desc;
descEl.style.display = desc ? '' : 'none';
}
}

View File

@@ -258,7 +258,7 @@
"streams.capture_template": "Engine Template:",
"streams.capture_template.hint": "Engine template defining how the screen is captured",
"streams.target_fps": "Target FPS:",
"streams.target_fps.hint": "Target frames per second for capture (10-90)",
"streams.target_fps.hint": "Target frames per second for capture (1-90)",
"streams.source": "Source:",
"streams.source.hint": "The source to apply processing filters to",
"streams.pp_template": "Filter Template:",
@@ -361,7 +361,7 @@
"targets.source.hint": "Which picture source to capture and process",
"targets.source.none": "-- No source assigned --",
"targets.fps": "Target FPS:",
"targets.fps.hint": "Target frames per second for capture and LED updates (10-90)",
"targets.fps.hint": "Target frames per second for capture and LED updates (1-90)",
"targets.border_width": "Border Width (px):",
"targets.border_width.hint": "How many pixels from the screen edge to sample for LED colors (1-100)",
"targets.interpolation": "Interpolation Mode:",
@@ -589,19 +589,26 @@
"color_strip.gradient.bidir.hint": "Add a second color on the right side of this stop to create a hard edge in the gradient.",
"color_strip.gradient.min_stops": "Gradient must have at least 2 stops",
"color_strip.animation": "Animation",
"color_strip.animation.enabled": "Enable Animation:",
"color_strip.animation.enabled.hint": "Enables procedural animation. The LEDs will update at 30 fps driven by the selected effect.",
"color_strip.animation.type": "Effect:",
"color_strip.animation.type.hint": "The animation effect to apply. Breathing, Strobe, Sparkle, Pulse, Candle, and Rainbow Fade work for both static and gradient sources; Gradient Shift and Wave are gradient-only.",
"color_strip.animation.type.hint": "Animation effect to apply.",
"color_strip.animation.type.none": "None (no animation effect)",
"color_strip.animation.type.breathing": "Breathing",
"color_strip.animation.type.breathing.desc": "Smooth brightness fade in and out",
"color_strip.animation.type.color_cycle": "Color Cycle",
"color_strip.animation.type.gradient_shift": "Gradient Shift",
"color_strip.animation.type.gradient_shift.desc": "Slides the gradient along the strip",
"color_strip.animation.type.wave": "Wave",
"color_strip.animation.type.wave.desc": "Sinusoidal brightness wave moving along the strip",
"color_strip.animation.type.strobe": "Strobe",
"color_strip.animation.type.strobe.desc": "Rapid on/off flashing",
"color_strip.animation.type.sparkle": "Sparkle",
"color_strip.animation.type.sparkle.desc": "Random LEDs flash briefly",
"color_strip.animation.type.pulse": "Pulse",
"color_strip.animation.type.pulse.desc": "Sharp brightness pulse with quick fade",
"color_strip.animation.type.candle": "Candle",
"color_strip.animation.type.candle.desc": "Warm flickering candle-like glow",
"color_strip.animation.type.rainbow_fade": "Rainbow Fade",
"color_strip.animation.type.rainbow_fade.desc": "Cycles through the entire hue spectrum",
"color_strip.animation.speed": "Speed:",
"color_strip.animation.speed.hint": "Animation speed multiplier. 1.0 ≈ one cycle per second for Breathing; higher values cycle faster.",
"color_strip.color_cycle.colors": "Colors:",

View File

@@ -258,7 +258,7 @@
"streams.capture_template": "Шаблон Движка:",
"streams.capture_template.hint": "Шаблон движка, определяющий способ захвата экрана",
"streams.target_fps": "Целевой FPS:",
"streams.target_fps.hint": "Целевое количество кадров в секунду (10-90)",
"streams.target_fps.hint": "Целевое количество кадров в секунду (1-90)",
"streams.source": "Источник:",
"streams.source.hint": "Источник, к которому применяются фильтры обработки",
"streams.pp_template": "Шаблон Фильтра:",
@@ -361,7 +361,7 @@
"targets.source.hint": "Какой источник изображения захватывать и обрабатывать",
"targets.source.none": "-- Источник не назначен --",
"targets.fps": "Целевой FPS:",
"targets.fps.hint": "Целевая частота кадров для захвата и обновления LED (10-90)",
"targets.fps.hint": "Целевая частота кадров для захвата и обновления LED (1-90)",
"targets.border_width": "Ширина границы (px):",
"targets.border_width.hint": "Сколько пикселей от края экрана выбирать для цвета LED (1-100)",
"targets.interpolation": "Режим интерполяции:",
@@ -589,19 +589,26 @@
"color_strip.gradient.bidir.hint": "Добавить второй цвет справа от этой остановки для создания резкого перехода в градиенте.",
"color_strip.gradient.min_stops": "Градиент должен содержать не менее 2 остановок",
"color_strip.animation": "Анимация",
"color_strip.animation.enabled": "Включить анимацию:",
"color_strip.animation.enabled.hint": "Включает процедурную анимацию. Светодиоды обновляются со скоростью 30 кадров в секунду по выбранному эффекту.",
"color_strip.animation.type": "Эффект:",
"color_strip.animation.type.hint": "Эффект анимации. Дыхание, стробоскоп, искры, пульс, свеча и радужный перелив работают для статического цвета и градиента; сдвиг градиента и волна — только для градиентов.",
"color_strip.animation.type.hint": "Эффект анимации.",
"color_strip.animation.type.none": "Нет (без эффекта анимации)",
"color_strip.animation.type.breathing": "Дыхание",
"color_strip.animation.type.breathing.desc": "Плавное угасание и нарастание яркости",
"color_strip.animation.type.color_cycle": "Смена цвета",
"color_strip.animation.type.gradient_shift": "Сдвиг градиента",
"color_strip.animation.type.gradient_shift.desc": "Сдвигает градиент вдоль ленты",
"color_strip.animation.type.wave": "Волна",
"color_strip.animation.type.wave.desc": "Синусоидальная волна яркости вдоль ленты",
"color_strip.animation.type.strobe": "Стробоскоп",
"color_strip.animation.type.strobe.desc": "Быстрое мигание вкл/выкл",
"color_strip.animation.type.sparkle": "Искры",
"color_strip.animation.type.sparkle.desc": "Случайные светодиоды кратковременно вспыхивают",
"color_strip.animation.type.pulse": "Пульс",
"color_strip.animation.type.pulse.desc": "Резкая вспышка яркости с быстрым затуханием",
"color_strip.animation.type.candle": "Свеча",
"color_strip.animation.type.candle.desc": "Тёплое мерцание, как у свечи",
"color_strip.animation.type.rainbow_fade": "Радужный перелив",
"color_strip.animation.type.rainbow_fade.desc": "Циклический переход по всему спектру оттенков",
"color_strip.animation.speed": "Скорость:",
"color_strip.animation.speed.hint": "Множитель скорости анимации. 1.0 ≈ один цикл в секунду для дыхания; большие значения ускоряют анимацию.",
"color_strip.color_cycle.colors": "Цвета:",