Skip to main content

cu29_runtime/
config.rs

1//! This module defines the configuration of the copper runtime.
2//! The configuration is a directed graph where nodes are tasks and edges are connections between tasks.
3//! The configuration is serialized in the RON format.
4//! The configuration is used to generate the runtime code at compile time.
5#[cfg(not(feature = "std"))]
6extern crate alloc;
7
8use ConfigGraphs::{Missions, Simple};
9use core::any::type_name;
10use core::fmt;
11use core::fmt::Display;
12use cu29_traits::{CuError, CuResult};
13use cu29_value::Value as CuValue;
14use hashbrown::HashMap;
15pub use petgraph::Direction::Incoming;
16pub use petgraph::Direction::Outgoing;
17use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableDiGraph};
18#[cfg(feature = "std")]
19use petgraph::visit::IntoEdgeReferences;
20use petgraph::visit::{Bfs, EdgeRef};
21use ron::extensions::Extensions;
22use ron::value::Value as RonValue;
23use ron::{Number, Options};
24use serde::de::DeserializeOwned;
25use serde::{Deserialize, Deserializer, Serialize, Serializer};
26
27#[cfg(not(feature = "std"))]
28use alloc::boxed::Box;
29#[cfg(not(feature = "std"))]
30use alloc::collections::BTreeMap;
31#[cfg(not(feature = "std"))]
32use alloc::vec;
33#[cfg(feature = "std")]
34use std::collections::BTreeMap;
35
36#[cfg(not(feature = "std"))]
37mod imp {
38    pub use alloc::borrow::ToOwned;
39    pub use alloc::format;
40    pub use alloc::string::String;
41    pub use alloc::string::ToString;
42    pub use alloc::vec::Vec;
43}
44
45#[cfg(feature = "std")]
46mod imp {
47    pub use html_escape::encode_text;
48    pub use std::fs::read_to_string;
49}
50
51use imp::*;
52
53/// NodeId is the unique identifier of a node in the configuration graph for petgraph
54/// and the code generation.
55pub type NodeId = u32;
56pub const DEFAULT_MISSION_ID: &str = "default";
57
58/// This is the configuration of a component (like a task config or a monitoring config):w
59/// It is a map of key-value pairs.
60/// It is given to the new method of the task implementation.
61#[derive(Serialize, Deserialize, Debug, Clone, Default)]
62pub struct ComponentConfig(pub HashMap<String, Value>);
63
64/// Mapping between resource binding names and bundle-scoped resource ids.
65#[allow(dead_code)]
66impl Display for ComponentConfig {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        let mut first = true;
69        let ComponentConfig(config) = self;
70        write!(f, "{{")?;
71        for (key, value) in config.iter() {
72            if !first {
73                write!(f, ", ")?;
74            }
75            write!(f, "{key}: {value}")?;
76            first = false;
77        }
78        write!(f, "}}")
79    }
80}
81
82// forward map interface
83impl ComponentConfig {
84    #[allow(dead_code)]
85    pub fn new() -> Self {
86        ComponentConfig(HashMap::new())
87    }
88
89    #[allow(dead_code)]
90    pub fn get<T>(&self, key: &str) -> Result<Option<T>, ConfigError>
91    where
92        T: for<'a> TryFrom<&'a Value, Error = ConfigError>,
93    {
94        let ComponentConfig(config) = self;
95        match config.get(key) {
96            Some(value) => T::try_from(value).map(Some),
97            None => Ok(None),
98        }
99    }
100
101    #[allow(dead_code)]
102    /// Retrieve a structured config value by deserializing it with cu29-value.
103    ///
104    /// Example RON:
105    /// `{ "calibration": { "matrix": [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], "enabled": true } }`
106    ///
107    /// ```rust,ignore
108    /// #[derive(serde::Deserialize)]
109    /// struct CalibrationCfg {
110    ///     matrix: [[f32; 3]; 3],
111    ///     enabled: bool,
112    /// }
113    /// let cfg: CalibrationCfg = config.get_value("calibration")?.unwrap();
114    /// ```
115    pub fn get_value<T>(&self, key: &str) -> Result<Option<T>, ConfigError>
116    where
117        T: DeserializeOwned,
118    {
119        let ComponentConfig(config) = self;
120        let Some(value) = config.get(key) else {
121            return Ok(None);
122        };
123        let cu_value = ron_value_to_cu_value(&value.0).map_err(|err| err.with_key(key))?;
124        cu_value
125            .deserialize_into::<T>()
126            .map(Some)
127            .map_err(|err| ConfigError {
128                message: format!(
129                    "Config key '{key}' failed to deserialize as {}: {err}",
130                    type_name::<T>()
131                ),
132            })
133    }
134
135    #[allow(dead_code)]
136    pub fn set<T: Into<Value>>(&mut self, key: &str, value: T) {
137        let ComponentConfig(config) = self;
138        config.insert(key.to_string(), value.into());
139    }
140}
141
142fn ron_value_to_cu_value(value: &RonValue) -> Result<CuValue, ConfigError> {
143    match value {
144        RonValue::Bool(v) => Ok(CuValue::Bool(*v)),
145        RonValue::Char(v) => Ok(CuValue::Char(*v)),
146        RonValue::String(v) => Ok(CuValue::String(v.clone())),
147        RonValue::Bytes(v) => Ok(CuValue::Bytes(v.clone())),
148        RonValue::Unit => Ok(CuValue::Unit),
149        RonValue::Option(v) => {
150            let mapped = match v {
151                Some(inner) => Some(Box::new(ron_value_to_cu_value(inner)?)),
152                None => None,
153            };
154            Ok(CuValue::Option(mapped))
155        }
156        RonValue::Seq(seq) => {
157            let mut mapped = Vec::with_capacity(seq.len());
158            for item in seq {
159                mapped.push(ron_value_to_cu_value(item)?);
160            }
161            Ok(CuValue::Seq(mapped))
162        }
163        RonValue::Map(map) => {
164            let mut mapped = BTreeMap::new();
165            for (key, value) in map.iter() {
166                let mapped_key = ron_value_to_cu_value(key)?;
167                let mapped_value = ron_value_to_cu_value(value)?;
168                mapped.insert(mapped_key, mapped_value);
169            }
170            Ok(CuValue::Map(mapped))
171        }
172        RonValue::Number(num) => match num {
173            Number::I8(v) => Ok(CuValue::I8(*v)),
174            Number::I16(v) => Ok(CuValue::I16(*v)),
175            Number::I32(v) => Ok(CuValue::I32(*v)),
176            Number::I64(v) => Ok(CuValue::I64(*v)),
177            Number::U8(v) => Ok(CuValue::U8(*v)),
178            Number::U16(v) => Ok(CuValue::U16(*v)),
179            Number::U32(v) => Ok(CuValue::U32(*v)),
180            Number::U64(v) => Ok(CuValue::U64(*v)),
181            Number::F32(v) => Ok(CuValue::F32(v.0)),
182            Number::F64(v) => Ok(CuValue::F64(v.0)),
183            Number::__NonExhaustive(_) => Err(ConfigError {
184                message: "Unsupported RON number variant".to_string(),
185            }),
186        },
187    }
188}
189
190// The configuration Serialization format is as follows:
191// (
192//   tasks : [ (id: "toto", type: "zorglub::MyType", config: {...}),
193//             (id: "titi", type: "zorglub::MyType2", config: {...})]
194//   cnx : [ (src: "toto", dst: "titi", msg: "zorglub::MyMsgType"),...]
195// )
196
197/// Wrapper around the ron::Value to allow for custom serialization.
198#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
199pub struct Value(RonValue);
200
201#[derive(Debug, Clone, PartialEq)]
202pub struct ConfigError {
203    message: String,
204}
205
206impl ConfigError {
207    fn type_mismatch(expected: &'static str, value: &Value) -> Self {
208        ConfigError {
209            message: format!("Expected {expected} but got {value:?}"),
210        }
211    }
212
213    fn with_key(self, key: &str) -> Self {
214        ConfigError {
215            message: format!("Config key '{key}': {}", self.message),
216        }
217    }
218}
219
220impl Display for ConfigError {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(f, "{}", self.message)
223    }
224}
225
226#[cfg(feature = "std")]
227impl std::error::Error for ConfigError {}
228
229#[cfg(not(feature = "std"))]
230impl core::error::Error for ConfigError {}
231
232impl From<ConfigError> for CuError {
233    fn from(err: ConfigError) -> Self {
234        CuError::from(err.to_string())
235    }
236}
237
238// Macro for implementing From<T> for Value where T is a numeric type
239macro_rules! impl_from_numeric_for_value {
240    ($($source:ty),* $(,)?) => {
241        $(impl From<$source> for Value {
242            fn from(value: $source) -> Self {
243                Value(RonValue::Number(value.into()))
244            }
245        })*
246    };
247}
248
249// Implement From for common numeric types
250impl_from_numeric_for_value!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
251
252impl TryFrom<&Value> for bool {
253    type Error = ConfigError;
254
255    fn try_from(value: &Value) -> Result<Self, Self::Error> {
256        if let Value(RonValue::Bool(v)) = value {
257            Ok(*v)
258        } else {
259            Err(ConfigError::type_mismatch("bool", value))
260        }
261    }
262}
263
264impl From<Value> for bool {
265    fn from(value: Value) -> Self {
266        if let Value(RonValue::Bool(v)) = value {
267            v
268        } else {
269            panic!("Expected a Boolean variant but got {value:?}")
270        }
271    }
272}
273macro_rules! impl_from_value_for_int {
274    ($($target:ty),* $(,)?) => {
275        $(
276            impl From<Value> for $target {
277                fn from(value: Value) -> Self {
278                    if let Value(RonValue::Number(num)) = value {
279                        match num {
280                            Number::I8(n) => n as $target,
281                            Number::I16(n) => n as $target,
282                            Number::I32(n) => n as $target,
283                            Number::I64(n) => n as $target,
284                            Number::U8(n) => n as $target,
285                            Number::U16(n) => n as $target,
286                            Number::U32(n) => n as $target,
287                            Number::U64(n) => n as $target,
288                            Number::F32(_) | Number::F64(_) | Number::__NonExhaustive(_) => {
289                                panic!("Expected an integer Number variant but got {num:?}")
290                            }
291                        }
292                    } else {
293                        panic!("Expected a Number variant but got {value:?}")
294                    }
295                }
296            }
297        )*
298    };
299}
300
301impl_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
302
303macro_rules! impl_try_from_value_for_int {
304    ($($target:ty),* $(,)?) => {
305        $(
306            impl TryFrom<&Value> for $target {
307                type Error = ConfigError;
308
309                fn try_from(value: &Value) -> Result<Self, Self::Error> {
310                    if let Value(RonValue::Number(num)) = value {
311                        match num {
312                            Number::I8(n) => Ok(*n as $target),
313                            Number::I16(n) => Ok(*n as $target),
314                            Number::I32(n) => Ok(*n as $target),
315                            Number::I64(n) => Ok(*n as $target),
316                            Number::U8(n) => Ok(*n as $target),
317                            Number::U16(n) => Ok(*n as $target),
318                            Number::U32(n) => Ok(*n as $target),
319                            Number::U64(n) => Ok(*n as $target),
320                            Number::F32(_) | Number::F64(_) | Number::__NonExhaustive(_) => {
321                                Err(ConfigError::type_mismatch("integer", value))
322                            }
323                        }
324                    } else {
325                        Err(ConfigError::type_mismatch("integer", value))
326                    }
327                }
328            }
329        )*
330    };
331}
332
333impl_try_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
334
335impl TryFrom<&Value> for f64 {
336    type Error = ConfigError;
337
338    fn try_from(value: &Value) -> Result<Self, Self::Error> {
339        if let Value(RonValue::Number(num)) = value {
340            let number = match num {
341                Number::I8(n) => *n as f64,
342                Number::I16(n) => *n as f64,
343                Number::I32(n) => *n as f64,
344                Number::I64(n) => *n as f64,
345                Number::U8(n) => *n as f64,
346                Number::U16(n) => *n as f64,
347                Number::U32(n) => *n as f64,
348                Number::U64(n) => *n as f64,
349                Number::F32(n) => n.0 as f64,
350                Number::F64(n) => n.0,
351                Number::__NonExhaustive(_) => {
352                    return Err(ConfigError::type_mismatch("number", value));
353                }
354            };
355            Ok(number)
356        } else {
357            Err(ConfigError::type_mismatch("number", value))
358        }
359    }
360}
361
362impl From<Value> for f64 {
363    fn from(value: Value) -> Self {
364        if let Value(RonValue::Number(num)) = value {
365            num.into_f64()
366        } else {
367            panic!("Expected a Number variant but got {value:?}")
368        }
369    }
370}
371
372impl From<String> for Value {
373    fn from(value: String) -> Self {
374        Value(RonValue::String(value))
375    }
376}
377
378impl TryFrom<&Value> for String {
379    type Error = ConfigError;
380
381    fn try_from(value: &Value) -> Result<Self, Self::Error> {
382        if let Value(RonValue::String(s)) = value {
383            Ok(s.clone())
384        } else {
385            Err(ConfigError::type_mismatch("string", value))
386        }
387    }
388}
389
390impl From<Value> for String {
391    fn from(value: Value) -> Self {
392        if let Value(RonValue::String(s)) = value {
393            s
394        } else {
395            panic!("Expected a String variant")
396        }
397    }
398}
399
400impl Display for Value {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        let Value(value) = self;
403        match value {
404            RonValue::Number(n) => {
405                let s = match n {
406                    Number::I8(n) => n.to_string(),
407                    Number::I16(n) => n.to_string(),
408                    Number::I32(n) => n.to_string(),
409                    Number::I64(n) => n.to_string(),
410                    Number::U8(n) => n.to_string(),
411                    Number::U16(n) => n.to_string(),
412                    Number::U32(n) => n.to_string(),
413                    Number::U64(n) => n.to_string(),
414                    Number::F32(n) => n.0.to_string(),
415                    Number::F64(n) => n.0.to_string(),
416                    _ => panic!("Expected a Number variant but got {value:?}"),
417                };
418                write!(f, "{s}")
419            }
420            RonValue::String(s) => write!(f, "{s}"),
421            RonValue::Bool(b) => write!(f, "{b}"),
422            RonValue::Map(m) => write!(f, "{m:?}"),
423            RonValue::Char(c) => write!(f, "{c:?}"),
424            RonValue::Unit => write!(f, "unit"),
425            RonValue::Option(o) => write!(f, "{o:?}"),
426            RonValue::Seq(s) => write!(f, "{s:?}"),
427            RonValue::Bytes(bytes) => write!(f, "{bytes:?}"),
428        }
429    }
430}
431
432/// Configuration for logging in the node.
433#[derive(Serialize, Deserialize, Debug, Clone)]
434pub struct NodeLogging {
435    enabled: bool,
436}
437
438/// Distinguishes regular tasks from bridge nodes so downstream stages can apply
439/// bridge-specific instantiation rules.
440#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
441pub enum Flavor {
442    #[default]
443    Task,
444    Bridge,
445}
446
447/// A node in the configuration graph.
448/// A node represents a Task in the system Graph.
449#[derive(Serialize, Deserialize, Debug, Clone)]
450pub struct Node {
451    /// Unique node identifier.
452    id: String,
453
454    /// Task rust struct underlying type, e.g. "mymodule::Sensor", etc.
455    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
456    type_: Option<String>,
457
458    /// Config passed to the task.
459    #[serde(skip_serializing_if = "Option::is_none")]
460    config: Option<ComponentConfig>,
461
462    /// Resources requested by the task.
463    #[serde(skip_serializing_if = "Option::is_none")]
464    resources: Option<HashMap<String, String>>,
465
466    /// Missions for which this task is run.
467    missions: Option<Vec<String>>,
468
469    /// Run this task in the background:
470    /// ie. Will be set to run on a background thread and until it is finished `CuTask::process` will return None.
471    #[serde(skip_serializing_if = "Option::is_none")]
472    background: Option<bool>,
473
474    /// Option to include/exclude stubbing for simulation.
475    /// By default, sources and sinks are replaces (stubbed) by the runtime to avoid trying to compile hardware specific code for sensing or actuation.
476    /// In some cases, for example a sink or source used as a middleware bridge, you might want to run the real code even in simulation.
477    /// This option allows to control this behavior.
478    /// Note: Normal tasks will be run in sim and this parameter ignored.
479    #[serde(skip_serializing_if = "Option::is_none")]
480    run_in_sim: Option<bool>,
481
482    /// Config passed to the task.
483    #[serde(skip_serializing_if = "Option::is_none")]
484    logging: Option<NodeLogging>,
485
486    /// Node role in the runtime graph (normal task or bridge endpoint).
487    #[serde(skip, default)]
488    flavor: Flavor,
489    /// Message types that are intentionally not connected (NC) in configuration.
490    #[serde(skip, default)]
491    nc_outputs: Vec<String>,
492    /// Original config connection order for each NC output message type.
493    #[serde(skip, default)]
494    nc_output_orders: Vec<usize>,
495}
496
497impl Node {
498    #[allow(dead_code)]
499    pub fn new(id: &str, ptype: &str) -> Self {
500        Node {
501            id: id.to_string(),
502            type_: Some(ptype.to_string()),
503            config: None,
504            resources: None,
505            missions: None,
506            background: None,
507            run_in_sim: None,
508            logging: None,
509            flavor: Flavor::Task,
510            nc_outputs: Vec::new(),
511            nc_output_orders: Vec::new(),
512        }
513    }
514
515    #[allow(dead_code)]
516    pub fn new_with_flavor(id: &str, ptype: &str, flavor: Flavor) -> Self {
517        let mut node = Self::new(id, ptype);
518        node.flavor = flavor;
519        node
520    }
521
522    #[allow(dead_code)]
523    pub fn get_id(&self) -> String {
524        self.id.clone()
525    }
526
527    #[allow(dead_code)]
528    pub fn get_type(&self) -> &str {
529        self.type_.as_ref().unwrap()
530    }
531
532    #[allow(dead_code)]
533    pub fn set_type(mut self, name: Option<String>) -> Self {
534        self.type_ = name;
535        self
536    }
537
538    #[allow(dead_code)]
539    pub fn set_resources<I>(&mut self, resources: Option<I>)
540    where
541        I: IntoIterator<Item = (String, String)>,
542    {
543        self.resources = resources.map(|iter| iter.into_iter().collect());
544    }
545
546    #[allow(dead_code)]
547    pub fn is_background(&self) -> bool {
548        self.background.unwrap_or(false)
549    }
550
551    #[allow(dead_code)]
552    pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
553        self.config.as_ref()
554    }
555
556    #[allow(dead_code)]
557    pub fn get_resources(&self) -> Option<&HashMap<String, String>> {
558        self.resources.as_ref()
559    }
560
561    /// By default, assume a source or a sink is not run in sim.
562    /// Normal tasks will be run in sim and this parameter ignored.
563    #[allow(dead_code)]
564    pub fn is_run_in_sim(&self) -> bool {
565        self.run_in_sim.unwrap_or(false)
566    }
567
568    #[allow(dead_code)]
569    pub fn is_logging_enabled(&self) -> bool {
570        if let Some(logging) = &self.logging {
571            logging.enabled
572        } else {
573            true
574        }
575    }
576
577    #[allow(dead_code)]
578    pub fn get_param<T>(&self, key: &str) -> Result<Option<T>, ConfigError>
579    where
580        T: for<'a> TryFrom<&'a Value, Error = ConfigError>,
581    {
582        let pc = match self.config.as_ref() {
583            Some(pc) => pc,
584            None => return Ok(None),
585        };
586        let ComponentConfig(pc) = pc;
587        match pc.get(key) {
588            Some(v) => T::try_from(v).map(Some),
589            None => Ok(None),
590        }
591    }
592
593    #[allow(dead_code)]
594    pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
595        if self.config.is_none() {
596            self.config = Some(ComponentConfig(HashMap::new()));
597        }
598        let ComponentConfig(config) = self.config.as_mut().unwrap();
599        config.insert(key.to_string(), value.into());
600    }
601
602    /// Returns whether this node is treated as a normal task or as a bridge.
603    #[allow(dead_code)]
604    pub fn get_flavor(&self) -> Flavor {
605        self.flavor
606    }
607
608    /// Overrides the node flavor; primarily used when injecting bridge nodes.
609    #[allow(dead_code)]
610    pub fn set_flavor(&mut self, flavor: Flavor) {
611        self.flavor = flavor;
612    }
613
614    /// Registers an intentionally unconnected output message type for this node.
615    #[allow(dead_code)]
616    pub fn add_nc_output(&mut self, msg_type: &str, order: usize) {
617        if let Some(pos) = self
618            .nc_outputs
619            .iter()
620            .position(|existing| existing == msg_type)
621        {
622            if order < self.nc_output_orders[pos] {
623                self.nc_output_orders[pos] = order;
624            }
625            return;
626        }
627        self.nc_outputs.push(msg_type.to_string());
628        self.nc_output_orders.push(order);
629    }
630
631    /// Returns message types intentionally marked as not connected.
632    #[allow(dead_code)]
633    pub fn nc_outputs(&self) -> &[String] {
634        &self.nc_outputs
635    }
636
637    /// Returns NC outputs paired with original config order.
638    #[allow(dead_code)]
639    pub fn nc_outputs_with_order(&self) -> impl Iterator<Item = (&String, usize)> {
640        self.nc_outputs
641            .iter()
642            .zip(self.nc_output_orders.iter().copied())
643    }
644}
645
646/// Directional mapping for bridge channels.
647#[derive(Serialize, Deserialize, Debug, Clone)]
648pub enum BridgeChannelConfigRepresentation {
649    /// Channel that receives data from the bridge into the graph.
650    Rx {
651        id: String,
652        /// Optional transport/topic identifier specific to the bridge backend.
653        #[serde(skip_serializing_if = "Option::is_none")]
654        route: Option<String>,
655        /// Optional per-channel configuration forwarded to the bridge implementation.
656        #[serde(skip_serializing_if = "Option::is_none")]
657        config: Option<ComponentConfig>,
658    },
659    /// Channel that transmits data from the graph into the bridge.
660    Tx {
661        id: String,
662        /// Optional transport/topic identifier specific to the bridge backend.
663        #[serde(skip_serializing_if = "Option::is_none")]
664        route: Option<String>,
665        /// Optional per-channel configuration forwarded to the bridge implementation.
666        #[serde(skip_serializing_if = "Option::is_none")]
667        config: Option<ComponentConfig>,
668    },
669}
670
671impl BridgeChannelConfigRepresentation {
672    /// Stable logical identifier to reference this channel in connections.
673    #[allow(dead_code)]
674    pub fn id(&self) -> &str {
675        match self {
676            BridgeChannelConfigRepresentation::Rx { id, .. }
677            | BridgeChannelConfigRepresentation::Tx { id, .. } => id,
678        }
679    }
680
681    /// Bridge-specific transport path (topic, route, path...) describing this channel.
682    #[allow(dead_code)]
683    pub fn route(&self) -> Option<&str> {
684        match self {
685            BridgeChannelConfigRepresentation::Rx { route, .. }
686            | BridgeChannelConfigRepresentation::Tx { route, .. } => route.as_deref(),
687        }
688    }
689}
690
691enum EndpointRole {
692    Source,
693    Destination,
694}
695
696fn validate_bridge_channel(
697    bridge: &BridgeConfig,
698    channel_id: &str,
699    role: EndpointRole,
700) -> Result<(), String> {
701    let channel = bridge
702        .channels
703        .iter()
704        .find(|ch| ch.id() == channel_id)
705        .ok_or_else(|| {
706            format!(
707                "Bridge '{}' does not declare a channel named '{}'",
708                bridge.id, channel_id
709            )
710        })?;
711
712    match (role, channel) {
713        (EndpointRole::Source, BridgeChannelConfigRepresentation::Rx { .. }) => Ok(()),
714        (EndpointRole::Destination, BridgeChannelConfigRepresentation::Tx { .. }) => Ok(()),
715        (EndpointRole::Source, BridgeChannelConfigRepresentation::Tx { .. }) => Err(format!(
716            "Bridge '{}' channel '{}' is Tx and cannot act as a source",
717            bridge.id, channel_id
718        )),
719        (EndpointRole::Destination, BridgeChannelConfigRepresentation::Rx { .. }) => Err(format!(
720            "Bridge '{}' channel '{}' is Rx and cannot act as a destination",
721            bridge.id, channel_id
722        )),
723    }
724}
725
726/// Declarative definition of a resource bundle.
727#[derive(Serialize, Deserialize, Debug, Clone)]
728pub struct ResourceBundleConfig {
729    pub id: String,
730    #[serde(rename = "provider")]
731    pub provider: String,
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub config: Option<ComponentConfig>,
734    #[serde(skip_serializing_if = "Option::is_none")]
735    pub missions: Option<Vec<String>>,
736}
737
738/// Declarative definition of a bridge component with a list of channels.
739#[derive(Serialize, Deserialize, Debug, Clone)]
740pub struct BridgeConfig {
741    pub id: String,
742    #[serde(rename = "type")]
743    pub type_: String,
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub config: Option<ComponentConfig>,
746    #[serde(skip_serializing_if = "Option::is_none")]
747    pub resources: Option<HashMap<String, String>>,
748    #[serde(skip_serializing_if = "Option::is_none")]
749    pub missions: Option<Vec<String>>,
750    /// Whether this bridge should run as the real implementation in simulation mode.
751    ///
752    /// Default is `true` to preserve historical behavior where bridges were always
753    /// instantiated in sim mode.
754    #[serde(skip_serializing_if = "Option::is_none")]
755    pub run_in_sim: Option<bool>,
756    /// List of logical endpoints exposed by this bridge.
757    pub channels: Vec<BridgeChannelConfigRepresentation>,
758}
759
760impl BridgeConfig {
761    /// By default, bridges run as real implementations in sim mode for backward compatibility.
762    #[allow(dead_code)]
763    pub fn is_run_in_sim(&self) -> bool {
764        self.run_in_sim.unwrap_or(true)
765    }
766
767    fn to_node(&self) -> Node {
768        let mut node = Node::new_with_flavor(&self.id, &self.type_, Flavor::Bridge);
769        node.config = self.config.clone();
770        node.resources = self.resources.clone();
771        node.missions = self.missions.clone();
772        node
773    }
774}
775
776fn insert_bridge_node(graph: &mut CuGraph, bridge: &BridgeConfig) -> Result<(), String> {
777    if graph.get_node_id_by_name(bridge.id.as_str()).is_some() {
778        return Err(format!(
779            "Bridge '{}' reuses an existing node id. Bridge ids must be unique.",
780            bridge.id
781        ));
782    }
783    graph
784        .add_node(bridge.to_node())
785        .map(|_| ())
786        .map_err(|e| e.to_string())
787}
788
789/// Serialized representation of a connection used for the RON config.
790#[derive(Serialize, Deserialize, Debug, Clone)]
791struct SerializedCnx {
792    src: String,
793    dst: String,
794    msg: String,
795    missions: Option<Vec<String>>,
796}
797
798/// Special destination endpoint used to mark an output as intentionally not connected.
799pub const NC_ENDPOINT: &str = "__nc__";
800
801/// This represents a connection between 2 tasks (nodes) in the configuration graph.
802#[derive(Debug, Clone)]
803pub struct Cnx {
804    /// Source node id.
805    pub src: String,
806    /// Destination node id.
807    pub dst: String,
808    /// Message type exchanged between src and dst.
809    pub msg: String,
810    /// Restrict this connection for this list of missions.
811    pub missions: Option<Vec<String>>,
812    /// Optional channel id when the source endpoint is a bridge.
813    pub src_channel: Option<String>,
814    /// Optional channel id when the destination endpoint is a bridge.
815    pub dst_channel: Option<String>,
816    /// Original serialized connection index used to preserve output ordering.
817    pub order: usize,
818}
819
820impl From<&Cnx> for SerializedCnx {
821    fn from(cnx: &Cnx) -> Self {
822        SerializedCnx {
823            src: format_endpoint(&cnx.src, cnx.src_channel.as_deref()),
824            dst: format_endpoint(&cnx.dst, cnx.dst_channel.as_deref()),
825            msg: cnx.msg.clone(),
826            missions: cnx.missions.clone(),
827        }
828    }
829}
830
831fn format_endpoint(node: &str, channel: Option<&str>) -> String {
832    match channel {
833        Some(ch) => format!("{node}/{ch}"),
834        None => node.to_string(),
835    }
836}
837
838fn parse_endpoint(
839    endpoint: &str,
840    role: EndpointRole,
841    bridges: &HashMap<&str, &BridgeConfig>,
842) -> Result<(String, Option<String>), String> {
843    if let Some((node, channel)) = endpoint.split_once('/') {
844        if let Some(bridge) = bridges.get(node) {
845            validate_bridge_channel(bridge, channel, role)?;
846            return Ok((node.to_string(), Some(channel.to_string())));
847        } else {
848            return Err(format!(
849                "Endpoint '{endpoint}' references an unknown bridge '{node}'"
850            ));
851        }
852    }
853
854    if let Some(bridge) = bridges.get(endpoint) {
855        return Err(format!(
856            "Bridge '{}' connections must reference a channel using '{}/<channel>'",
857            bridge.id, bridge.id
858        ));
859    }
860
861    Ok((endpoint.to_string(), None))
862}
863
864fn build_bridge_lookup(bridges: Option<&Vec<BridgeConfig>>) -> HashMap<&str, &BridgeConfig> {
865    let mut map = HashMap::new();
866    if let Some(bridges) = bridges {
867        for bridge in bridges {
868            map.insert(bridge.id.as_str(), bridge);
869        }
870    }
871    map
872}
873
874fn mission_applies(missions: &Option<Vec<String>>, mission_id: &str) -> bool {
875    missions
876        .as_ref()
877        .map(|mission_list| mission_list.iter().any(|m| m == mission_id))
878        .unwrap_or(true)
879}
880
881fn merge_connection_missions(existing: &mut Option<Vec<String>>, incoming: &Option<Vec<String>>) {
882    if incoming.is_none() {
883        *existing = None;
884        return;
885    }
886    if existing.is_none() {
887        return;
888    }
889
890    if let (Some(existing_missions), Some(incoming_missions)) =
891        (existing.as_mut(), incoming.as_ref())
892    {
893        for mission in incoming_missions {
894            if !existing_missions
895                .iter()
896                .any(|existing_mission| existing_mission == mission)
897            {
898                existing_missions.push(mission.clone());
899            }
900        }
901        existing_missions.sort();
902        existing_missions.dedup();
903    }
904}
905
906fn register_nc_output<E>(
907    graph: &mut CuGraph,
908    src_endpoint: &str,
909    msg_type: &str,
910    order: usize,
911    bridge_lookup: &HashMap<&str, &BridgeConfig>,
912) -> Result<(), E>
913where
914    E: From<String>,
915{
916    let (src_name, src_channel) =
917        parse_endpoint(src_endpoint, EndpointRole::Source, bridge_lookup).map_err(E::from)?;
918    if src_channel.is_some() {
919        return Err(E::from(format!(
920            "NC destination '{}' does not support bridge channels in source endpoint '{}'",
921            NC_ENDPOINT, src_endpoint
922        )));
923    }
924
925    let src = graph
926        .get_node_id_by_name(src_name.as_str())
927        .ok_or_else(|| E::from(format!("Source node not found: {src_endpoint}")))?;
928    let src_node = graph
929        .get_node_mut(src)
930        .ok_or_else(|| E::from(format!("Source node id {src} not found for NC output")))?;
931    if src_node.get_flavor() != Flavor::Task {
932        return Err(E::from(format!(
933            "NC destination '{}' is only supported for task outputs (source '{}')",
934            NC_ENDPOINT, src_endpoint
935        )));
936    }
937    src_node.add_nc_output(msg_type, order);
938    Ok(())
939}
940
941/// A simple wrapper enum for `petgraph::Direction`,
942/// designed to be converted *into* it via the `From` trait.
943#[derive(Debug, Clone, Copy, PartialEq, Eq)]
944pub enum CuDirection {
945    Outgoing,
946    Incoming,
947}
948
949impl From<CuDirection> for petgraph::Direction {
950    fn from(dir: CuDirection) -> Self {
951        match dir {
952            CuDirection::Outgoing => petgraph::Direction::Outgoing,
953            CuDirection::Incoming => petgraph::Direction::Incoming,
954        }
955    }
956}
957
958#[derive(Default, Debug, Clone)]
959pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
960
961impl CuGraph {
962    #[allow(dead_code)]
963    pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
964        self.0
965            .node_indices()
966            .map(|index| (index.index() as u32, &self.0[index]))
967            .collect()
968    }
969
970    #[allow(dead_code)]
971    pub fn get_neighbor_ids(&self, node_id: NodeId, dir: CuDirection) -> Vec<NodeId> {
972        self.0
973            .neighbors_directed(node_id.into(), dir.into())
974            .map(|petgraph_index| petgraph_index.index() as NodeId)
975            .collect()
976    }
977
978    #[allow(dead_code)]
979    pub fn node_ids(&self) -> Vec<NodeId> {
980        self.0
981            .node_indices()
982            .map(|index| index.index() as NodeId)
983            .collect()
984    }
985
986    #[allow(dead_code)]
987    pub fn edge_id_between(&self, source: NodeId, target: NodeId) -> Option<usize> {
988        self.0
989            .find_edge(source.into(), target.into())
990            .map(|edge| edge.index())
991    }
992
993    #[allow(dead_code)]
994    pub fn edge(&self, edge_id: usize) -> Option<&Cnx> {
995        self.0.edge_weight(EdgeIndex::new(edge_id))
996    }
997
998    #[allow(dead_code)]
999    pub fn edges(&self) -> impl Iterator<Item = &Cnx> {
1000        self.0
1001            .edge_indices()
1002            .filter_map(|edge| self.0.edge_weight(edge))
1003    }
1004
1005    #[allow(dead_code)]
1006    pub fn bfs_nodes(&self, start: NodeId) -> Vec<NodeId> {
1007        let mut visitor = Bfs::new(&self.0, start.into());
1008        let mut nodes = Vec::new();
1009        while let Some(node) = visitor.next(&self.0) {
1010            nodes.push(node.index() as NodeId);
1011        }
1012        nodes
1013    }
1014
1015    #[allow(dead_code)]
1016    pub fn incoming_neighbor_count(&self, node_id: NodeId) -> usize {
1017        self.0.neighbors_directed(node_id.into(), Incoming).count()
1018    }
1019
1020    #[allow(dead_code)]
1021    pub fn outgoing_neighbor_count(&self, node_id: NodeId) -> usize {
1022        self.0.neighbors_directed(node_id.into(), Outgoing).count()
1023    }
1024
1025    pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
1026        self.0.node_indices().collect()
1027    }
1028
1029    pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
1030        Ok(self.0.add_node(node).index() as NodeId)
1031    }
1032
1033    #[allow(dead_code)]
1034    pub fn connection_exists(&self, source: NodeId, target: NodeId) -> bool {
1035        self.0.find_edge(source.into(), target.into()).is_some()
1036    }
1037
1038    pub fn connect_ext(
1039        &mut self,
1040        source: NodeId,
1041        target: NodeId,
1042        msg_type: &str,
1043        missions: Option<Vec<String>>,
1044        src_channel: Option<String>,
1045        dst_channel: Option<String>,
1046    ) -> CuResult<()> {
1047        self.connect_ext_with_order(
1048            source,
1049            target,
1050            msg_type,
1051            missions,
1052            src_channel,
1053            dst_channel,
1054            usize::MAX,
1055        )
1056    }
1057
1058    #[allow(clippy::too_many_arguments)]
1059    pub fn connect_ext_with_order(
1060        &mut self,
1061        source: NodeId,
1062        target: NodeId,
1063        msg_type: &str,
1064        missions: Option<Vec<String>>,
1065        src_channel: Option<String>,
1066        dst_channel: Option<String>,
1067        order: usize,
1068    ) -> CuResult<()> {
1069        let (src_id, dst_id) = (
1070            self.0
1071                .node_weight(source.into())
1072                .ok_or("Source node not found")?
1073                .id
1074                .clone(),
1075            self.0
1076                .node_weight(target.into())
1077                .ok_or("Target node not found")?
1078                .id
1079                .clone(),
1080        );
1081
1082        let _ = self.0.add_edge(
1083            petgraph::stable_graph::NodeIndex::from(source),
1084            petgraph::stable_graph::NodeIndex::from(target),
1085            Cnx {
1086                src: src_id,
1087                dst: dst_id,
1088                msg: msg_type.to_string(),
1089                missions,
1090                src_channel,
1091                dst_channel,
1092                order,
1093            },
1094        );
1095        Ok(())
1096    }
1097    /// Get the node with the given id.
1098    /// If mission_id is provided, get the node from that mission's graph.
1099    /// Otherwise get the node from the simple graph.
1100    #[allow(dead_code)]
1101    pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
1102        self.0.node_weight(node_id.into())
1103    }
1104
1105    #[allow(dead_code)]
1106    pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
1107        self.0.node_weight(index.into())
1108    }
1109
1110    #[allow(dead_code)]
1111    pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
1112        self.0.node_weight_mut(node_id.into())
1113    }
1114
1115    pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
1116        self.0
1117            .node_indices()
1118            .into_iter()
1119            .find(|idx| self.0[*idx].get_id() == name)
1120            .map(|i| i.index() as NodeId)
1121    }
1122
1123    #[allow(dead_code)]
1124    pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
1125        self.0.edge_weight(EdgeIndex::new(index)).cloned()
1126    }
1127
1128    #[allow(dead_code)]
1129    pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
1130        self.0.node_indices().find_map(|node_index| {
1131            if let Some(node) = self.0.node_weight(node_index) {
1132                if node.id != node_id {
1133                    return None;
1134                }
1135                let edges: Vec<_> = self
1136                    .0
1137                    .edges_directed(node_index, Outgoing)
1138                    .map(|edge| edge.id().index())
1139                    .collect();
1140                if edges.is_empty() {
1141                    return None;
1142                }
1143                let cnx = self
1144                    .0
1145                    .edge_weight(EdgeIndex::new(edges[0]))
1146                    .expect("Found an cnx id but could not retrieve it back");
1147                return Some(cnx.msg.clone());
1148            }
1149            None
1150        })
1151    }
1152
1153    #[allow(dead_code)]
1154    pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
1155        self.get_node_input_msg_types(node_id)
1156            .and_then(|mut v| v.pop())
1157    }
1158
1159    pub fn get_node_input_msg_types(&self, node_id: &str) -> Option<Vec<String>> {
1160        self.0.node_indices().find_map(|node_index| {
1161            if let Some(node) = self.0.node_weight(node_index) {
1162                if node.id != node_id {
1163                    return None;
1164                }
1165                let edges: Vec<_> = self
1166                    .0
1167                    .edges_directed(node_index, Incoming)
1168                    .map(|edge| edge.id().index())
1169                    .collect();
1170                if edges.is_empty() {
1171                    return None;
1172                }
1173                let msgs = edges
1174                    .into_iter()
1175                    .map(|edge_id| {
1176                        let cnx = self
1177                            .0
1178                            .edge_weight(EdgeIndex::new(edge_id))
1179                            .expect("Found an cnx id but could not retrieve it back");
1180                        cnx.msg.clone()
1181                    })
1182                    .collect();
1183                return Some(msgs);
1184            }
1185            None
1186        })
1187    }
1188
1189    #[allow(dead_code)]
1190    pub fn get_connection_msg_type(&self, source: NodeId, target: NodeId) -> Option<&str> {
1191        self.0
1192            .find_edge(source.into(), target.into())
1193            .map(|edge_index| self.0[edge_index].msg.as_str())
1194    }
1195
1196    /// Get the list of edges that are connected to the given node as a source.
1197    fn get_edges_by_direction(
1198        &self,
1199        node_id: NodeId,
1200        direction: petgraph::Direction,
1201    ) -> CuResult<Vec<usize>> {
1202        Ok(self
1203            .0
1204            .edges_directed(node_id.into(), direction)
1205            .map(|edge| edge.id().index())
1206            .collect())
1207    }
1208
1209    pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
1210        self.get_edges_by_direction(node_id, Outgoing)
1211    }
1212
1213    /// Get the list of edges that are connected to the given node as a destination.
1214    pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
1215        self.get_edges_by_direction(node_id, Incoming)
1216    }
1217
1218    #[allow(dead_code)]
1219    pub fn node_count(&self) -> usize {
1220        self.0.node_count()
1221    }
1222
1223    #[allow(dead_code)]
1224    pub fn edge_count(&self) -> usize {
1225        self.0.edge_count()
1226    }
1227
1228    /// Adds an edge between two nodes/tasks in the configuration graph.
1229    /// msg_type is the type of message exchanged between the two nodes/tasks.
1230    #[allow(dead_code)]
1231    pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
1232        self.connect_ext(source, target, msg_type, None, None, None)
1233    }
1234}
1235
1236impl core::ops::Index<NodeIndex> for CuGraph {
1237    type Output = Node;
1238
1239    fn index(&self, index: NodeIndex) -> &Self::Output {
1240        &self.0[index]
1241    }
1242}
1243
1244#[derive(Debug, Clone)]
1245pub enum ConfigGraphs {
1246    Simple(CuGraph),
1247    Missions(HashMap<String, CuGraph>),
1248}
1249
1250impl ConfigGraphs {
1251    /// Returns a consistent hashmap of mission names to Graphs whatever the shape of the config is.
1252    /// Note: if there is only one anonymous mission it will be called "default"
1253    #[allow(dead_code)]
1254    pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
1255        match self {
1256            Simple(graph) => HashMap::from([(DEFAULT_MISSION_ID.to_string(), graph.clone())]),
1257            Missions(graphs) => graphs.clone(),
1258        }
1259    }
1260
1261    #[allow(dead_code)]
1262    pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
1263        match self {
1264            Simple(graph) => Ok(graph),
1265            Missions(graphs) => {
1266                if graphs.len() == 1 {
1267                    Ok(graphs.values().next().unwrap())
1268                } else {
1269                    Err("Cannot get default mission graph from mission config".into())
1270                }
1271            }
1272        }
1273    }
1274
1275    #[allow(dead_code)]
1276    pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1277        match self {
1278            Simple(graph) => match mission_id {
1279                None | Some(DEFAULT_MISSION_ID) => Ok(graph),
1280                Some(_) => Err("Cannot get mission graph from simple config".into()),
1281            },
1282            Missions(graphs) => {
1283                let id = mission_id
1284                    .ok_or_else(|| "Mission ID required for mission configs".to_string())?;
1285                graphs
1286                    .get(id)
1287                    .ok_or_else(|| format!("Mission {id} not found").into())
1288            }
1289        }
1290    }
1291
1292    #[allow(dead_code)]
1293    pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1294        match self {
1295            Simple(graph) => match mission_id {
1296                None => Ok(graph),
1297                Some(_) => Err("Cannot get mission graph from simple config".into()),
1298            },
1299            Missions(graphs) => {
1300                let id = mission_id
1301                    .ok_or_else(|| "Mission ID required for mission configs".to_string())?;
1302                graphs
1303                    .get_mut(id)
1304                    .ok_or_else(|| format!("Mission {id} not found").into())
1305            }
1306        }
1307    }
1308
1309    pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
1310        match self {
1311            Simple(_) => Err("Cannot add mission to simple config".into()),
1312            Missions(graphs) => match graphs.entry(mission_id.to_string()) {
1313                hashbrown::hash_map::Entry::Occupied(_) => {
1314                    Err(format!("Mission {mission_id} already exists").into())
1315                }
1316                hashbrown::hash_map::Entry::Vacant(entry) => Ok(entry.insert(CuGraph::default())),
1317            },
1318        }
1319    }
1320}
1321
1322/// CuConfig is the programmatic representation of the configuration graph.
1323/// It is a directed graph where nodes are tasks and edges are connections between tasks.
1324///
1325/// The core of CuConfig is its `graphs` field which can be either a simple graph
1326/// or a collection of mission-specific graphs. The graph structure is based on petgraph.
1327#[derive(Debug, Clone)]
1328pub struct CuConfig {
1329    /// Monitoring configuration list.
1330    pub monitors: Vec<MonitorConfig>,
1331    /// Optional logging configuration
1332    pub logging: Option<LoggingConfig>,
1333    /// Optional runtime configuration
1334    pub runtime: Option<RuntimeConfig>,
1335    /// Declarative resource bundle definitions
1336    pub resources: Vec<ResourceBundleConfig>,
1337    /// Declarative bridge definitions that are yet to be expanded into the graph
1338    pub bridges: Vec<BridgeConfig>,
1339    /// Graph structure - either a single graph or multiple mission-specific graphs
1340    pub graphs: ConfigGraphs,
1341}
1342
1343impl CuConfig {
1344    #[cfg(feature = "std")]
1345    fn ensure_threadpool_bundle(&mut self) {
1346        if !self.has_background_tasks() {
1347            return;
1348        }
1349        if self
1350            .resources
1351            .iter()
1352            .any(|bundle| bundle.id == "threadpool")
1353        {
1354            return;
1355        }
1356
1357        let mut config = ComponentConfig::default();
1358        config.set("threads", 2u64);
1359        self.resources.push(ResourceBundleConfig {
1360            id: "threadpool".to_string(),
1361            provider: "cu29::resource::ThreadPoolBundle".to_string(),
1362            config: Some(config),
1363            missions: None,
1364        });
1365    }
1366
1367    #[cfg(feature = "std")]
1368    fn has_background_tasks(&self) -> bool {
1369        match &self.graphs {
1370            ConfigGraphs::Simple(graph) => graph
1371                .get_all_nodes()
1372                .iter()
1373                .any(|(_, node)| node.is_background()),
1374            ConfigGraphs::Missions(graphs) => graphs.values().any(|graph| {
1375                graph
1376                    .get_all_nodes()
1377                    .iter()
1378                    .any(|(_, node)| node.is_background())
1379            }),
1380        }
1381    }
1382}
1383
1384#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1385pub struct MonitorConfig {
1386    #[serde(rename = "type")]
1387    type_: String,
1388    #[serde(skip_serializing_if = "Option::is_none")]
1389    config: Option<ComponentConfig>,
1390}
1391
1392impl MonitorConfig {
1393    #[allow(dead_code)]
1394    pub fn get_type(&self) -> &str {
1395        &self.type_
1396    }
1397
1398    #[allow(dead_code)]
1399    pub fn get_config(&self) -> Option<&ComponentConfig> {
1400        self.config.as_ref()
1401    }
1402}
1403
1404fn default_as_true() -> bool {
1405    true
1406}
1407
1408pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
1409
1410fn default_keyframe_interval() -> Option<u32> {
1411    Some(DEFAULT_KEYFRAME_INTERVAL)
1412}
1413
1414#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1415pub struct LoggingConfig {
1416    /// Enable task logging to the log file.
1417    #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
1418    pub enable_task_logging: bool,
1419
1420    /// Size of each slab in the log file. (it is the size of the memory mapped file at a time)
1421    #[serde(skip_serializing_if = "Option::is_none")]
1422    pub slab_size_mib: Option<u64>,
1423
1424    /// Pre-allocated size for each section in the log file.
1425    #[serde(skip_serializing_if = "Option::is_none")]
1426    pub section_size_mib: Option<u64>,
1427
1428    /// Interval in copperlists between two "keyframes" in the log file i.e. freezing tasks.
1429    #[serde(
1430        default = "default_keyframe_interval",
1431        skip_serializing_if = "Option::is_none"
1432    )]
1433    pub keyframe_interval: Option<u32>,
1434}
1435
1436#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1437pub struct RuntimeConfig {
1438    /// Set a CopperList execution rate target in Hz
1439    /// It will act as a rate limiter: if the execution is slower than this rate,
1440    /// it will continue to execute at "best effort".
1441    ///
1442    /// The main usecase is to not waste cycles when the system doesn't need an unbounded execution rate.
1443    #[serde(skip_serializing_if = "Option::is_none")]
1444    pub rate_target_hz: Option<u64>,
1445}
1446
1447/// Missions are used to generate alternative DAGs within the same configuration.
1448#[derive(Serialize, Deserialize, Debug, Clone)]
1449pub struct MissionsConfig {
1450    pub id: String,
1451}
1452
1453/// Includes are used to include other configuration files.
1454#[derive(Serialize, Deserialize, Debug, Clone)]
1455pub struct IncludesConfig {
1456    pub path: String,
1457    pub params: HashMap<String, Value>,
1458    pub missions: Option<Vec<String>>,
1459}
1460
1461/// This is the main Copper configuration representation.
1462#[derive(Serialize, Deserialize, Default)]
1463struct CuConfigRepresentation {
1464    tasks: Option<Vec<Node>>,
1465    resources: Option<Vec<ResourceBundleConfig>>,
1466    bridges: Option<Vec<BridgeConfig>>,
1467    cnx: Option<Vec<SerializedCnx>>,
1468    #[serde(
1469        default,
1470        alias = "monitor",
1471        deserialize_with = "deserialize_monitor_configs"
1472    )]
1473    monitors: Option<Vec<MonitorConfig>>,
1474    logging: Option<LoggingConfig>,
1475    runtime: Option<RuntimeConfig>,
1476    missions: Option<Vec<MissionsConfig>>,
1477    includes: Option<Vec<IncludesConfig>>,
1478}
1479
1480#[derive(Deserialize)]
1481#[serde(untagged)]
1482enum OneOrManyMonitorConfig {
1483    One(MonitorConfig),
1484    Many(Vec<MonitorConfig>),
1485}
1486
1487fn deserialize_monitor_configs<'de, D>(
1488    deserializer: D,
1489) -> Result<Option<Vec<MonitorConfig>>, D::Error>
1490where
1491    D: Deserializer<'de>,
1492{
1493    let parsed = Option::<OneOrManyMonitorConfig>::deserialize(deserializer)?;
1494    Ok(parsed.map(|value| match value {
1495        OneOrManyMonitorConfig::One(single) => vec![single],
1496        OneOrManyMonitorConfig::Many(many) => many,
1497    }))
1498}
1499
1500/// Shared implementation for deserializing a CuConfigRepresentation into a CuConfig
1501fn deserialize_config_representation<E>(
1502    representation: &CuConfigRepresentation,
1503) -> Result<CuConfig, E>
1504where
1505    E: From<String>,
1506{
1507    let mut cuconfig = CuConfig::default();
1508    let bridge_lookup = build_bridge_lookup(representation.bridges.as_ref());
1509
1510    if let Some(mission_configs) = &representation.missions {
1511        // This is the multi-mission case
1512        let mut missions = Missions(HashMap::new());
1513
1514        for mission_config in mission_configs {
1515            let mission_id = mission_config.id.as_str();
1516            let graph = missions
1517                .add_mission(mission_id)
1518                .map_err(|e| E::from(e.to_string()))?;
1519
1520            if let Some(tasks) = &representation.tasks {
1521                for task in tasks {
1522                    if let Some(task_missions) = &task.missions {
1523                        // if there is a filter by mission on the task, only add the task to the mission if it matches the filter.
1524                        if task_missions.contains(&mission_id.to_owned()) {
1525                            graph
1526                                .add_node(task.clone())
1527                                .map_err(|e| E::from(e.to_string()))?;
1528                        }
1529                    } else {
1530                        // if there is no filter by mission on the task, add the task to the mission.
1531                        graph
1532                            .add_node(task.clone())
1533                            .map_err(|e| E::from(e.to_string()))?;
1534                    }
1535                }
1536            }
1537
1538            if let Some(bridges) = &representation.bridges {
1539                for bridge in bridges {
1540                    if mission_applies(&bridge.missions, mission_id) {
1541                        insert_bridge_node(graph, bridge).map_err(E::from)?;
1542                    }
1543                }
1544            }
1545
1546            if let Some(cnx) = &representation.cnx {
1547                for (connection_order, c) in cnx.iter().enumerate() {
1548                    if let Some(cnx_missions) = &c.missions {
1549                        // if there is a filter by mission on the connection, only add the connection to the mission if it matches the filter.
1550                        if cnx_missions.contains(&mission_id.to_owned()) {
1551                            if c.dst == NC_ENDPOINT {
1552                                register_nc_output::<E>(
1553                                    graph,
1554                                    &c.src,
1555                                    &c.msg,
1556                                    connection_order,
1557                                    &bridge_lookup,
1558                                )?;
1559                                continue;
1560                            }
1561                            let (src_name, src_channel) =
1562                                parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1563                                    .map_err(E::from)?;
1564                            let (dst_name, dst_channel) =
1565                                parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1566                                    .map_err(E::from)?;
1567                            let src =
1568                                graph
1569                                    .get_node_id_by_name(src_name.as_str())
1570                                    .ok_or_else(|| {
1571                                        E::from(format!("Source node not found: {}", c.src))
1572                                    })?;
1573                            let dst =
1574                                graph
1575                                    .get_node_id_by_name(dst_name.as_str())
1576                                    .ok_or_else(|| {
1577                                        E::from(format!("Destination node not found: {}", c.dst))
1578                                    })?;
1579                            graph
1580                                .connect_ext_with_order(
1581                                    src,
1582                                    dst,
1583                                    &c.msg,
1584                                    Some(cnx_missions.clone()),
1585                                    src_channel,
1586                                    dst_channel,
1587                                    connection_order,
1588                                )
1589                                .map_err(|e| E::from(e.to_string()))?;
1590                        }
1591                    } else {
1592                        // if there is no filter by mission on the connection, add the connection to the mission.
1593                        if c.dst == NC_ENDPOINT {
1594                            register_nc_output::<E>(
1595                                graph,
1596                                &c.src,
1597                                &c.msg,
1598                                connection_order,
1599                                &bridge_lookup,
1600                            )?;
1601                            continue;
1602                        }
1603                        let (src_name, src_channel) =
1604                            parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1605                                .map_err(E::from)?;
1606                        let (dst_name, dst_channel) =
1607                            parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1608                                .map_err(E::from)?;
1609                        let src = graph
1610                            .get_node_id_by_name(src_name.as_str())
1611                            .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1612                        let dst =
1613                            graph
1614                                .get_node_id_by_name(dst_name.as_str())
1615                                .ok_or_else(|| {
1616                                    E::from(format!("Destination node not found: {}", c.dst))
1617                                })?;
1618                        graph
1619                            .connect_ext_with_order(
1620                                src,
1621                                dst,
1622                                &c.msg,
1623                                None,
1624                                src_channel,
1625                                dst_channel,
1626                                connection_order,
1627                            )
1628                            .map_err(|e| E::from(e.to_string()))?;
1629                    }
1630                }
1631            }
1632        }
1633        cuconfig.graphs = missions;
1634    } else {
1635        // this is the simple case
1636        let mut graph = CuGraph::default();
1637
1638        if let Some(tasks) = &representation.tasks {
1639            for task in tasks {
1640                graph
1641                    .add_node(task.clone())
1642                    .map_err(|e| E::from(e.to_string()))?;
1643            }
1644        }
1645
1646        if let Some(bridges) = &representation.bridges {
1647            for bridge in bridges {
1648                insert_bridge_node(&mut graph, bridge).map_err(E::from)?;
1649            }
1650        }
1651
1652        if let Some(cnx) = &representation.cnx {
1653            for (connection_order, c) in cnx.iter().enumerate() {
1654                if c.dst == NC_ENDPOINT {
1655                    register_nc_output::<E>(
1656                        &mut graph,
1657                        &c.src,
1658                        &c.msg,
1659                        connection_order,
1660                        &bridge_lookup,
1661                    )?;
1662                    continue;
1663                }
1664                let (src_name, src_channel) =
1665                    parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1666                        .map_err(E::from)?;
1667                let (dst_name, dst_channel) =
1668                    parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1669                        .map_err(E::from)?;
1670                let src = graph
1671                    .get_node_id_by_name(src_name.as_str())
1672                    .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1673                let dst = graph
1674                    .get_node_id_by_name(dst_name.as_str())
1675                    .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
1676                graph
1677                    .connect_ext_with_order(
1678                        src,
1679                        dst,
1680                        &c.msg,
1681                        None,
1682                        src_channel,
1683                        dst_channel,
1684                        connection_order,
1685                    )
1686                    .map_err(|e| E::from(e.to_string()))?;
1687            }
1688        }
1689        cuconfig.graphs = Simple(graph);
1690    }
1691
1692    cuconfig.monitors = representation.monitors.clone().unwrap_or_default();
1693    cuconfig.logging = representation.logging.clone();
1694    cuconfig.runtime = representation.runtime.clone();
1695    cuconfig.resources = representation.resources.clone().unwrap_or_default();
1696    cuconfig.bridges = representation.bridges.clone().unwrap_or_default();
1697
1698    Ok(cuconfig)
1699}
1700
1701impl<'de> Deserialize<'de> for CuConfig {
1702    /// This is a custom serialization to make this implementation independent of petgraph.
1703    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1704    where
1705        D: Deserializer<'de>,
1706    {
1707        let representation =
1708            CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
1709
1710        // Convert String errors to D::Error using serde::de::Error::custom
1711        match deserialize_config_representation::<String>(&representation) {
1712            Ok(config) => Ok(config),
1713            Err(e) => Err(serde::de::Error::custom(e)),
1714        }
1715    }
1716}
1717
1718impl Serialize for CuConfig {
1719    /// This is a custom serialization to make this implementation independent of petgraph.
1720    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1721    where
1722        S: Serializer,
1723    {
1724        let bridges = if self.bridges.is_empty() {
1725            None
1726        } else {
1727            Some(self.bridges.clone())
1728        };
1729        let resources = if self.resources.is_empty() {
1730            None
1731        } else {
1732            Some(self.resources.clone())
1733        };
1734        let monitors = (!self.monitors.is_empty()).then_some(self.monitors.clone());
1735        match &self.graphs {
1736            Simple(graph) => {
1737                let tasks: Vec<Node> = graph
1738                    .0
1739                    .node_indices()
1740                    .map(|idx| graph.0[idx].clone())
1741                    .filter(|node| node.get_flavor() == Flavor::Task)
1742                    .collect();
1743
1744                let mut ordered_cnx: Vec<(usize, SerializedCnx)> = graph
1745                    .0
1746                    .edge_indices()
1747                    .map(|edge_idx| {
1748                        let edge = &graph.0[edge_idx];
1749                        let order = if edge.order == usize::MAX {
1750                            edge_idx.index()
1751                        } else {
1752                            edge.order
1753                        };
1754                        (order, SerializedCnx::from(edge))
1755                    })
1756                    .collect();
1757                for node_idx in graph.0.node_indices() {
1758                    let node = &graph.0[node_idx];
1759                    if node.get_flavor() != Flavor::Task {
1760                        continue;
1761                    }
1762                    for (msg, order) in node.nc_outputs_with_order() {
1763                        ordered_cnx.push((
1764                            order,
1765                            SerializedCnx {
1766                                src: node.get_id(),
1767                                dst: NC_ENDPOINT.to_string(),
1768                                msg: msg.clone(),
1769                                missions: None,
1770                            },
1771                        ));
1772                    }
1773                }
1774                ordered_cnx.sort_by(|(order_a, cnx_a), (order_b, cnx_b)| {
1775                    order_a
1776                        .cmp(order_b)
1777                        .then_with(|| cnx_a.src.cmp(&cnx_b.src))
1778                        .then_with(|| cnx_a.dst.cmp(&cnx_b.dst))
1779                        .then_with(|| cnx_a.msg.cmp(&cnx_b.msg))
1780                });
1781                let cnx: Vec<SerializedCnx> = ordered_cnx
1782                    .into_iter()
1783                    .map(|(_, serialized)| serialized)
1784                    .collect();
1785
1786                CuConfigRepresentation {
1787                    tasks: Some(tasks),
1788                    bridges: bridges.clone(),
1789                    cnx: Some(cnx),
1790                    monitors: monitors.clone(),
1791                    logging: self.logging.clone(),
1792                    runtime: self.runtime.clone(),
1793                    resources: resources.clone(),
1794                    missions: None,
1795                    includes: None,
1796                }
1797                .serialize(serializer)
1798            }
1799            Missions(graphs) => {
1800                let missions = graphs
1801                    .keys()
1802                    .map(|id| MissionsConfig { id: id.clone() })
1803                    .collect();
1804
1805                // Collect all unique tasks across missions
1806                let mut tasks = Vec::new();
1807                let mut ordered_cnx: Vec<(usize, SerializedCnx)> = Vec::new();
1808
1809                for (mission_id, graph) in graphs {
1810                    // Add all nodes from this mission
1811                    for node_idx in graph.node_indices() {
1812                        let node = &graph[node_idx];
1813                        if node.get_flavor() == Flavor::Task
1814                            && !tasks.iter().any(|n: &Node| n.id == node.id)
1815                        {
1816                            tasks.push(node.clone());
1817                        }
1818                    }
1819
1820                    // Add all edges from this mission
1821                    for edge_idx in graph.0.edge_indices() {
1822                        let edge = &graph.0[edge_idx];
1823                        let order = if edge.order == usize::MAX {
1824                            edge_idx.index()
1825                        } else {
1826                            edge.order
1827                        };
1828                        let serialized = SerializedCnx::from(edge);
1829                        if let Some((existing_order, existing_serialized)) =
1830                            ordered_cnx.iter_mut().find(|(_, c)| {
1831                                c.src == serialized.src
1832                                    && c.dst == serialized.dst
1833                                    && c.msg == serialized.msg
1834                            })
1835                        {
1836                            if order < *existing_order {
1837                                *existing_order = order;
1838                            }
1839                            merge_connection_missions(
1840                                &mut existing_serialized.missions,
1841                                &serialized.missions,
1842                            );
1843                        } else {
1844                            ordered_cnx.push((order, serialized));
1845                        }
1846                    }
1847                    for node_idx in graph.0.node_indices() {
1848                        let node = &graph.0[node_idx];
1849                        if node.get_flavor() != Flavor::Task {
1850                            continue;
1851                        }
1852                        for (msg, order) in node.nc_outputs_with_order() {
1853                            let serialized = SerializedCnx {
1854                                src: node.get_id(),
1855                                dst: NC_ENDPOINT.to_string(),
1856                                msg: msg.clone(),
1857                                missions: Some(vec![mission_id.clone()]),
1858                            };
1859                            if let Some((existing_order, existing_serialized)) =
1860                                ordered_cnx.iter_mut().find(|(_, c)| {
1861                                    c.src == serialized.src
1862                                        && c.dst == serialized.dst
1863                                        && c.msg == serialized.msg
1864                                })
1865                            {
1866                                if order < *existing_order {
1867                                    *existing_order = order;
1868                                }
1869                                merge_connection_missions(
1870                                    &mut existing_serialized.missions,
1871                                    &serialized.missions,
1872                                );
1873                            } else {
1874                                ordered_cnx.push((order, serialized));
1875                            }
1876                        }
1877                    }
1878                }
1879                ordered_cnx.sort_by(|(order_a, cnx_a), (order_b, cnx_b)| {
1880                    order_a
1881                        .cmp(order_b)
1882                        .then_with(|| cnx_a.src.cmp(&cnx_b.src))
1883                        .then_with(|| cnx_a.dst.cmp(&cnx_b.dst))
1884                        .then_with(|| cnx_a.msg.cmp(&cnx_b.msg))
1885                });
1886                let cnx: Vec<SerializedCnx> = ordered_cnx
1887                    .into_iter()
1888                    .map(|(_, serialized)| serialized)
1889                    .collect();
1890
1891                CuConfigRepresentation {
1892                    tasks: Some(tasks),
1893                    resources: resources.clone(),
1894                    bridges,
1895                    cnx: Some(cnx),
1896                    monitors,
1897                    logging: self.logging.clone(),
1898                    runtime: self.runtime.clone(),
1899                    missions: Some(missions),
1900                    includes: None,
1901                }
1902                .serialize(serializer)
1903            }
1904        }
1905    }
1906}
1907
1908impl Default for CuConfig {
1909    fn default() -> Self {
1910        CuConfig {
1911            graphs: Simple(CuGraph(StableDiGraph::new())),
1912            monitors: Vec::new(),
1913            logging: None,
1914            runtime: None,
1915            resources: Vec::new(),
1916            bridges: Vec::new(),
1917        }
1918    }
1919}
1920
1921/// The implementation has a lot of convenience methods to manipulate
1922/// the configuration to give some flexibility into programmatically creating the configuration.
1923impl CuConfig {
1924    #[allow(dead_code)]
1925    pub fn new_simple_type() -> Self {
1926        Self::default()
1927    }
1928
1929    #[allow(dead_code)]
1930    pub fn new_mission_type() -> Self {
1931        CuConfig {
1932            graphs: Missions(HashMap::new()),
1933            monitors: Vec::new(),
1934            logging: None,
1935            runtime: None,
1936            resources: Vec::new(),
1937            bridges: Vec::new(),
1938        }
1939    }
1940
1941    fn get_options() -> Options {
1942        Options::default()
1943            .with_default_extension(Extensions::IMPLICIT_SOME)
1944            .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1945            .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1946    }
1947
1948    #[allow(dead_code)]
1949    pub fn serialize_ron(&self) -> CuResult<String> {
1950        let ron = Self::get_options();
1951        let pretty = ron::ser::PrettyConfig::default();
1952        ron.to_string_pretty(&self, pretty)
1953            .map_err(|e| CuError::from(format!("Error serializing configuration: {e}")))
1954    }
1955
1956    #[allow(dead_code)]
1957    pub fn deserialize_ron(ron: &str) -> CuResult<Self> {
1958        let representation = Self::get_options().from_str(ron).map_err(|e| {
1959            CuError::from(format!(
1960                "Syntax Error in config: {} at position {}",
1961                e.code, e.span
1962            ))
1963        })?;
1964        Self::deserialize_impl(representation)
1965            .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))
1966    }
1967
1968    fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
1969        deserialize_config_representation(&representation)
1970    }
1971
1972    /// Render the configuration graph in the dot format.
1973    #[cfg(feature = "std")]
1974    #[allow(dead_code)]
1975    pub fn render(
1976        &self,
1977        output: &mut dyn std::io::Write,
1978        mission_id: Option<&str>,
1979    ) -> CuResult<()> {
1980        writeln!(output, "digraph G {{")
1981            .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1982        writeln!(output, "    graph [rankdir=LR, nodesep=0.8, ranksep=1.2];")
1983            .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1984        writeln!(output, "    node [shape=plain, fontname=\"Noto Sans\"];")
1985            .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1986        writeln!(output, "    edge [fontname=\"Noto Sans\"];")
1987            .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1988
1989        let sections = match (&self.graphs, mission_id) {
1990            (Simple(graph), _) => vec![RenderSection { label: None, graph }],
1991            (Missions(graphs), Some(id)) => {
1992                let graph = graphs
1993                    .get(id)
1994                    .ok_or_else(|| CuError::from(format!("Mission {id} not found")))?;
1995                vec![RenderSection {
1996                    label: Some(id.to_string()),
1997                    graph,
1998                }]
1999            }
2000            (Missions(graphs), None) => {
2001                let mut missions: Vec<_> = graphs.iter().collect();
2002                missions.sort_by(|a, b| a.0.cmp(b.0));
2003                missions
2004                    .into_iter()
2005                    .map(|(label, graph)| RenderSection {
2006                        label: Some(label.clone()),
2007                        graph,
2008                    })
2009                    .collect()
2010            }
2011        };
2012
2013        for section in sections {
2014            self.render_section(output, section.graph, section.label.as_deref())?;
2015        }
2016
2017        writeln!(output, "}}")
2018            .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2019        Ok(())
2020    }
2021
2022    #[allow(dead_code)]
2023    pub fn get_all_instances_configs(
2024        &self,
2025        mission_id: Option<&str>,
2026    ) -> Vec<Option<&ComponentConfig>> {
2027        let graph = self.graphs.get_graph(mission_id).unwrap();
2028        graph
2029            .get_all_nodes()
2030            .iter()
2031            .map(|(_, node)| node.get_instance_config())
2032            .collect()
2033    }
2034
2035    #[allow(dead_code)]
2036    pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
2037        self.graphs.get_graph(mission_id)
2038    }
2039
2040    #[allow(dead_code)]
2041    pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
2042        self.graphs.get_graph_mut(mission_id)
2043    }
2044
2045    #[allow(dead_code)]
2046    pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
2047        self.monitors.first()
2048    }
2049
2050    #[allow(dead_code)]
2051    pub fn get_monitor_configs(&self) -> &[MonitorConfig] {
2052        &self.monitors
2053    }
2054
2055    #[allow(dead_code)]
2056    pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
2057        self.runtime.as_ref()
2058    }
2059
2060    /// Validate the logging configuration to ensure section pre-allocation sizes do not exceed slab sizes.
2061    /// This method is wrapper around [LoggingConfig::validate]
2062    pub fn validate_logging_config(&self) -> CuResult<()> {
2063        if let Some(logging) = &self.logging {
2064            return logging.validate();
2065        }
2066        Ok(())
2067    }
2068}
2069
2070#[cfg(feature = "std")]
2071#[derive(Default)]
2072pub(crate) struct PortLookup {
2073    pub inputs: HashMap<String, String>,
2074    pub outputs: HashMap<String, String>,
2075    pub default_input: Option<String>,
2076    pub default_output: Option<String>,
2077}
2078
2079#[cfg(feature = "std")]
2080#[derive(Clone)]
2081pub(crate) struct RenderNode {
2082    pub id: String,
2083    pub type_name: String,
2084    pub flavor: Flavor,
2085    pub inputs: Vec<String>,
2086    pub outputs: Vec<String>,
2087}
2088
2089#[cfg(feature = "std")]
2090#[derive(Clone)]
2091pub(crate) struct RenderConnection {
2092    pub src: String,
2093    pub src_port: Option<String>,
2094    #[allow(dead_code)]
2095    pub src_channel: Option<String>,
2096    pub dst: String,
2097    pub dst_port: Option<String>,
2098    #[allow(dead_code)]
2099    pub dst_channel: Option<String>,
2100    pub msg: String,
2101}
2102
2103#[cfg(feature = "std")]
2104pub(crate) struct RenderTopology {
2105    pub nodes: Vec<RenderNode>,
2106    pub connections: Vec<RenderConnection>,
2107}
2108
2109#[cfg(feature = "std")]
2110impl RenderTopology {
2111    pub fn sort_connections(&mut self) {
2112        self.connections.sort_by(|a, b| {
2113            a.src
2114                .cmp(&b.src)
2115                .then(a.dst.cmp(&b.dst))
2116                .then(a.msg.cmp(&b.msg))
2117        });
2118    }
2119}
2120
2121#[cfg(feature = "std")]
2122#[allow(dead_code)]
2123struct RenderSection<'a> {
2124    label: Option<String>,
2125    graph: &'a CuGraph,
2126}
2127
2128#[cfg(feature = "std")]
2129impl CuConfig {
2130    #[allow(dead_code)]
2131    fn render_section(
2132        &self,
2133        output: &mut dyn std::io::Write,
2134        graph: &CuGraph,
2135        label: Option<&str>,
2136    ) -> CuResult<()> {
2137        use std::fmt::Write as FmtWrite;
2138
2139        let mut topology = build_render_topology(graph, &self.bridges);
2140        topology.nodes.sort_by(|a, b| a.id.cmp(&b.id));
2141        topology.sort_connections();
2142
2143        let cluster_id = label.map(|lbl| format!("cluster_{}", sanitize_identifier(lbl)));
2144        if let Some(ref cluster_id) = cluster_id {
2145            writeln!(output, "    subgraph \"{cluster_id}\" {{")
2146                .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2147            writeln!(
2148                output,
2149                "        label=<<B>Mission: {}</B>>;",
2150                encode_text(label.unwrap())
2151            )
2152            .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2153            writeln!(
2154                output,
2155                "        labelloc=t; labeljust=l; color=\"#bbbbbb\"; style=\"rounded\"; margin=20;"
2156            )
2157            .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2158        }
2159        let indent = if cluster_id.is_some() {
2160            "        "
2161        } else {
2162            "    "
2163        };
2164        let node_prefix = label
2165            .map(|lbl| format!("{}__", sanitize_identifier(lbl)))
2166            .unwrap_or_default();
2167
2168        let mut port_lookup: HashMap<String, PortLookup> = HashMap::new();
2169        let mut id_lookup: HashMap<String, String> = HashMap::new();
2170
2171        for node in &topology.nodes {
2172            let node_idx = graph
2173                .get_node_id_by_name(node.id.as_str())
2174                .ok_or_else(|| CuError::from(format!("Node '{}' missing from graph", node.id)))?;
2175            let node_weight = graph
2176                .get_node(node_idx)
2177                .ok_or_else(|| CuError::from(format!("Node '{}' missing weight", node.id)))?;
2178
2179            let is_src = graph.get_dst_edges(node_idx).unwrap_or_default().is_empty();
2180            let is_sink = graph.get_src_edges(node_idx).unwrap_or_default().is_empty();
2181
2182            let fillcolor = match node.flavor {
2183                Flavor::Bridge => "#faedcd",
2184                Flavor::Task if is_src => "#ddefc7",
2185                Flavor::Task if is_sink => "#cce0ff",
2186                _ => "#f2f2f2",
2187            };
2188
2189            let port_base = format!("{}{}", node_prefix, sanitize_identifier(&node.id));
2190            let (inputs_table, input_map, default_input) =
2191                build_port_table("Inputs", &node.inputs, &port_base, "in");
2192            let (outputs_table, output_map, default_output) =
2193                build_port_table("Outputs", &node.outputs, &port_base, "out");
2194            let config_html = node_weight.config.as_ref().and_then(build_config_table);
2195
2196            let mut label_html = String::new();
2197            write!(
2198                label_html,
2199                "<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"6\" COLOR=\"gray\" BGCOLOR=\"white\">"
2200            )
2201            .unwrap();
2202            write!(
2203                label_html,
2204                "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\" BGCOLOR=\"{fillcolor}\"><FONT POINT-SIZE=\"12\"><B>{}</B></FONT><BR/><FONT COLOR=\"dimgray\">[{}]</FONT></TD></TR>",
2205                encode_text(&node.id),
2206                encode_text(&node.type_name)
2207            )
2208            .unwrap();
2209            write!(
2210                label_html,
2211                "<TR><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{inputs_table}</TD><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{outputs_table}</TD></TR>"
2212            )
2213            .unwrap();
2214
2215            if let Some(config_html) = config_html {
2216                write!(
2217                    label_html,
2218                    "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\">{config_html}</TD></TR>"
2219                )
2220                .unwrap();
2221            }
2222
2223            label_html.push_str("</TABLE>");
2224
2225            let identifier_raw = if node_prefix.is_empty() {
2226                node.id.clone()
2227            } else {
2228                format!("{node_prefix}{}", node.id)
2229            };
2230            let identifier = escape_dot_id(&identifier_raw);
2231            writeln!(output, "{indent}\"{identifier}\" [label=<{label_html}>];")
2232                .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2233
2234            id_lookup.insert(node.id.clone(), identifier);
2235            port_lookup.insert(
2236                node.id.clone(),
2237                PortLookup {
2238                    inputs: input_map,
2239                    outputs: output_map,
2240                    default_input,
2241                    default_output,
2242                },
2243            );
2244        }
2245
2246        for cnx in &topology.connections {
2247            let src_id = id_lookup
2248                .get(&cnx.src)
2249                .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.src)))?;
2250            let dst_id = id_lookup
2251                .get(&cnx.dst)
2252                .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.dst)))?;
2253            let src_suffix = port_lookup
2254                .get(&cnx.src)
2255                .and_then(|lookup| lookup.resolve_output(cnx.src_port.as_deref()))
2256                .map(|port| format!(":\"{port}\":e"))
2257                .unwrap_or_default();
2258            let dst_suffix = port_lookup
2259                .get(&cnx.dst)
2260                .and_then(|lookup| lookup.resolve_input(cnx.dst_port.as_deref()))
2261                .map(|port| format!(":\"{port}\":w"))
2262                .unwrap_or_default();
2263            let msg = encode_text(&cnx.msg);
2264            writeln!(
2265                output,
2266                "{indent}\"{src_id}\"{src_suffix} -> \"{dst_id}\"{dst_suffix} [label=< <B><FONT COLOR=\"gray\">{msg}</FONT></B> >];"
2267            )
2268            .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2269        }
2270
2271        if cluster_id.is_some() {
2272            writeln!(output, "    }}")
2273                .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2274        }
2275
2276        Ok(())
2277    }
2278}
2279
2280#[cfg(feature = "std")]
2281pub(crate) fn build_render_topology(graph: &CuGraph, bridges: &[BridgeConfig]) -> RenderTopology {
2282    let mut bridge_lookup = HashMap::new();
2283    for bridge in bridges {
2284        bridge_lookup.insert(bridge.id.as_str(), bridge);
2285    }
2286
2287    let mut nodes: Vec<RenderNode> = Vec::new();
2288    let mut node_lookup: HashMap<String, usize> = HashMap::new();
2289    for (_, node) in graph.get_all_nodes() {
2290        let node_id = node.get_id();
2291        let mut inputs = Vec::new();
2292        let mut outputs = Vec::new();
2293        if node.get_flavor() == Flavor::Bridge
2294            && let Some(bridge) = bridge_lookup.get(node_id.as_str())
2295        {
2296            for channel in &bridge.channels {
2297                match channel {
2298                    // Rx brings data from the bridge into the graph, so treat it as an output.
2299                    BridgeChannelConfigRepresentation::Rx { id, .. } => outputs.push(id.clone()),
2300                    // Tx consumes data from the graph heading into the bridge, so show it on the input side.
2301                    BridgeChannelConfigRepresentation::Tx { id, .. } => inputs.push(id.clone()),
2302                }
2303            }
2304        }
2305
2306        node_lookup.insert(node_id.clone(), nodes.len());
2307        nodes.push(RenderNode {
2308            id: node_id,
2309            type_name: node.get_type().to_string(),
2310            flavor: node.get_flavor(),
2311            inputs,
2312            outputs,
2313        });
2314    }
2315
2316    let mut output_port_lookup: Vec<HashMap<String, String>> = vec![HashMap::new(); nodes.len()];
2317    let mut output_edges: Vec<_> = graph.0.edge_references().collect();
2318    output_edges.sort_by_key(|edge| edge.id().index());
2319    for edge in output_edges {
2320        let cnx = edge.weight();
2321        if let Some(&idx) = node_lookup.get(&cnx.src)
2322            && nodes[idx].flavor == Flavor::Task
2323            && cnx.src_channel.is_none()
2324        {
2325            let port_map = &mut output_port_lookup[idx];
2326            if !port_map.contains_key(&cnx.msg) {
2327                let label = format!("out{}: {}", port_map.len(), cnx.msg);
2328                port_map.insert(cnx.msg.clone(), label.clone());
2329                nodes[idx].outputs.push(label);
2330            }
2331        }
2332    }
2333
2334    let mut auto_input_counts = vec![0usize; nodes.len()];
2335    for edge in graph.0.edge_references() {
2336        let cnx = edge.weight();
2337        if let Some(&idx) = node_lookup.get(&cnx.dst)
2338            && nodes[idx].flavor == Flavor::Task
2339            && cnx.dst_channel.is_none()
2340        {
2341            auto_input_counts[idx] += 1;
2342        }
2343    }
2344
2345    let mut next_auto_input = vec![0usize; nodes.len()];
2346    let mut connections = Vec::new();
2347    for edge in graph.0.edge_references() {
2348        let cnx = edge.weight();
2349        let mut src_port = cnx.src_channel.clone();
2350        let mut dst_port = cnx.dst_channel.clone();
2351
2352        if let Some(&idx) = node_lookup.get(&cnx.src) {
2353            let node = &mut nodes[idx];
2354            if node.flavor == Flavor::Task && src_port.is_none() {
2355                src_port = output_port_lookup[idx].get(&cnx.msg).cloned();
2356            }
2357        }
2358        if let Some(&idx) = node_lookup.get(&cnx.dst) {
2359            let node = &mut nodes[idx];
2360            if node.flavor == Flavor::Task && dst_port.is_none() {
2361                let count = auto_input_counts[idx];
2362                let next = if count <= 1 {
2363                    "in".to_string()
2364                } else {
2365                    let next = format!("in.{}", next_auto_input[idx]);
2366                    next_auto_input[idx] += 1;
2367                    next
2368                };
2369                node.inputs.push(next.clone());
2370                dst_port = Some(next);
2371            }
2372        }
2373
2374        connections.push(RenderConnection {
2375            src: cnx.src.clone(),
2376            src_port,
2377            src_channel: cnx.src_channel.clone(),
2378            dst: cnx.dst.clone(),
2379            dst_port,
2380            dst_channel: cnx.dst_channel.clone(),
2381            msg: cnx.msg.clone(),
2382        });
2383    }
2384
2385    RenderTopology { nodes, connections }
2386}
2387
2388#[cfg(feature = "std")]
2389impl PortLookup {
2390    pub fn resolve_input(&self, name: Option<&str>) -> Option<&str> {
2391        if let Some(name) = name
2392            && let Some(port) = self.inputs.get(name)
2393        {
2394            return Some(port.as_str());
2395        }
2396        self.default_input.as_deref()
2397    }
2398
2399    pub fn resolve_output(&self, name: Option<&str>) -> Option<&str> {
2400        if let Some(name) = name
2401            && let Some(port) = self.outputs.get(name)
2402        {
2403            return Some(port.as_str());
2404        }
2405        self.default_output.as_deref()
2406    }
2407}
2408
2409#[cfg(feature = "std")]
2410#[allow(dead_code)]
2411fn build_port_table(
2412    title: &str,
2413    names: &[String],
2414    base_id: &str,
2415    prefix: &str,
2416) -> (String, HashMap<String, String>, Option<String>) {
2417    use std::fmt::Write as FmtWrite;
2418
2419    let mut html = String::new();
2420    write!(
2421        html,
2422        "<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">"
2423    )
2424    .unwrap();
2425    write!(
2426        html,
2427        "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT></TD></TR>",
2428        encode_text(title)
2429    )
2430    .unwrap();
2431
2432    let mut lookup = HashMap::new();
2433    let mut default_port = None;
2434
2435    if names.is_empty() {
2436        html.push_str("<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"lightgray\">&mdash;</FONT></TD></TR>");
2437    } else {
2438        for (idx, name) in names.iter().enumerate() {
2439            let port_id = format!("{base_id}_{prefix}_{idx}");
2440            write!(
2441                html,
2442                "<TR><TD PORT=\"{port_id}\" ALIGN=\"LEFT\">{}</TD></TR>",
2443                encode_text(name)
2444            )
2445            .unwrap();
2446            lookup.insert(name.clone(), port_id.clone());
2447            if idx == 0 {
2448                default_port = Some(port_id);
2449            }
2450        }
2451    }
2452
2453    html.push_str("</TABLE>");
2454    (html, lookup, default_port)
2455}
2456
2457#[cfg(feature = "std")]
2458#[allow(dead_code)]
2459fn build_config_table(config: &ComponentConfig) -> Option<String> {
2460    use std::fmt::Write as FmtWrite;
2461
2462    if config.0.is_empty() {
2463        return None;
2464    }
2465
2466    let mut entries: Vec<_> = config.0.iter().collect();
2467    entries.sort_by(|a, b| a.0.cmp(b.0));
2468
2469    let mut html = String::new();
2470    html.push_str("<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">");
2471    for (key, value) in entries {
2472        let value_txt = format!("{value}");
2473        write!(
2474            html,
2475            "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT> = {}</TD></TR>",
2476            encode_text(key),
2477            encode_text(&value_txt)
2478        )
2479        .unwrap();
2480    }
2481    html.push_str("</TABLE>");
2482    Some(html)
2483}
2484
2485#[cfg(feature = "std")]
2486#[allow(dead_code)]
2487fn sanitize_identifier(value: &str) -> String {
2488    value
2489        .chars()
2490        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
2491        .collect()
2492}
2493
2494#[cfg(feature = "std")]
2495#[allow(dead_code)]
2496fn escape_dot_id(value: &str) -> String {
2497    let mut escaped = String::with_capacity(value.len());
2498    for ch in value.chars() {
2499        match ch {
2500            '"' => escaped.push_str("\\\""),
2501            '\\' => escaped.push_str("\\\\"),
2502            _ => escaped.push(ch),
2503        }
2504    }
2505    escaped
2506}
2507
2508impl LoggingConfig {
2509    /// Validate the logging configuration to ensure section pre-allocation sizes do not exceed slab sizes.
2510    pub fn validate(&self) -> CuResult<()> {
2511        if let Some(section_size_mib) = self.section_size_mib
2512            && let Some(slab_size_mib) = self.slab_size_mib
2513            && section_size_mib > slab_size_mib
2514        {
2515            return Err(CuError::from(format!(
2516                "Section size ({section_size_mib} MiB) cannot be larger than slab size ({slab_size_mib} MiB). Adjust the parameters accordingly."
2517            )));
2518        }
2519
2520        Ok(())
2521    }
2522}
2523
2524#[allow(dead_code)] // dead in no-std
2525fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
2526    let mut result = content.to_string();
2527
2528    for (key, value) in params {
2529        let pattern = format!("{{{{{key}}}}}");
2530        result = result.replace(&pattern, &value.to_string());
2531    }
2532
2533    result
2534}
2535
2536/// Returns a merged CuConfigRepresentation.
2537#[cfg(feature = "std")]
2538fn process_includes(
2539    file_path: &str,
2540    base_representation: CuConfigRepresentation,
2541    processed_files: &mut Vec<String>,
2542) -> CuResult<CuConfigRepresentation> {
2543    // Note: Circular dependency detection removed
2544    processed_files.push(file_path.to_string());
2545
2546    let mut result = base_representation;
2547
2548    if let Some(includes) = result.includes.take() {
2549        for include in includes {
2550            let include_path = if include.path.starts_with('/') {
2551                include.path.clone()
2552            } else {
2553                let current_dir = std::path::Path::new(file_path)
2554                    .parent()
2555                    .unwrap_or_else(|| std::path::Path::new(""))
2556                    .to_string_lossy()
2557                    .to_string();
2558
2559                format!("{}/{}", current_dir, include.path)
2560            };
2561
2562            let include_content = read_to_string(&include_path).map_err(|e| {
2563                CuError::from(format!("Failed to read include file: {include_path}"))
2564                    .add_cause(e.to_string().as_str())
2565            })?;
2566
2567            let processed_content = substitute_parameters(&include_content, &include.params);
2568
2569            let mut included_representation: CuConfigRepresentation = match Options::default()
2570                .with_default_extension(Extensions::IMPLICIT_SOME)
2571                .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2572                .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2573                .from_str(&processed_content)
2574            {
2575                Ok(rep) => rep,
2576                Err(e) => {
2577                    return Err(CuError::from(format!(
2578                        "Failed to parse include file: {} - Error: {} at position {}",
2579                        include_path, e.code, e.span
2580                    )));
2581                }
2582            };
2583
2584            included_representation =
2585                process_includes(&include_path, included_representation, processed_files)?;
2586
2587            if let Some(included_tasks) = included_representation.tasks {
2588                if result.tasks.is_none() {
2589                    result.tasks = Some(included_tasks);
2590                } else {
2591                    let mut tasks = result.tasks.take().unwrap();
2592                    for included_task in included_tasks {
2593                        if !tasks.iter().any(|t| t.id == included_task.id) {
2594                            tasks.push(included_task);
2595                        }
2596                    }
2597                    result.tasks = Some(tasks);
2598                }
2599            }
2600
2601            if let Some(included_bridges) = included_representation.bridges {
2602                if result.bridges.is_none() {
2603                    result.bridges = Some(included_bridges);
2604                } else {
2605                    let mut bridges = result.bridges.take().unwrap();
2606                    for included_bridge in included_bridges {
2607                        if !bridges.iter().any(|b| b.id == included_bridge.id) {
2608                            bridges.push(included_bridge);
2609                        }
2610                    }
2611                    result.bridges = Some(bridges);
2612                }
2613            }
2614
2615            if let Some(included_resources) = included_representation.resources {
2616                if result.resources.is_none() {
2617                    result.resources = Some(included_resources);
2618                } else {
2619                    let mut resources = result.resources.take().unwrap();
2620                    for included_resource in included_resources {
2621                        if !resources.iter().any(|r| r.id == included_resource.id) {
2622                            resources.push(included_resource);
2623                        }
2624                    }
2625                    result.resources = Some(resources);
2626                }
2627            }
2628
2629            if let Some(included_cnx) = included_representation.cnx {
2630                if result.cnx.is_none() {
2631                    result.cnx = Some(included_cnx);
2632                } else {
2633                    let mut cnx = result.cnx.take().unwrap();
2634                    for included_c in included_cnx {
2635                        if !cnx
2636                            .iter()
2637                            .any(|c| c.src == included_c.src && c.dst == included_c.dst)
2638                        {
2639                            cnx.push(included_c);
2640                        }
2641                    }
2642                    result.cnx = Some(cnx);
2643                }
2644            }
2645
2646            if let Some(included_monitors) = included_representation.monitors {
2647                if result.monitors.is_none() {
2648                    result.monitors = Some(included_monitors);
2649                } else {
2650                    let mut monitors = result.monitors.take().unwrap();
2651                    for included_monitor in included_monitors {
2652                        if !monitors.iter().any(|m| m.type_ == included_monitor.type_) {
2653                            monitors.push(included_monitor);
2654                        }
2655                    }
2656                    result.monitors = Some(monitors);
2657                }
2658            }
2659
2660            if result.logging.is_none() {
2661                result.logging = included_representation.logging;
2662            }
2663
2664            if result.runtime.is_none() {
2665                result.runtime = included_representation.runtime;
2666            }
2667
2668            if let Some(included_missions) = included_representation.missions {
2669                if result.missions.is_none() {
2670                    result.missions = Some(included_missions);
2671                } else {
2672                    let mut missions = result.missions.take().unwrap();
2673                    for included_mission in included_missions {
2674                        if !missions.iter().any(|m| m.id == included_mission.id) {
2675                            missions.push(included_mission);
2676                        }
2677                    }
2678                    result.missions = Some(missions);
2679                }
2680            }
2681        }
2682    }
2683
2684    Ok(result)
2685}
2686
2687/// Read a copper configuration from a file.
2688#[cfg(feature = "std")]
2689pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
2690    let config_content = read_to_string(config_filename).map_err(|e| {
2691        CuError::from(format!(
2692            "Failed to read configuration file: {:?}",
2693            &config_filename
2694        ))
2695        .add_cause(e.to_string().as_str())
2696    })?;
2697    read_configuration_str(config_content, Some(config_filename))
2698}
2699
2700/// Read a copper configuration from a String.
2701/// Parse a RON string into a CuConfigRepresentation, using the standard options.
2702/// Returns an error if the parsing fails.
2703fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
2704    Options::default()
2705        .with_default_extension(Extensions::IMPLICIT_SOME)
2706        .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2707        .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2708        .from_str(content)
2709        .map_err(|e| {
2710            CuError::from(format!(
2711                "Failed to parse configuration: Error: {} at position {}",
2712                e.code, e.span
2713            ))
2714        })
2715}
2716
2717/// Convert a CuConfigRepresentation to a CuConfig.
2718/// Uses the deserialize_impl method and validates the logging configuration.
2719fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
2720    #[allow(unused_mut)]
2721    let mut cuconfig = CuConfig::deserialize_impl(representation)
2722        .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
2723
2724    #[cfg(feature = "std")]
2725    cuconfig.ensure_threadpool_bundle();
2726
2727    cuconfig.validate_logging_config()?;
2728
2729    Ok(cuconfig)
2730}
2731
2732#[allow(unused_variables)]
2733pub fn read_configuration_str(
2734    config_content: String,
2735    file_path: Option<&str>,
2736) -> CuResult<CuConfig> {
2737    // Parse the configuration string
2738    let representation = parse_config_string(&config_content)?;
2739
2740    // Process includes and generate a merged configuration if a file path is provided
2741    // includes are only available with std.
2742    #[cfg(feature = "std")]
2743    let representation = if let Some(path) = file_path {
2744        process_includes(path, representation, &mut Vec::new())?
2745    } else {
2746        representation
2747    };
2748
2749    // Convert the representation to a CuConfig and validate
2750    config_representation_to_config(representation)
2751}
2752
2753// tests
2754#[cfg(test)]
2755mod tests {
2756    use super::*;
2757    #[cfg(not(feature = "std"))]
2758    use alloc::vec;
2759    use serde::Deserialize;
2760
2761    #[test]
2762    fn test_plain_serialize() {
2763        let mut config = CuConfig::default();
2764        let graph = config.get_graph_mut(None).unwrap();
2765        let n1 = graph
2766            .add_node(Node::new("test1", "package::Plugin1"))
2767            .unwrap();
2768        let n2 = graph
2769            .add_node(Node::new("test2", "package::Plugin2"))
2770            .unwrap();
2771        graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
2772        let serialized = config.serialize_ron().unwrap();
2773        let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
2774        let graph = config.graphs.get_graph(None).unwrap();
2775        let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
2776        assert_eq!(graph.node_count(), deserialized_graph.node_count());
2777        assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
2778    }
2779
2780    #[test]
2781    fn test_serialize_with_params() {
2782        let mut config = CuConfig::default();
2783        let graph = config.get_graph_mut(None).unwrap();
2784        let mut camera = Node::new("copper-camera", "camerapkg::Camera");
2785        camera.set_param::<Value>("resolution-height", 1080.into());
2786        graph.add_node(camera).unwrap();
2787        let serialized = config.serialize_ron().unwrap();
2788        let config = CuConfig::deserialize_ron(&serialized).unwrap();
2789        let deserialized = config.get_graph(None).unwrap();
2790        let resolution = deserialized
2791            .get_node(0)
2792            .unwrap()
2793            .get_param::<i32>("resolution-height")
2794            .expect("resolution-height lookup failed");
2795        assert_eq!(resolution, Some(1080));
2796    }
2797
2798    #[derive(Debug, Deserialize, PartialEq)]
2799    struct InnerSettings {
2800        threshold: u32,
2801        flags: Option<bool>,
2802    }
2803
2804    #[derive(Debug, Deserialize, PartialEq)]
2805    struct SettingsConfig {
2806        gain: f32,
2807        matrix: [[f32; 3]; 3],
2808        inner: InnerSettings,
2809        tags: Vec<String>,
2810    }
2811
2812    #[test]
2813    fn test_component_config_get_value_structured() {
2814        let txt = r#"
2815            (
2816                tasks: [
2817                    (
2818                        id: "task",
2819                        type: "pkg::Task",
2820                        config: {
2821                            "settings": {
2822                                "gain": 1.5,
2823                                "matrix": [
2824                                    [1.0, 0.0, 0.0],
2825                                    [0.0, 1.0, 0.0],
2826                                    [0.0, 0.0, 1.0],
2827                                ],
2828                                "inner": { "threshold": 42, "flags": Some(true) },
2829                                "tags": ["alpha", "beta"],
2830                            },
2831                        },
2832                    ),
2833                ],
2834                cnx: [],
2835            )
2836        "#;
2837        let config = CuConfig::deserialize_ron(txt).unwrap();
2838        let graph = config.graphs.get_graph(None).unwrap();
2839        let node = graph.get_node(0).unwrap();
2840        let component = node.get_instance_config().expect("missing config");
2841        let settings = component
2842            .get_value::<SettingsConfig>("settings")
2843            .expect("settings lookup failed")
2844            .expect("missing settings");
2845        let expected = SettingsConfig {
2846            gain: 1.5,
2847            matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
2848            inner: InnerSettings {
2849                threshold: 42,
2850                flags: Some(true),
2851            },
2852            tags: vec!["alpha".to_string(), "beta".to_string()],
2853        };
2854        assert_eq!(settings, expected);
2855    }
2856
2857    #[test]
2858    fn test_component_config_get_value_scalar_compatibility() {
2859        let txt = r#"
2860            (
2861                tasks: [
2862                    (id: "task", type: "pkg::Task", config: { "scalar": 7 }),
2863                ],
2864                cnx: [],
2865            )
2866        "#;
2867        let config = CuConfig::deserialize_ron(txt).unwrap();
2868        let graph = config.graphs.get_graph(None).unwrap();
2869        let node = graph.get_node(0).unwrap();
2870        let component = node.get_instance_config().expect("missing config");
2871        let scalar = component
2872            .get::<u32>("scalar")
2873            .expect("scalar lookup failed");
2874        assert_eq!(scalar, Some(7));
2875    }
2876
2877    #[test]
2878    fn test_component_config_get_value_mixed_usage() {
2879        let txt = r#"
2880            (
2881                tasks: [
2882                    (
2883                        id: "task",
2884                        type: "pkg::Task",
2885                        config: {
2886                            "scalar": 12,
2887                            "settings": {
2888                                "gain": 2.5,
2889                                "matrix": [
2890                                    [1.0, 2.0, 3.0],
2891                                    [4.0, 5.0, 6.0],
2892                                    [7.0, 8.0, 9.0],
2893                                ],
2894                                "inner": { "threshold": 7, "flags": None },
2895                                "tags": ["gamma"],
2896                            },
2897                        },
2898                    ),
2899                ],
2900                cnx: [],
2901            )
2902        "#;
2903        let config = CuConfig::deserialize_ron(txt).unwrap();
2904        let graph = config.graphs.get_graph(None).unwrap();
2905        let node = graph.get_node(0).unwrap();
2906        let component = node.get_instance_config().expect("missing config");
2907        let scalar = component
2908            .get::<u32>("scalar")
2909            .expect("scalar lookup failed");
2910        let settings = component
2911            .get_value::<SettingsConfig>("settings")
2912            .expect("settings lookup failed");
2913        assert_eq!(scalar, Some(12));
2914        assert!(settings.is_some());
2915    }
2916
2917    #[test]
2918    fn test_component_config_get_value_error_includes_key() {
2919        let txt = r#"
2920            (
2921                tasks: [
2922                    (
2923                        id: "task",
2924                        type: "pkg::Task",
2925                        config: { "settings": { "gain": 1.0 } },
2926                    ),
2927                ],
2928                cnx: [],
2929            )
2930        "#;
2931        let config = CuConfig::deserialize_ron(txt).unwrap();
2932        let graph = config.graphs.get_graph(None).unwrap();
2933        let node = graph.get_node(0).unwrap();
2934        let component = node.get_instance_config().expect("missing config");
2935        let err = component
2936            .get_value::<u32>("settings")
2937            .expect_err("expected type mismatch");
2938        assert!(err.to_string().contains("settings"));
2939    }
2940
2941    #[test]
2942    fn test_deserialization_error() {
2943        // Task needs to be an array, but provided tuple wrongfully
2944        let txt = r#"( tasks: (), cnx: [], monitors: [(type: "ExampleMonitor", )] ) "#;
2945        let err = CuConfig::deserialize_ron(txt).expect_err("expected deserialization error");
2946        assert!(
2947            err.to_string()
2948                .contains("Syntax Error in config: Expected opening `[` at position 1:9-1:10")
2949        );
2950    }
2951    #[test]
2952    fn test_missions() {
2953        let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
2954        let config = CuConfig::deserialize_ron(txt).unwrap();
2955        let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
2956        assert!(graph.node_count() == 0);
2957        let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
2958        assert!(graph.node_count() == 0);
2959    }
2960
2961    #[test]
2962    fn test_monitor_plural_syntax() {
2963        let txt = r#"( tasks: [], cnx: [], monitors: [(type: "ExampleMonitor", )] ) "#;
2964        let config = CuConfig::deserialize_ron(txt).unwrap();
2965        assert_eq!(config.get_monitor_config().unwrap().type_, "ExampleMonitor");
2966
2967        let txt = r#"( tasks: [], cnx: [], monitors: [(type: "ExampleMonitor", config: { "toto": 4, } )] ) "#;
2968        let config = CuConfig::deserialize_ron(txt).unwrap();
2969        assert_eq!(
2970            config
2971                .get_monitor_config()
2972                .unwrap()
2973                .config
2974                .as_ref()
2975                .unwrap()
2976                .0["toto"]
2977                .0,
2978            4u8.into()
2979        );
2980    }
2981
2982    #[test]
2983    fn test_monitor_singular_syntax() {
2984        let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } ) ) "#;
2985        let config = CuConfig::deserialize_ron(txt).unwrap();
2986        assert_eq!(config.get_monitor_configs().len(), 1);
2987        assert_eq!(config.get_monitor_config().unwrap().type_, "ExampleMonitor");
2988        assert_eq!(
2989            config
2990                .get_monitor_config()
2991                .unwrap()
2992                .config
2993                .as_ref()
2994                .unwrap()
2995                .0["toto"]
2996                .0,
2997            4u8.into()
2998        );
2999    }
3000
3001    #[test]
3002    #[cfg(feature = "std")]
3003    fn test_render_topology_multi_input_ports() {
3004        let mut config = CuConfig::default();
3005        let graph = config.get_graph_mut(None).unwrap();
3006        let src1 = graph.add_node(Node::new("src1", "tasks::Source1")).unwrap();
3007        let src2 = graph.add_node(Node::new("src2", "tasks::Source2")).unwrap();
3008        let dst = graph.add_node(Node::new("dst", "tasks::Dst")).unwrap();
3009        graph.connect(src1, dst, "msg::A").unwrap();
3010        graph.connect(src2, dst, "msg::B").unwrap();
3011
3012        let topology = build_render_topology(graph, &[]);
3013        let dst_node = topology
3014            .nodes
3015            .iter()
3016            .find(|node| node.id == "dst")
3017            .expect("missing dst node");
3018        assert_eq!(dst_node.inputs.len(), 2);
3019
3020        let mut dst_ports: Vec<_> = topology
3021            .connections
3022            .iter()
3023            .filter(|cnx| cnx.dst == "dst")
3024            .map(|cnx| cnx.dst_port.as_deref().expect("missing dst port"))
3025            .collect();
3026        dst_ports.sort();
3027        assert_eq!(dst_ports, vec!["in.0", "in.1"]);
3028    }
3029
3030    #[test]
3031    fn test_logging_parameters() {
3032        // Test with `enable_task_logging: false`
3033        let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
3034
3035        let config = CuConfig::deserialize_ron(txt).unwrap();
3036        assert!(config.logging.is_some());
3037        let logging_config = config.logging.unwrap();
3038        assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
3039        assert_eq!(logging_config.section_size_mib.unwrap(), 100);
3040        assert!(!logging_config.enable_task_logging);
3041
3042        // Test with `enable_task_logging` not provided
3043        let txt =
3044            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
3045        let config = CuConfig::deserialize_ron(txt).unwrap();
3046        assert!(config.logging.is_some());
3047        let logging_config = config.logging.unwrap();
3048        assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
3049        assert_eq!(logging_config.section_size_mib.unwrap(), 100);
3050        assert!(logging_config.enable_task_logging);
3051    }
3052
3053    #[test]
3054    fn test_bridge_parsing() {
3055        let txt = r#"
3056        (
3057            tasks: [
3058                (id: "dst", type: "tasks::Destination"),
3059                (id: "src", type: "tasks::Source"),
3060            ],
3061            bridges: [
3062                (
3063                    id: "radio",
3064                    type: "tasks::SerialBridge",
3065                    config: { "path": "/dev/ttyACM0", "baud": 921600 },
3066                    channels: [
3067                        Rx ( id: "status", route: "sys/status" ),
3068                        Tx ( id: "motor", route: "motor/cmd" ),
3069                    ],
3070                ),
3071            ],
3072            cnx: [
3073                (src: "radio/status", dst: "dst", msg: "mymsgs::Status"),
3074                (src: "src", dst: "radio/motor", msg: "mymsgs::MotorCmd"),
3075            ],
3076        )
3077        "#;
3078
3079        let config = CuConfig::deserialize_ron(txt).unwrap();
3080        assert_eq!(config.bridges.len(), 1);
3081        let bridge = &config.bridges[0];
3082        assert_eq!(bridge.id, "radio");
3083        assert_eq!(bridge.channels.len(), 2);
3084        match &bridge.channels[0] {
3085            BridgeChannelConfigRepresentation::Rx { id, route, .. } => {
3086                assert_eq!(id, "status");
3087                assert_eq!(route.as_deref(), Some("sys/status"));
3088            }
3089            _ => panic!("expected Rx channel"),
3090        }
3091        match &bridge.channels[1] {
3092            BridgeChannelConfigRepresentation::Tx { id, route, .. } => {
3093                assert_eq!(id, "motor");
3094                assert_eq!(route.as_deref(), Some("motor/cmd"));
3095            }
3096            _ => panic!("expected Tx channel"),
3097        }
3098        let graph = config.graphs.get_graph(None).unwrap();
3099        let bridge_id = graph
3100            .get_node_id_by_name("radio")
3101            .expect("bridge node missing");
3102        let bridge_node = graph.get_node(bridge_id).unwrap();
3103        assert_eq!(bridge_node.get_flavor(), Flavor::Bridge);
3104
3105        // Edges should retain channel metadata.
3106        let mut edges = Vec::new();
3107        for edge_idx in graph.0.edge_indices() {
3108            edges.push(graph.0[edge_idx].clone());
3109        }
3110        assert_eq!(edges.len(), 2);
3111        let status_edge = edges
3112            .iter()
3113            .find(|e| e.dst == "dst")
3114            .expect("status edge missing");
3115        assert_eq!(status_edge.src_channel.as_deref(), Some("status"));
3116        assert!(status_edge.dst_channel.is_none());
3117        let motor_edge = edges
3118            .iter()
3119            .find(|e| e.dst_channel.is_some())
3120            .expect("motor edge missing");
3121        assert_eq!(motor_edge.dst_channel.as_deref(), Some("motor"));
3122    }
3123
3124    #[test]
3125    fn test_bridge_roundtrip() {
3126        let mut config = CuConfig::default();
3127        let mut bridge_config = ComponentConfig::default();
3128        bridge_config.set("port", "/dev/ttyACM0".to_string());
3129        config.bridges.push(BridgeConfig {
3130            id: "radio".to_string(),
3131            type_: "tasks::SerialBridge".to_string(),
3132            config: Some(bridge_config),
3133            resources: None,
3134            missions: None,
3135            run_in_sim: None,
3136            channels: vec![
3137                BridgeChannelConfigRepresentation::Rx {
3138                    id: "status".to_string(),
3139                    route: Some("sys/status".to_string()),
3140                    config: None,
3141                },
3142                BridgeChannelConfigRepresentation::Tx {
3143                    id: "motor".to_string(),
3144                    route: Some("motor/cmd".to_string()),
3145                    config: None,
3146                },
3147            ],
3148        });
3149
3150        let serialized = config.serialize_ron().unwrap();
3151        assert!(
3152            serialized.contains("bridges"),
3153            "bridges section missing from serialized config"
3154        );
3155        let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3156        assert_eq!(deserialized.bridges.len(), 1);
3157        let bridge = &deserialized.bridges[0];
3158        assert!(bridge.is_run_in_sim());
3159        assert_eq!(bridge.channels.len(), 2);
3160        assert!(matches!(
3161            bridge.channels[0],
3162            BridgeChannelConfigRepresentation::Rx { .. }
3163        ));
3164        assert!(matches!(
3165            bridge.channels[1],
3166            BridgeChannelConfigRepresentation::Tx { .. }
3167        ));
3168    }
3169
3170    #[test]
3171    fn test_resource_parsing() {
3172        let txt = r#"
3173        (
3174            resources: [
3175                (
3176                    id: "fc",
3177                    provider: "copper_board_px4::Px4Bundle",
3178                    config: { "baud": 921600 },
3179                    missions: ["m1"],
3180                ),
3181                (
3182                    id: "misc",
3183                    provider: "cu29_runtime::StdClockBundle",
3184                ),
3185            ],
3186        )
3187        "#;
3188
3189        let config = CuConfig::deserialize_ron(txt).unwrap();
3190        assert_eq!(config.resources.len(), 2);
3191        let fc = &config.resources[0];
3192        assert_eq!(fc.id, "fc");
3193        assert_eq!(fc.provider, "copper_board_px4::Px4Bundle");
3194        assert_eq!(fc.missions.as_deref(), Some(&["m1".to_string()][..]));
3195        let baud: u32 = fc
3196            .config
3197            .as_ref()
3198            .expect("missing config")
3199            .get::<u32>("baud")
3200            .expect("baud lookup failed")
3201            .expect("missing baud");
3202        assert_eq!(baud, 921_600);
3203        let misc = &config.resources[1];
3204        assert_eq!(misc.id, "misc");
3205        assert_eq!(misc.provider, "cu29_runtime::StdClockBundle");
3206        assert!(misc.config.is_none());
3207    }
3208
3209    #[test]
3210    fn test_resource_roundtrip() {
3211        let mut config = CuConfig::default();
3212        let mut bundle_cfg = ComponentConfig::default();
3213        bundle_cfg.set("path", "/dev/ttyACM0".to_string());
3214        config.resources.push(ResourceBundleConfig {
3215            id: "fc".to_string(),
3216            provider: "copper_board_px4::Px4Bundle".to_string(),
3217            config: Some(bundle_cfg),
3218            missions: Some(vec!["m1".to_string()]),
3219        });
3220
3221        let serialized = config.serialize_ron().unwrap();
3222        let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3223        assert_eq!(deserialized.resources.len(), 1);
3224        let res = &deserialized.resources[0];
3225        assert_eq!(res.id, "fc");
3226        assert_eq!(res.provider, "copper_board_px4::Px4Bundle");
3227        assert_eq!(res.missions.as_deref(), Some(&["m1".to_string()][..]));
3228        let path: String = res
3229            .config
3230            .as_ref()
3231            .expect("missing config")
3232            .get::<String>("path")
3233            .expect("path lookup failed")
3234            .expect("missing path");
3235        assert_eq!(path, "/dev/ttyACM0");
3236    }
3237
3238    #[test]
3239    fn test_bridge_channel_config() {
3240        let txt = r#"
3241        (
3242            tasks: [],
3243            bridges: [
3244                (
3245                    id: "radio",
3246                    type: "tasks::SerialBridge",
3247                    channels: [
3248                        Rx ( id: "status", route: "sys/status", config: { "filter": "fast" } ),
3249                        Tx ( id: "imu", route: "telemetry/imu", config: { "rate": 100 } ),
3250                    ],
3251                ),
3252            ],
3253            cnx: [],
3254        )
3255        "#;
3256
3257        let config = CuConfig::deserialize_ron(txt).unwrap();
3258        let bridge = &config.bridges[0];
3259        match &bridge.channels[0] {
3260            BridgeChannelConfigRepresentation::Rx {
3261                config: Some(cfg), ..
3262            } => {
3263                let val = cfg
3264                    .get::<String>("filter")
3265                    .expect("filter lookup failed")
3266                    .expect("filter missing");
3267                assert_eq!(val, "fast");
3268            }
3269            _ => panic!("expected Rx channel with config"),
3270        }
3271        match &bridge.channels[1] {
3272            BridgeChannelConfigRepresentation::Tx {
3273                config: Some(cfg), ..
3274            } => {
3275                let rate = cfg
3276                    .get::<i32>("rate")
3277                    .expect("rate lookup failed")
3278                    .expect("rate missing");
3279                assert_eq!(rate, 100);
3280            }
3281            _ => panic!("expected Tx channel with config"),
3282        }
3283    }
3284
3285    #[test]
3286    fn test_task_resources_roundtrip() {
3287        let txt = r#"
3288        (
3289            tasks: [
3290                (
3291                    id: "imu",
3292                    type: "tasks::ImuDriver",
3293                    resources: { "bus": "fc.spi_1", "irq": "fc.gpio_imu" },
3294                ),
3295            ],
3296            cnx: [],
3297        )
3298        "#;
3299
3300        let config = CuConfig::deserialize_ron(txt).unwrap();
3301        let graph = config.graphs.get_graph(None).unwrap();
3302        let node = graph.get_node(0).expect("missing task node");
3303        let resources = node.get_resources().expect("missing resources map");
3304        assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
3305        assert_eq!(
3306            resources.get("irq").map(String::as_str),
3307            Some("fc.gpio_imu")
3308        );
3309
3310        let serialized = config.serialize_ron().unwrap();
3311        let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3312        let graph = deserialized.graphs.get_graph(None).unwrap();
3313        let node = graph.get_node(0).expect("missing task node");
3314        let resources = node
3315            .get_resources()
3316            .expect("missing resources map after roundtrip");
3317        assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
3318        assert_eq!(
3319            resources.get("irq").map(String::as_str),
3320            Some("fc.gpio_imu")
3321        );
3322    }
3323
3324    #[test]
3325    fn test_bridge_resources_preserved() {
3326        let mut config = CuConfig::default();
3327        config.resources.push(ResourceBundleConfig {
3328            id: "fc".to_string(),
3329            provider: "board::Bundle".to_string(),
3330            config: None,
3331            missions: None,
3332        });
3333        let bridge_resources = HashMap::from([("serial".to_string(), "fc.serial0".to_string())]);
3334        config.bridges.push(BridgeConfig {
3335            id: "radio".to_string(),
3336            type_: "tasks::SerialBridge".to_string(),
3337            config: None,
3338            resources: Some(bridge_resources),
3339            missions: None,
3340            run_in_sim: None,
3341            channels: vec![BridgeChannelConfigRepresentation::Tx {
3342                id: "uplink".to_string(),
3343                route: None,
3344                config: None,
3345            }],
3346        });
3347
3348        let serialized = config.serialize_ron().unwrap();
3349        let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3350        let graph = deserialized.graphs.get_graph(None).expect("missing graph");
3351        let bridge_id = graph
3352            .get_node_id_by_name("radio")
3353            .expect("bridge node missing");
3354        let node = graph.get_node(bridge_id).expect("missing bridge node");
3355        let resources = node
3356            .get_resources()
3357            .expect("bridge resources were not preserved");
3358        assert_eq!(
3359            resources.get("serial").map(String::as_str),
3360            Some("fc.serial0")
3361        );
3362    }
3363
3364    #[test]
3365    fn test_demo_config_parses() {
3366        let txt = r#"(
3367    resources: [
3368        (
3369            id: "fc",
3370            provider: "crate::resources::RadioBundle",
3371        ),
3372    ],
3373    tasks: [
3374        (id: "thr", type: "tasks::ThrottleControl"),
3375        (id: "tele0", type: "tasks::TelemetrySink0"),
3376        (id: "tele1", type: "tasks::TelemetrySink1"),
3377        (id: "tele2", type: "tasks::TelemetrySink2"),
3378        (id: "tele3", type: "tasks::TelemetrySink3"),
3379    ],
3380    bridges: [
3381        (  id: "crsf",
3382           type: "cu_crsf::CrsfBridge<SerialResource, SerialPortError>",
3383           resources: { "serial": "fc.serial" },
3384           channels: [
3385                Rx ( id: "rc_rx" ),  // receiving RC Channels
3386                Tx ( id: "lq_tx" ),  // Sending LineQuality back
3387            ],
3388        ),
3389        (
3390            id: "bdshot",
3391            type: "cu_bdshot::RpBdshotBridge",
3392            channels: [
3393                Tx ( id: "esc0_tx" ),
3394                Tx ( id: "esc1_tx" ),
3395                Tx ( id: "esc2_tx" ),
3396                Tx ( id: "esc3_tx" ),
3397                Rx ( id: "esc0_rx" ),
3398                Rx ( id: "esc1_rx" ),
3399                Rx ( id: "esc2_rx" ),
3400                Rx ( id: "esc3_rx" ),
3401            ],
3402        ),
3403    ],
3404    cnx: [
3405        (src: "crsf/rc_rx", dst: "thr", msg: "cu_crsf::messages::RcChannelsPayload"),
3406        (src: "thr", dst: "bdshot/esc0_tx", msg: "cu_bdshot::EscCommand"),
3407        (src: "thr", dst: "bdshot/esc1_tx", msg: "cu_bdshot::EscCommand"),
3408        (src: "thr", dst: "bdshot/esc2_tx", msg: "cu_bdshot::EscCommand"),
3409        (src: "thr", dst: "bdshot/esc3_tx", msg: "cu_bdshot::EscCommand"),
3410        (src: "bdshot/esc0_rx", dst: "tele0", msg: "cu_bdshot::EscTelemetry"),
3411        (src: "bdshot/esc1_rx", dst: "tele1", msg: "cu_bdshot::EscTelemetry"),
3412        (src: "bdshot/esc2_rx", dst: "tele2", msg: "cu_bdshot::EscTelemetry"),
3413        (src: "bdshot/esc3_rx", dst: "tele3", msg: "cu_bdshot::EscTelemetry"),
3414    ],
3415)"#;
3416        let config = CuConfig::deserialize_ron(txt).unwrap();
3417        assert_eq!(config.resources.len(), 1);
3418        assert_eq!(config.bridges.len(), 2);
3419    }
3420
3421    #[test]
3422    fn test_bridge_tx_cannot_be_source() {
3423        let txt = r#"
3424        (
3425            tasks: [
3426                (id: "dst", type: "tasks::Destination"),
3427            ],
3428            bridges: [
3429                (
3430                    id: "radio",
3431                    type: "tasks::SerialBridge",
3432                    channels: [
3433                        Tx ( id: "motor", route: "motor/cmd" ),
3434                    ],
3435                ),
3436            ],
3437            cnx: [
3438                (src: "radio/motor", dst: "dst", msg: "mymsgs::MotorCmd"),
3439            ],
3440        )
3441        "#;
3442
3443        let err = CuConfig::deserialize_ron(txt).expect_err("expected bridge source error");
3444        assert!(
3445            err.to_string()
3446                .contains("channel 'motor' is Tx and cannot act as a source")
3447        );
3448    }
3449
3450    #[test]
3451    fn test_bridge_rx_cannot_be_destination() {
3452        let txt = r#"
3453        (
3454            tasks: [
3455                (id: "src", type: "tasks::Source"),
3456            ],
3457            bridges: [
3458                (
3459                    id: "radio",
3460                    type: "tasks::SerialBridge",
3461                    channels: [
3462                        Rx ( id: "status", route: "sys/status" ),
3463                    ],
3464                ),
3465            ],
3466            cnx: [
3467                (src: "src", dst: "radio/status", msg: "mymsgs::Status"),
3468            ],
3469        )
3470        "#;
3471
3472        let err = CuConfig::deserialize_ron(txt).expect_err("expected bridge destination error");
3473        assert!(
3474            err.to_string()
3475                .contains("channel 'status' is Rx and cannot act as a destination")
3476        );
3477    }
3478
3479    #[test]
3480    fn test_validate_logging_config() {
3481        // Test with valid logging configuration
3482        let txt =
3483            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
3484        let config = CuConfig::deserialize_ron(txt).unwrap();
3485        assert!(config.validate_logging_config().is_ok());
3486
3487        // Test with invalid logging configuration
3488        let txt =
3489            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
3490        let config = CuConfig::deserialize_ron(txt).unwrap();
3491        assert!(config.validate_logging_config().is_err());
3492    }
3493
3494    // this test makes sure the edge id is suitable to be used to sort the inputs of a task
3495    #[test]
3496    fn test_deserialization_edge_id_assignment() {
3497        // note here that the src1 task is added before src2 in the tasks array,
3498        // however, src1 connection is added AFTER src2 in the cnx array
3499        let txt = r#"(
3500            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
3501            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
3502        )"#;
3503        let config = CuConfig::deserialize_ron(txt).unwrap();
3504        let graph = config.graphs.get_graph(None).unwrap();
3505        assert!(config.validate_logging_config().is_ok());
3506
3507        // the node id depends on the order in which the tasks are added
3508        let src1_id = 0;
3509        assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
3510        let src2_id = 1;
3511        assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
3512
3513        // the edge id depends on the order the connection is created
3514        // the src2 was added second in the tasks, but the connection was added first
3515        let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
3516        assert_eq!(src1_edge_id, 1);
3517        let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
3518        assert_eq!(src2_edge_id, 0);
3519    }
3520
3521    #[test]
3522    fn test_simple_missions() {
3523        // A simple config that selection a source depending on the mission it is in.
3524        let txt = r#"(
3525                    missions: [ (id: "m1"),
3526                                (id: "m2"),
3527                                ],
3528                    tasks: [(id: "src1", type: "a", missions: ["m1"]),
3529                            (id: "src2", type: "b", missions: ["m2"]),
3530                            (id: "sink", type: "c")],
3531
3532                    cnx: [
3533                            (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
3534                            (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
3535                         ],
3536              )
3537              "#;
3538
3539        let config = CuConfig::deserialize_ron(txt).unwrap();
3540        let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
3541        assert_eq!(m1_graph.edge_count(), 1);
3542        assert_eq!(m1_graph.node_count(), 2);
3543        let index = 0;
3544        let cnx = m1_graph.get_edge_weight(index).unwrap();
3545
3546        assert_eq!(cnx.src, "src1");
3547        assert_eq!(cnx.dst, "sink");
3548        assert_eq!(cnx.msg, "u32");
3549        assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
3550
3551        let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
3552        assert_eq!(m2_graph.edge_count(), 1);
3553        assert_eq!(m2_graph.node_count(), 2);
3554        let index = 0;
3555        let cnx = m2_graph.get_edge_weight(index).unwrap();
3556        assert_eq!(cnx.src, "src2");
3557        assert_eq!(cnx.dst, "sink");
3558        assert_eq!(cnx.msg, "u32");
3559        assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
3560    }
3561    #[test]
3562    fn test_mission_serde() {
3563        // A simple config that selection a source depending on the mission it is in.
3564        let txt = r#"(
3565                    missions: [ (id: "m1"),
3566                                (id: "m2"),
3567                                ],
3568                    tasks: [(id: "src1", type: "a", missions: ["m1"]),
3569                            (id: "src2", type: "b", missions: ["m2"]),
3570                            (id: "sink", type: "c")],
3571
3572                    cnx: [
3573                            (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
3574                            (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
3575                         ],
3576              )
3577              "#;
3578
3579        let config = CuConfig::deserialize_ron(txt).unwrap();
3580        let serialized = config.serialize_ron().unwrap();
3581        let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3582        let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
3583        assert_eq!(m1_graph.edge_count(), 1);
3584        assert_eq!(m1_graph.node_count(), 2);
3585        let index = 0;
3586        let cnx = m1_graph.get_edge_weight(index).unwrap();
3587        assert_eq!(cnx.src, "src1");
3588        assert_eq!(cnx.dst, "sink");
3589        assert_eq!(cnx.msg, "u32");
3590        assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
3591    }
3592
3593    #[test]
3594    fn test_mission_scoped_nc_connection_survives_serialize_roundtrip() {
3595        let txt = r#"(
3596            missions: [(id: "m1"), (id: "m2")],
3597            tasks: [
3598                (id: "src_m1", type: "a", missions: ["m1"]),
3599                (id: "src_m2", type: "b", missions: ["m2"]),
3600            ],
3601            cnx: [
3602                (src: "src_m1", dst: "__nc__", msg: "msg::A", missions: ["m1"]),
3603                (src: "src_m2", dst: "__nc__", msg: "msg::B", missions: ["m2"]),
3604            ]
3605        )"#;
3606
3607        let config = CuConfig::deserialize_ron(txt).unwrap();
3608        let serialized = config.serialize_ron().unwrap();
3609        let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3610
3611        let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
3612        let src_m1_id = m1_graph.get_node_id_by_name("src_m1").unwrap();
3613        let src_m1 = m1_graph.get_node(src_m1_id).unwrap();
3614        assert_eq!(src_m1.nc_outputs(), &["msg::A".to_string()]);
3615
3616        let m2_graph = deserialized.graphs.get_graph(Some("m2")).unwrap();
3617        let src_m2_id = m2_graph.get_node_id_by_name("src_m2").unwrap();
3618        let src_m2 = m2_graph.get_node(src_m2_id).unwrap();
3619        assert_eq!(src_m2.nc_outputs(), &["msg::B".to_string()]);
3620    }
3621
3622    #[test]
3623    fn test_keyframe_interval() {
3624        // note here that the src1 task is added before src2 in the tasks array,
3625        // however, src1 connection is added AFTER src2 in the cnx array
3626        let txt = r#"(
3627            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
3628            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
3629            logging: ( keyframe_interval: 314 )
3630        )"#;
3631        let config = CuConfig::deserialize_ron(txt).unwrap();
3632        let logging_config = config.logging.unwrap();
3633        assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
3634    }
3635
3636    #[test]
3637    fn test_default_keyframe_interval() {
3638        // note here that the src1 task is added before src2 in the tasks array,
3639        // however, src1 connection is added AFTER src2 in the cnx array
3640        let txt = r#"(
3641            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
3642            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
3643            logging: ( slab_size_mib: 200, section_size_mib: 1024, )
3644        )"#;
3645        let config = CuConfig::deserialize_ron(txt).unwrap();
3646        let logging_config = config.logging.unwrap();
3647        assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
3648    }
3649
3650    #[test]
3651    fn test_nc_connection_marks_source_output_without_creating_edge() {
3652        let txt = r#"(
3653            tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3654            cnx: [
3655                (src: "src", dst: "sink", msg: "msg::A"),
3656                (src: "src", dst: "__nc__", msg: "msg::B"),
3657            ]
3658        )"#;
3659        let config = CuConfig::deserialize_ron(txt).unwrap();
3660        let graph = config.get_graph(None).unwrap();
3661        let src_id = graph.get_node_id_by_name("src").unwrap();
3662        let src_node = graph.get_node(src_id).unwrap();
3663
3664        assert_eq!(graph.edge_count(), 1);
3665        assert_eq!(src_node.nc_outputs(), &["msg::B".to_string()]);
3666    }
3667
3668    #[test]
3669    fn test_nc_connection_survives_serialize_roundtrip() {
3670        let txt = r#"(
3671            tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3672            cnx: [
3673                (src: "src", dst: "sink", msg: "msg::A"),
3674                (src: "src", dst: "__nc__", msg: "msg::B"),
3675            ]
3676        )"#;
3677        let config = CuConfig::deserialize_ron(txt).unwrap();
3678        let serialized = config.serialize_ron().unwrap();
3679        let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3680        let graph = deserialized.get_graph(None).unwrap();
3681        let src_id = graph.get_node_id_by_name("src").unwrap();
3682        let src_node = graph.get_node(src_id).unwrap();
3683
3684        assert_eq!(graph.edge_count(), 1);
3685        assert_eq!(src_node.nc_outputs(), &["msg::B".to_string()]);
3686    }
3687
3688    #[test]
3689    fn test_nc_connection_preserves_original_connection_order() {
3690        let txt = r#"(
3691            tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3692            cnx: [
3693                (src: "src", dst: "__nc__", msg: "msg::A"),
3694                (src: "src", dst: "sink", msg: "msg::B"),
3695            ]
3696        )"#;
3697        let config = CuConfig::deserialize_ron(txt).unwrap();
3698        let graph = config.get_graph(None).unwrap();
3699        let src_id = graph.get_node_id_by_name("src").unwrap();
3700        let src_node = graph.get_node(src_id).unwrap();
3701        let edge_id = graph.get_src_edges(src_id).unwrap()[0];
3702        let edge = graph.edge(edge_id).unwrap();
3703
3704        assert_eq!(edge.msg, "msg::B");
3705        assert_eq!(edge.order, 1);
3706        assert_eq!(
3707            src_node
3708                .nc_outputs_with_order()
3709                .map(|(msg, order)| (msg.as_str(), order))
3710                .collect::<Vec<_>>(),
3711            vec![("msg::A", 0)]
3712        );
3713    }
3714}