Der nächste Layer ist jetzt als echte gemeinsame Außenkante umgesetzt. Das neue API-Crate in [server.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host_api/src/server.rs>), [dto.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host_api/src/dto.rs>) und [main.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host_api/src/main.rs>) stellt die geforderten versionierten Endpunkte bereit: GET /api/v1/snapshot, GET /api/v1/catalog, GET /api/v1/presets, GET /api/v1/groups, POST /api/v1/command und WS /api/v1/stream. Die API trennt jetzt sauber zwischen Command-, State-, Preview- und Event-Modell, inklusive stabiler Außen-Enums und dokumentierten Fehlerobjekten statt eines unreflektierten 1:1-Core-Leaks.

Darauf sitzt die erste Web-UI V1 direkt gegen diese API in [index.html](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/web/v1/index.html>), [app.js](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/web/v1/app.js>) und [styles.css](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/web/v1/styles.css>). Sie enthält Pattern-/Preset-Auswahl, globale Regler, Gruppenauswahl, Übergänge, Preview, Snapshot-Anzeige und Event-Feed. Die technische Desktop-GUI bleibt bewusst die Engineering-/Diagnoseoberfläche; die kreative Web-UI baut nicht an einer Parallelarchitektur vorbei.

Contract-Tests liegen in [contract.rs](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/crates/infinity_host_api/tests/contract.rs>), und die API ist mit Beispiel-Responses in [docs/host_api.md](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/docs/host_api.md>) dokumentiert. Zusätzlich habe ich [README.md](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/README.md>), [docs/build_and_deploy.md](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/docs/build_and_deploy.md>) und [docs/architecture.md](</C:/Users/janni/Documents/RFP/Infinity_Vis _Rust/docs/architecture.md>) auf den neuen Stand gebracht.

Verifiziert habe ich:
- `cargo check -p infinity_host_api`
- `cargo test -p infinity_host_api`
- `cargo test -q`

Nicht verifiziert habe ich eine separate JS-Syntaxprüfung mit `node --check`, weil `node` in dieser Umgebung nicht installiert ist.
This commit is contained in:
2026-04-17 11:58:07 +02:00
parent 9457666fd6
commit a37a3c5cbe
16 changed files with 3166 additions and 55 deletions

View File

