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, pub presets: Vec, pub groups: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ApiPresetListResponse { pub api_version: &'static str, pub presets: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ApiGroupListResponse { pub api_version: &'static str, pub groups: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ApiCommandRequest { #[serde(default)] pub request_id: Option, pub command: ApiCommand, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ApiCommandResponse { pub api_version: &'static str, pub accepted: bool, pub request_id: Option, 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, pub panels: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ApiPreviewSnapshot { pub generated_at_millis: u64, pub panels: Vec, } #[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, 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, } #[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, pub pattern_id: String, pub seed: u64, pub palette: Vec, pub parameters: Vec, pub target_group: Option, 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, pub max_scalar: Option, pub step: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ApiPatternCatalogEntry { pub pattern_id: String, pub display_name: String, pub description: String, pub parameters: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ApiPatternParameter { pub key: String, pub label: String, pub kind: ApiParameterKind, pub min_scalar: Option, pub max_scalar: Option, pub step: Option, 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, 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, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ApiNodeStatus { pub node_id: String, pub display_name: String, pub reserved_ip: Option, pub connection: ApiConnectionState, pub last_contact_ms: u64, pub error_status: Option, 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, pub error_status: Option, } #[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, 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, }, 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(¶meter.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(¶meter.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 { 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, message: impl Into) -> Self { Self { api_version: API_VERSION, error: ApiErrorBody { code: code.into(), message: message.into(), }, } } }