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