@@ -0,0 +1,745 @@
use infinity_config::{ColorOrder, LedDirection, PanelPosition, ValidationState};
use infinity_host::{
HostCommand, HostSnapshot, NodeConnectionState, PreviewSource, SceneParameterKind,
SceneParameterValue, SceneTransitionStyle, TestPatternKind,
};
use serde::{Deserialize, Serialize};
pub const API_VERSION: &str = "v1";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiSnapshotResponse {
pub api_version: &'static str,
pub generated_at_millis: u64,
pub state: ApiStateSnapshot,
pub preview: ApiPreviewSnapshot,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiCatalogResponse {
pub api_version: &'static str,
pub patterns: Vec<ApiPatternCatalogEntry>,
pub presets: Vec<ApiPresetSummary>,
pub groups: Vec<ApiGroupSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiPresetListResponse {
pub api_version: &'static str,
pub presets: Vec<ApiPresetSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiGroupListResponse {
pub api_version: &'static str,
pub groups: Vec<ApiGroupSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiCommandRequest {
#[serde(default)]
pub request_id: Option<String>,
pub command: ApiCommand,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiCommandResponse {
pub api_version: &'static str,
pub accepted: bool,
pub request_id: Option<String>,
pub generated_at_millis: u64,
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiErrorResponse {
pub api_version: &'static str,
pub error: ApiErrorBody,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiErrorBody {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiStateSnapshot {
pub system: ApiSystemInfo,
pub global: ApiGlobalState,
pub engine: ApiEngineState,
pub active_scene: ApiActiveScene,
pub nodes: Vec<ApiNodeStatus>,
pub panels: Vec<ApiPanelStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiPreviewSnapshot {
pub generated_at_millis: u64,
pub panels: Vec<ApiPreviewPanel>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiStreamEnvelope {
pub api_version: &'static str,
pub sequence: u64,
pub generated_at_millis: u64,
pub message: ApiStreamMessage,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case", tag = "type", content = "payload")]
pub enum ApiStreamMessage {
Snapshot(ApiStateSnapshot),
Preview(ApiPreviewSnapshot),
Event(ApiEventNotice),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiEventNotice {
pub kind: ApiEventKind,
pub message: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiEventKind {
Info,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiSystemInfo {
pub project_name: String,
pub schema_version: u32,
pub topology_label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiGlobalState {
pub blackout: bool,
pub master_brightness: f32,
pub selected_pattern: String,
pub selected_group: Option<String>,
pub transition_duration_ms: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiEngineState {
pub logic_hz: u16,
pub frame_hz: u16,
pub preview_hz: u16,
pub uptime_ms: u64,
pub frame_index: u64,
pub dropped_frames: u64,
pub active_transition: Option<ApiTransitionState>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiTransitionState {
pub style: ApiTransitionStyle,
pub from_pattern_id: String,
pub to_pattern_id: String,
pub duration_ms: u32,
pub progress: f32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiTransitionStyle {
Snap,
Crossfade,
Chase,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiActiveScene {
pub preset_id: Option<String>,
pub pattern_id: String,
pub seed: u64,
pub palette: Vec<String>,
pub parameters: Vec<ApiSceneParameter>,
pub target_group: Option<String>,
pub blackout: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiSceneParameter {
pub key: String,
pub label: String,
pub kind: ApiParameterKind,
pub value: ApiParameterValue,
pub min_scalar: Option<f32>,
pub max_scalar: Option<f32>,
pub step: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiPatternCatalogEntry {
pub pattern_id: String,
pub display_name: String,
pub description: String,
pub parameters: Vec<ApiPatternParameter>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiPatternParameter {
pub key: String,
pub label: String,
pub kind: ApiParameterKind,
pub min_scalar: Option<f32>,
pub max_scalar: Option<f32>,
pub step: Option<f32>,
pub default_value: ApiParameterValue,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiPresetSummary {
pub preset_id: String,
pub pattern_id: String,
pub target_group: Option<String>,
pub transition_duration_ms: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiGroupSummary {
pub group_id: String,
pub member_count: usize,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiNodeStatus {
pub node_id: String,
pub display_name: String,
pub reserved_ip: Option<String>,
pub connection: ApiConnectionState,
pub last_contact_ms: u64,
pub error_status: Option<String>,
pub panel_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiPanelStatus {
pub node_id: String,
pub panel_position: ApiPanelPosition,
pub physical_output_name: String,
pub driver_reference: String,
pub led_count: u16,
pub direction: ApiLedDirection,
pub color_order: ApiColorOrder,
pub enabled: bool,
pub validation_state: ApiValidationState,
pub connection: ApiConnectionState,
pub last_test_trigger_ms: Option<u64>,
pub error_status: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiPreviewPanel {
pub node_id: String,
pub panel_position: ApiPanelPosition,
pub representative_color_hex: String,
pub sample_led_hex: Vec<String>,
pub energy_percent: u8,
pub source: ApiPreviewSource,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiPanelPosition {
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiConnectionState {
Online,
Degraded,
Offline,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiPreviewSource {
Scene,
Transition,
PanelTest,
Blackout,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiParameterKind {
Scalar,
Toggle,
Text,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiLedDirection {
Forward,
Reverse,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiColorOrder {
Rgb,
Rbg,
Grb,
Gbr,
Brg,
Bgr,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiValidationState {
PendingHardwareValidation,
Validated,
Retired,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
pub enum ApiParameterValue {
Scalar(f32),
Toggle(bool),
Text(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case", tag = "type", content = "payload")]
pub enum ApiCommand {
SetBlackout {
enabled: bool,
},
SetMasterBrightness {
value: f32,
},
SelectPattern {
pattern_id: String,
},
RecallPreset {
preset_id: String,
},
SelectGroup {
group_id: Option<String>,
},
SetSceneParameter {
key: String,
value: ApiParameterValue,
},
SetTransitionDurationMs {
duration_ms: u32,
},
TriggerPanelTest {
node_id: String,
panel_position: ApiPanelPosition,
pattern: ApiTestPattern,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiTestPattern {
WalkingPixel106,
}
impl ApiSnapshotResponse {
pub fn from_snapshot(snapshot: &HostSnapshot) -> Self {
let state = ApiStateSnapshot::from_snapshot(snapshot);
let preview = ApiPreviewSnapshot::from_snapshot(snapshot);
Self {
api_version: API_VERSION,
generated_at_millis: snapshot.generated_at_millis,
state,
preview,
}
}
}
impl ApiCatalogResponse {
pub fn from_snapshot(snapshot: &HostSnapshot) -> Self {
Self {
api_version: API_VERSION,
patterns: snapshot
.catalog
.patterns
.iter()
.map(|pattern| ApiPatternCatalogEntry {
pattern_id: pattern.pattern_id.clone(),
display_name: pattern.display_name.clone(),
description: pattern.description.clone(),
parameters: pattern
.parameters
.iter()
.map(|parameter| ApiPatternParameter {
key: parameter.key.clone(),
label: parameter.label.clone(),
kind: map_parameter_kind(parameter.kind),
min_scalar: parameter.min_scalar,
max_scalar: parameter.max_scalar,
step: parameter.step,
default_value: map_parameter_value(&parameter.default_value),
})
.collect(),
})
.collect(),
presets: snapshot
.catalog
.presets
.iter()
.map(|preset| ApiPresetSummary {
preset_id: preset.preset_id.clone(),
pattern_id: preset.pattern_id.clone(),
target_group: preset.target_group.clone(),
transition_duration_ms: preset.transition_duration_ms,
})
.collect(),
groups: snapshot
.catalog
.groups
.iter()
.map(|group| ApiGroupSummary {
group_id: group.group_id.clone(),
member_count: group.member_count,
tags: group.tags.clone(),
})
.collect(),
}
}
}
impl ApiPresetListResponse {
pub fn from_snapshot(snapshot: &HostSnapshot) -> Self {
Self {
api_version: API_VERSION,
presets: ApiCatalogResponse::from_snapshot(snapshot).presets,
}
}
}
impl ApiGroupListResponse {
pub fn from_snapshot(snapshot: &HostSnapshot) -> Self {
Self {
api_version: API_VERSION,
groups: ApiCatalogResponse::from_snapshot(snapshot).groups,
}
}
}
impl ApiStateSnapshot {
pub fn from_snapshot(snapshot: &HostSnapshot) -> Self {
Self {
system: ApiSystemInfo {
project_name: snapshot.system.project_name.clone(),
schema_version: snapshot.system.schema_version,
topology_label: snapshot.system.topology_label.clone(),
},
global: ApiGlobalState {
blackout: snapshot.global.blackout,
master_brightness: snapshot.global.master_brightness,
selected_pattern: snapshot.global.selected_pattern.clone(),
selected_group: snapshot.global.selected_group.clone(),
transition_duration_ms: snapshot.global.transition_duration_ms,
},
engine: ApiEngineState {
logic_hz: snapshot.engine.logic_hz,
frame_hz: snapshot.engine.frame_hz,
preview_hz: snapshot.engine.preview_hz,
uptime_ms: snapshot.engine.uptime_ms,
frame_index: snapshot.engine.frame_index,
dropped_frames: snapshot.engine.dropped_frames,
active_transition: snapshot.engine.active_transition.as_ref().map(|transition| {
ApiTransitionState {
style: map_transition_style(transition.style),
from_pattern_id: transition.from_pattern_id.clone(),
to_pattern_id: transition.to_pattern_id.clone(),
duration_ms: transition.duration_ms,
progress: transition.progress,
}
}),
},
active_scene: ApiActiveScene {
preset_id: snapshot.active_scene.preset_id.clone(),
pattern_id: snapshot.active_scene.pattern_id.clone(),
seed: snapshot.active_scene.seed,
palette: snapshot.active_scene.palette.clone(),
parameters: snapshot
.active_scene
.parameters
.iter()
.map(|parameter| ApiSceneParameter {
key: parameter.key.clone(),
label: parameter.label.clone(),
kind: map_parameter_kind(parameter.kind),
value: map_parameter_value(&parameter.value),
min_scalar: parameter.min_scalar,
max_scalar: parameter.max_scalar,
step: parameter.step,
})
.collect(),
target_group: snapshot.active_scene.target_group.clone(),
blackout: snapshot.active_scene.blackout,
},
nodes: snapshot
.nodes
.iter()
.map(|node| ApiNodeStatus {
node_id: node.node_id.clone(),
display_name: node.display_name.clone(),
reserved_ip: node.reserved_ip.clone(),
connection: map_connection_state(node.connection),
last_contact_ms: node.last_contact_ms,
error_status: node.error_status.clone(),
panel_count: node.panel_count,
})
.collect(),
panels: snapshot
.panels
.iter()
.map(|panel| ApiPanelStatus {
node_id: panel.target.node_id.clone(),
panel_position: map_panel_position(&panel.target.panel_position),
physical_output_name: panel.physical_output_name.clone(),
driver_reference: panel.driver_reference.clone(),
led_count: panel.led_count,
direction: map_led_direction(panel.direction.clone()),
color_order: map_color_order(panel.color_order.clone()),
enabled: panel.enabled,
validation_state: map_validation_state(panel.validation_state.clone()),
connection: map_connection_state(panel.connection),
last_test_trigger_ms: panel.last_test_trigger_ms,
error_status: panel.error_status.clone(),
})
.collect(),
}
}
}
impl ApiPreviewSnapshot {
pub fn from_snapshot(snapshot: &HostSnapshot) -> Self {
Self {
generated_at_millis: snapshot.generated_at_millis,
panels: snapshot
.preview
.panels
.iter()
.map(|panel| ApiPreviewPanel {
node_id: panel.target.node_id.clone(),
panel_position: map_panel_position(&panel.target.panel_position),
representative_color_hex: panel.representative_color_hex.clone(),
sample_led_hex: panel.sample_led_hex.clone(),
energy_percent: panel.energy_percent,
source: map_preview_source(panel.preview_source),
})
.collect(),
}
}
}
impl ApiCommandRequest {
pub fn into_host_command(self) -> Result<HostCommand, String> {
match self.command {
ApiCommand::SetBlackout { enabled } => Ok(HostCommand::SetBlackout(enabled)),
ApiCommand::SetMasterBrightness { value } => {
Ok(HostCommand::SetMasterBrightness(value))
}
ApiCommand::SelectPattern { pattern_id } => {
Ok(HostCommand::SelectPattern(pattern_id))
}
ApiCommand::RecallPreset { preset_id } => {
Ok(HostCommand::RecallPreset { preset_id })
}
ApiCommand::SelectGroup { group_id } => {
Ok(HostCommand::SelectGroup { group_id })
}
ApiCommand::SetSceneParameter { key, value } => Ok(HostCommand::SetSceneParameter {
key,
value: map_command_parameter_value(value),
}),
ApiCommand::SetTransitionDurationMs { duration_ms } => {
Ok(HostCommand::SetTransitionDurationMs(duration_ms))
}
ApiCommand::TriggerPanelTest {
node_id,
panel_position,
pattern,
} => Ok(HostCommand::TriggerPanelTest {
target: infinity_host::PanelTarget {
node_id,
panel_position: map_command_panel_position(panel_position),
},
pattern: match pattern {
ApiTestPattern::WalkingPixel106 => TestPatternKind::WalkingPixel106,
},
}),
}
}
pub fn summary(&self) -> String {
self.command.summary()
}
}
fn map_panel_position(position: &PanelPosition) -> ApiPanelPosition {
match position {
PanelPosition::Top => ApiPanelPosition::Top,
PanelPosition::Middle => ApiPanelPosition::Middle,
PanelPosition::Bottom => ApiPanelPosition::Bottom,
}
}
fn map_command_panel_position(position: ApiPanelPosition) -> PanelPosition {
match position {
ApiPanelPosition::Top => PanelPosition::Top,
ApiPanelPosition::Middle => PanelPosition::Middle,
ApiPanelPosition::Bottom => PanelPosition::Bottom,
}
}
fn map_connection_state(state: NodeConnectionState) -> ApiConnectionState {
match state {
NodeConnectionState::Online => ApiConnectionState::Online,
NodeConnectionState::Degraded => ApiConnectionState::Degraded,
NodeConnectionState::Offline => ApiConnectionState::Offline,
}
}
fn map_led_direction(direction: LedDirection) -> ApiLedDirection {
match direction {
LedDirection::Forward => ApiLedDirection::Forward,
LedDirection::Reverse => ApiLedDirection::Reverse,
}
}
fn map_color_order(color_order: ColorOrder) -> ApiColorOrder {
match color_order {
ColorOrder::Rgb => ApiColorOrder::Rgb,
ColorOrder::Rbg => ApiColorOrder::Rbg,
ColorOrder::Grb => ApiColorOrder::Grb,
ColorOrder::Gbr => ApiColorOrder::Gbr,
ColorOrder::Brg => ApiColorOrder::Brg,
ColorOrder::Bgr => ApiColorOrder::Bgr,
}
}
fn map_validation_state(state: ValidationState) -> ApiValidationState {
match state {
ValidationState::PendingHardwareValidation => ApiValidationState::PendingHardwareValidation,
ValidationState::Validated => ApiValidationState::Validated,
ValidationState::Retired => ApiValidationState::Retired,
}
}
fn map_preview_source(source: PreviewSource) -> ApiPreviewSource {
match source {
PreviewSource::Scene => ApiPreviewSource::Scene,
PreviewSource::Transition => ApiPreviewSource::Transition,
PreviewSource::PanelTest => ApiPreviewSource::PanelTest,
PreviewSource::Blackout => ApiPreviewSource::Blackout,
}
}
fn map_transition_style(style: SceneTransitionStyle) -> ApiTransitionStyle {
match style {
SceneTransitionStyle::Snap => ApiTransitionStyle::Snap,
SceneTransitionStyle::Crossfade => ApiTransitionStyle::Crossfade,
SceneTransitionStyle::Chase => ApiTransitionStyle::Chase,
}
}
fn map_parameter_kind(kind: SceneParameterKind) -> ApiParameterKind {
match kind {
SceneParameterKind::Scalar => ApiParameterKind::Scalar,
SceneParameterKind::Toggle => ApiParameterKind::Toggle,
SceneParameterKind::Text => ApiParameterKind::Text,
}
}
fn map_parameter_value(value: &SceneParameterValue) -> ApiParameterValue {
match value {
SceneParameterValue::Scalar(value) => ApiParameterValue::Scalar(*value),
SceneParameterValue::Toggle(value) => ApiParameterValue::Toggle(*value),
SceneParameterValue::Text(value) => ApiParameterValue::Text(value.clone()),
}
}
fn map_command_parameter_value(value: ApiParameterValue) -> SceneParameterValue {
match value {
ApiParameterValue::Scalar(value) => SceneParameterValue::Scalar(value),
ApiParameterValue::Toggle(value) => SceneParameterValue::Toggle(value),
ApiParameterValue::Text(value) => SceneParameterValue::Text(value),
}
}
impl ApiCommand {
pub fn summary(&self) -> String {
match self {
Self::SetBlackout { enabled } => {
if *enabled {
"blackout enabled".to_string()
} else {
"blackout released".to_string()
}
}
Self::SetMasterBrightness { value } => {
format!("master brightness set to {:.0}%", value.clamp(0.0, 1.0) * 100.0)
}
Self::SelectPattern { pattern_id } => format!("pattern selected: {pattern_id}"),
Self::RecallPreset { preset_id } => format!("preset recalled: {preset_id}"),
Self::SelectGroup { group_id } => format!(
"group selected: {}",
group_id.as_deref().unwrap_or("all_panels")
),
Self::SetSceneParameter { key, .. } => format!("scene parameter updated: {key}"),
Self::SetTransitionDurationMs { duration_ms } => {
format!("transition duration set to {duration_ms} ms")
}
Self::TriggerPanelTest {
node_id,
panel_position,
pattern,
} => format!(
"panel test {} on {}:{}",
pattern.label(),
node_id,
panel_position.label()
),
}
}
}
impl ApiPanelPosition {
pub fn label(self) -> &'static str {
match self {
Self::Top => "top",
Self::Middle => "middle",
Self::Bottom => "bottom",
}
}
}
impl ApiTestPattern {
pub fn label(self) -> &'static str {
match self {
Self::WalkingPixel106 => "walking_pixel_106",
}
}
}
impl ApiErrorResponse {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
api_version: API_VERSION,
error: ApiErrorBody {
code: code.into(),
message: message.into(),
},
}
}
}