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