cu29_runtime/
config.rs

1//! This module defines the configuration of the copper runtime.
2//! The configuration is a directed graph where nodes are tasks and edges are connections between tasks.
3//! The configuration is serialized in the RON format.
4//! The configuration is used to generate the runtime code at compile time.
5#[cfg(not(feature = "std"))]
6extern crate alloc;
7
8use 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.0.node_indices().find_map(|node_index| {
716            if let Some(node) = self.0.node_weight(node_index) {
717                if node.id != node_id {
718                    return None;
719                }
720                let edges: Vec<_> = self
721                    .0
722                    .edges_directed(node_index, Incoming)
723                    .map(|edge| edge.id().index())
724                    .collect();
725                if edges.is_empty() {
726                    return None;
727                }
728                let cnx = self
729                    .0
730                    .edge_weight(EdgeIndex::new(edges[0]))
731                    .expect("Found an cnx id but could not retrieve it back");
732                return Some(cnx.msg.clone());
733            }
734            None
735        })
736    }
737
738    #[allow(dead_code)]
739    pub fn get_connection_msg_type(&self, source: NodeId, target: NodeId) -> Option<&str> {
740        self.0
741            .find_edge(source.into(), target.into())
742            .map(|edge_index| self.0[edge_index].msg.as_str())
743    }
744
745    /// Get the list of edges that are connected to the given node as a source.
746    fn get_edges_by_direction(
747        &self,
748        node_id: NodeId,
749        direction: petgraph::Direction,
750    ) -> CuResult<Vec<usize>> {
751        Ok(self
752            .0
753            .edges_directed(node_id.into(), direction)
754            .map(|edge| edge.id().index())
755            .collect())
756    }
757
758    pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
759        self.get_edges_by_direction(node_id, Outgoing)
760    }
761
762    /// Get the list of edges that are connected to the given node as a destination.
763    pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
764        self.get_edges_by_direction(node_id, Incoming)
765    }
766
767    #[allow(dead_code)]
768    pub fn node_count(&self) -> usize {
769        self.0.node_count()
770    }
771
772    #[allow(dead_code)]
773    pub fn edge_count(&self) -> usize {
774        self.0.edge_count()
775    }
776
777    /// Adds an edge between two nodes/tasks in the configuration graph.
778    /// msg_type is the type of message exchanged between the two nodes/tasks.
779    #[allow(dead_code)]
780    pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
781        self.connect_ext(source, target, msg_type, None, None, None)
782    }
783}
784
785impl core::ops::Index<NodeIndex> for CuGraph {
786    type Output = Node;
787
788    fn index(&self, index: NodeIndex) -> &Self::Output {
789        &self.0[index]
790    }
791}
792
793#[derive(Debug, Clone)]
794pub enum ConfigGraphs {
795    Simple(CuGraph),
796    Missions(HashMap<String, CuGraph>),
797}
798
799impl ConfigGraphs {
800    /// Returns a consistent hashmap of mission names to Graphs whatever the shape of the config is.
801    /// Note: if there is only one anonymous mission it will be called "default"
802    #[allow(dead_code)]
803    pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
804        match self {
805            Simple(graph) => {
806                let mut map = HashMap::new();
807                map.insert("default".to_string(), graph.clone());
808                map
809            }
810            Missions(graphs) => graphs.clone(),
811        }
812    }
813
814    #[allow(dead_code)]
815    pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
816        match self {
817            Simple(graph) => Ok(graph),
818            Missions(graphs) => {
819                if graphs.len() == 1 {
820                    Ok(graphs.values().next().unwrap())
821                } else {
822                    Err("Cannot get default mission graph from mission config".into())
823                }
824            }
825        }
826    }
827
828    #[allow(dead_code)]
829    pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
830        match self {
831            Simple(graph) => {
832                if mission_id.is_none() || mission_id.unwrap() == "default" {
833                    Ok(graph)
834                } else {
835                    Err("Cannot get mission graph from simple config".into())
836                }
837            }
838            Missions(graphs) => {
839                if let Some(id) = mission_id {
840                    graphs
841                        .get(id)
842                        .ok_or_else(|| format!("Mission {id} not found").into())
843                } else {
844                    Err("Mission ID required for mission configs".into())
845                }
846            }
847        }
848    }
849
850    #[allow(dead_code)]
851    pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
852        match self {
853            Simple(ref mut graph) => {
854                if mission_id.is_none() {
855                    Ok(graph)
856                } else {
857                    Err("Cannot get mission graph from simple config".into())
858                }
859            }
860            Missions(ref mut graphs) => {
861                if let Some(id) = mission_id {
862                    graphs
863                        .get_mut(id)
864                        .ok_or_else(|| format!("Mission {id} not found").into())
865                } else {
866                    Err("Mission ID required for mission configs".into())
867                }
868            }
869        }
870    }
871
872    pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
873        match self {
874            Simple(_) => Err("Cannot add mission to simple config".into()),
875            Missions(graphs) => {
876                if graphs.contains_key(mission_id) {
877                    Err(format!("Mission {mission_id} already exists").into())
878                } else {
879                    let graph = CuGraph::default();
880                    graphs.insert(mission_id.to_string(), graph);
881                    // Get a mutable reference to the newly inserted graph
882                    Ok(graphs.get_mut(mission_id).unwrap())
883                }
884            }
885        }
886    }
887}
888
889/// CuConfig is the programmatic representation of the configuration graph.
890/// It is a directed graph where nodes are tasks and edges are connections between tasks.
891///
892/// The core of CuConfig is its `graphs` field which can be either a simple graph
893/// or a collection of mission-specific graphs. The graph structure is based on petgraph.
894#[derive(Debug, Clone)]
895pub struct CuConfig {
896    /// Optional monitoring configuration
897    pub monitor: Option<MonitorConfig>,
898    /// Optional logging configuration
899    pub logging: Option<LoggingConfig>,
900    /// Optional runtime configuration
901    pub runtime: Option<RuntimeConfig>,
902    /// Declarative bridge definitions that are yet to be expanded into the graph
903    pub bridges: Vec<BridgeConfig>,
904    /// Graph structure - either a single graph or multiple mission-specific graphs
905    pub graphs: ConfigGraphs,
906}
907
908#[derive(Serialize, Deserialize, Default, Debug, Clone)]
909pub struct MonitorConfig {
910    #[serde(rename = "type")]
911    type_: String,
912    #[serde(skip_serializing_if = "Option::is_none")]
913    config: Option<ComponentConfig>,
914}
915
916impl MonitorConfig {
917    #[allow(dead_code)]
918    pub fn get_type(&self) -> &str {
919        &self.type_
920    }
921
922    #[allow(dead_code)]
923    pub fn get_config(&self) -> Option<&ComponentConfig> {
924        self.config.as_ref()
925    }
926}
927
928fn default_as_true() -> bool {
929    true
930}
931
932pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
933
934fn default_keyframe_interval() -> Option<u32> {
935    Some(DEFAULT_KEYFRAME_INTERVAL)
936}
937
938#[derive(Serialize, Deserialize, Default, Debug, Clone)]
939pub struct LoggingConfig {
940    /// Enable task logging to the log file.
941    #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
942    pub enable_task_logging: bool,
943
944    /// Size of each slab in the log file. (it is the size of the memory mapped file at a time)
945    #[serde(skip_serializing_if = "Option::is_none")]
946    pub slab_size_mib: Option<u64>,
947
948    /// Pre-allocated size for each section in the log file.
949    #[serde(skip_serializing_if = "Option::is_none")]
950    pub section_size_mib: Option<u64>,
951
952    /// Interval in copperlists between two "keyframes" in the log file i.e. freezing tasks.
953    #[serde(
954        default = "default_keyframe_interval",
955        skip_serializing_if = "Option::is_none"
956    )]
957    pub keyframe_interval: Option<u32>,
958}
959
960#[derive(Serialize, Deserialize, Default, Debug, Clone)]
961pub struct RuntimeConfig {
962    /// Set a CopperList execution rate target in Hz
963    /// It will act as a rate limiter: if the execution is slower than this rate,
964    /// it will continue to execute at "best effort".
965    ///
966    /// The main usecase is to not waste cycles when the system doesn't need an unbounded execution rate.
967    #[serde(skip_serializing_if = "Option::is_none")]
968    pub rate_target_hz: Option<u64>,
969}
970
971/// Missions are used to generate alternative DAGs within the same configuration.
972#[derive(Serialize, Deserialize, Debug, Clone)]
973pub struct MissionsConfig {
974    pub id: String,
975}
976
977/// Includes are used to include other configuration files.
978#[derive(Serialize, Deserialize, Debug, Clone)]
979pub struct IncludesConfig {
980    pub path: String,
981    pub params: HashMap<String, Value>,
982    pub missions: Option<Vec<String>>,
983}
984
985/// This is the main Copper configuration representation.
986#[derive(Serialize, Deserialize, Default)]
987struct CuConfigRepresentation {
988    tasks: Option<Vec<Node>>,
989    bridges: Option<Vec<BridgeConfig>>,
990    cnx: Option<Vec<SerializedCnx>>,
991    monitor: Option<MonitorConfig>,
992    logging: Option<LoggingConfig>,
993    runtime: Option<RuntimeConfig>,
994    missions: Option<Vec<MissionsConfig>>,
995    includes: Option<Vec<IncludesConfig>>,
996}
997
998/// Shared implementation for deserializing a CuConfigRepresentation into a CuConfig
999fn deserialize_config_representation<E>(
1000    representation: &CuConfigRepresentation,
1001) -> Result<CuConfig, E>
1002where
1003    E: From<String>,
1004{
1005    let mut cuconfig = CuConfig::default();
1006    let bridge_lookup = build_bridge_lookup(representation.bridges.as_ref());
1007
1008    if let Some(mission_configs) = &representation.missions {
1009        // This is the multi-mission case
1010        let mut missions = Missions(HashMap::new());
1011
1012        for mission_config in mission_configs {
1013            let mission_id = mission_config.id.as_str();
1014            let graph = missions
1015                .add_mission(mission_id)
1016                .map_err(|e| E::from(e.to_string()))?;
1017
1018            if let Some(tasks) = &representation.tasks {
1019                for task in tasks {
1020                    if let Some(task_missions) = &task.missions {
1021                        // if there is a filter by mission on the task, only add the task to the mission if it matches the filter.
1022                        if task_missions.contains(&mission_id.to_owned()) {
1023                            graph
1024                                .add_node(task.clone())
1025                                .map_err(|e| E::from(e.to_string()))?;
1026                        }
1027                    } else {
1028                        // if there is no filter by mission on the task, add the task to the mission.
1029                        graph
1030                            .add_node(task.clone())
1031                            .map_err(|e| E::from(e.to_string()))?;
1032                    }
1033                }
1034            }
1035
1036            if let Some(bridges) = &representation.bridges {
1037                for bridge in bridges {
1038                    if mission_applies(&bridge.missions, mission_id) {
1039                        insert_bridge_node(graph, bridge).map_err(E::from)?;
1040                    }
1041                }
1042            }
1043
1044            if let Some(cnx) = &representation.cnx {
1045                for c in cnx {
1046                    if let Some(cnx_missions) = &c.missions {
1047                        // if there is a filter by mission on the connection, only add the connection to the mission if it matches the filter.
1048                        if cnx_missions.contains(&mission_id.to_owned()) {
1049                            let (src_name, src_channel) =
1050                                parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1051                                    .map_err(E::from)?;
1052                            let (dst_name, dst_channel) =
1053                                parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1054                                    .map_err(E::from)?;
1055                            let src =
1056                                graph
1057                                    .get_node_id_by_name(src_name.as_str())
1058                                    .ok_or_else(|| {
1059                                        E::from(format!("Source node not found: {}", c.src))
1060                                    })?;
1061                            let dst =
1062                                graph
1063                                    .get_node_id_by_name(dst_name.as_str())
1064                                    .ok_or_else(|| {
1065                                        E::from(format!("Destination node not found: {}", c.dst))
1066                                    })?;
1067                            graph
1068                                .connect_ext(
1069                                    src,
1070                                    dst,
1071                                    &c.msg,
1072                                    Some(cnx_missions.clone()),
1073                                    src_channel,
1074                                    dst_channel,
1075                                )
1076                                .map_err(|e| E::from(e.to_string()))?;
1077                        }
1078                    } else {
1079                        // if there is no filter by mission on the connection, add the connection to the mission.
1080                        let (src_name, src_channel) =
1081                            parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1082                                .map_err(E::from)?;
1083                        let (dst_name, dst_channel) =
1084                            parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1085                                .map_err(E::from)?;
1086                        let src = graph
1087                            .get_node_id_by_name(src_name.as_str())
1088                            .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1089                        let dst =
1090                            graph
1091                                .get_node_id_by_name(dst_name.as_str())
1092                                .ok_or_else(|| {
1093                                    E::from(format!("Destination node not found: {}", c.dst))
1094                                })?;
1095                        graph
1096                            .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1097                            .map_err(|e| E::from(e.to_string()))?;
1098                    }
1099                }
1100            }
1101        }
1102        cuconfig.graphs = missions;
1103    } else {
1104        // this is the simple case
1105        let mut graph = CuGraph::default();
1106
1107        if let Some(tasks) = &representation.tasks {
1108            for task in tasks {
1109                graph
1110                    .add_node(task.clone())
1111                    .map_err(|e| E::from(e.to_string()))?;
1112            }
1113        }
1114
1115        if let Some(bridges) = &representation.bridges {
1116            for bridge in bridges {
1117                insert_bridge_node(&mut graph, bridge).map_err(E::from)?;
1118            }
1119        }
1120
1121        if let Some(cnx) = &representation.cnx {
1122            for c in cnx {
1123                let (src_name, src_channel) =
1124                    parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1125                        .map_err(E::from)?;
1126                let (dst_name, dst_channel) =
1127                    parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1128                        .map_err(E::from)?;
1129                let src = graph
1130                    .get_node_id_by_name(src_name.as_str())
1131                    .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1132                let dst = graph
1133                    .get_node_id_by_name(dst_name.as_str())
1134                    .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
1135                graph
1136                    .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1137                    .map_err(|e| E::from(e.to_string()))?;
1138            }
1139        }
1140        cuconfig.graphs = Simple(graph);
1141    }
1142
1143    cuconfig.monitor = representation.monitor.clone();
1144    cuconfig.logging = representation.logging.clone();
1145    cuconfig.runtime = representation.runtime.clone();
1146    cuconfig.bridges = representation.bridges.clone().unwrap_or_default();
1147
1148    Ok(cuconfig)
1149}
1150
1151impl<'de> Deserialize<'de> for CuConfig {
1152    /// This is a custom serialization to make this implementation independent of petgraph.
1153    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1154    where
1155        D: Deserializer<'de>,
1156    {
1157        let representation =
1158            CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
1159
1160        // Convert String errors to D::Error using serde::de::Error::custom
1161        match deserialize_config_representation::<String>(&representation) {
1162            Ok(config) => Ok(config),
1163            Err(e) => Err(serde::de::Error::custom(e)),
1164        }
1165    }
1166}
1167
1168impl Serialize for CuConfig {
1169    /// This is a custom serialization to make this implementation independent of petgraph.
1170    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1171    where
1172        S: Serializer,
1173    {
1174        let bridges = if self.bridges.is_empty() {
1175            None
1176        } else {
1177            Some(self.bridges.clone())
1178        };
1179        match &self.graphs {
1180            Simple(graph) => {
1181                let tasks: Vec<Node> = graph
1182                    .0
1183                    .node_indices()
1184                    .map(|idx| graph.0[idx].clone())
1185                    .filter(|node| node.get_flavor() == Flavor::Task)
1186                    .collect();
1187
1188                let cnx: Vec<SerializedCnx> = graph
1189                    .0
1190                    .edge_indices()
1191                    .map(|edge| SerializedCnx::from(&graph.0[edge]))
1192                    .collect();
1193
1194                CuConfigRepresentation {
1195                    tasks: Some(tasks),
1196                    bridges: bridges.clone(),
1197                    cnx: Some(cnx),
1198                    monitor: self.monitor.clone(),
1199                    logging: self.logging.clone(),
1200                    runtime: self.runtime.clone(),
1201                    missions: None,
1202                    includes: None,
1203                }
1204                .serialize(serializer)
1205            }
1206            Missions(graphs) => {
1207                let missions = graphs
1208                    .keys()
1209                    .map(|id| MissionsConfig { id: id.clone() })
1210                    .collect();
1211
1212                // Collect all unique tasks across missions
1213                let mut tasks = Vec::new();
1214                let mut cnx = Vec::new();
1215
1216                for graph in graphs.values() {
1217                    // Add all nodes from this mission
1218                    for node_idx in graph.node_indices() {
1219                        let node = &graph[node_idx];
1220                        if node.get_flavor() == Flavor::Task
1221                            && !tasks.iter().any(|n: &Node| n.id == node.id)
1222                        {
1223                            tasks.push(node.clone());
1224                        }
1225                    }
1226
1227                    // Add all edges from this mission
1228                    for edge_idx in graph.0.edge_indices() {
1229                        let edge = &graph.0[edge_idx];
1230                        let serialized = SerializedCnx::from(edge);
1231                        if !cnx.iter().any(|c: &SerializedCnx| {
1232                            c.src == serialized.src
1233                                && c.dst == serialized.dst
1234                                && c.msg == serialized.msg
1235                        }) {
1236                            cnx.push(serialized);
1237                        }
1238                    }
1239                }
1240
1241                CuConfigRepresentation {
1242                    tasks: Some(tasks),
1243                    bridges,
1244                    cnx: Some(cnx),
1245                    monitor: self.monitor.clone(),
1246                    logging: self.logging.clone(),
1247                    runtime: self.runtime.clone(),
1248                    missions: Some(missions),
1249                    includes: None,
1250                }
1251                .serialize(serializer)
1252            }
1253        }
1254    }
1255}
1256
1257impl Default for CuConfig {
1258    fn default() -> Self {
1259        CuConfig {
1260            graphs: Simple(CuGraph(StableDiGraph::new())),
1261            monitor: None,
1262            logging: None,
1263            runtime: None,
1264            bridges: Vec::new(),
1265        }
1266    }
1267}
1268
1269/// The implementation has a lot of convenience methods to manipulate
1270/// the configuration to give some flexibility into programmatically creating the configuration.
1271impl CuConfig {
1272    #[allow(dead_code)]
1273    pub fn new_simple_type() -> Self {
1274        Self::default()
1275    }
1276
1277    #[allow(dead_code)]
1278    pub fn new_mission_type() -> Self {
1279        CuConfig {
1280            graphs: Missions(HashMap::new()),
1281            monitor: None,
1282            logging: None,
1283            runtime: None,
1284            bridges: Vec::new(),
1285        }
1286    }
1287
1288    fn get_options() -> Options {
1289        Options::default()
1290            .with_default_extension(Extensions::IMPLICIT_SOME)
1291            .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1292            .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1293    }
1294
1295    #[allow(dead_code)]
1296    pub fn serialize_ron(&self) -> String {
1297        let ron = Self::get_options();
1298        let pretty = ron::ser::PrettyConfig::default();
1299        ron.to_string_pretty(&self, pretty).unwrap()
1300    }
1301
1302    #[allow(dead_code)]
1303    pub fn deserialize_ron(ron: &str) -> Self {
1304        match Self::get_options().from_str(ron) {
1305            Ok(representation) => Self::deserialize_impl(representation).unwrap_or_else(|e| {
1306                panic!("Error deserializing configuration: {e}");
1307            }),
1308            Err(e) => panic!("Syntax Error in config: {} at position {}", e.code, e.span),
1309        }
1310    }
1311
1312    fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
1313        deserialize_config_representation(&representation)
1314    }
1315
1316    /// Render the configuration graph in the dot format.
1317    #[cfg(feature = "std")]
1318    pub fn render(
1319        &self,
1320        output: &mut dyn std::io::Write,
1321        mission_id: Option<&str>,
1322    ) -> CuResult<()> {
1323        writeln!(output, "digraph G {{").unwrap();
1324        writeln!(output, "    graph [rankdir=LR, nodesep=0.8, ranksep=1.2];").unwrap();
1325        writeln!(output, "    node [shape=plain, fontname=\"Noto Sans\"];").unwrap();
1326        writeln!(output, "    edge [fontname=\"Noto Sans\"];").unwrap();
1327
1328        let sections = match (&self.graphs, mission_id) {
1329            (Simple(graph), _) => vec![RenderSection { label: None, graph }],
1330            (Missions(graphs), Some(id)) => {
1331                let graph = graphs
1332                    .get(id)
1333                    .ok_or_else(|| CuError::from(format!("Mission {id} not found")))?;
1334                vec![RenderSection {
1335                    label: Some(id.to_string()),
1336                    graph,
1337                }]
1338            }
1339            (Missions(graphs), None) => {
1340                let mut missions: Vec<_> = graphs.iter().collect();
1341                missions.sort_by(|a, b| a.0.cmp(b.0));
1342                missions
1343                    .into_iter()
1344                    .map(|(label, graph)| RenderSection {
1345                        label: Some(label.clone()),
1346                        graph,
1347                    })
1348                    .collect()
1349            }
1350        };
1351
1352        for section in sections {
1353            self.render_section(output, section.graph, section.label.as_deref())?;
1354        }
1355
1356        writeln!(output, "}}").unwrap();
1357        Ok(())
1358    }
1359
1360    #[allow(dead_code)]
1361    pub fn get_all_instances_configs(
1362        &self,
1363        mission_id: Option<&str>,
1364    ) -> Vec<Option<&ComponentConfig>> {
1365        let graph = self.graphs.get_graph(mission_id).unwrap();
1366        graph
1367            .get_all_nodes()
1368            .iter()
1369            .map(|(_, node)| node.get_instance_config())
1370            .collect()
1371    }
1372
1373    #[allow(dead_code)]
1374    pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1375        self.graphs.get_graph(mission_id)
1376    }
1377
1378    #[allow(dead_code)]
1379    pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1380        self.graphs.get_graph_mut(mission_id)
1381    }
1382
1383    #[allow(dead_code)]
1384    pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1385        self.monitor.as_ref()
1386    }
1387
1388    #[allow(dead_code)]
1389    pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
1390        self.runtime.as_ref()
1391    }
1392
1393    /// Validate the logging configuration to ensure section pre-allocation sizes do not exceed slab sizes.
1394    /// This method is wrapper around [LoggingConfig::validate]
1395    pub fn validate_logging_config(&self) -> CuResult<()> {
1396        if let Some(logging) = &self.logging {
1397            return logging.validate();
1398        }
1399        Ok(())
1400    }
1401}
1402
1403#[cfg(feature = "std")]
1404struct PortLookup {
1405    inputs: HashMap<String, String>,
1406    outputs: HashMap<String, String>,
1407    default_input: Option<String>,
1408    default_output: Option<String>,
1409}
1410
1411#[cfg(feature = "std")]
1412#[derive(Clone)]
1413struct RenderNode {
1414    id: String,
1415    type_name: String,
1416    flavor: Flavor,
1417    inputs: Vec<String>,
1418    outputs: Vec<String>,
1419}
1420
1421#[cfg(feature = "std")]
1422#[derive(Clone)]
1423struct RenderConnection {
1424    src: String,
1425    src_port: Option<String>,
1426    dst: String,
1427    dst_port: Option<String>,
1428    msg: String,
1429}
1430
1431#[cfg(feature = "std")]
1432struct RenderTopology {
1433    nodes: Vec<RenderNode>,
1434    connections: Vec<RenderConnection>,
1435}
1436
1437#[cfg(feature = "std")]
1438struct RenderSection<'a> {
1439    label: Option<String>,
1440    graph: &'a CuGraph,
1441}
1442
1443#[cfg(feature = "std")]
1444impl CuConfig {
1445    fn render_section(
1446        &self,
1447        output: &mut dyn std::io::Write,
1448        graph: &CuGraph,
1449        label: Option<&str>,
1450    ) -> CuResult<()> {
1451        use std::fmt::Write as FmtWrite;
1452
1453        let mut topology = build_render_topology(graph, &self.bridges);
1454        topology.nodes.sort_by(|a, b| a.id.cmp(&b.id));
1455        topology.connections.sort_by(|a, b| {
1456            a.src
1457                .cmp(&b.src)
1458                .then(a.dst.cmp(&b.dst))
1459                .then(a.msg.cmp(&b.msg))
1460        });
1461
1462        let cluster_id = label.map(|lbl| format!("cluster_{}", sanitize_identifier(lbl)));
1463        if let Some(ref cluster_id) = cluster_id {
1464            writeln!(output, "    subgraph \"{cluster_id}\" {{").unwrap();
1465            writeln!(
1466                output,
1467                "        label=<<B>Mission: {}</B>>;",
1468                encode_text(label.unwrap())
1469            )
1470            .unwrap();
1471            writeln!(
1472                output,
1473                "        labelloc=t; labeljust=l; color=\"#bbbbbb\"; style=\"rounded\"; margin=20;"
1474            )
1475            .unwrap();
1476        }
1477        let indent = if cluster_id.is_some() {
1478            "        "
1479        } else {
1480            "    "
1481        };
1482        let node_prefix = label
1483            .map(|lbl| format!("{}__", sanitize_identifier(lbl)))
1484            .unwrap_or_default();
1485
1486        let mut port_lookup: HashMap<String, PortLookup> = HashMap::new();
1487        let mut id_lookup: HashMap<String, String> = HashMap::new();
1488
1489        for node in &topology.nodes {
1490            let node_idx = graph
1491                .get_node_id_by_name(node.id.as_str())
1492                .ok_or_else(|| CuError::from(format!("Node '{}' missing from graph", node.id)))?;
1493            let node_weight = graph
1494                .get_node(node_idx)
1495                .ok_or_else(|| CuError::from(format!("Node '{}' missing weight", node.id)))?;
1496
1497            let is_src = graph.get_dst_edges(node_idx).unwrap_or_default().is_empty();
1498            let is_sink = graph.get_src_edges(node_idx).unwrap_or_default().is_empty();
1499
1500            let fillcolor = match node.flavor {
1501                Flavor::Bridge => "#faedcd",
1502                Flavor::Task if is_src => "#ddefc7",
1503                Flavor::Task if is_sink => "#cce0ff",
1504                _ => "#f2f2f2",
1505            };
1506
1507            let port_base = format!("{}{}", node_prefix, sanitize_identifier(&node.id));
1508            let (inputs_table, input_map, default_input) =
1509                build_port_table("Inputs", &node.inputs, &port_base, "in");
1510            let (outputs_table, output_map, default_output) =
1511                build_port_table("Outputs", &node.outputs, &port_base, "out");
1512            let config_html = node_weight.config.as_ref().and_then(build_config_table);
1513
1514            let mut label_html = String::new();
1515            write!(
1516                label_html,
1517                "<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"6\" COLOR=\"gray\" BGCOLOR=\"white\">"
1518            )
1519            .unwrap();
1520            write!(
1521                label_html,
1522                "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\" BGCOLOR=\"{fillcolor}\"><FONT POINT-SIZE=\"12\"><B>{}</B></FONT><BR/><FONT COLOR=\"dimgray\">[{}]</FONT></TD></TR>",
1523                encode_text(&node.id),
1524                encode_text(&node.type_name)
1525            )
1526            .unwrap();
1527            write!(
1528                label_html,
1529                "<TR><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{inputs_table}</TD><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{outputs_table}</TD></TR>"
1530            )
1531            .unwrap();
1532
1533            if let Some(config_html) = config_html {
1534                write!(
1535                    label_html,
1536                    "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\">{config_html}</TD></TR>"
1537                )
1538                .unwrap();
1539            }
1540
1541            label_html.push_str("</TABLE>");
1542
1543            let identifier_raw = if node_prefix.is_empty() {
1544                node.id.clone()
1545            } else {
1546                format!("{node_prefix}{}", node.id)
1547            };
1548            let identifier = escape_dot_id(&identifier_raw);
1549            writeln!(output, "{indent}\"{identifier}\" [label=<{label_html}>];").unwrap();
1550
1551            id_lookup.insert(node.id.clone(), identifier);
1552            port_lookup.insert(
1553                node.id.clone(),
1554                PortLookup {
1555                    inputs: input_map,
1556                    outputs: output_map,
1557                    default_input,
1558                    default_output,
1559                },
1560            );
1561        }
1562
1563        for cnx in &topology.connections {
1564            let src_id = id_lookup
1565                .get(&cnx.src)
1566                .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.src)))?;
1567            let dst_id = id_lookup
1568                .get(&cnx.dst)
1569                .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.dst)))?;
1570            let src_suffix = port_lookup
1571                .get(&cnx.src)
1572                .and_then(|lookup| lookup.resolve_output(cnx.src_port.as_deref()))
1573                .map(|port| format!(":\"{port}\":e"))
1574                .unwrap_or_default();
1575            let dst_suffix = port_lookup
1576                .get(&cnx.dst)
1577                .and_then(|lookup| lookup.resolve_input(cnx.dst_port.as_deref()))
1578                .map(|port| format!(":\"{port}\":w"))
1579                .unwrap_or_default();
1580            let msg = encode_text(&cnx.msg);
1581            writeln!(
1582                output,
1583                "{indent}\"{src_id}\"{src_suffix} -> \"{dst_id}\"{dst_suffix} [label=< <B><FONT COLOR=\"gray\">{msg}</FONT></B> >];"
1584            )
1585            .unwrap();
1586        }
1587
1588        if cluster_id.is_some() {
1589            writeln!(output, "    }}").unwrap();
1590        }
1591
1592        Ok(())
1593    }
1594}
1595
1596#[cfg(feature = "std")]
1597fn build_render_topology(graph: &CuGraph, bridges: &[BridgeConfig]) -> RenderTopology {
1598    let mut bridge_lookup = HashMap::new();
1599    for bridge in bridges {
1600        bridge_lookup.insert(bridge.id.as_str(), bridge);
1601    }
1602
1603    let mut nodes: HashMap<String, RenderNode> = HashMap::new();
1604    for (_, node) in graph.get_all_nodes() {
1605        let node_id = node.get_id();
1606        let mut inputs = Vec::new();
1607        let mut outputs = Vec::new();
1608        if node.get_flavor() == Flavor::Bridge {
1609            if let Some(bridge) = bridge_lookup.get(node_id.as_str()) {
1610                for channel in &bridge.channels {
1611                    match channel {
1612                        // Rx brings data from the bridge into the graph, so treat it as an output.
1613                        BridgeChannelConfigRepresentation::Rx { id, .. } => {
1614                            outputs.push(id.clone())
1615                        }
1616                        // Tx consumes data from the graph heading into the bridge, so show it on the input side.
1617                        BridgeChannelConfigRepresentation::Tx { id, .. } => inputs.push(id.clone()),
1618                    }
1619                }
1620            }
1621        }
1622
1623        nodes.insert(
1624            node_id.clone(),
1625            RenderNode {
1626                id: node_id,
1627                type_name: node.get_type().to_string(),
1628                flavor: node.get_flavor(),
1629                inputs,
1630                outputs,
1631            },
1632        );
1633    }
1634
1635    let mut connections = Vec::new();
1636    for edge in graph.0.edge_references() {
1637        let cnx = edge.weight();
1638        if let Some(node) = nodes.get_mut(&cnx.src) {
1639            if node.flavor == Flavor::Task && cnx.src_channel.is_none() && node.outputs.is_empty() {
1640                node.outputs.push("out0".to_string());
1641            }
1642        }
1643        if let Some(node) = nodes.get_mut(&cnx.dst) {
1644            if node.flavor == Flavor::Task && cnx.dst_channel.is_none() {
1645                let next = format!("in{}", node.inputs.len());
1646                node.inputs.push(next);
1647            }
1648        }
1649
1650        connections.push(RenderConnection {
1651            src: cnx.src.clone(),
1652            src_port: cnx.src_channel.clone(),
1653            dst: cnx.dst.clone(),
1654            dst_port: cnx.dst_channel.clone(),
1655            msg: cnx.msg.clone(),
1656        });
1657    }
1658
1659    RenderTopology {
1660        nodes: nodes.into_values().collect(),
1661        connections,
1662    }
1663}
1664
1665#[cfg(feature = "std")]
1666impl PortLookup {
1667    fn resolve_input(&self, name: Option<&str>) -> Option<&str> {
1668        if let Some(name) = name {
1669            if let Some(port) = self.inputs.get(name) {
1670                return Some(port.as_str());
1671            }
1672        }
1673        self.default_input.as_deref()
1674    }
1675
1676    fn resolve_output(&self, name: Option<&str>) -> Option<&str> {
1677        if let Some(name) = name {
1678            if let Some(port) = self.outputs.get(name) {
1679                return Some(port.as_str());
1680            }
1681        }
1682        self.default_output.as_deref()
1683    }
1684}
1685
1686#[cfg(feature = "std")]
1687fn build_port_table(
1688    title: &str,
1689    names: &[String],
1690    base_id: &str,
1691    prefix: &str,
1692) -> (String, HashMap<String, String>, Option<String>) {
1693    use std::fmt::Write as FmtWrite;
1694
1695    let mut html = String::new();
1696    write!(
1697        html,
1698        "<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">"
1699    )
1700    .unwrap();
1701    write!(
1702        html,
1703        "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT></TD></TR>",
1704        encode_text(title)
1705    )
1706    .unwrap();
1707
1708    let mut lookup = HashMap::new();
1709    let mut default_port = None;
1710
1711    if names.is_empty() {
1712        html.push_str("<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"lightgray\">&mdash;</FONT></TD></TR>");
1713    } else {
1714        for (idx, name) in names.iter().enumerate() {
1715            let port_id = format!("{base_id}_{prefix}_{idx}");
1716            write!(
1717                html,
1718                "<TR><TD PORT=\"{port_id}\" ALIGN=\"LEFT\">{}</TD></TR>",
1719                encode_text(name)
1720            )
1721            .unwrap();
1722            lookup.insert(name.clone(), port_id.clone());
1723            if idx == 0 {
1724                default_port = Some(port_id);
1725            }
1726        }
1727    }
1728
1729    html.push_str("</TABLE>");
1730    (html, lookup, default_port)
1731}
1732
1733#[cfg(feature = "std")]
1734fn build_config_table(config: &ComponentConfig) -> Option<String> {
1735    use std::fmt::Write as FmtWrite;
1736
1737    if config.0.is_empty() {
1738        return None;
1739    }
1740
1741    let mut entries: Vec<_> = config.0.iter().collect();
1742    entries.sort_by(|a, b| a.0.cmp(b.0));
1743
1744    let mut html = String::new();
1745    html.push_str("<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">");
1746    for (key, value) in entries {
1747        let value_txt = format!("{value}");
1748        write!(
1749            html,
1750            "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT> = {}</TD></TR>",
1751            encode_text(key),
1752            encode_text(&value_txt)
1753        )
1754        .unwrap();
1755    }
1756    html.push_str("</TABLE>");
1757    Some(html)
1758}
1759
1760#[cfg(feature = "std")]
1761fn sanitize_identifier(value: &str) -> String {
1762    value
1763        .chars()
1764        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
1765        .collect()
1766}
1767
1768#[cfg(feature = "std")]
1769fn escape_dot_id(value: &str) -> String {
1770    let mut escaped = String::with_capacity(value.len());
1771    for ch in value.chars() {
1772        match ch {
1773            '"' => escaped.push_str("\\\""),
1774            '\\' => escaped.push_str("\\\\"),
1775            _ => escaped.push(ch),
1776        }
1777    }
1778    escaped
1779}
1780
1781impl LoggingConfig {
1782    /// Validate the logging configuration to ensure section pre-allocation sizes do not exceed slab sizes.
1783    pub fn validate(&self) -> CuResult<()> {
1784        if let Some(section_size_mib) = self.section_size_mib {
1785            if let Some(slab_size_mib) = self.slab_size_mib {
1786                if section_size_mib > slab_size_mib {
1787                    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.")));
1788                }
1789            }
1790        }
1791
1792        Ok(())
1793    }
1794}
1795
1796#[allow(dead_code)] // dead in no-std
1797fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
1798    let mut result = content.to_string();
1799
1800    for (key, value) in params {
1801        let pattern = format!("{{{{{key}}}}}");
1802        result = result.replace(&pattern, &value.to_string());
1803    }
1804
1805    result
1806}
1807
1808/// Returns a merged CuConfigRepresentation.
1809#[cfg(feature = "std")]
1810fn process_includes(
1811    file_path: &str,
1812    base_representation: CuConfigRepresentation,
1813    processed_files: &mut Vec<String>,
1814) -> CuResult<CuConfigRepresentation> {
1815    // Note: Circular dependency detection removed
1816    processed_files.push(file_path.to_string());
1817
1818    let mut result = base_representation;
1819
1820    if let Some(includes) = result.includes.take() {
1821        for include in includes {
1822            let include_path = if include.path.starts_with('/') {
1823                include.path.clone()
1824            } else {
1825                let current_dir = std::path::Path::new(file_path)
1826                    .parent()
1827                    .unwrap_or_else(|| std::path::Path::new(""))
1828                    .to_string_lossy()
1829                    .to_string();
1830
1831                format!("{}/{}", current_dir, include.path)
1832            };
1833
1834            let include_content = read_to_string(&include_path).map_err(|e| {
1835                CuError::from(format!("Failed to read include file: {include_path}"))
1836                    .add_cause(e.to_string().as_str())
1837            })?;
1838
1839            let processed_content = substitute_parameters(&include_content, &include.params);
1840
1841            let mut included_representation: CuConfigRepresentation = match Options::default()
1842                .with_default_extension(Extensions::IMPLICIT_SOME)
1843                .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1844                .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1845                .from_str(&processed_content)
1846            {
1847                Ok(rep) => rep,
1848                Err(e) => {
1849                    return Err(CuError::from(format!(
1850                        "Failed to parse include file: {} - Error: {} at position {}",
1851                        include_path, e.code, e.span
1852                    )));
1853                }
1854            };
1855
1856            included_representation =
1857                process_includes(&include_path, included_representation, processed_files)?;
1858
1859            if let Some(included_tasks) = included_representation.tasks {
1860                if result.tasks.is_none() {
1861                    result.tasks = Some(included_tasks);
1862                } else {
1863                    let mut tasks = result.tasks.take().unwrap();
1864                    for included_task in included_tasks {
1865                        if !tasks.iter().any(|t| t.id == included_task.id) {
1866                            tasks.push(included_task);
1867                        }
1868                    }
1869                    result.tasks = Some(tasks);
1870                }
1871            }
1872
1873            if let Some(included_bridges) = included_representation.bridges {
1874                if result.bridges.is_none() {
1875                    result.bridges = Some(included_bridges);
1876                } else {
1877                    let mut bridges = result.bridges.take().unwrap();
1878                    for included_bridge in included_bridges {
1879                        if !bridges.iter().any(|b| b.id == included_bridge.id) {
1880                            bridges.push(included_bridge);
1881                        }
1882                    }
1883                    result.bridges = Some(bridges);
1884                }
1885            }
1886
1887            if let Some(included_cnx) = included_representation.cnx {
1888                if result.cnx.is_none() {
1889                    result.cnx = Some(included_cnx);
1890                } else {
1891                    let mut cnx = result.cnx.take().unwrap();
1892                    for included_c in included_cnx {
1893                        if !cnx
1894                            .iter()
1895                            .any(|c| c.src == included_c.src && c.dst == included_c.dst)
1896                        {
1897                            cnx.push(included_c);
1898                        }
1899                    }
1900                    result.cnx = Some(cnx);
1901                }
1902            }
1903
1904            if result.monitor.is_none() {
1905                result.monitor = included_representation.monitor;
1906            }
1907
1908            if result.logging.is_none() {
1909                result.logging = included_representation.logging;
1910            }
1911
1912            if result.runtime.is_none() {
1913                result.runtime = included_representation.runtime;
1914            }
1915
1916            if let Some(included_missions) = included_representation.missions {
1917                if result.missions.is_none() {
1918                    result.missions = Some(included_missions);
1919                } else {
1920                    let mut missions = result.missions.take().unwrap();
1921                    for included_mission in included_missions {
1922                        if !missions.iter().any(|m| m.id == included_mission.id) {
1923                            missions.push(included_mission);
1924                        }
1925                    }
1926                    result.missions = Some(missions);
1927                }
1928            }
1929        }
1930    }
1931
1932    Ok(result)
1933}
1934
1935/// Read a copper configuration from a file.
1936#[cfg(feature = "std")]
1937pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
1938    let config_content = read_to_string(config_filename).map_err(|e| {
1939        CuError::from(format!(
1940            "Failed to read configuration file: {:?}",
1941            &config_filename
1942        ))
1943        .add_cause(e.to_string().as_str())
1944    })?;
1945    read_configuration_str(config_content, Some(config_filename))
1946}
1947
1948/// Read a copper configuration from a String.
1949/// Parse a RON string into a CuConfigRepresentation, using the standard options.
1950/// Returns an error if the parsing fails.
1951fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
1952    Options::default()
1953        .with_default_extension(Extensions::IMPLICIT_SOME)
1954        .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1955        .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1956        .from_str(content)
1957        .map_err(|e| {
1958            CuError::from(format!(
1959                "Failed to parse configuration: Error: {} at position {}",
1960                e.code, e.span
1961            ))
1962        })
1963}
1964
1965/// Convert a CuConfigRepresentation to a CuConfig.
1966/// Uses the deserialize_impl method and validates the logging configuration.
1967fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
1968    let cuconfig = CuConfig::deserialize_impl(representation)
1969        .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
1970
1971    cuconfig.validate_logging_config()?;
1972
1973    Ok(cuconfig)
1974}
1975
1976#[allow(unused_variables)]
1977pub fn read_configuration_str(
1978    config_content: String,
1979    file_path: Option<&str>,
1980) -> CuResult<CuConfig> {
1981    // Parse the configuration string
1982    let representation = parse_config_string(&config_content)?;
1983
1984    // Process includes and generate a merged configuration if a file path is provided
1985    // includes are only available with std.
1986    #[cfg(feature = "std")]
1987    let representation = if let Some(path) = file_path {
1988        process_includes(path, representation, &mut Vec::new())?
1989    } else {
1990        representation
1991    };
1992
1993    // Convert the representation to a CuConfig and validate
1994    config_representation_to_config(representation)
1995}
1996
1997// tests
1998#[cfg(test)]
1999mod tests {
2000    use super::*;
2001    #[cfg(not(feature = "std"))]
2002    use alloc::vec;
2003
2004    #[test]
2005    fn test_plain_serialize() {
2006        let mut config = CuConfig::default();
2007        let graph = config.get_graph_mut(None).unwrap();
2008        let n1 = graph
2009            .add_node(Node::new("test1", "package::Plugin1"))
2010            .unwrap();
2011        let n2 = graph
2012            .add_node(Node::new("test2", "package::Plugin2"))
2013            .unwrap();
2014        graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
2015        let serialized = config.serialize_ron();
2016        let deserialized = CuConfig::deserialize_ron(&serialized);
2017        let graph = config.graphs.get_graph(None).unwrap();
2018        let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
2019        assert_eq!(graph.node_count(), deserialized_graph.node_count());
2020        assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
2021    }
2022
2023    #[test]
2024    fn test_serialize_with_params() {
2025        let mut config = CuConfig::default();
2026        let graph = config.get_graph_mut(None).unwrap();
2027        let mut camera = Node::new("copper-camera", "camerapkg::Camera");
2028        camera.set_param::<Value>("resolution-height", 1080.into());
2029        graph.add_node(camera).unwrap();
2030        let serialized = config.serialize_ron();
2031        let config = CuConfig::deserialize_ron(&serialized);
2032        let deserialized = config.get_graph(None).unwrap();
2033        assert_eq!(
2034            deserialized
2035                .get_node(0)
2036                .unwrap()
2037                .get_param::<i32>("resolution-height")
2038                .unwrap(),
2039            1080
2040        );
2041    }
2042
2043    #[test]
2044    #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:9-1:10")]
2045    fn test_deserialization_error() {
2046        // Task needs to be an array, but provided tuple wrongfully
2047        let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
2048        CuConfig::deserialize_ron(txt);
2049    }
2050    #[test]
2051    fn test_missions() {
2052        let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
2053        let config = CuConfig::deserialize_ron(txt);
2054        let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
2055        assert!(graph.node_count() == 0);
2056        let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
2057        assert!(graph.node_count() == 0);
2058    }
2059
2060    #[test]
2061    fn test_monitor() {
2062        let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
2063        let config = CuConfig::deserialize_ron(txt);
2064        assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
2065
2066        let txt =
2067            r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
2068        let config = CuConfig::deserialize_ron(txt);
2069        assert_eq!(
2070            config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
2071            4u8.into()
2072        );
2073    }
2074
2075    #[test]
2076    fn test_logging_parameters() {
2077        // Test with `enable_task_logging: false`
2078        let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
2079
2080        let config = CuConfig::deserialize_ron(txt);
2081        assert!(config.logging.is_some());
2082        let logging_config = config.logging.unwrap();
2083        assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
2084        assert_eq!(logging_config.section_size_mib.unwrap(), 100);
2085        assert!(!logging_config.enable_task_logging);
2086
2087        // Test with `enable_task_logging` not provided
2088        let txt =
2089            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
2090        let config = CuConfig::deserialize_ron(txt);
2091        assert!(config.logging.is_some());
2092        let logging_config = config.logging.unwrap();
2093        assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
2094        assert_eq!(logging_config.section_size_mib.unwrap(), 100);
2095        assert!(logging_config.enable_task_logging);
2096    }
2097
2098    #[test]
2099    fn test_bridge_parsing() {
2100        let txt = r#"
2101        (
2102            tasks: [
2103                (id: "dst", type: "tasks::Destination"),
2104                (id: "src", type: "tasks::Source"),
2105            ],
2106            bridges: [
2107                (
2108                    id: "radio",
2109                    type: "tasks::SerialBridge",
2110                    config: { "path": "/dev/ttyACM0", "baud": 921600 },
2111                    channels: [
2112                        Rx ( id: "status", route: "sys/status" ),
2113                        Tx ( id: "motor", route: "motor/cmd" ),
2114                    ],
2115                ),
2116            ],
2117            cnx: [
2118                (src: "radio/status", dst: "dst", msg: "mymsgs::Status"),
2119                (src: "src", dst: "radio/motor", msg: "mymsgs::MotorCmd"),
2120            ],
2121        )
2122        "#;
2123
2124        let config = CuConfig::deserialize_ron(txt);
2125        assert_eq!(config.bridges.len(), 1);
2126        let bridge = &config.bridges[0];
2127        assert_eq!(bridge.id, "radio");
2128        assert_eq!(bridge.channels.len(), 2);
2129        match &bridge.channels[0] {
2130            BridgeChannelConfigRepresentation::Rx { id, route, .. } => {
2131                assert_eq!(id, "status");
2132                assert_eq!(route.as_deref(), Some("sys/status"));
2133            }
2134            _ => panic!("expected Rx channel"),
2135        }
2136        match &bridge.channels[1] {
2137            BridgeChannelConfigRepresentation::Tx { id, route, .. } => {
2138                assert_eq!(id, "motor");
2139                assert_eq!(route.as_deref(), Some("motor/cmd"));
2140            }
2141            _ => panic!("expected Tx channel"),
2142        }
2143        let graph = config.graphs.get_graph(None).unwrap();
2144        let bridge_id = graph
2145            .get_node_id_by_name("radio")
2146            .expect("bridge node missing");
2147        let bridge_node = graph.get_node(bridge_id).unwrap();
2148        assert_eq!(bridge_node.get_flavor(), Flavor::Bridge);
2149
2150        // Edges should retain channel metadata.
2151        let mut edges = Vec::new();
2152        for edge_idx in graph.0.edge_indices() {
2153            edges.push(graph.0[edge_idx].clone());
2154        }
2155        assert_eq!(edges.len(), 2);
2156        let status_edge = edges
2157            .iter()
2158            .find(|e| e.dst == "dst")
2159            .expect("status edge missing");
2160        assert_eq!(status_edge.src_channel.as_deref(), Some("status"));
2161        assert!(status_edge.dst_channel.is_none());
2162        let motor_edge = edges
2163            .iter()
2164            .find(|e| e.dst_channel.is_some())
2165            .expect("motor edge missing");
2166        assert_eq!(motor_edge.dst_channel.as_deref(), Some("motor"));
2167    }
2168
2169    #[test]
2170    fn test_bridge_roundtrip() {
2171        let mut config = CuConfig::default();
2172        let mut bridge_config = ComponentConfig::default();
2173        bridge_config.set("port", "/dev/ttyACM0".to_string());
2174        config.bridges.push(BridgeConfig {
2175            id: "radio".to_string(),
2176            type_: "tasks::SerialBridge".to_string(),
2177            config: Some(bridge_config),
2178            missions: None,
2179            channels: vec![
2180                BridgeChannelConfigRepresentation::Rx {
2181                    id: "status".to_string(),
2182                    route: Some("sys/status".to_string()),
2183                    config: None,
2184                },
2185                BridgeChannelConfigRepresentation::Tx {
2186                    id: "motor".to_string(),
2187                    route: Some("motor/cmd".to_string()),
2188                    config: None,
2189                },
2190            ],
2191        });
2192
2193        let serialized = config.serialize_ron();
2194        assert!(
2195            serialized.contains("bridges"),
2196            "bridges section missing from serialized config"
2197        );
2198        let deserialized = CuConfig::deserialize_ron(&serialized);
2199        assert_eq!(deserialized.bridges.len(), 1);
2200        let bridge = &deserialized.bridges[0];
2201        assert_eq!(bridge.channels.len(), 2);
2202        assert!(matches!(
2203            bridge.channels[0],
2204            BridgeChannelConfigRepresentation::Rx { .. }
2205        ));
2206        assert!(matches!(
2207            bridge.channels[1],
2208            BridgeChannelConfigRepresentation::Tx { .. }
2209        ));
2210    }
2211
2212    #[test]
2213    fn test_bridge_channel_config() {
2214        let txt = r#"
2215        (
2216            tasks: [],
2217            bridges: [
2218                (
2219                    id: "radio",
2220                    type: "tasks::SerialBridge",
2221                    channels: [
2222                        Rx ( id: "status", route: "sys/status", config: { "filter": "fast" } ),
2223                        Tx ( id: "imu", route: "telemetry/imu", config: { "rate": 100 } ),
2224                    ],
2225                ),
2226            ],
2227            cnx: [],
2228        )
2229        "#;
2230
2231        let config = CuConfig::deserialize_ron(txt);
2232        let bridge = &config.bridges[0];
2233        match &bridge.channels[0] {
2234            BridgeChannelConfigRepresentation::Rx {
2235                config: Some(cfg), ..
2236            } => {
2237                let val: String = cfg.get("filter").expect("filter missing");
2238                assert_eq!(val, "fast");
2239            }
2240            _ => panic!("expected Rx channel with config"),
2241        }
2242        match &bridge.channels[1] {
2243            BridgeChannelConfigRepresentation::Tx {
2244                config: Some(cfg), ..
2245            } => {
2246                let rate: i32 = cfg.get("rate").expect("rate missing");
2247                assert_eq!(rate, 100);
2248            }
2249            _ => panic!("expected Tx channel with config"),
2250        }
2251    }
2252
2253    #[test]
2254    #[should_panic(expected = "channel 'motor' is Tx and cannot act as a source")]
2255    fn test_bridge_tx_cannot_be_source() {
2256        let txt = r#"
2257        (
2258            tasks: [
2259                (id: "dst", type: "tasks::Destination"),
2260            ],
2261            bridges: [
2262                (
2263                    id: "radio",
2264                    type: "tasks::SerialBridge",
2265                    channels: [
2266                        Tx ( id: "motor", route: "motor/cmd" ),
2267                    ],
2268                ),
2269            ],
2270            cnx: [
2271                (src: "radio/motor", dst: "dst", msg: "mymsgs::MotorCmd"),
2272            ],
2273        )
2274        "#;
2275
2276        CuConfig::deserialize_ron(txt);
2277    }
2278
2279    #[test]
2280    #[should_panic(expected = "channel 'status' is Rx and cannot act as a destination")]
2281    fn test_bridge_rx_cannot_be_destination() {
2282        let txt = r#"
2283        (
2284            tasks: [
2285                (id: "src", type: "tasks::Source"),
2286            ],
2287            bridges: [
2288                (
2289                    id: "radio",
2290                    type: "tasks::SerialBridge",
2291                    channels: [
2292                        Rx ( id: "status", route: "sys/status" ),
2293                    ],
2294                ),
2295            ],
2296            cnx: [
2297                (src: "src", dst: "radio/status", msg: "mymsgs::Status"),
2298            ],
2299        )
2300        "#;
2301
2302        CuConfig::deserialize_ron(txt);
2303    }
2304
2305    #[test]
2306    fn test_validate_logging_config() {
2307        // Test with valid logging configuration
2308        let txt =
2309            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
2310        let config = CuConfig::deserialize_ron(txt);
2311        assert!(config.validate_logging_config().is_ok());
2312
2313        // Test with invalid logging configuration
2314        let txt =
2315            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
2316        let config = CuConfig::deserialize_ron(txt);
2317        assert!(config.validate_logging_config().is_err());
2318    }
2319
2320    // this test makes sure the edge id is suitable to be used to sort the inputs of a task
2321    #[test]
2322    fn test_deserialization_edge_id_assignment() {
2323        // note here that the src1 task is added before src2 in the tasks array,
2324        // however, src1 connection is added AFTER src2 in the cnx array
2325        let txt = r#"(
2326            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2327            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
2328        )"#;
2329        let config = CuConfig::deserialize_ron(txt);
2330        let graph = config.graphs.get_graph(None).unwrap();
2331        assert!(config.validate_logging_config().is_ok());
2332
2333        // the node id depends on the order in which the tasks are added
2334        let src1_id = 0;
2335        assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
2336        let src2_id = 1;
2337        assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
2338
2339        // the edge id depends on the order the connection is created
2340        // the src2 was added second in the tasks, but the connection was added first
2341        let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
2342        assert_eq!(src1_edge_id, 1);
2343        let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
2344        assert_eq!(src2_edge_id, 0);
2345    }
2346
2347    #[test]
2348    fn test_simple_missions() {
2349        // A simple config that selection a source depending on the mission it is in.
2350        let txt = r#"(
2351                    missions: [ (id: "m1"),
2352                                (id: "m2"),
2353                                ],
2354                    tasks: [(id: "src1", type: "a", missions: ["m1"]),
2355                            (id: "src2", type: "b", missions: ["m2"]),
2356                            (id: "sink", type: "c")],
2357
2358                    cnx: [
2359                            (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2360                            (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2361                         ],
2362              )
2363              "#;
2364
2365        let config = CuConfig::deserialize_ron(txt);
2366        let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
2367        assert_eq!(m1_graph.edge_count(), 1);
2368        assert_eq!(m1_graph.node_count(), 2);
2369        let index = 0;
2370        let cnx = m1_graph.get_edge_weight(index).unwrap();
2371
2372        assert_eq!(cnx.src, "src1");
2373        assert_eq!(cnx.dst, "sink");
2374        assert_eq!(cnx.msg, "u32");
2375        assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2376
2377        let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
2378        assert_eq!(m2_graph.edge_count(), 1);
2379        assert_eq!(m2_graph.node_count(), 2);
2380        let index = 0;
2381        let cnx = m2_graph.get_edge_weight(index).unwrap();
2382        assert_eq!(cnx.src, "src2");
2383        assert_eq!(cnx.dst, "sink");
2384        assert_eq!(cnx.msg, "u32");
2385        assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
2386    }
2387    #[test]
2388    fn test_mission_serde() {
2389        // A simple config that selection a source depending on the mission it is in.
2390        let txt = r#"(
2391                    missions: [ (id: "m1"),
2392                                (id: "m2"),
2393                                ],
2394                    tasks: [(id: "src1", type: "a", missions: ["m1"]),
2395                            (id: "src2", type: "b", missions: ["m2"]),
2396                            (id: "sink", type: "c")],
2397
2398                    cnx: [
2399                            (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2400                            (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2401                         ],
2402              )
2403              "#;
2404
2405        let config = CuConfig::deserialize_ron(txt);
2406        let serialized = config.serialize_ron();
2407        let deserialized = CuConfig::deserialize_ron(&serialized);
2408        let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
2409        assert_eq!(m1_graph.edge_count(), 1);
2410        assert_eq!(m1_graph.node_count(), 2);
2411        let index = 0;
2412        let cnx = m1_graph.get_edge_weight(index).unwrap();
2413        assert_eq!(cnx.src, "src1");
2414        assert_eq!(cnx.dst, "sink");
2415        assert_eq!(cnx.msg, "u32");
2416        assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2417    }
2418
2419    #[test]
2420    fn test_keyframe_interval() {
2421        // note here that the src1 task is added before src2 in the tasks array,
2422        // however, src1 connection is added AFTER src2 in the cnx array
2423        let txt = r#"(
2424            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2425            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2426            logging: ( keyframe_interval: 314 )
2427        )"#;
2428        let config = CuConfig::deserialize_ron(txt);
2429        let logging_config = config.logging.unwrap();
2430        assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
2431    }
2432
2433    #[test]
2434    fn test_default_keyframe_interval() {
2435        // note here that the src1 task is added before src2 in the tasks array,
2436        // however, src1 connection is added AFTER src2 in the cnx array
2437        let txt = r#"(
2438            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2439            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2440            logging: ( slab_size_mib: 200, section_size_mib: 1024, )
2441        )"#;
2442        let config = CuConfig::deserialize_ron(txt);
2443        let logging_config = config.logging.unwrap();
2444        assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
2445    }
2446}