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