Stabilize control surface and external bridge v1

This commit is contained in:
jan
2026-04-20 01:13:27 +02:00
parent a56cecb23d
commit 07c52db5fb
29 changed files with 8818 additions and 1510 deletions

View File

@@ -1,11 +1,11 @@
use crate::{
control::{
CatalogSnapshot, CatalogSource, CreativeSnapshotSummary, GroupSummary, HostCommandError,
PanelTarget, PresetSummary, SceneTransitionStyle,
OutputBackendMode, PanelTarget, PresetSummary, SceneTransitionStyle,
},
scene::{PatternRegistry, SceneRuntime},
};
use infinity_config::{PanelPosition, ProjectConfig};
use infinity_config::{ColorOrder, DriverKind, LedDirection, PanelPosition, ProjectConfig};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, BTreeSet},
@@ -59,16 +59,64 @@ impl Default for PersistedGlobalState {
Self {
blackout: false,
master_brightness: 0.20,
transition_duration_ms: 150,
transition_duration_ms: 2_000,
transition_style: SceneTransitionStyle::Crossfade,
}
}
}
fn default_output_fps() -> u16 {
40
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PersistedNodeState {
pub node_id: String,
pub reserved_ip: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PersistedPanelState {
pub target: PanelTarget,
pub physical_output_name: String,
pub driver_kind: DriverKind,
pub driver_reference: String,
pub led_count: u16,
pub direction: LedDirection,
pub color_order: ColorOrder,
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PersistedTechnicalState {
pub backend_mode: OutputBackendMode,
pub output_enabled: bool,
#[serde(default = "default_output_fps")]
pub output_fps: u16,
#[serde(default)]
pub nodes: Vec<PersistedNodeState>,
#[serde(default)]
pub panels: Vec<PersistedPanelState>,
}
impl Default for PersistedTechnicalState {
fn default() -> Self {
Self {
backend_mode: OutputBackendMode::PreviewOnly,
output_enabled: false,
output_fps: default_output_fps(),
nodes: Vec::new(),
panels: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct PersistedRuntimeState {
pub active_scene: Option<SceneRuntime>,
pub global: PersistedGlobalState,
#[serde(default)]
pub technical: PersistedTechnicalState,
pub user_presets: Vec<StoredPreset>,
pub user_groups: Vec<StoredGroup>,
pub creative_snapshots: Vec<StoredCreativeSnapshot>,
@@ -467,6 +515,36 @@ impl ShowStore {
Ok(())
}
pub fn delete_preset(&mut self, preset_id: &str) -> Result<(), HostCommandError> {
if preset_id.trim().is_empty() {
return Err(HostCommandError::new(
"invalid_preset_id",
"preset_id must not be empty",
));
}
let preset_index = self
.presets
.iter()
.position(|preset| preset.preset_id == preset_id)
.ok_or_else(|| {
HostCommandError::new(
"unknown_preset",
format!("preset '{preset_id}' does not exist"),
)
})?;
if self.presets[preset_index].source != CatalogSource::RuntimeUser {
return Err(HostCommandError::new(
"preset_delete_forbidden",
format!("preset '{preset_id}' is built-in and cannot be deleted"),
));
}
self.presets.remove(preset_index);
Ok(())
}
pub fn save_creative_snapshot(
&mut self,
snapshot_id: &str,
@@ -554,10 +632,12 @@ impl ShowStore {
&self,
active_scene: &SceneRuntime,
global: PersistedGlobalState,
technical: PersistedTechnicalState,
) -> PersistedRuntimeState {
PersistedRuntimeState {
active_scene: Some(active_scene.clone()),
global,
technical,
user_presets: self
.presets
.iter()
@@ -663,6 +743,45 @@ mod tests {
assert!(store.recall_creative_snapshot("variant_a").is_some());
}
#[test]
fn runtime_presets_can_be_deleted_but_builtins_cannot() {
let registry = PatternRegistry::new();
let mut store = ShowStore::from_project(&sample_project(), &registry);
let scene = registry.scene_for_pattern(
"noise",
None,
Some("top_panels".to_string()),
31,
vec!["#AA8844".to_string()],
false,
);
store
.save_preset_from_scene(
"runtime_delete_me",
&scene,
210,
SceneTransitionStyle::Crossfade,
false,
)
.expect("runtime preset save should succeed");
store
.delete_preset("runtime_delete_me")
.expect("runtime preset delete should succeed");
assert!(store.scene_from_preset_id("runtime_delete_me").is_none());
let built_in_preset_id = store
.catalog(&registry)
.presets
.iter()
.find(|preset| preset.source == CatalogSource::BuiltIn)
.map(|preset| preset.preset_id.clone())
.expect("sample project should contain built-in presets");
let delete_error = store
.delete_preset(&built_in_preset_id)
.expect_err("built-in presets should not be deletable");
assert_eq!(delete_error.code, "preset_delete_forbidden");
}
#[test]
fn runtime_state_storage_roundtrip_preserves_scene_and_library() {
let registry = PatternRegistry::new();
@@ -701,6 +820,7 @@ mod tests {
transition_duration_ms: 220,
transition_style: SceneTransitionStyle::Chase,
},
PersistedTechnicalState::default(),
);
storage.save(&runtime).expect("save should work");
let loaded = storage.load().expect("load should work");