Initial commit
This commit is contained in:
13
crates/infinity_host/Cargo.toml
Normal file
13
crates/infinity_host/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "infinity_host"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
infinity_config = { path = "../infinity_config" }
|
||||
infinity_protocol = { path = "../infinity_protocol" }
|
||||
130
crates/infinity_host/src/main.rs
Normal file
130
crates/infinity_host/src/main.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
mod runtime;
|
||||
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use infinity_config::{load_project_from_path, ValidationMode, ValidationSeverity};
|
||||
use runtime::RealtimeEngine;
|
||||
use std::{path::PathBuf, process::ExitCode};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(author, version, about = "Infinity Vis host-side validation and planning CLI")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
Validate {
|
||||
#[arg(long)]
|
||||
config: PathBuf,
|
||||
#[arg(long, value_enum, default_value_t = CliValidationMode::Structural)]
|
||||
mode: CliValidationMode,
|
||||
},
|
||||
PlanBootScene {
|
||||
#[arg(long)]
|
||||
config: PathBuf,
|
||||
#[arg(long)]
|
||||
preset_id: String,
|
||||
},
|
||||
OpenValidationPoints,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
enum CliValidationMode {
|
||||
Structural,
|
||||
Activation,
|
||||
}
|
||||
|
||||
impl From<CliValidationMode> for ValidationMode {
|
||||
fn from(value: CliValidationMode) -> Self {
|
||||
match value {
|
||||
CliValidationMode::Structural => ValidationMode::Structural,
|
||||
CliValidationMode::Activation => ValidationMode::Activation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Command::Validate { config, mode } => validate_command(config, mode),
|
||||
Command::PlanBootScene { config, preset_id } => plan_boot_scene_command(config, &preset_id),
|
||||
Command::OpenValidationPoints => {
|
||||
print_open_validation_points();
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_command(config: PathBuf, mode: CliValidationMode) -> ExitCode {
|
||||
let project = match load_project_from_path(&config) {
|
||||
Ok(project) => project,
|
||||
Err(error) => {
|
||||
eprintln!("Failed to load config '{}': {error}", config.display());
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let engine = RealtimeEngine::default();
|
||||
let report = engine.validate_project(&project, mode.into());
|
||||
println!(
|
||||
"Validation finished: {} error(s), {} warning(s)",
|
||||
report.error_count(),
|
||||
report.warning_count()
|
||||
);
|
||||
for issue in &report.issues {
|
||||
let level = match issue.severity {
|
||||
ValidationSeverity::Warning => "WARN",
|
||||
ValidationSeverity::Error => "ERROR",
|
||||
};
|
||||
println!("[{level}] {} at {}: {}", issue.code, issue.path, issue.message);
|
||||
}
|
||||
|
||||
if report.is_ok() {
|
||||
ExitCode::SUCCESS
|
||||
} else {
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_boot_scene_command(config: PathBuf, preset_id: &str) -> ExitCode {
|
||||
let project = match load_project_from_path(&config) {
|
||||
Ok(project) => project,
|
||||
Err(error) => {
|
||||
eprintln!("Failed to load config '{}': {error}", config.display());
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let engine = RealtimeEngine::default();
|
||||
let plan = engine.plan_boot_scene(&project, preset_id);
|
||||
if plan.is_empty() {
|
||||
eprintln!("Preset '{preset_id}' was not found.");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
match serde_json::to_string_pretty(&plan) {
|
||||
Ok(output) => {
|
||||
println!("{output}");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Failed to serialize boot plan: {error}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_open_validation_points() {
|
||||
for line in [
|
||||
"Pending hardware validation gates:",
|
||||
"1. Confirm whether 'UART 6 / 5 / 4' are GPIO labels, logical channels, or real UART peripherals.",
|
||||
"2. Confirm the exact LED chipset and timing backend required per output.",
|
||||
"3. Confirm color order, start pixel, and direction for all 18 outputs.",
|
||||
"4. Confirm whether all outputs truly have 106 active LEDs without dummy or reserve pixels.",
|
||||
"5. Confirm the final node-to-physical-panel mapping before enabling layout-specific scenes.",
|
||||
] {
|
||||
println!("{line}");
|
||||
}
|
||||
}
|
||||
|
||||
167
crates/infinity_host/src/runtime.rs
Normal file
167
crates/infinity_host/src/runtime.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use infinity_config::{
|
||||
PanelPosition, ProjectConfig, TransportMode, ValidationIssue, ValidationMode, ValidationReport,
|
||||
ValidationSeverity,
|
||||
};
|
||||
use infinity_protocol::{
|
||||
ControlEnvelope, ControlMessage, PanelAddress, PanelAssignment, PanelSlot, RealtimeEnvelope,
|
||||
RealtimeMessage, RealtimeMode, SceneParametersFrame, TransitionMode, TransitionSpec,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TickSchedule {
|
||||
pub logic_hz: u16,
|
||||
pub frame_synthesis_hz: u16,
|
||||
pub network_send_hz: u16,
|
||||
pub preview_hz: u16,
|
||||
}
|
||||
|
||||
impl Default for TickSchedule {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
logic_hz: 120,
|
||||
frame_synthesis_hz: 60,
|
||||
network_send_hz: 60,
|
||||
preview_hz: 15,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub struct PlannedSend {
|
||||
pub node_id: String,
|
||||
pub control: Vec<ControlEnvelope>,
|
||||
pub realtime: Vec<RealtimeEnvelope>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RealtimeEngine {
|
||||
pub schedule: TickSchedule,
|
||||
}
|
||||
|
||||
impl Default for RealtimeEngine {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
schedule: TickSchedule::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RealtimeEngine {
|
||||
pub fn validate_project(&self, project: &ProjectConfig, mode: ValidationMode) -> ValidationReport {
|
||||
let mut report = project.validate(mode);
|
||||
if self.schedule.preview_hz >= self.schedule.frame_synthesis_hz {
|
||||
report.issues.push(ValidationIssue {
|
||||
severity: ValidationSeverity::Warning,
|
||||
code: "preview_rate_too_high",
|
||||
path: "runtime.schedule.preview_hz".to_string(),
|
||||
message: "preview rate should stay below frame synthesis rate".to_string(),
|
||||
});
|
||||
}
|
||||
report
|
||||
}
|
||||
|
||||
pub fn plan_boot_scene(&self, project: &ProjectConfig, preset_id: &str) -> Vec<PlannedSend> {
|
||||
let Some(preset) = project.presets.iter().find(|preset| preset.preset_id == preset_id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let transport = project
|
||||
.transport_profiles
|
||||
.iter()
|
||||
.find(|profile| profile.profile_id == project.metadata.default_transport_profile);
|
||||
|
||||
let safety = project
|
||||
.safety_profiles
|
||||
.iter()
|
||||
.find(|profile| profile.profile_id == project.metadata.default_safety_profile);
|
||||
|
||||
project
|
||||
.topology
|
||||
.nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, node)| {
|
||||
let assignments = node
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|output| PanelAssignment {
|
||||
panel: PanelAddress {
|
||||
node_id: node.node_id.clone(),
|
||||
panel_slot: map_panel_slot(&output.panel_position),
|
||||
},
|
||||
physical_output_name: output.physical_output_name.clone(),
|
||||
driver_reference: output.driver_channel.reference.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let targets = assignments
|
||||
.iter()
|
||||
.map(|assignment| assignment.panel.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let transition = TransitionSpec {
|
||||
transition_ms: preset.transition_ms,
|
||||
mode: TransitionMode::Crossfade,
|
||||
};
|
||||
|
||||
let mode = match transport.map(|profile| &profile.mode) {
|
||||
Some(TransportMode::FrameStreaming) => RealtimeMode::FrameStreaming,
|
||||
_ => RealtimeMode::DistributedScene,
|
||||
};
|
||||
|
||||
PlannedSend {
|
||||
node_id: node.node_id.clone(),
|
||||
control: vec![
|
||||
ControlEnvelope::new(
|
||||
(index as u32) * 2 + 1,
|
||||
0,
|
||||
ControlMessage::ConfigSync {
|
||||
topology_revision: format!(
|
||||
"schema-{}",
|
||||
project.metadata.schema_version
|
||||
),
|
||||
outputs: assignments,
|
||||
},
|
||||
),
|
||||
ControlEnvelope::new(
|
||||
(index as u32) * 2 + 2,
|
||||
0,
|
||||
ControlMessage::PresetRecall {
|
||||
preset_id: preset.preset_id.clone(),
|
||||
transition,
|
||||
},
|
||||
),
|
||||
],
|
||||
realtime: vec![RealtimeEnvelope::new(
|
||||
(index as u32) + 1,
|
||||
0,
|
||||
RealtimeMessage::SceneParameters(SceneParametersFrame {
|
||||
node_id: node.node_id.clone(),
|
||||
mode,
|
||||
preset_id: Some(preset.preset_id.clone()),
|
||||
effect: preset.scene.effect.clone(),
|
||||
seed: preset.scene.seed,
|
||||
palette: preset.scene.palette.clone(),
|
||||
master_brightness: safety
|
||||
.map(|profile| profile.default_start_brightness)
|
||||
.unwrap_or(0.2),
|
||||
speed: preset.scene.speed,
|
||||
intensity: preset.scene.intensity,
|
||||
target_group: preset.target_group.clone(),
|
||||
target_outputs: targets,
|
||||
}),
|
||||
)],
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn map_panel_slot(position: &PanelPosition) -> PanelSlot {
|
||||
match position {
|
||||
PanelPosition::Top => PanelSlot::Top,
|
||||
PanelPosition::Middle => PanelSlot::Middle,
|
||||
PanelPosition::Bottom => PanelSlot::Bottom,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user