1#[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;
14pub use petgraph::Direction::Incoming;
15pub use petgraph::Direction::Outgoing;
16use ron::extensions::Extensions;
17use ron::value::Value as RonValue;
18use ron::{Number, Options};
19use serde::{Deserialize, Deserializer, Serialize, Serializer};
20use ConfigGraphs::{Missions, Simple};
21
22#[cfg(not(feature = "std"))]
23mod imp {
24 pub use alloc::borrow::ToOwned;
25 pub use alloc::format;
26 pub use alloc::string::String;
27 pub use alloc::string::ToString;
28 pub use alloc::vec::Vec;
29}
30
31#[cfg(feature = "std")]
32mod imp {
33 pub use html_escape::encode_text;
34 pub use std::fs::read_to_string;
35}
36
37use imp::*;
38
39pub type NodeId = u32;
42
43#[derive(Serialize, Deserialize, Debug, Clone, Default)]
47pub struct ComponentConfig(pub HashMap<String, Value>);
48
49impl Display for ComponentConfig {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 let mut first = true;
52 let ComponentConfig(config) = self;
53 write!(f, "{{")?;
54 for (key, value) in config.iter() {
55 if !first {
56 write!(f, ", ")?;
57 }
58 write!(f, "{key}: {value}")?;
59 first = false;
60 }
61 write!(f, "}}")
62 }
63}
64
65impl ComponentConfig {
67 #[allow(dead_code)]
68 pub fn new() -> Self {
69 ComponentConfig(HashMap::new())
70 }
71
72 #[allow(dead_code)]
73 pub fn get<T: From<Value>>(&self, key: &str) -> Option<T> {
74 let ComponentConfig(config) = self;
75 config.get(key).map(|v| T::from(v.clone()))
76 }
77
78 #[allow(dead_code)]
79 pub fn set<T: Into<Value>>(&mut self, key: &str, value: T) {
80 let ComponentConfig(config) = self;
81 config.insert(key.to_string(), value.into());
82 }
83}
84
85#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
94pub struct Value(RonValue);
95
96macro_rules! impl_from_numeric_for_value {
98 ($($source:ty),* $(,)?) => {
99 $(impl From<$source> for Value {
100 fn from(value: $source) -> Self {
101 Value(RonValue::Number(value.into()))
102 }
103 })*
104 };
105}
106
107impl_from_numeric_for_value!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
109
110impl From<Value> for bool {
111 fn from(value: Value) -> Self {
112 if let Value(RonValue::Bool(v)) = value {
113 v
114 } else {
115 panic!("Expected a Boolean variant but got {value:?}")
116 }
117 }
118}
119macro_rules! impl_from_value_for_int {
120 ($($target:ty),* $(,)?) => {
121 $(
122 impl From<Value> for $target {
123 fn from(value: Value) -> Self {
124 if let Value(RonValue::Number(num)) = value {
125 match num {
126 Number::I8(n) => n as $target,
127 Number::I16(n) => n as $target,
128 Number::I32(n) => n as $target,
129 Number::I64(n) => n as $target,
130 Number::U8(n) => n as $target,
131 Number::U16(n) => n as $target,
132 Number::U32(n) => n as $target,
133 Number::U64(n) => n as $target,
134 Number::F32(_) | Number::F64(_) | Number::__NonExhaustive(_) => {
135 panic!("Expected an integer Number variant but got {num:?}")
136 }
137 }
138 } else {
139 panic!("Expected a Number variant but got {value:?}")
140 }
141 }
142 }
143 )*
144 };
145}
146
147impl_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
148
149impl From<Value> for f64 {
150 fn from(value: Value) -> Self {
151 if let Value(RonValue::Number(num)) = value {
152 num.into_f64()
153 } else {
154 panic!("Expected a Number variant but got {value:?}")
155 }
156 }
157}
158
159impl From<String> for Value {
160 fn from(value: String) -> Self {
161 Value(RonValue::String(value))
162 }
163}
164
165impl From<Value> for String {
166 fn from(value: Value) -> Self {
167 if let Value(RonValue::String(s)) = value {
168 s
169 } else {
170 panic!("Expected a String variant")
171 }
172 }
173}
174
175impl Display for Value {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 let Value(value) = self;
178 match value {
179 RonValue::Number(n) => {
180 let s = match n {
181 Number::I8(n) => n.to_string(),
182 Number::I16(n) => n.to_string(),
183 Number::I32(n) => n.to_string(),
184 Number::I64(n) => n.to_string(),
185 Number::U8(n) => n.to_string(),
186 Number::U16(n) => n.to_string(),
187 Number::U32(n) => n.to_string(),
188 Number::U64(n) => n.to_string(),
189 Number::F32(n) => n.0.to_string(),
190 Number::F64(n) => n.0.to_string(),
191 _ => panic!("Expected a Number variant but got {value:?}"),
192 };
193 write!(f, "{s}")
194 }
195 RonValue::String(s) => write!(f, "{s}"),
196 RonValue::Bool(b) => write!(f, "{b}"),
197 RonValue::Map(m) => write!(f, "{m:?}"),
198 RonValue::Char(c) => write!(f, "{c:?}"),
199 RonValue::Unit => write!(f, "unit"),
200 RonValue::Option(o) => write!(f, "{o:?}"),
201 RonValue::Seq(s) => write!(f, "{s:?}"),
202 RonValue::Bytes(bytes) => write!(f, "{bytes:?}"),
203 }
204 }
205}
206
207#[derive(Serialize, Deserialize, Debug, Clone)]
209pub struct NodeLogging {
210 enabled: bool,
211}
212
213#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
216pub enum Flavor {
217 #[default]
218 Task,
219 Bridge,
220}
221
222#[derive(Serialize, Deserialize, Debug, Clone)]
225pub struct Node {
226 id: String,
228
229 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
231 type_: Option<String>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
235 config: Option<ComponentConfig>,
236
237 missions: Option<Vec<String>>,
239
240 #[serde(skip_serializing_if = "Option::is_none")]
243 background: Option<bool>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
251 run_in_sim: Option<bool>,
252
253 #[serde(skip_serializing_if = "Option::is_none")]
255 logging: Option<NodeLogging>,
256
257 #[serde(skip, default)]
259 flavor: Flavor,
260}
261
262impl Node {
263 #[allow(dead_code)]
264 pub fn new(id: &str, ptype: &str) -> Self {
265 Node {
266 id: id.to_string(),
267 type_: Some(ptype.to_string()),
268 config: None,
269 missions: None,
270 background: None,
271 run_in_sim: None,
272 logging: None,
273 flavor: Flavor::Task,
274 }
275 }
276
277 #[allow(dead_code)]
278 pub fn new_with_flavor(id: &str, ptype: &str, flavor: Flavor) -> Self {
279 let mut node = Self::new(id, ptype);
280 node.flavor = flavor;
281 node
282 }
283
284 #[allow(dead_code)]
285 pub fn get_id(&self) -> String {
286 self.id.clone()
287 }
288
289 #[allow(dead_code)]
290 pub fn get_type(&self) -> &str {
291 self.type_.as_ref().unwrap()
292 }
293
294 #[allow(dead_code)]
295 pub fn set_type(mut self, name: Option<String>) -> Self {
296 self.type_ = name;
297 self
298 }
299
300 #[allow(dead_code)]
301 pub fn is_background(&self) -> bool {
302 self.background.unwrap_or(false)
303 }
304
305 #[allow(dead_code)]
306 pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
307 self.config.as_ref()
308 }
309
310 #[allow(dead_code)]
313 pub fn is_run_in_sim(&self) -> bool {
314 self.run_in_sim.unwrap_or(false)
315 }
316
317 #[allow(dead_code)]
318 pub fn is_logging_enabled(&self) -> bool {
319 if let Some(logging) = &self.logging {
320 logging.enabled
321 } else {
322 true
323 }
324 }
325
326 #[allow(dead_code)]
327 pub fn get_param<T: From<Value>>(&self, key: &str) -> Option<T> {
328 let pc = self.config.as_ref()?;
329 let ComponentConfig(pc) = pc;
330 let v = pc.get(key)?;
331 Some(T::from(v.clone()))
332 }
333
334 #[allow(dead_code)]
335 pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
336 if self.config.is_none() {
337 self.config = Some(ComponentConfig(HashMap::new()));
338 }
339 let ComponentConfig(config) = self.config.as_mut().unwrap();
340 config.insert(key.to_string(), value.into());
341 }
342
343 #[allow(dead_code)]
345 pub fn get_flavor(&self) -> Flavor {
346 self.flavor
347 }
348
349 #[allow(dead_code)]
351 pub fn set_flavor(&mut self, flavor: Flavor) {
352 self.flavor = flavor;
353 }
354}
355
356#[derive(Serialize, Deserialize, Debug, Clone)]
358pub enum BridgeChannel {
359 Rx {
361 id: String,
362 route: String,
364 #[serde(skip_serializing_if = "Option::is_none")]
366 config: Option<ComponentConfig>,
367 },
368 Tx {
370 id: String,
371 route: String,
373 #[serde(skip_serializing_if = "Option::is_none")]
375 config: Option<ComponentConfig>,
376 },
377}
378
379impl BridgeChannel {
380 #[allow(dead_code)]
382 pub fn id(&self) -> &str {
383 match self {
384 BridgeChannel::Rx { id, .. } | BridgeChannel::Tx { id, .. } => id,
385 }
386 }
387
388 #[allow(dead_code)]
390 pub fn route(&self) -> &str {
391 match self {
392 BridgeChannel::Rx { route, .. } | BridgeChannel::Tx { route, .. } => route,
393 }
394 }
395}
396
397enum EndpointRole {
398 Source,
399 Destination,
400}
401
402fn validate_bridge_channel(
403 bridge: &BridgeConfig,
404 channel_id: &str,
405 role: EndpointRole,
406) -> Result<(), String> {
407 let channel = bridge
408 .channels
409 .iter()
410 .find(|ch| ch.id() == channel_id)
411 .ok_or_else(|| {
412 format!(
413 "Bridge '{}' does not declare a channel named '{}'",
414 bridge.id, channel_id
415 )
416 })?;
417
418 match (role, channel) {
419 (EndpointRole::Source, BridgeChannel::Rx { .. }) => Ok(()),
420 (EndpointRole::Destination, BridgeChannel::Tx { .. }) => Ok(()),
421 (EndpointRole::Source, BridgeChannel::Tx { .. }) => Err(format!(
422 "Bridge '{}' channel '{}' is Tx and cannot act as a source",
423 bridge.id, channel_id
424 )),
425 (EndpointRole::Destination, BridgeChannel::Rx { .. }) => Err(format!(
426 "Bridge '{}' channel '{}' is Rx and cannot act as a destination",
427 bridge.id, channel_id
428 )),
429 }
430}
431
432#[derive(Serialize, Deserialize, Debug, Clone)]
434pub struct BridgeConfig {
435 pub id: String,
436 #[serde(rename = "type")]
437 pub type_: String,
438 #[serde(skip_serializing_if = "Option::is_none")]
439 pub config: Option<ComponentConfig>,
440 #[serde(skip_serializing_if = "Option::is_none")]
441 pub missions: Option<Vec<String>>,
442 pub channels: Vec<BridgeChannel>,
444}
445
446impl BridgeConfig {
447 fn to_node(&self) -> Node {
448 let mut node = Node::new_with_flavor(&self.id, &self.type_, Flavor::Bridge);
449 node.config = self.config.clone();
450 node.missions = self.missions.clone();
451 node
452 }
453}
454
455fn insert_bridge_node(graph: &mut CuGraph, bridge: &BridgeConfig) -> Result<(), String> {
456 if graph.get_node_id_by_name(bridge.id.as_str()).is_some() {
457 return Err(format!(
458 "Bridge '{}' reuses an existing node id. Bridge ids must be unique.",
459 bridge.id
460 ));
461 }
462 graph
463 .add_node(bridge.to_node())
464 .map(|_| ())
465 .map_err(|e| e.to_string())
466}
467
468#[derive(Serialize, Deserialize, Debug, Clone)]
470struct SerializedCnx {
471 src: String,
472 dst: String,
473 msg: String,
474 missions: Option<Vec<String>>,
475}
476
477#[derive(Debug, Clone)]
479pub struct Cnx {
480 pub src: String,
482 pub dst: String,
484 pub msg: String,
486 pub missions: Option<Vec<String>>,
488 pub src_channel: Option<String>,
490 pub dst_channel: Option<String>,
492}
493
494impl From<&Cnx> for SerializedCnx {
495 fn from(cnx: &Cnx) -> Self {
496 SerializedCnx {
497 src: format_endpoint(&cnx.src, cnx.src_channel.as_deref()),
498 dst: format_endpoint(&cnx.dst, cnx.dst_channel.as_deref()),
499 msg: cnx.msg.clone(),
500 missions: cnx.missions.clone(),
501 }
502 }
503}
504
505fn format_endpoint(node: &str, channel: Option<&str>) -> String {
506 match channel {
507 Some(ch) => format!("{node}/{ch}"),
508 None => node.to_string(),
509 }
510}
511
512fn parse_endpoint(
513 endpoint: &str,
514 role: EndpointRole,
515 bridges: &HashMap<&str, &BridgeConfig>,
516) -> Result<(String, Option<String>), String> {
517 if let Some((node, channel)) = endpoint.split_once('/') {
518 if let Some(bridge) = bridges.get(node) {
519 validate_bridge_channel(bridge, channel, role)?;
520 return Ok((node.to_string(), Some(channel.to_string())));
521 } else {
522 return Err(format!(
523 "Endpoint '{endpoint}' references an unknown bridge '{node}'"
524 ));
525 }
526 }
527
528 if let Some(bridge) = bridges.get(endpoint) {
529 return Err(format!(
530 "Bridge '{}' connections must reference a channel using '{}/<channel>'",
531 bridge.id, bridge.id
532 ));
533 }
534
535 Ok((endpoint.to_string(), None))
536}
537
538fn build_bridge_lookup(bridges: Option<&Vec<BridgeConfig>>) -> HashMap<&str, &BridgeConfig> {
539 let mut map = HashMap::new();
540 if let Some(bridges) = bridges {
541 for bridge in bridges {
542 map.insert(bridge.id.as_str(), bridge);
543 }
544 }
545 map
546}
547
548fn mission_applies(missions: &Option<Vec<String>>, mission_id: &str) -> bool {
549 missions
550 .as_ref()
551 .map(|mission_list| mission_list.iter().any(|m| m == mission_id))
552 .unwrap_or(true)
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
558pub enum CuDirection {
559 Outgoing,
560 Incoming,
561}
562
563impl From<CuDirection> for petgraph::Direction {
564 fn from(dir: CuDirection) -> Self {
565 match dir {
566 CuDirection::Outgoing => petgraph::Direction::Outgoing,
567 CuDirection::Incoming => petgraph::Direction::Incoming,
568 }
569 }
570}
571
572#[derive(Default, Debug, Clone)]
573pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
574
575impl CuGraph {
576 #[allow(dead_code)]
577 pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
578 self.0
579 .node_indices()
580 .map(|index| (index.index() as u32, &self.0[index]))
581 .collect()
582 }
583
584 #[allow(dead_code)]
585 pub fn get_neighbor_ids(&self, node_id: NodeId, dir: CuDirection) -> Vec<NodeId> {
586 self.0
587 .neighbors_directed(node_id.into(), dir.into())
588 .map(|petgraph_index| petgraph_index.index() as NodeId)
589 .collect()
590 }
591
592 #[allow(dead_code)]
593 pub fn incoming_neighbor_count(&self, node_id: NodeId) -> usize {
594 self.0.neighbors_directed(node_id.into(), Incoming).count()
595 }
596
597 #[allow(dead_code)]
598 pub fn outgoing_neighbor_count(&self, node_id: NodeId) -> usize {
599 self.0.neighbors_directed(node_id.into(), Outgoing).count()
600 }
601
602 pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
603 self.0.node_indices().collect()
604 }
605
606 pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
607 Ok(self.0.add_node(node).index() as NodeId)
608 }
609
610 #[allow(dead_code)]
611 pub fn connection_exists(&self, source: NodeId, target: NodeId) -> bool {
612 self.0.find_edge(source.into(), target.into()).is_some()
613 }
614
615 pub fn connect_ext(
616 &mut self,
617 source: NodeId,
618 target: NodeId,
619 msg_type: &str,
620 missions: Option<Vec<String>>,
621 src_channel: Option<String>,
622 dst_channel: Option<String>,
623 ) -> CuResult<()> {
624 let (src_id, dst_id) = (
625 self.0
626 .node_weight(source.into())
627 .ok_or("Source node not found")?
628 .id
629 .clone(),
630 self.0
631 .node_weight(target.into())
632 .ok_or("Target node not found")?
633 .id
634 .clone(),
635 );
636
637 let _ = self.0.add_edge(
638 petgraph::stable_graph::NodeIndex::from(source),
639 petgraph::stable_graph::NodeIndex::from(target),
640 Cnx {
641 src: src_id,
642 dst: dst_id,
643 msg: msg_type.to_string(),
644 missions,
645 src_channel,
646 dst_channel,
647 },
648 );
649 Ok(())
650 }
651 #[allow(dead_code)]
655 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
656 self.0.node_weight(node_id.into())
657 }
658
659 #[allow(dead_code)]
660 pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
661 self.0.node_weight(index.into())
662 }
663
664 #[allow(dead_code)]
665 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
666 self.0.node_weight_mut(node_id.into())
667 }
668
669 pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
670 self.0
671 .node_indices()
672 .into_iter()
673 .find(|idx| self.0[*idx].get_id() == name)
674 .map(|i| i.index() as NodeId)
675 }
676
677 #[allow(dead_code)]
678 pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
679 self.0.edge_weight(EdgeIndex::new(index)).cloned()
680 }
681
682 #[allow(dead_code)]
683 pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
684 self.0.node_indices().find_map(|node_index| {
685 if let Some(node) = self.0.node_weight(node_index) {
686 if node.id != node_id {
687 return None;
688 }
689 let edges: Vec<_> = self
690 .0
691 .edges_directed(node_index, Outgoing)
692 .map(|edge| edge.id().index())
693 .collect();
694 if edges.is_empty() {
695 return None;
696 }
697 let cnx = self
698 .0
699 .edge_weight(EdgeIndex::new(edges[0]))
700 .expect("Found an cnx id but could not retrieve it back");
701 return Some(cnx.msg.clone());
702 }
703 None
704 })
705 }
706
707 #[allow(dead_code)]
708 pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
709 self.0.node_indices().find_map(|node_index| {
710 if let Some(node) = self.0.node_weight(node_index) {
711 if node.id != node_id {
712 return None;
713 }
714 let edges: Vec<_> = self
715 .0
716 .edges_directed(node_index, Incoming)
717 .map(|edge| edge.id().index())
718 .collect();
719 if edges.is_empty() {
720 return None;
721 }
722 let cnx = self
723 .0
724 .edge_weight(EdgeIndex::new(edges[0]))
725 .expect("Found an cnx id but could not retrieve it back");
726 return Some(cnx.msg.clone());
727 }
728 None
729 })
730 }
731
732 #[allow(dead_code)]
733 pub fn get_connection_msg_type(&self, source: NodeId, target: NodeId) -> Option<&str> {
734 self.0
735 .find_edge(source.into(), target.into())
736 .map(|edge_index| self.0[edge_index].msg.as_str())
737 }
738
739 fn get_edges_by_direction(
741 &self,
742 node_id: NodeId,
743 direction: petgraph::Direction,
744 ) -> CuResult<Vec<usize>> {
745 Ok(self
746 .0
747 .edges_directed(node_id.into(), direction)
748 .map(|edge| edge.id().index())
749 .collect())
750 }
751
752 pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
753 self.get_edges_by_direction(node_id, Outgoing)
754 }
755
756 pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
758 self.get_edges_by_direction(node_id, Incoming)
759 }
760
761 #[allow(dead_code)]
762 pub fn node_count(&self) -> usize {
763 self.0.node_count()
764 }
765
766 #[allow(dead_code)]
767 pub fn edge_count(&self) -> usize {
768 self.0.edge_count()
769 }
770
771 #[allow(dead_code)]
774 pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
775 self.connect_ext(source, target, msg_type, None, None, None)
776 }
777}
778
779impl core::ops::Index<NodeIndex> for CuGraph {
780 type Output = Node;
781
782 fn index(&self, index: NodeIndex) -> &Self::Output {
783 &self.0[index]
784 }
785}
786
787#[derive(Debug, Clone)]
788pub enum ConfigGraphs {
789 Simple(CuGraph),
790 Missions(HashMap<String, CuGraph>),
791}
792
793impl ConfigGraphs {
794 #[allow(dead_code)]
797 pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
798 match self {
799 Simple(graph) => {
800 let mut map = HashMap::new();
801 map.insert("default".to_string(), graph.clone());
802 map
803 }
804 Missions(graphs) => graphs.clone(),
805 }
806 }
807
808 #[allow(dead_code)]
809 pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
810 match self {
811 Simple(graph) => Ok(graph),
812 Missions(graphs) => {
813 if graphs.len() == 1 {
814 Ok(graphs.values().next().unwrap())
815 } else {
816 Err("Cannot get default mission graph from mission config".into())
817 }
818 }
819 }
820 }
821
822 #[allow(dead_code)]
823 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
824 match self {
825 Simple(graph) => {
826 if mission_id.is_none() || mission_id.unwrap() == "default" {
827 Ok(graph)
828 } else {
829 Err("Cannot get mission graph from simple config".into())
830 }
831 }
832 Missions(graphs) => {
833 if let Some(id) = mission_id {
834 graphs
835 .get(id)
836 .ok_or_else(|| format!("Mission {id} not found").into())
837 } else {
838 Err("Mission ID required for mission configs".into())
839 }
840 }
841 }
842 }
843
844 #[allow(dead_code)]
845 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
846 match self {
847 Simple(ref mut graph) => {
848 if mission_id.is_none() {
849 Ok(graph)
850 } else {
851 Err("Cannot get mission graph from simple config".into())
852 }
853 }
854 Missions(ref mut graphs) => {
855 if let Some(id) = mission_id {
856 graphs
857 .get_mut(id)
858 .ok_or_else(|| format!("Mission {id} not found").into())
859 } else {
860 Err("Mission ID required for mission configs".into())
861 }
862 }
863 }
864 }
865
866 pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
867 match self {
868 Simple(_) => Err("Cannot add mission to simple config".into()),
869 Missions(graphs) => {
870 if graphs.contains_key(mission_id) {
871 Err(format!("Mission {mission_id} already exists").into())
872 } else {
873 let graph = CuGraph::default();
874 graphs.insert(mission_id.to_string(), graph);
875 Ok(graphs.get_mut(mission_id).unwrap())
877 }
878 }
879 }
880 }
881}
882
883#[derive(Debug, Clone)]
889pub struct CuConfig {
890 pub monitor: Option<MonitorConfig>,
892 pub logging: Option<LoggingConfig>,
894 pub runtime: Option<RuntimeConfig>,
896 pub bridges: Vec<BridgeConfig>,
898 pub graphs: ConfigGraphs,
900}
901
902#[derive(Serialize, Deserialize, Default, Debug, Clone)]
903pub struct MonitorConfig {
904 #[serde(rename = "type")]
905 type_: String,
906 #[serde(skip_serializing_if = "Option::is_none")]
907 config: Option<ComponentConfig>,
908}
909
910impl MonitorConfig {
911 #[allow(dead_code)]
912 pub fn get_type(&self) -> &str {
913 &self.type_
914 }
915
916 #[allow(dead_code)]
917 pub fn get_config(&self) -> Option<&ComponentConfig> {
918 self.config.as_ref()
919 }
920}
921
922fn default_as_true() -> bool {
923 true
924}
925
926pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
927
928fn default_keyframe_interval() -> Option<u32> {
929 Some(DEFAULT_KEYFRAME_INTERVAL)
930}
931
932#[derive(Serialize, Deserialize, Default, Debug, Clone)]
933pub struct LoggingConfig {
934 #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
936 pub enable_task_logging: bool,
937
938 #[serde(skip_serializing_if = "Option::is_none")]
940 pub slab_size_mib: Option<u64>,
941
942 #[serde(skip_serializing_if = "Option::is_none")]
944 pub section_size_mib: Option<u64>,
945
946 #[serde(
948 default = "default_keyframe_interval",
949 skip_serializing_if = "Option::is_none"
950 )]
951 pub keyframe_interval: Option<u32>,
952}
953
954#[derive(Serialize, Deserialize, Default, Debug, Clone)]
955pub struct RuntimeConfig {
956 #[serde(skip_serializing_if = "Option::is_none")]
962 pub rate_target_hz: Option<u64>,
963}
964
965#[derive(Serialize, Deserialize, Debug, Clone)]
967pub struct MissionsConfig {
968 pub id: String,
969}
970
971#[derive(Serialize, Deserialize, Debug, Clone)]
973pub struct IncludesConfig {
974 pub path: String,
975 pub params: HashMap<String, Value>,
976 pub missions: Option<Vec<String>>,
977}
978
979#[derive(Serialize, Deserialize, Default)]
981struct CuConfigRepresentation {
982 tasks: Option<Vec<Node>>,
983 bridges: Option<Vec<BridgeConfig>>,
984 cnx: Option<Vec<SerializedCnx>>,
985 monitor: Option<MonitorConfig>,
986 logging: Option<LoggingConfig>,
987 runtime: Option<RuntimeConfig>,
988 missions: Option<Vec<MissionsConfig>>,
989 includes: Option<Vec<IncludesConfig>>,
990}
991
992fn deserialize_config_representation<E>(
994 representation: &CuConfigRepresentation,
995) -> Result<CuConfig, E>
996where
997 E: From<String>,
998{
999 let mut cuconfig = CuConfig::default();
1000 let bridge_lookup = build_bridge_lookup(representation.bridges.as_ref());
1001
1002 if let Some(mission_configs) = &representation.missions {
1003 let mut missions = Missions(HashMap::new());
1005
1006 for mission_config in mission_configs {
1007 let mission_id = mission_config.id.as_str();
1008 let graph = missions
1009 .add_mission(mission_id)
1010 .map_err(|e| E::from(e.to_string()))?;
1011
1012 if let Some(tasks) = &representation.tasks {
1013 for task in tasks {
1014 if let Some(task_missions) = &task.missions {
1015 if task_missions.contains(&mission_id.to_owned()) {
1017 graph
1018 .add_node(task.clone())
1019 .map_err(|e| E::from(e.to_string()))?;
1020 }
1021 } else {
1022 graph
1024 .add_node(task.clone())
1025 .map_err(|e| E::from(e.to_string()))?;
1026 }
1027 }
1028 }
1029
1030 if let Some(bridges) = &representation.bridges {
1031 for bridge in bridges {
1032 if mission_applies(&bridge.missions, mission_id) {
1033 insert_bridge_node(graph, bridge).map_err(E::from)?;
1034 }
1035 }
1036 }
1037
1038 if let Some(cnx) = &representation.cnx {
1039 for c in cnx {
1040 if let Some(cnx_missions) = &c.missions {
1041 if cnx_missions.contains(&mission_id.to_owned()) {
1043 let (src_name, src_channel) =
1044 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1045 .map_err(E::from)?;
1046 let (dst_name, dst_channel) =
1047 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1048 .map_err(E::from)?;
1049 let src =
1050 graph
1051 .get_node_id_by_name(src_name.as_str())
1052 .ok_or_else(|| {
1053 E::from(format!("Source node not found: {}", c.src))
1054 })?;
1055 let dst =
1056 graph
1057 .get_node_id_by_name(dst_name.as_str())
1058 .ok_or_else(|| {
1059 E::from(format!("Destination node not found: {}", c.dst))
1060 })?;
1061 graph
1062 .connect_ext(
1063 src,
1064 dst,
1065 &c.msg,
1066 Some(cnx_missions.clone()),
1067 src_channel,
1068 dst_channel,
1069 )
1070 .map_err(|e| E::from(e.to_string()))?;
1071 }
1072 } else {
1073 let (src_name, src_channel) =
1075 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1076 .map_err(E::from)?;
1077 let (dst_name, dst_channel) =
1078 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1079 .map_err(E::from)?;
1080 let src = graph
1081 .get_node_id_by_name(src_name.as_str())
1082 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1083 let dst =
1084 graph
1085 .get_node_id_by_name(dst_name.as_str())
1086 .ok_or_else(|| {
1087 E::from(format!("Destination node not found: {}", c.dst))
1088 })?;
1089 graph
1090 .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1091 .map_err(|e| E::from(e.to_string()))?;
1092 }
1093 }
1094 }
1095 }
1096 cuconfig.graphs = missions;
1097 } else {
1098 let mut graph = CuGraph::default();
1100
1101 if let Some(tasks) = &representation.tasks {
1102 for task in tasks {
1103 graph
1104 .add_node(task.clone())
1105 .map_err(|e| E::from(e.to_string()))?;
1106 }
1107 }
1108
1109 if let Some(bridges) = &representation.bridges {
1110 for bridge in bridges {
1111 insert_bridge_node(&mut graph, bridge).map_err(E::from)?;
1112 }
1113 }
1114
1115 if let Some(cnx) = &representation.cnx {
1116 for c in cnx {
1117 let (src_name, src_channel) =
1118 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1119 .map_err(E::from)?;
1120 let (dst_name, dst_channel) =
1121 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1122 .map_err(E::from)?;
1123 let src = graph
1124 .get_node_id_by_name(src_name.as_str())
1125 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1126 let dst = graph
1127 .get_node_id_by_name(dst_name.as_str())
1128 .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
1129 graph
1130 .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1131 .map_err(|e| E::from(e.to_string()))?;
1132 }
1133 }
1134 cuconfig.graphs = Simple(graph);
1135 }
1136
1137 cuconfig.monitor = representation.monitor.clone();
1138 cuconfig.logging = representation.logging.clone();
1139 cuconfig.runtime = representation.runtime.clone();
1140 cuconfig.bridges = representation.bridges.clone().unwrap_or_default();
1141
1142 Ok(cuconfig)
1143}
1144
1145impl<'de> Deserialize<'de> for CuConfig {
1146 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1148 where
1149 D: Deserializer<'de>,
1150 {
1151 let representation =
1152 CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
1153
1154 match deserialize_config_representation::<String>(&representation) {
1156 Ok(config) => Ok(config),
1157 Err(e) => Err(serde::de::Error::custom(e)),
1158 }
1159 }
1160}
1161
1162impl Serialize for CuConfig {
1163 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1165 where
1166 S: Serializer,
1167 {
1168 let bridges = if self.bridges.is_empty() {
1169 None
1170 } else {
1171 Some(self.bridges.clone())
1172 };
1173 match &self.graphs {
1174 Simple(graph) => {
1175 let tasks: Vec<Node> = graph
1176 .0
1177 .node_indices()
1178 .map(|idx| graph.0[idx].clone())
1179 .filter(|node| node.get_flavor() == Flavor::Task)
1180 .collect();
1181
1182 let cnx: Vec<SerializedCnx> = graph
1183 .0
1184 .edge_indices()
1185 .map(|edge| SerializedCnx::from(&graph.0[edge]))
1186 .collect();
1187
1188 CuConfigRepresentation {
1189 tasks: Some(tasks),
1190 bridges: bridges.clone(),
1191 cnx: Some(cnx),
1192 monitor: self.monitor.clone(),
1193 logging: self.logging.clone(),
1194 runtime: self.runtime.clone(),
1195 missions: None,
1196 includes: None,
1197 }
1198 .serialize(serializer)
1199 }
1200 Missions(graphs) => {
1201 let missions = graphs
1202 .keys()
1203 .map(|id| MissionsConfig { id: id.clone() })
1204 .collect();
1205
1206 let mut tasks = Vec::new();
1208 let mut cnx = Vec::new();
1209
1210 for graph in graphs.values() {
1211 for node_idx in graph.node_indices() {
1213 let node = &graph[node_idx];
1214 if node.get_flavor() == Flavor::Task
1215 && !tasks.iter().any(|n: &Node| n.id == node.id)
1216 {
1217 tasks.push(node.clone());
1218 }
1219 }
1220
1221 for edge_idx in graph.0.edge_indices() {
1223 let edge = &graph.0[edge_idx];
1224 let serialized = SerializedCnx::from(edge);
1225 if !cnx.iter().any(|c: &SerializedCnx| {
1226 c.src == serialized.src
1227 && c.dst == serialized.dst
1228 && c.msg == serialized.msg
1229 }) {
1230 cnx.push(serialized);
1231 }
1232 }
1233 }
1234
1235 CuConfigRepresentation {
1236 tasks: Some(tasks),
1237 bridges,
1238 cnx: Some(cnx),
1239 monitor: self.monitor.clone(),
1240 logging: self.logging.clone(),
1241 runtime: self.runtime.clone(),
1242 missions: Some(missions),
1243 includes: None,
1244 }
1245 .serialize(serializer)
1246 }
1247 }
1248 }
1249}
1250
1251impl Default for CuConfig {
1252 fn default() -> Self {
1253 CuConfig {
1254 graphs: Simple(CuGraph(StableDiGraph::new())),
1255 monitor: None,
1256 logging: None,
1257 runtime: None,
1258 bridges: Vec::new(),
1259 }
1260 }
1261}
1262
1263impl CuConfig {
1266 #[allow(dead_code)]
1267 pub fn new_simple_type() -> Self {
1268 Self::default()
1269 }
1270
1271 #[allow(dead_code)]
1272 pub fn new_mission_type() -> Self {
1273 CuConfig {
1274 graphs: Missions(HashMap::new()),
1275 monitor: None,
1276 logging: None,
1277 runtime: None,
1278 bridges: Vec::new(),
1279 }
1280 }
1281
1282 fn get_options() -> Options {
1283 Options::default()
1284 .with_default_extension(Extensions::IMPLICIT_SOME)
1285 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1286 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1287 }
1288
1289 #[allow(dead_code)]
1290 pub fn serialize_ron(&self) -> String {
1291 let ron = Self::get_options();
1292 let pretty = ron::ser::PrettyConfig::default();
1293 ron.to_string_pretty(&self, pretty).unwrap()
1294 }
1295
1296 #[allow(dead_code)]
1297 pub fn deserialize_ron(ron: &str) -> Self {
1298 match Self::get_options().from_str(ron) {
1299 Ok(representation) => Self::deserialize_impl(representation).unwrap_or_else(|e| {
1300 panic!("Error deserializing configuration: {e}");
1301 }),
1302 Err(e) => panic!("Syntax Error in config: {} at position {}", e.code, e.span),
1303 }
1304 }
1305
1306 fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
1307 deserialize_config_representation(&representation)
1308 }
1309
1310 #[cfg(feature = "std")]
1312 pub fn render(
1313 &self,
1314 output: &mut dyn std::io::Write,
1315 mission_id: Option<&str>,
1316 ) -> CuResult<()> {
1317 writeln!(output, "digraph G {{").unwrap();
1318
1319 let graph = self.get_graph(mission_id)?;
1320
1321 for index in graph.node_indices() {
1322 let node = &graph[index];
1323 let config_str = match &node.config {
1324 Some(config) => {
1325 let config_str = config
1326 .0
1327 .iter()
1328 .map(|(k, v)| format!("<B>{k}</B> = {v}<BR ALIGN=\"LEFT\"/>"))
1329 .collect::<Vec<String>>()
1330 .join("\n");
1331 format!("____________<BR/><BR ALIGN=\"LEFT\"/>{config_str}")
1332 }
1333 None => String::new(),
1334 };
1335 writeln!(output, "{} [", index.index()).unwrap();
1336 writeln!(output, "shape=box,").unwrap();
1337 writeln!(output, "style=\"rounded, filled\",").unwrap();
1338 writeln!(output, "fontname=\"Noto Sans\"").unwrap();
1339
1340 let is_src = graph
1341 .get_dst_edges(index.index() as NodeId)
1342 .unwrap_or_default()
1343 .is_empty();
1344 let is_sink = graph
1345 .get_src_edges(index.index() as NodeId)
1346 .unwrap_or_default()
1347 .is_empty();
1348 if is_src {
1349 writeln!(output, "fillcolor=lightgreen,").unwrap();
1350 } else if is_sink {
1351 writeln!(output, "fillcolor=lightblue,").unwrap();
1352 } else {
1353 writeln!(output, "fillcolor=lightgrey,").unwrap();
1354 }
1355 writeln!(output, "color=grey,").unwrap();
1356
1357 writeln!(output, "labeljust=l,").unwrap();
1358 writeln!(
1359 output,
1360 "label=< <FONT COLOR=\"red\"><B>{}</B></FONT> <FONT COLOR=\"dimgray\">[{}]</FONT><BR ALIGN=\"LEFT\"/>{} >",
1361 node.id,
1362 node.get_type(),
1363 config_str
1364 )
1365 .unwrap();
1366
1367 writeln!(output, "];").unwrap();
1368 }
1369 for edge in graph.0.edge_indices() {
1370 let (src, dst) = graph.0.edge_endpoints(edge).unwrap();
1371
1372 let cnx = &graph.0[edge];
1373 let msg = encode_text(&cnx.msg);
1374 writeln!(
1375 output,
1376 "{} -> {} [label=< <B><FONT COLOR=\"gray\">{}</FONT></B> >];",
1377 src.index(),
1378 dst.index(),
1379 msg
1380 )
1381 .unwrap();
1382 }
1383 writeln!(output, "}}").unwrap();
1384 Ok(())
1385 }
1386
1387 #[allow(dead_code)]
1388 pub fn get_all_instances_configs(
1389 &self,
1390 mission_id: Option<&str>,
1391 ) -> Vec<Option<&ComponentConfig>> {
1392 let graph = self.graphs.get_graph(mission_id).unwrap();
1393 graph
1394 .get_all_nodes()
1395 .iter()
1396 .map(|(_, node)| node.get_instance_config())
1397 .collect()
1398 }
1399
1400 #[allow(dead_code)]
1401 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1402 self.graphs.get_graph(mission_id)
1403 }
1404
1405 #[allow(dead_code)]
1406 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1407 self.graphs.get_graph_mut(mission_id)
1408 }
1409
1410 #[allow(dead_code)]
1411 pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1412 self.monitor.as_ref()
1413 }
1414
1415 #[allow(dead_code)]
1416 pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
1417 self.runtime.as_ref()
1418 }
1419
1420 pub fn validate_logging_config(&self) -> CuResult<()> {
1423 if let Some(logging) = &self.logging {
1424 return logging.validate();
1425 }
1426 Ok(())
1427 }
1428}
1429
1430impl LoggingConfig {
1431 pub fn validate(&self) -> CuResult<()> {
1433 if let Some(section_size_mib) = self.section_size_mib {
1434 if let Some(slab_size_mib) = self.slab_size_mib {
1435 if section_size_mib > slab_size_mib {
1436 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.")));
1437 }
1438 }
1439 }
1440
1441 Ok(())
1442 }
1443}
1444
1445#[allow(dead_code)] fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
1447 let mut result = content.to_string();
1448
1449 for (key, value) in params {
1450 let pattern = format!("{{{{{key}}}}}");
1451 result = result.replace(&pattern, &value.to_string());
1452 }
1453
1454 result
1455}
1456
1457#[cfg(feature = "std")]
1459fn process_includes(
1460 file_path: &str,
1461 base_representation: CuConfigRepresentation,
1462 processed_files: &mut Vec<String>,
1463) -> CuResult<CuConfigRepresentation> {
1464 processed_files.push(file_path.to_string());
1466
1467 let mut result = base_representation;
1468
1469 if let Some(includes) = result.includes.take() {
1470 for include in includes {
1471 let include_path = if include.path.starts_with('/') {
1472 include.path.clone()
1473 } else {
1474 let current_dir = std::path::Path::new(file_path)
1475 .parent()
1476 .unwrap_or_else(|| std::path::Path::new(""))
1477 .to_string_lossy()
1478 .to_string();
1479
1480 format!("{}/{}", current_dir, include.path)
1481 };
1482
1483 let include_content = read_to_string(&include_path).map_err(|e| {
1484 CuError::from(format!("Failed to read include file: {include_path}"))
1485 .add_cause(e.to_string().as_str())
1486 })?;
1487
1488 let processed_content = substitute_parameters(&include_content, &include.params);
1489
1490 let mut included_representation: CuConfigRepresentation = match Options::default()
1491 .with_default_extension(Extensions::IMPLICIT_SOME)
1492 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1493 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1494 .from_str(&processed_content)
1495 {
1496 Ok(rep) => rep,
1497 Err(e) => {
1498 return Err(CuError::from(format!(
1499 "Failed to parse include file: {} - Error: {} at position {}",
1500 include_path, e.code, e.span
1501 )));
1502 }
1503 };
1504
1505 included_representation =
1506 process_includes(&include_path, included_representation, processed_files)?;
1507
1508 if let Some(included_tasks) = included_representation.tasks {
1509 if result.tasks.is_none() {
1510 result.tasks = Some(included_tasks);
1511 } else {
1512 let mut tasks = result.tasks.take().unwrap();
1513 for included_task in included_tasks {
1514 if !tasks.iter().any(|t| t.id == included_task.id) {
1515 tasks.push(included_task);
1516 }
1517 }
1518 result.tasks = Some(tasks);
1519 }
1520 }
1521
1522 if let Some(included_bridges) = included_representation.bridges {
1523 if result.bridges.is_none() {
1524 result.bridges = Some(included_bridges);
1525 } else {
1526 let mut bridges = result.bridges.take().unwrap();
1527 for included_bridge in included_bridges {
1528 if !bridges.iter().any(|b| b.id == included_bridge.id) {
1529 bridges.push(included_bridge);
1530 }
1531 }
1532 result.bridges = Some(bridges);
1533 }
1534 }
1535
1536 if let Some(included_cnx) = included_representation.cnx {
1537 if result.cnx.is_none() {
1538 result.cnx = Some(included_cnx);
1539 } else {
1540 let mut cnx = result.cnx.take().unwrap();
1541 for included_c in included_cnx {
1542 if !cnx
1543 .iter()
1544 .any(|c| c.src == included_c.src && c.dst == included_c.dst)
1545 {
1546 cnx.push(included_c);
1547 }
1548 }
1549 result.cnx = Some(cnx);
1550 }
1551 }
1552
1553 if result.monitor.is_none() {
1554 result.monitor = included_representation.monitor;
1555 }
1556
1557 if result.logging.is_none() {
1558 result.logging = included_representation.logging;
1559 }
1560
1561 if result.runtime.is_none() {
1562 result.runtime = included_representation.runtime;
1563 }
1564
1565 if let Some(included_missions) = included_representation.missions {
1566 if result.missions.is_none() {
1567 result.missions = Some(included_missions);
1568 } else {
1569 let mut missions = result.missions.take().unwrap();
1570 for included_mission in included_missions {
1571 if !missions.iter().any(|m| m.id == included_mission.id) {
1572 missions.push(included_mission);
1573 }
1574 }
1575 result.missions = Some(missions);
1576 }
1577 }
1578 }
1579 }
1580
1581 Ok(result)
1582}
1583
1584#[cfg(feature = "std")]
1586pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
1587 let config_content = read_to_string(config_filename).map_err(|e| {
1588 CuError::from(format!(
1589 "Failed to read configuration file: {:?}",
1590 &config_filename
1591 ))
1592 .add_cause(e.to_string().as_str())
1593 })?;
1594 read_configuration_str(config_content, Some(config_filename))
1595}
1596
1597fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
1601 Options::default()
1602 .with_default_extension(Extensions::IMPLICIT_SOME)
1603 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1604 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1605 .from_str(content)
1606 .map_err(|e| {
1607 CuError::from(format!(
1608 "Failed to parse configuration: Error: {} at position {}",
1609 e.code, e.span
1610 ))
1611 })
1612}
1613
1614fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
1617 let cuconfig = CuConfig::deserialize_impl(representation)
1618 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
1619
1620 cuconfig.validate_logging_config()?;
1621
1622 Ok(cuconfig)
1623}
1624
1625#[allow(unused_variables)]
1626pub fn read_configuration_str(
1627 config_content: String,
1628 file_path: Option<&str>,
1629) -> CuResult<CuConfig> {
1630 let representation = parse_config_string(&config_content)?;
1632
1633 #[cfg(feature = "std")]
1636 let representation = if let Some(path) = file_path {
1637 process_includes(path, representation, &mut Vec::new())?
1638 } else {
1639 representation
1640 };
1641
1642 config_representation_to_config(representation)
1644}
1645
1646#[cfg(test)]
1648mod tests {
1649 use super::*;
1650 #[cfg(not(feature = "std"))]
1651 use alloc::vec;
1652
1653 #[test]
1654 fn test_plain_serialize() {
1655 let mut config = CuConfig::default();
1656 let graph = config.get_graph_mut(None).unwrap();
1657 let n1 = graph
1658 .add_node(Node::new("test1", "package::Plugin1"))
1659 .unwrap();
1660 let n2 = graph
1661 .add_node(Node::new("test2", "package::Plugin2"))
1662 .unwrap();
1663 graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
1664 let serialized = config.serialize_ron();
1665 let deserialized = CuConfig::deserialize_ron(&serialized);
1666 let graph = config.graphs.get_graph(None).unwrap();
1667 let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
1668 assert_eq!(graph.node_count(), deserialized_graph.node_count());
1669 assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
1670 }
1671
1672 #[test]
1673 fn test_serialize_with_params() {
1674 let mut config = CuConfig::default();
1675 let graph = config.get_graph_mut(None).unwrap();
1676 let mut camera = Node::new("copper-camera", "camerapkg::Camera");
1677 camera.set_param::<Value>("resolution-height", 1080.into());
1678 graph.add_node(camera).unwrap();
1679 let serialized = config.serialize_ron();
1680 let config = CuConfig::deserialize_ron(&serialized);
1681 let deserialized = config.get_graph(None).unwrap();
1682 assert_eq!(
1683 deserialized
1684 .get_node(0)
1685 .unwrap()
1686 .get_param::<i32>("resolution-height")
1687 .unwrap(),
1688 1080
1689 );
1690 }
1691
1692 #[test]
1693 #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:9-1:10")]
1694 fn test_deserialization_error() {
1695 let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1697 CuConfig::deserialize_ron(txt);
1698 }
1699 #[test]
1700 fn test_missions() {
1701 let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
1702 let config = CuConfig::deserialize_ron(txt);
1703 let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
1704 assert!(graph.node_count() == 0);
1705 let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
1706 assert!(graph.node_count() == 0);
1707 }
1708
1709 #[test]
1710 fn test_monitor() {
1711 let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1712 let config = CuConfig::deserialize_ron(txt);
1713 assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
1714
1715 let txt =
1716 r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
1717 let config = CuConfig::deserialize_ron(txt);
1718 assert_eq!(
1719 config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
1720 4u8.into()
1721 );
1722 }
1723
1724 #[test]
1725 fn test_logging_parameters() {
1726 let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
1728
1729 let config = CuConfig::deserialize_ron(txt);
1730 assert!(config.logging.is_some());
1731 let logging_config = config.logging.unwrap();
1732 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1733 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1734 assert!(!logging_config.enable_task_logging);
1735
1736 let txt =
1738 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
1739 let config = CuConfig::deserialize_ron(txt);
1740 assert!(config.logging.is_some());
1741 let logging_config = config.logging.unwrap();
1742 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1743 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1744 assert!(logging_config.enable_task_logging);
1745 }
1746
1747 #[test]
1748 fn test_bridge_parsing() {
1749 let txt = r#"
1750 (
1751 tasks: [
1752 (id: "dst", type: "tasks::Destination"),
1753 (id: "src", type: "tasks::Source"),
1754 ],
1755 bridges: [
1756 (
1757 id: "radio",
1758 type: "tasks::SerialBridge",
1759 config: { "path": "/dev/ttyACM0", "baud": 921600 },
1760 channels: [
1761 Rx ( id: "status", route: "sys/status" ),
1762 Tx ( id: "motor", route: "motor/cmd" ),
1763 ],
1764 ),
1765 ],
1766 cnx: [
1767 (src: "radio/status", dst: "dst", msg: "mymsgs::Status"),
1768 (src: "src", dst: "radio/motor", msg: "mymsgs::MotorCmd"),
1769 ],
1770 )
1771 "#;
1772
1773 let config = CuConfig::deserialize_ron(txt);
1774 assert_eq!(config.bridges.len(), 1);
1775 let bridge = &config.bridges[0];
1776 assert_eq!(bridge.id, "radio");
1777 assert_eq!(bridge.channels.len(), 2);
1778 match &bridge.channels[0] {
1779 BridgeChannel::Rx { id, route, .. } => {
1780 assert_eq!(id, "status");
1781 assert_eq!(route, "sys/status");
1782 }
1783 _ => panic!("expected Rx channel"),
1784 }
1785 match &bridge.channels[1] {
1786 BridgeChannel::Tx { id, route, .. } => {
1787 assert_eq!(id, "motor");
1788 assert_eq!(route, "motor/cmd");
1789 }
1790 _ => panic!("expected Tx channel"),
1791 }
1792 let graph = config.graphs.get_graph(None).unwrap();
1793 let bridge_id = graph
1794 .get_node_id_by_name("radio")
1795 .expect("bridge node missing");
1796 let bridge_node = graph.get_node(bridge_id).unwrap();
1797 assert_eq!(bridge_node.get_flavor(), Flavor::Bridge);
1798
1799 let mut edges = Vec::new();
1801 for edge_idx in graph.0.edge_indices() {
1802 edges.push(graph.0[edge_idx].clone());
1803 }
1804 assert_eq!(edges.len(), 2);
1805 let status_edge = edges
1806 .iter()
1807 .find(|e| e.dst == "dst")
1808 .expect("status edge missing");
1809 assert_eq!(status_edge.src_channel.as_deref(), Some("status"));
1810 assert!(status_edge.dst_channel.is_none());
1811 let motor_edge = edges
1812 .iter()
1813 .find(|e| e.dst_channel.is_some())
1814 .expect("motor edge missing");
1815 assert_eq!(motor_edge.dst_channel.as_deref(), Some("motor"));
1816 }
1817
1818 #[test]
1819 fn test_bridge_roundtrip() {
1820 let mut config = CuConfig::default();
1821 let mut bridge_config = ComponentConfig::default();
1822 bridge_config.set("port", "/dev/ttyACM0".to_string());
1823 config.bridges.push(BridgeConfig {
1824 id: "radio".to_string(),
1825 type_: "tasks::SerialBridge".to_string(),
1826 config: Some(bridge_config),
1827 missions: None,
1828 channels: vec![
1829 BridgeChannel::Rx {
1830 id: "status".to_string(),
1831 route: "sys/status".to_string(),
1832 config: None,
1833 },
1834 BridgeChannel::Tx {
1835 id: "motor".to_string(),
1836 route: "motor/cmd".to_string(),
1837 config: None,
1838 },
1839 ],
1840 });
1841
1842 let serialized = config.serialize_ron();
1843 assert!(
1844 serialized.contains("bridges"),
1845 "bridges section missing from serialized config"
1846 );
1847 let deserialized = CuConfig::deserialize_ron(&serialized);
1848 assert_eq!(deserialized.bridges.len(), 1);
1849 let bridge = &deserialized.bridges[0];
1850 assert_eq!(bridge.channels.len(), 2);
1851 assert!(matches!(bridge.channels[0], BridgeChannel::Rx { .. }));
1852 assert!(matches!(bridge.channels[1], BridgeChannel::Tx { .. }));
1853 }
1854
1855 #[test]
1856 fn test_bridge_channel_config() {
1857 let txt = r#"
1858 (
1859 tasks: [],
1860 bridges: [
1861 (
1862 id: "radio",
1863 type: "tasks::SerialBridge",
1864 channels: [
1865 Rx ( id: "status", route: "sys/status", config: { "filter": "fast" } ),
1866 Tx ( id: "imu", route: "telemetry/imu", config: { "rate": 100 } ),
1867 ],
1868 ),
1869 ],
1870 cnx: [],
1871 )
1872 "#;
1873
1874 let config = CuConfig::deserialize_ron(txt);
1875 let bridge = &config.bridges[0];
1876 match &bridge.channels[0] {
1877 BridgeChannel::Rx {
1878 config: Some(cfg), ..
1879 } => {
1880 let val: String = cfg.get("filter").expect("filter missing");
1881 assert_eq!(val, "fast");
1882 }
1883 _ => panic!("expected Rx channel with config"),
1884 }
1885 match &bridge.channels[1] {
1886 BridgeChannel::Tx {
1887 config: Some(cfg), ..
1888 } => {
1889 let rate: i32 = cfg.get("rate").expect("rate missing");
1890 assert_eq!(rate, 100);
1891 }
1892 _ => panic!("expected Tx channel with config"),
1893 }
1894 }
1895
1896 #[test]
1897 #[should_panic(expected = "channel 'motor' is Tx and cannot act as a source")]
1898 fn test_bridge_tx_cannot_be_source() {
1899 let txt = r#"
1900 (
1901 tasks: [
1902 (id: "dst", type: "tasks::Destination"),
1903 ],
1904 bridges: [
1905 (
1906 id: "radio",
1907 type: "tasks::SerialBridge",
1908 channels: [
1909 Tx ( id: "motor", route: "motor/cmd" ),
1910 ],
1911 ),
1912 ],
1913 cnx: [
1914 (src: "radio/motor", dst: "dst", msg: "mymsgs::MotorCmd"),
1915 ],
1916 )
1917 "#;
1918
1919 CuConfig::deserialize_ron(txt);
1920 }
1921
1922 #[test]
1923 #[should_panic(expected = "channel 'status' is Rx and cannot act as a destination")]
1924 fn test_bridge_rx_cannot_be_destination() {
1925 let txt = r#"
1926 (
1927 tasks: [
1928 (id: "src", type: "tasks::Source"),
1929 ],
1930 bridges: [
1931 (
1932 id: "radio",
1933 type: "tasks::SerialBridge",
1934 channels: [
1935 Rx ( id: "status", route: "sys/status" ),
1936 ],
1937 ),
1938 ],
1939 cnx: [
1940 (src: "src", dst: "radio/status", msg: "mymsgs::Status"),
1941 ],
1942 )
1943 "#;
1944
1945 CuConfig::deserialize_ron(txt);
1946 }
1947
1948 #[test]
1949 fn test_validate_logging_config() {
1950 let txt =
1952 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
1953 let config = CuConfig::deserialize_ron(txt);
1954 assert!(config.validate_logging_config().is_ok());
1955
1956 let txt =
1958 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
1959 let config = CuConfig::deserialize_ron(txt);
1960 assert!(config.validate_logging_config().is_err());
1961 }
1962
1963 #[test]
1965 fn test_deserialization_edge_id_assignment() {
1966 let txt = r#"(
1969 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1970 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
1971 )"#;
1972 let config = CuConfig::deserialize_ron(txt);
1973 let graph = config.graphs.get_graph(None).unwrap();
1974 assert!(config.validate_logging_config().is_ok());
1975
1976 let src1_id = 0;
1978 assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
1979 let src2_id = 1;
1980 assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
1981
1982 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
1985 assert_eq!(src1_edge_id, 1);
1986 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
1987 assert_eq!(src2_edge_id, 0);
1988 }
1989
1990 #[test]
1991 fn test_simple_missions() {
1992 let txt = r#"(
1994 missions: [ (id: "m1"),
1995 (id: "m2"),
1996 ],
1997 tasks: [(id: "src1", type: "a", missions: ["m1"]),
1998 (id: "src2", type: "b", missions: ["m2"]),
1999 (id: "sink", type: "c")],
2000
2001 cnx: [
2002 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2003 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2004 ],
2005 )
2006 "#;
2007
2008 let config = CuConfig::deserialize_ron(txt);
2009 let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
2010 assert_eq!(m1_graph.edge_count(), 1);
2011 assert_eq!(m1_graph.node_count(), 2);
2012 let index = 0;
2013 let cnx = m1_graph.get_edge_weight(index).unwrap();
2014
2015 assert_eq!(cnx.src, "src1");
2016 assert_eq!(cnx.dst, "sink");
2017 assert_eq!(cnx.msg, "u32");
2018 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2019
2020 let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
2021 assert_eq!(m2_graph.edge_count(), 1);
2022 assert_eq!(m2_graph.node_count(), 2);
2023 let index = 0;
2024 let cnx = m2_graph.get_edge_weight(index).unwrap();
2025 assert_eq!(cnx.src, "src2");
2026 assert_eq!(cnx.dst, "sink");
2027 assert_eq!(cnx.msg, "u32");
2028 assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
2029 }
2030 #[test]
2031 fn test_mission_serde() {
2032 let txt = r#"(
2034 missions: [ (id: "m1"),
2035 (id: "m2"),
2036 ],
2037 tasks: [(id: "src1", type: "a", missions: ["m1"]),
2038 (id: "src2", type: "b", missions: ["m2"]),
2039 (id: "sink", type: "c")],
2040
2041 cnx: [
2042 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2043 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2044 ],
2045 )
2046 "#;
2047
2048 let config = CuConfig::deserialize_ron(txt);
2049 let serialized = config.serialize_ron();
2050 let deserialized = CuConfig::deserialize_ron(&serialized);
2051 let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
2052 assert_eq!(m1_graph.edge_count(), 1);
2053 assert_eq!(m1_graph.node_count(), 2);
2054 let index = 0;
2055 let cnx = m1_graph.get_edge_weight(index).unwrap();
2056 assert_eq!(cnx.src, "src1");
2057 assert_eq!(cnx.dst, "sink");
2058 assert_eq!(cnx.msg, "u32");
2059 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2060 }
2061
2062 #[test]
2063 fn test_keyframe_interval() {
2064 let txt = r#"(
2067 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2068 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2069 logging: ( keyframe_interval: 314 )
2070 )"#;
2071 let config = CuConfig::deserialize_ron(txt);
2072 let logging_config = config.logging.unwrap();
2073 assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
2074 }
2075
2076 #[test]
2077 fn test_default_keyframe_interval() {
2078 let txt = r#"(
2081 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2082 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2083 logging: ( slab_size_mib: 200, section_size_mib: 1024, )
2084 )"#;
2085 let config = CuConfig::deserialize_ron(txt);
2086 let logging_config = config.logging.unwrap();
2087 assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
2088 }
2089}