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 BridgeChannelConfigRepresentation {
359 Rx {
361 id: String,
362 #[serde(skip_serializing_if = "Option::is_none")]
364 route: Option<String>,
365 #[serde(skip_serializing_if = "Option::is_none")]
367 config: Option<ComponentConfig>,
368 },
369 Tx {
371 id: String,
372 #[serde(skip_serializing_if = "Option::is_none")]
374 route: Option<String>,
375 #[serde(skip_serializing_if = "Option::is_none")]
377 config: Option<ComponentConfig>,
378 },
379}
380
381impl BridgeChannelConfigRepresentation {
382 #[allow(dead_code)]
384 pub fn id(&self) -> &str {
385 match self {
386 BridgeChannelConfigRepresentation::Rx { id, .. }
387 | BridgeChannelConfigRepresentation::Tx { id, .. } => id,
388 }
389 }
390
391 #[allow(dead_code)]
393 pub fn route(&self) -> Option<&str> {
394 match self {
395 BridgeChannelConfigRepresentation::Rx { route, .. }
396 | BridgeChannelConfigRepresentation::Tx { route, .. } => route.as_deref(),
397 }
398 }
399}
400
401enum EndpointRole {
402 Source,
403 Destination,
404}
405
406fn validate_bridge_channel(
407 bridge: &BridgeConfig,
408 channel_id: &str,
409 role: EndpointRole,
410) -> Result<(), String> {
411 let channel = bridge
412 .channels
413 .iter()
414 .find(|ch| ch.id() == channel_id)
415 .ok_or_else(|| {
416 format!(
417 "Bridge '{}' does not declare a channel named '{}'",
418 bridge.id, channel_id
419 )
420 })?;
421
422 match (role, channel) {
423 (EndpointRole::Source, BridgeChannelConfigRepresentation::Rx { .. }) => Ok(()),
424 (EndpointRole::Destination, BridgeChannelConfigRepresentation::Tx { .. }) => Ok(()),
425 (EndpointRole::Source, BridgeChannelConfigRepresentation::Tx { .. }) => Err(format!(
426 "Bridge '{}' channel '{}' is Tx and cannot act as a source",
427 bridge.id, channel_id
428 )),
429 (EndpointRole::Destination, BridgeChannelConfigRepresentation::Rx { .. }) => Err(format!(
430 "Bridge '{}' channel '{}' is Rx and cannot act as a destination",
431 bridge.id, channel_id
432 )),
433 }
434}
435
436#[derive(Serialize, Deserialize, Debug, Clone)]
438pub struct BridgeConfig {
439 pub id: String,
440 #[serde(rename = "type")]
441 pub type_: String,
442 #[serde(skip_serializing_if = "Option::is_none")]
443 pub config: Option<ComponentConfig>,
444 #[serde(skip_serializing_if = "Option::is_none")]
445 pub missions: Option<Vec<String>>,
446 pub channels: Vec<BridgeChannelConfigRepresentation>,
448}
449
450impl BridgeConfig {
451 fn to_node(&self) -> Node {
452 let mut node = Node::new_with_flavor(&self.id, &self.type_, Flavor::Bridge);
453 node.config = self.config.clone();
454 node.missions = self.missions.clone();
455 node
456 }
457}
458
459fn insert_bridge_node(graph: &mut CuGraph, bridge: &BridgeConfig) -> Result<(), String> {
460 if graph.get_node_id_by_name(bridge.id.as_str()).is_some() {
461 return Err(format!(
462 "Bridge '{}' reuses an existing node id. Bridge ids must be unique.",
463 bridge.id
464 ));
465 }
466 graph
467 .add_node(bridge.to_node())
468 .map(|_| ())
469 .map_err(|e| e.to_string())
470}
471
472#[derive(Serialize, Deserialize, Debug, Clone)]
474struct SerializedCnx {
475 src: String,
476 dst: String,
477 msg: String,
478 missions: Option<Vec<String>>,
479}
480
481#[derive(Debug, Clone)]
483pub struct Cnx {
484 pub src: String,
486 pub dst: String,
488 pub msg: String,
490 pub missions: Option<Vec<String>>,
492 pub src_channel: Option<String>,
494 pub dst_channel: Option<String>,
496}
497
498impl From<&Cnx> for SerializedCnx {
499 fn from(cnx: &Cnx) -> Self {
500 SerializedCnx {
501 src: format_endpoint(&cnx.src, cnx.src_channel.as_deref()),
502 dst: format_endpoint(&cnx.dst, cnx.dst_channel.as_deref()),
503 msg: cnx.msg.clone(),
504 missions: cnx.missions.clone(),
505 }
506 }
507}
508
509fn format_endpoint(node: &str, channel: Option<&str>) -> String {
510 match channel {
511 Some(ch) => format!("{node}/{ch}"),
512 None => node.to_string(),
513 }
514}
515
516fn parse_endpoint(
517 endpoint: &str,
518 role: EndpointRole,
519 bridges: &HashMap<&str, &BridgeConfig>,
520) -> Result<(String, Option<String>), String> {
521 if let Some((node, channel)) = endpoint.split_once('/') {
522 if let Some(bridge) = bridges.get(node) {
523 validate_bridge_channel(bridge, channel, role)?;
524 return Ok((node.to_string(), Some(channel.to_string())));
525 } else {
526 return Err(format!(
527 "Endpoint '{endpoint}' references an unknown bridge '{node}'"
528 ));
529 }
530 }
531
532 if let Some(bridge) = bridges.get(endpoint) {
533 return Err(format!(
534 "Bridge '{}' connections must reference a channel using '{}/<channel>'",
535 bridge.id, bridge.id
536 ));
537 }
538
539 Ok((endpoint.to_string(), None))
540}
541
542fn build_bridge_lookup(bridges: Option<&Vec<BridgeConfig>>) -> HashMap<&str, &BridgeConfig> {
543 let mut map = HashMap::new();
544 if let Some(bridges) = bridges {
545 for bridge in bridges {
546 map.insert(bridge.id.as_str(), bridge);
547 }
548 }
549 map
550}
551
552fn mission_applies(missions: &Option<Vec<String>>, mission_id: &str) -> bool {
553 missions
554 .as_ref()
555 .map(|mission_list| mission_list.iter().any(|m| m == mission_id))
556 .unwrap_or(true)
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum CuDirection {
563 Outgoing,
564 Incoming,
565}
566
567impl From<CuDirection> for petgraph::Direction {
568 fn from(dir: CuDirection) -> Self {
569 match dir {
570 CuDirection::Outgoing => petgraph::Direction::Outgoing,
571 CuDirection::Incoming => petgraph::Direction::Incoming,
572 }
573 }
574}
575
576#[derive(Default, Debug, Clone)]
577pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
578
579impl CuGraph {
580 #[allow(dead_code)]
581 pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
582 self.0
583 .node_indices()
584 .map(|index| (index.index() as u32, &self.0[index]))
585 .collect()
586 }
587
588 #[allow(dead_code)]
589 pub fn get_neighbor_ids(&self, node_id: NodeId, dir: CuDirection) -> Vec<NodeId> {
590 self.0
591 .neighbors_directed(node_id.into(), dir.into())
592 .map(|petgraph_index| petgraph_index.index() as NodeId)
593 .collect()
594 }
595
596 #[allow(dead_code)]
597 pub fn incoming_neighbor_count(&self, node_id: NodeId) -> usize {
598 self.0.neighbors_directed(node_id.into(), Incoming).count()
599 }
600
601 #[allow(dead_code)]
602 pub fn outgoing_neighbor_count(&self, node_id: NodeId) -> usize {
603 self.0.neighbors_directed(node_id.into(), Outgoing).count()
604 }
605
606 pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
607 self.0.node_indices().collect()
608 }
609
610 pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
611 Ok(self.0.add_node(node).index() as NodeId)
612 }
613
614 #[allow(dead_code)]
615 pub fn connection_exists(&self, source: NodeId, target: NodeId) -> bool {
616 self.0.find_edge(source.into(), target.into()).is_some()
617 }
618
619 pub fn connect_ext(
620 &mut self,
621 source: NodeId,
622 target: NodeId,
623 msg_type: &str,
624 missions: Option<Vec<String>>,
625 src_channel: Option<String>,
626 dst_channel: Option<String>,
627 ) -> CuResult<()> {
628 let (src_id, dst_id) = (
629 self.0
630 .node_weight(source.into())
631 .ok_or("Source node not found")?
632 .id
633 .clone(),
634 self.0
635 .node_weight(target.into())
636 .ok_or("Target node not found")?
637 .id
638 .clone(),
639 );
640
641 let _ = self.0.add_edge(
642 petgraph::stable_graph::NodeIndex::from(source),
643 petgraph::stable_graph::NodeIndex::from(target),
644 Cnx {
645 src: src_id,
646 dst: dst_id,
647 msg: msg_type.to_string(),
648 missions,
649 src_channel,
650 dst_channel,
651 },
652 );
653 Ok(())
654 }
655 #[allow(dead_code)]
659 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
660 self.0.node_weight(node_id.into())
661 }
662
663 #[allow(dead_code)]
664 pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
665 self.0.node_weight(index.into())
666 }
667
668 #[allow(dead_code)]
669 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
670 self.0.node_weight_mut(node_id.into())
671 }
672
673 pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
674 self.0
675 .node_indices()
676 .into_iter()
677 .find(|idx| self.0[*idx].get_id() == name)
678 .map(|i| i.index() as NodeId)
679 }
680
681 #[allow(dead_code)]
682 pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
683 self.0.edge_weight(EdgeIndex::new(index)).cloned()
684 }
685
686 #[allow(dead_code)]
687 pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
688 self.0.node_indices().find_map(|node_index| {
689 if let Some(node) = self.0.node_weight(node_index) {
690 if node.id != node_id {
691 return None;
692 }
693 let edges: Vec<_> = self
694 .0
695 .edges_directed(node_index, Outgoing)
696 .map(|edge| edge.id().index())
697 .collect();
698 if edges.is_empty() {
699 return None;
700 }
701 let cnx = self
702 .0
703 .edge_weight(EdgeIndex::new(edges[0]))
704 .expect("Found an cnx id but could not retrieve it back");
705 return Some(cnx.msg.clone());
706 }
707 None
708 })
709 }
710
711 #[allow(dead_code)]
712 pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
713 self.0.node_indices().find_map(|node_index| {
714 if let Some(node) = self.0.node_weight(node_index) {
715 if node.id != node_id {
716 return None;
717 }
718 let edges: Vec<_> = self
719 .0
720 .edges_directed(node_index, Incoming)
721 .map(|edge| edge.id().index())
722 .collect();
723 if edges.is_empty() {
724 return None;
725 }
726 let cnx = self
727 .0
728 .edge_weight(EdgeIndex::new(edges[0]))
729 .expect("Found an cnx id but could not retrieve it back");
730 return Some(cnx.msg.clone());
731 }
732 None
733 })
734 }
735
736 #[allow(dead_code)]
737 pub fn get_connection_msg_type(&self, source: NodeId, target: NodeId) -> Option<&str> {
738 self.0
739 .find_edge(source.into(), target.into())
740 .map(|edge_index| self.0[edge_index].msg.as_str())
741 }
742
743 fn get_edges_by_direction(
745 &self,
746 node_id: NodeId,
747 direction: petgraph::Direction,
748 ) -> CuResult<Vec<usize>> {
749 Ok(self
750 .0
751 .edges_directed(node_id.into(), direction)
752 .map(|edge| edge.id().index())
753 .collect())
754 }
755
756 pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
757 self.get_edges_by_direction(node_id, Outgoing)
758 }
759
760 pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
762 self.get_edges_by_direction(node_id, Incoming)
763 }
764
765 #[allow(dead_code)]
766 pub fn node_count(&self) -> usize {
767 self.0.node_count()
768 }
769
770 #[allow(dead_code)]
771 pub fn edge_count(&self) -> usize {
772 self.0.edge_count()
773 }
774
775 #[allow(dead_code)]
778 pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
779 self.connect_ext(source, target, msg_type, None, None, None)
780 }
781}
782
783impl core::ops::Index<NodeIndex> for CuGraph {
784 type Output = Node;
785
786 fn index(&self, index: NodeIndex) -> &Self::Output {
787 &self.0[index]
788 }
789}
790
791#[derive(Debug, Clone)]
792pub enum ConfigGraphs {
793 Simple(CuGraph),
794 Missions(HashMap<String, CuGraph>),
795}
796
797impl ConfigGraphs {
798 #[allow(dead_code)]
801 pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
802 match self {
803 Simple(graph) => {
804 let mut map = HashMap::new();
805 map.insert("default".to_string(), graph.clone());
806 map
807 }
808 Missions(graphs) => graphs.clone(),
809 }
810 }
811
812 #[allow(dead_code)]
813 pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
814 match self {
815 Simple(graph) => Ok(graph),
816 Missions(graphs) => {
817 if graphs.len() == 1 {
818 Ok(graphs.values().next().unwrap())
819 } else {
820 Err("Cannot get default mission graph from mission config".into())
821 }
822 }
823 }
824 }
825
826 #[allow(dead_code)]
827 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
828 match self {
829 Simple(graph) => {
830 if mission_id.is_none() || mission_id.unwrap() == "default" {
831 Ok(graph)
832 } else {
833 Err("Cannot get mission graph from simple config".into())
834 }
835 }
836 Missions(graphs) => {
837 if let Some(id) = mission_id {
838 graphs
839 .get(id)
840 .ok_or_else(|| format!("Mission {id} not found").into())
841 } else {
842 Err("Mission ID required for mission configs".into())
843 }
844 }
845 }
846 }
847
848 #[allow(dead_code)]
849 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
850 match self {
851 Simple(ref mut graph) => {
852 if mission_id.is_none() {
853 Ok(graph)
854 } else {
855 Err("Cannot get mission graph from simple config".into())
856 }
857 }
858 Missions(ref mut graphs) => {
859 if let Some(id) = mission_id {
860 graphs
861 .get_mut(id)
862 .ok_or_else(|| format!("Mission {id} not found").into())
863 } else {
864 Err("Mission ID required for mission configs".into())
865 }
866 }
867 }
868 }
869
870 pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
871 match self {
872 Simple(_) => Err("Cannot add mission to simple config".into()),
873 Missions(graphs) => {
874 if graphs.contains_key(mission_id) {
875 Err(format!("Mission {mission_id} already exists").into())
876 } else {
877 let graph = CuGraph::default();
878 graphs.insert(mission_id.to_string(), graph);
879 Ok(graphs.get_mut(mission_id).unwrap())
881 }
882 }
883 }
884 }
885}
886
887#[derive(Debug, Clone)]
893pub struct CuConfig {
894 pub monitor: Option<MonitorConfig>,
896 pub logging: Option<LoggingConfig>,
898 pub runtime: Option<RuntimeConfig>,
900 pub bridges: Vec<BridgeConfig>,
902 pub graphs: ConfigGraphs,
904}
905
906#[derive(Serialize, Deserialize, Default, Debug, Clone)]
907pub struct MonitorConfig {
908 #[serde(rename = "type")]
909 type_: String,
910 #[serde(skip_serializing_if = "Option::is_none")]
911 config: Option<ComponentConfig>,
912}
913
914impl MonitorConfig {
915 #[allow(dead_code)]
916 pub fn get_type(&self) -> &str {
917 &self.type_
918 }
919
920 #[allow(dead_code)]
921 pub fn get_config(&self) -> Option<&ComponentConfig> {
922 self.config.as_ref()
923 }
924}
925
926fn default_as_true() -> bool {
927 true
928}
929
930pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
931
932fn default_keyframe_interval() -> Option<u32> {
933 Some(DEFAULT_KEYFRAME_INTERVAL)
934}
935
936#[derive(Serialize, Deserialize, Default, Debug, Clone)]
937pub struct LoggingConfig {
938 #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
940 pub enable_task_logging: bool,
941
942 #[serde(skip_serializing_if = "Option::is_none")]
944 pub slab_size_mib: Option<u64>,
945
946 #[serde(skip_serializing_if = "Option::is_none")]
948 pub section_size_mib: Option<u64>,
949
950 #[serde(
952 default = "default_keyframe_interval",
953 skip_serializing_if = "Option::is_none"
954 )]
955 pub keyframe_interval: Option<u32>,
956}
957
958#[derive(Serialize, Deserialize, Default, Debug, Clone)]
959pub struct RuntimeConfig {
960 #[serde(skip_serializing_if = "Option::is_none")]
966 pub rate_target_hz: Option<u64>,
967}
968
969#[derive(Serialize, Deserialize, Debug, Clone)]
971pub struct MissionsConfig {
972 pub id: String,
973}
974
975#[derive(Serialize, Deserialize, Debug, Clone)]
977pub struct IncludesConfig {
978 pub path: String,
979 pub params: HashMap<String, Value>,
980 pub missions: Option<Vec<String>>,
981}
982
983#[derive(Serialize, Deserialize, Default)]
985struct CuConfigRepresentation {
986 tasks: Option<Vec<Node>>,
987 bridges: Option<Vec<BridgeConfig>>,
988 cnx: Option<Vec<SerializedCnx>>,
989 monitor: Option<MonitorConfig>,
990 logging: Option<LoggingConfig>,
991 runtime: Option<RuntimeConfig>,
992 missions: Option<Vec<MissionsConfig>>,
993 includes: Option<Vec<IncludesConfig>>,
994}
995
996fn deserialize_config_representation<E>(
998 representation: &CuConfigRepresentation,
999) -> Result<CuConfig, E>
1000where
1001 E: From<String>,
1002{
1003 let mut cuconfig = CuConfig::default();
1004 let bridge_lookup = build_bridge_lookup(representation.bridges.as_ref());
1005
1006 if let Some(mission_configs) = &representation.missions {
1007 let mut missions = Missions(HashMap::new());
1009
1010 for mission_config in mission_configs {
1011 let mission_id = mission_config.id.as_str();
1012 let graph = missions
1013 .add_mission(mission_id)
1014 .map_err(|e| E::from(e.to_string()))?;
1015
1016 if let Some(tasks) = &representation.tasks {
1017 for task in tasks {
1018 if let Some(task_missions) = &task.missions {
1019 if task_missions.contains(&mission_id.to_owned()) {
1021 graph
1022 .add_node(task.clone())
1023 .map_err(|e| E::from(e.to_string()))?;
1024 }
1025 } else {
1026 graph
1028 .add_node(task.clone())
1029 .map_err(|e| E::from(e.to_string()))?;
1030 }
1031 }
1032 }
1033
1034 if let Some(bridges) = &representation.bridges {
1035 for bridge in bridges {
1036 if mission_applies(&bridge.missions, mission_id) {
1037 insert_bridge_node(graph, bridge).map_err(E::from)?;
1038 }
1039 }
1040 }
1041
1042 if let Some(cnx) = &representation.cnx {
1043 for c in cnx {
1044 if let Some(cnx_missions) = &c.missions {
1045 if cnx_missions.contains(&mission_id.to_owned()) {
1047 let (src_name, src_channel) =
1048 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1049 .map_err(E::from)?;
1050 let (dst_name, dst_channel) =
1051 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1052 .map_err(E::from)?;
1053 let src =
1054 graph
1055 .get_node_id_by_name(src_name.as_str())
1056 .ok_or_else(|| {
1057 E::from(format!("Source node not found: {}", c.src))
1058 })?;
1059 let dst =
1060 graph
1061 .get_node_id_by_name(dst_name.as_str())
1062 .ok_or_else(|| {
1063 E::from(format!("Destination node not found: {}", c.dst))
1064 })?;
1065 graph
1066 .connect_ext(
1067 src,
1068 dst,
1069 &c.msg,
1070 Some(cnx_missions.clone()),
1071 src_channel,
1072 dst_channel,
1073 )
1074 .map_err(|e| E::from(e.to_string()))?;
1075 }
1076 } else {
1077 let (src_name, src_channel) =
1079 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1080 .map_err(E::from)?;
1081 let (dst_name, dst_channel) =
1082 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1083 .map_err(E::from)?;
1084 let src = graph
1085 .get_node_id_by_name(src_name.as_str())
1086 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1087 let dst =
1088 graph
1089 .get_node_id_by_name(dst_name.as_str())
1090 .ok_or_else(|| {
1091 E::from(format!("Destination node not found: {}", c.dst))
1092 })?;
1093 graph
1094 .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1095 .map_err(|e| E::from(e.to_string()))?;
1096 }
1097 }
1098 }
1099 }
1100 cuconfig.graphs = missions;
1101 } else {
1102 let mut graph = CuGraph::default();
1104
1105 if let Some(tasks) = &representation.tasks {
1106 for task in tasks {
1107 graph
1108 .add_node(task.clone())
1109 .map_err(|e| E::from(e.to_string()))?;
1110 }
1111 }
1112
1113 if let Some(bridges) = &representation.bridges {
1114 for bridge in bridges {
1115 insert_bridge_node(&mut graph, bridge).map_err(E::from)?;
1116 }
1117 }
1118
1119 if let Some(cnx) = &representation.cnx {
1120 for c in cnx {
1121 let (src_name, src_channel) =
1122 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1123 .map_err(E::from)?;
1124 let (dst_name, dst_channel) =
1125 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1126 .map_err(E::from)?;
1127 let src = graph
1128 .get_node_id_by_name(src_name.as_str())
1129 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1130 let dst = graph
1131 .get_node_id_by_name(dst_name.as_str())
1132 .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
1133 graph
1134 .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1135 .map_err(|e| E::from(e.to_string()))?;
1136 }
1137 }
1138 cuconfig.graphs = Simple(graph);
1139 }
1140
1141 cuconfig.monitor = representation.monitor.clone();
1142 cuconfig.logging = representation.logging.clone();
1143 cuconfig.runtime = representation.runtime.clone();
1144 cuconfig.bridges = representation.bridges.clone().unwrap_or_default();
1145
1146 Ok(cuconfig)
1147}
1148
1149impl<'de> Deserialize<'de> for CuConfig {
1150 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1152 where
1153 D: Deserializer<'de>,
1154 {
1155 let representation =
1156 CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
1157
1158 match deserialize_config_representation::<String>(&representation) {
1160 Ok(config) => Ok(config),
1161 Err(e) => Err(serde::de::Error::custom(e)),
1162 }
1163 }
1164}
1165
1166impl Serialize for CuConfig {
1167 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1169 where
1170 S: Serializer,
1171 {
1172 let bridges = if self.bridges.is_empty() {
1173 None
1174 } else {
1175 Some(self.bridges.clone())
1176 };
1177 match &self.graphs {
1178 Simple(graph) => {
1179 let tasks: Vec<Node> = graph
1180 .0
1181 .node_indices()
1182 .map(|idx| graph.0[idx].clone())
1183 .filter(|node| node.get_flavor() == Flavor::Task)
1184 .collect();
1185
1186 let cnx: Vec<SerializedCnx> = graph
1187 .0
1188 .edge_indices()
1189 .map(|edge| SerializedCnx::from(&graph.0[edge]))
1190 .collect();
1191
1192 CuConfigRepresentation {
1193 tasks: Some(tasks),
1194 bridges: bridges.clone(),
1195 cnx: Some(cnx),
1196 monitor: self.monitor.clone(),
1197 logging: self.logging.clone(),
1198 runtime: self.runtime.clone(),
1199 missions: None,
1200 includes: None,
1201 }
1202 .serialize(serializer)
1203 }
1204 Missions(graphs) => {
1205 let missions = graphs
1206 .keys()
1207 .map(|id| MissionsConfig { id: id.clone() })
1208 .collect();
1209
1210 let mut tasks = Vec::new();
1212 let mut cnx = Vec::new();
1213
1214 for graph in graphs.values() {
1215 for node_idx in graph.node_indices() {
1217 let node = &graph[node_idx];
1218 if node.get_flavor() == Flavor::Task
1219 && !tasks.iter().any(|n: &Node| n.id == node.id)
1220 {
1221 tasks.push(node.clone());
1222 }
1223 }
1224
1225 for edge_idx in graph.0.edge_indices() {
1227 let edge = &graph.0[edge_idx];
1228 let serialized = SerializedCnx::from(edge);
1229 if !cnx.iter().any(|c: &SerializedCnx| {
1230 c.src == serialized.src
1231 && c.dst == serialized.dst
1232 && c.msg == serialized.msg
1233 }) {
1234 cnx.push(serialized);
1235 }
1236 }
1237 }
1238
1239 CuConfigRepresentation {
1240 tasks: Some(tasks),
1241 bridges,
1242 cnx: Some(cnx),
1243 monitor: self.monitor.clone(),
1244 logging: self.logging.clone(),
1245 runtime: self.runtime.clone(),
1246 missions: Some(missions),
1247 includes: None,
1248 }
1249 .serialize(serializer)
1250 }
1251 }
1252 }
1253}
1254
1255impl Default for CuConfig {
1256 fn default() -> Self {
1257 CuConfig {
1258 graphs: Simple(CuGraph(StableDiGraph::new())),
1259 monitor: None,
1260 logging: None,
1261 runtime: None,
1262 bridges: Vec::new(),
1263 }
1264 }
1265}
1266
1267impl CuConfig {
1270 #[allow(dead_code)]
1271 pub fn new_simple_type() -> Self {
1272 Self::default()
1273 }
1274
1275 #[allow(dead_code)]
1276 pub fn new_mission_type() -> Self {
1277 CuConfig {
1278 graphs: Missions(HashMap::new()),
1279 monitor: None,
1280 logging: None,
1281 runtime: None,
1282 bridges: Vec::new(),
1283 }
1284 }
1285
1286 fn get_options() -> Options {
1287 Options::default()
1288 .with_default_extension(Extensions::IMPLICIT_SOME)
1289 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1290 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1291 }
1292
1293 #[allow(dead_code)]
1294 pub fn serialize_ron(&self) -> String {
1295 let ron = Self::get_options();
1296 let pretty = ron::ser::PrettyConfig::default();
1297 ron.to_string_pretty(&self, pretty).unwrap()
1298 }
1299
1300 #[allow(dead_code)]
1301 pub fn deserialize_ron(ron: &str) -> Self {
1302 match Self::get_options().from_str(ron) {
1303 Ok(representation) => Self::deserialize_impl(representation).unwrap_or_else(|e| {
1304 panic!("Error deserializing configuration: {e}");
1305 }),
1306 Err(e) => panic!("Syntax Error in config: {} at position {}", e.code, e.span),
1307 }
1308 }
1309
1310 fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
1311 deserialize_config_representation(&representation)
1312 }
1313
1314 #[cfg(feature = "std")]
1316 pub fn render(
1317 &self,
1318 output: &mut dyn std::io::Write,
1319 mission_id: Option<&str>,
1320 ) -> CuResult<()> {
1321 writeln!(output, "digraph G {{").unwrap();
1322
1323 let graph = self.get_graph(mission_id)?;
1324
1325 for index in graph.node_indices() {
1326 let node = &graph[index];
1327 let config_str = match &node.config {
1328 Some(config) => {
1329 let config_str = config
1330 .0
1331 .iter()
1332 .map(|(k, v)| format!("<B>{k}</B> = {v}<BR ALIGN=\"LEFT\"/>"))
1333 .collect::<Vec<String>>()
1334 .join("\n");
1335 format!("____________<BR/><BR ALIGN=\"LEFT\"/>{config_str}")
1336 }
1337 None => String::new(),
1338 };
1339 writeln!(output, "{} [", index.index()).unwrap();
1340 writeln!(output, "shape=box,").unwrap();
1341 writeln!(output, "style=\"rounded, filled\",").unwrap();
1342 writeln!(output, "fontname=\"Noto Sans\"").unwrap();
1343
1344 let is_src = graph
1345 .get_dst_edges(index.index() as NodeId)
1346 .unwrap_or_default()
1347 .is_empty();
1348 let is_sink = graph
1349 .get_src_edges(index.index() as NodeId)
1350 .unwrap_or_default()
1351 .is_empty();
1352 if is_src {
1353 writeln!(output, "fillcolor=lightgreen,").unwrap();
1354 } else if is_sink {
1355 writeln!(output, "fillcolor=lightblue,").unwrap();
1356 } else {
1357 writeln!(output, "fillcolor=lightgrey,").unwrap();
1358 }
1359 writeln!(output, "color=grey,").unwrap();
1360
1361 writeln!(output, "labeljust=l,").unwrap();
1362 writeln!(
1363 output,
1364 "label=< <FONT COLOR=\"red\"><B>{}</B></FONT> <FONT COLOR=\"dimgray\">[{}]</FONT><BR ALIGN=\"LEFT\"/>{} >",
1365 node.id,
1366 node.get_type(),
1367 config_str
1368 )
1369 .unwrap();
1370
1371 writeln!(output, "];").unwrap();
1372 }
1373 for edge in graph.0.edge_indices() {
1374 let (src, dst) = graph.0.edge_endpoints(edge).unwrap();
1375
1376 let cnx = &graph.0[edge];
1377 let msg = encode_text(&cnx.msg);
1378 writeln!(
1379 output,
1380 "{} -> {} [label=< <B><FONT COLOR=\"gray\">{}</FONT></B> >];",
1381 src.index(),
1382 dst.index(),
1383 msg
1384 )
1385 .unwrap();
1386 }
1387 writeln!(output, "}}").unwrap();
1388 Ok(())
1389 }
1390
1391 #[allow(dead_code)]
1392 pub fn get_all_instances_configs(
1393 &self,
1394 mission_id: Option<&str>,
1395 ) -> Vec<Option<&ComponentConfig>> {
1396 let graph = self.graphs.get_graph(mission_id).unwrap();
1397 graph
1398 .get_all_nodes()
1399 .iter()
1400 .map(|(_, node)| node.get_instance_config())
1401 .collect()
1402 }
1403
1404 #[allow(dead_code)]
1405 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1406 self.graphs.get_graph(mission_id)
1407 }
1408
1409 #[allow(dead_code)]
1410 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1411 self.graphs.get_graph_mut(mission_id)
1412 }
1413
1414 #[allow(dead_code)]
1415 pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1416 self.monitor.as_ref()
1417 }
1418
1419 #[allow(dead_code)]
1420 pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
1421 self.runtime.as_ref()
1422 }
1423
1424 pub fn validate_logging_config(&self) -> CuResult<()> {
1427 if let Some(logging) = &self.logging {
1428 return logging.validate();
1429 }
1430 Ok(())
1431 }
1432}
1433
1434impl LoggingConfig {
1435 pub fn validate(&self) -> CuResult<()> {
1437 if let Some(section_size_mib) = self.section_size_mib {
1438 if let Some(slab_size_mib) = self.slab_size_mib {
1439 if section_size_mib > slab_size_mib {
1440 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.")));
1441 }
1442 }
1443 }
1444
1445 Ok(())
1446 }
1447}
1448
1449#[allow(dead_code)] fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
1451 let mut result = content.to_string();
1452
1453 for (key, value) in params {
1454 let pattern = format!("{{{{{key}}}}}");
1455 result = result.replace(&pattern, &value.to_string());
1456 }
1457
1458 result
1459}
1460
1461#[cfg(feature = "std")]
1463fn process_includes(
1464 file_path: &str,
1465 base_representation: CuConfigRepresentation,
1466 processed_files: &mut Vec<String>,
1467) -> CuResult<CuConfigRepresentation> {
1468 processed_files.push(file_path.to_string());
1470
1471 let mut result = base_representation;
1472
1473 if let Some(includes) = result.includes.take() {
1474 for include in includes {
1475 let include_path = if include.path.starts_with('/') {
1476 include.path.clone()
1477 } else {
1478 let current_dir = std::path::Path::new(file_path)
1479 .parent()
1480 .unwrap_or_else(|| std::path::Path::new(""))
1481 .to_string_lossy()
1482 .to_string();
1483
1484 format!("{}/{}", current_dir, include.path)
1485 };
1486
1487 let include_content = read_to_string(&include_path).map_err(|e| {
1488 CuError::from(format!("Failed to read include file: {include_path}"))
1489 .add_cause(e.to_string().as_str())
1490 })?;
1491
1492 let processed_content = substitute_parameters(&include_content, &include.params);
1493
1494 let mut included_representation: CuConfigRepresentation = match Options::default()
1495 .with_default_extension(Extensions::IMPLICIT_SOME)
1496 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1497 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1498 .from_str(&processed_content)
1499 {
1500 Ok(rep) => rep,
1501 Err(e) => {
1502 return Err(CuError::from(format!(
1503 "Failed to parse include file: {} - Error: {} at position {}",
1504 include_path, e.code, e.span
1505 )));
1506 }
1507 };
1508
1509 included_representation =
1510 process_includes(&include_path, included_representation, processed_files)?;
1511
1512 if let Some(included_tasks) = included_representation.tasks {
1513 if result.tasks.is_none() {
1514 result.tasks = Some(included_tasks);
1515 } else {
1516 let mut tasks = result.tasks.take().unwrap();
1517 for included_task in included_tasks {
1518 if !tasks.iter().any(|t| t.id == included_task.id) {
1519 tasks.push(included_task);
1520 }
1521 }
1522 result.tasks = Some(tasks);
1523 }
1524 }
1525
1526 if let Some(included_bridges) = included_representation.bridges {
1527 if result.bridges.is_none() {
1528 result.bridges = Some(included_bridges);
1529 } else {
1530 let mut bridges = result.bridges.take().unwrap();
1531 for included_bridge in included_bridges {
1532 if !bridges.iter().any(|b| b.id == included_bridge.id) {
1533 bridges.push(included_bridge);
1534 }
1535 }
1536 result.bridges = Some(bridges);
1537 }
1538 }
1539
1540 if let Some(included_cnx) = included_representation.cnx {
1541 if result.cnx.is_none() {
1542 result.cnx = Some(included_cnx);
1543 } else {
1544 let mut cnx = result.cnx.take().unwrap();
1545 for included_c in included_cnx {
1546 if !cnx
1547 .iter()
1548 .any(|c| c.src == included_c.src && c.dst == included_c.dst)
1549 {
1550 cnx.push(included_c);
1551 }
1552 }
1553 result.cnx = Some(cnx);
1554 }
1555 }
1556
1557 if result.monitor.is_none() {
1558 result.monitor = included_representation.monitor;
1559 }
1560
1561 if result.logging.is_none() {
1562 result.logging = included_representation.logging;
1563 }
1564
1565 if result.runtime.is_none() {
1566 result.runtime = included_representation.runtime;
1567 }
1568
1569 if let Some(included_missions) = included_representation.missions {
1570 if result.missions.is_none() {
1571 result.missions = Some(included_missions);
1572 } else {
1573 let mut missions = result.missions.take().unwrap();
1574 for included_mission in included_missions {
1575 if !missions.iter().any(|m| m.id == included_mission.id) {
1576 missions.push(included_mission);
1577 }
1578 }
1579 result.missions = Some(missions);
1580 }
1581 }
1582 }
1583 }
1584
1585 Ok(result)
1586}
1587
1588#[cfg(feature = "std")]
1590pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
1591 let config_content = read_to_string(config_filename).map_err(|e| {
1592 CuError::from(format!(
1593 "Failed to read configuration file: {:?}",
1594 &config_filename
1595 ))
1596 .add_cause(e.to_string().as_str())
1597 })?;
1598 read_configuration_str(config_content, Some(config_filename))
1599}
1600
1601fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
1605 Options::default()
1606 .with_default_extension(Extensions::IMPLICIT_SOME)
1607 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1608 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1609 .from_str(content)
1610 .map_err(|e| {
1611 CuError::from(format!(
1612 "Failed to parse configuration: Error: {} at position {}",
1613 e.code, e.span
1614 ))
1615 })
1616}
1617
1618fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
1621 let cuconfig = CuConfig::deserialize_impl(representation)
1622 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
1623
1624 cuconfig.validate_logging_config()?;
1625
1626 Ok(cuconfig)
1627}
1628
1629#[allow(unused_variables)]
1630pub fn read_configuration_str(
1631 config_content: String,
1632 file_path: Option<&str>,
1633) -> CuResult<CuConfig> {
1634 let representation = parse_config_string(&config_content)?;
1636
1637 #[cfg(feature = "std")]
1640 let representation = if let Some(path) = file_path {
1641 process_includes(path, representation, &mut Vec::new())?
1642 } else {
1643 representation
1644 };
1645
1646 config_representation_to_config(representation)
1648}
1649
1650#[cfg(test)]
1652mod tests {
1653 use super::*;
1654 #[cfg(not(feature = "std"))]
1655 use alloc::vec;
1656
1657 #[test]
1658 fn test_plain_serialize() {
1659 let mut config = CuConfig::default();
1660 let graph = config.get_graph_mut(None).unwrap();
1661 let n1 = graph
1662 .add_node(Node::new("test1", "package::Plugin1"))
1663 .unwrap();
1664 let n2 = graph
1665 .add_node(Node::new("test2", "package::Plugin2"))
1666 .unwrap();
1667 graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
1668 let serialized = config.serialize_ron();
1669 let deserialized = CuConfig::deserialize_ron(&serialized);
1670 let graph = config.graphs.get_graph(None).unwrap();
1671 let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
1672 assert_eq!(graph.node_count(), deserialized_graph.node_count());
1673 assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
1674 }
1675
1676 #[test]
1677 fn test_serialize_with_params() {
1678 let mut config = CuConfig::default();
1679 let graph = config.get_graph_mut(None).unwrap();
1680 let mut camera = Node::new("copper-camera", "camerapkg::Camera");
1681 camera.set_param::<Value>("resolution-height", 1080.into());
1682 graph.add_node(camera).unwrap();
1683 let serialized = config.serialize_ron();
1684 let config = CuConfig::deserialize_ron(&serialized);
1685 let deserialized = config.get_graph(None).unwrap();
1686 assert_eq!(
1687 deserialized
1688 .get_node(0)
1689 .unwrap()
1690 .get_param::<i32>("resolution-height")
1691 .unwrap(),
1692 1080
1693 );
1694 }
1695
1696 #[test]
1697 #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:9-1:10")]
1698 fn test_deserialization_error() {
1699 let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1701 CuConfig::deserialize_ron(txt);
1702 }
1703 #[test]
1704 fn test_missions() {
1705 let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
1706 let config = CuConfig::deserialize_ron(txt);
1707 let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
1708 assert!(graph.node_count() == 0);
1709 let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
1710 assert!(graph.node_count() == 0);
1711 }
1712
1713 #[test]
1714 fn test_monitor() {
1715 let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1716 let config = CuConfig::deserialize_ron(txt);
1717 assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
1718
1719 let txt =
1720 r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
1721 let config = CuConfig::deserialize_ron(txt);
1722 assert_eq!(
1723 config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
1724 4u8.into()
1725 );
1726 }
1727
1728 #[test]
1729 fn test_logging_parameters() {
1730 let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
1732
1733 let config = CuConfig::deserialize_ron(txt);
1734 assert!(config.logging.is_some());
1735 let logging_config = config.logging.unwrap();
1736 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1737 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1738 assert!(!logging_config.enable_task_logging);
1739
1740 let txt =
1742 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
1743 let config = CuConfig::deserialize_ron(txt);
1744 assert!(config.logging.is_some());
1745 let logging_config = config.logging.unwrap();
1746 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1747 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1748 assert!(logging_config.enable_task_logging);
1749 }
1750
1751 #[test]
1752 fn test_bridge_parsing() {
1753 let txt = r#"
1754 (
1755 tasks: [
1756 (id: "dst", type: "tasks::Destination"),
1757 (id: "src", type: "tasks::Source"),
1758 ],
1759 bridges: [
1760 (
1761 id: "radio",
1762 type: "tasks::SerialBridge",
1763 config: { "path": "/dev/ttyACM0", "baud": 921600 },
1764 channels: [
1765 Rx ( id: "status", route: "sys/status" ),
1766 Tx ( id: "motor", route: "motor/cmd" ),
1767 ],
1768 ),
1769 ],
1770 cnx: [
1771 (src: "radio/status", dst: "dst", msg: "mymsgs::Status"),
1772 (src: "src", dst: "radio/motor", msg: "mymsgs::MotorCmd"),
1773 ],
1774 )
1775 "#;
1776
1777 let config = CuConfig::deserialize_ron(txt);
1778 assert_eq!(config.bridges.len(), 1);
1779 let bridge = &config.bridges[0];
1780 assert_eq!(bridge.id, "radio");
1781 assert_eq!(bridge.channels.len(), 2);
1782 match &bridge.channels[0] {
1783 BridgeChannelConfigRepresentation::Rx { id, route, .. } => {
1784 assert_eq!(id, "status");
1785 assert_eq!(route.as_deref(), Some("sys/status"));
1786 }
1787 _ => panic!("expected Rx channel"),
1788 }
1789 match &bridge.channels[1] {
1790 BridgeChannelConfigRepresentation::Tx { id, route, .. } => {
1791 assert_eq!(id, "motor");
1792 assert_eq!(route.as_deref(), Some("motor/cmd"));
1793 }
1794 _ => panic!("expected Tx channel"),
1795 }
1796 let graph = config.graphs.get_graph(None).unwrap();
1797 let bridge_id = graph
1798 .get_node_id_by_name("radio")
1799 .expect("bridge node missing");
1800 let bridge_node = graph.get_node(bridge_id).unwrap();
1801 assert_eq!(bridge_node.get_flavor(), Flavor::Bridge);
1802
1803 let mut edges = Vec::new();
1805 for edge_idx in graph.0.edge_indices() {
1806 edges.push(graph.0[edge_idx].clone());
1807 }
1808 assert_eq!(edges.len(), 2);
1809 let status_edge = edges
1810 .iter()
1811 .find(|e| e.dst == "dst")
1812 .expect("status edge missing");
1813 assert_eq!(status_edge.src_channel.as_deref(), Some("status"));
1814 assert!(status_edge.dst_channel.is_none());
1815 let motor_edge = edges
1816 .iter()
1817 .find(|e| e.dst_channel.is_some())
1818 .expect("motor edge missing");
1819 assert_eq!(motor_edge.dst_channel.as_deref(), Some("motor"));
1820 }
1821
1822 #[test]
1823 fn test_bridge_roundtrip() {
1824 let mut config = CuConfig::default();
1825 let mut bridge_config = ComponentConfig::default();
1826 bridge_config.set("port", "/dev/ttyACM0".to_string());
1827 config.bridges.push(BridgeConfig {
1828 id: "radio".to_string(),
1829 type_: "tasks::SerialBridge".to_string(),
1830 config: Some(bridge_config),
1831 missions: None,
1832 channels: vec![
1833 BridgeChannelConfigRepresentation::Rx {
1834 id: "status".to_string(),
1835 route: Some("sys/status".to_string()),
1836 config: None,
1837 },
1838 BridgeChannelConfigRepresentation::Tx {
1839 id: "motor".to_string(),
1840 route: Some("motor/cmd".to_string()),
1841 config: None,
1842 },
1843 ],
1844 });
1845
1846 let serialized = config.serialize_ron();
1847 assert!(
1848 serialized.contains("bridges"),
1849 "bridges section missing from serialized config"
1850 );
1851 let deserialized = CuConfig::deserialize_ron(&serialized);
1852 assert_eq!(deserialized.bridges.len(), 1);
1853 let bridge = &deserialized.bridges[0];
1854 assert_eq!(bridge.channels.len(), 2);
1855 assert!(matches!(
1856 bridge.channels[0],
1857 BridgeChannelConfigRepresentation::Rx { .. }
1858 ));
1859 assert!(matches!(
1860 bridge.channels[1],
1861 BridgeChannelConfigRepresentation::Tx { .. }
1862 ));
1863 }
1864
1865 #[test]
1866 fn test_bridge_channel_config() {
1867 let txt = r#"
1868 (
1869 tasks: [],
1870 bridges: [
1871 (
1872 id: "radio",
1873 type: "tasks::SerialBridge",
1874 channels: [
1875 Rx ( id: "status", route: "sys/status", config: { "filter": "fast" } ),
1876 Tx ( id: "imu", route: "telemetry/imu", config: { "rate": 100 } ),
1877 ],
1878 ),
1879 ],
1880 cnx: [],
1881 )
1882 "#;
1883
1884 let config = CuConfig::deserialize_ron(txt);
1885 let bridge = &config.bridges[0];
1886 match &bridge.channels[0] {
1887 BridgeChannelConfigRepresentation::Rx {
1888 config: Some(cfg), ..
1889 } => {
1890 let val: String = cfg.get("filter").expect("filter missing");
1891 assert_eq!(val, "fast");
1892 }
1893 _ => panic!("expected Rx channel with config"),
1894 }
1895 match &bridge.channels[1] {
1896 BridgeChannelConfigRepresentation::Tx {
1897 config: Some(cfg), ..
1898 } => {
1899 let rate: i32 = cfg.get("rate").expect("rate missing");
1900 assert_eq!(rate, 100);
1901 }
1902 _ => panic!("expected Tx channel with config"),
1903 }
1904 }
1905
1906 #[test]
1907 #[should_panic(expected = "channel 'motor' is Tx and cannot act as a source")]
1908 fn test_bridge_tx_cannot_be_source() {
1909 let txt = r#"
1910 (
1911 tasks: [
1912 (id: "dst", type: "tasks::Destination"),
1913 ],
1914 bridges: [
1915 (
1916 id: "radio",
1917 type: "tasks::SerialBridge",
1918 channels: [
1919 Tx ( id: "motor", route: "motor/cmd" ),
1920 ],
1921 ),
1922 ],
1923 cnx: [
1924 (src: "radio/motor", dst: "dst", msg: "mymsgs::MotorCmd"),
1925 ],
1926 )
1927 "#;
1928
1929 CuConfig::deserialize_ron(txt);
1930 }
1931
1932 #[test]
1933 #[should_panic(expected = "channel 'status' is Rx and cannot act as a destination")]
1934 fn test_bridge_rx_cannot_be_destination() {
1935 let txt = r#"
1936 (
1937 tasks: [
1938 (id: "src", type: "tasks::Source"),
1939 ],
1940 bridges: [
1941 (
1942 id: "radio",
1943 type: "tasks::SerialBridge",
1944 channels: [
1945 Rx ( id: "status", route: "sys/status" ),
1946 ],
1947 ),
1948 ],
1949 cnx: [
1950 (src: "src", dst: "radio/status", msg: "mymsgs::Status"),
1951 ],
1952 )
1953 "#;
1954
1955 CuConfig::deserialize_ron(txt);
1956 }
1957
1958 #[test]
1959 fn test_validate_logging_config() {
1960 let txt =
1962 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
1963 let config = CuConfig::deserialize_ron(txt);
1964 assert!(config.validate_logging_config().is_ok());
1965
1966 let txt =
1968 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
1969 let config = CuConfig::deserialize_ron(txt);
1970 assert!(config.validate_logging_config().is_err());
1971 }
1972
1973 #[test]
1975 fn test_deserialization_edge_id_assignment() {
1976 let txt = r#"(
1979 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1980 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
1981 )"#;
1982 let config = CuConfig::deserialize_ron(txt);
1983 let graph = config.graphs.get_graph(None).unwrap();
1984 assert!(config.validate_logging_config().is_ok());
1985
1986 let src1_id = 0;
1988 assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
1989 let src2_id = 1;
1990 assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
1991
1992 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
1995 assert_eq!(src1_edge_id, 1);
1996 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
1997 assert_eq!(src2_edge_id, 0);
1998 }
1999
2000 #[test]
2001 fn test_simple_missions() {
2002 let txt = r#"(
2004 missions: [ (id: "m1"),
2005 (id: "m2"),
2006 ],
2007 tasks: [(id: "src1", type: "a", missions: ["m1"]),
2008 (id: "src2", type: "b", missions: ["m2"]),
2009 (id: "sink", type: "c")],
2010
2011 cnx: [
2012 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2013 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2014 ],
2015 )
2016 "#;
2017
2018 let config = CuConfig::deserialize_ron(txt);
2019 let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
2020 assert_eq!(m1_graph.edge_count(), 1);
2021 assert_eq!(m1_graph.node_count(), 2);
2022 let index = 0;
2023 let cnx = m1_graph.get_edge_weight(index).unwrap();
2024
2025 assert_eq!(cnx.src, "src1");
2026 assert_eq!(cnx.dst, "sink");
2027 assert_eq!(cnx.msg, "u32");
2028 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2029
2030 let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
2031 assert_eq!(m2_graph.edge_count(), 1);
2032 assert_eq!(m2_graph.node_count(), 2);
2033 let index = 0;
2034 let cnx = m2_graph.get_edge_weight(index).unwrap();
2035 assert_eq!(cnx.src, "src2");
2036 assert_eq!(cnx.dst, "sink");
2037 assert_eq!(cnx.msg, "u32");
2038 assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
2039 }
2040 #[test]
2041 fn test_mission_serde() {
2042 let txt = r#"(
2044 missions: [ (id: "m1"),
2045 (id: "m2"),
2046 ],
2047 tasks: [(id: "src1", type: "a", missions: ["m1"]),
2048 (id: "src2", type: "b", missions: ["m2"]),
2049 (id: "sink", type: "c")],
2050
2051 cnx: [
2052 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2053 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2054 ],
2055 )
2056 "#;
2057
2058 let config = CuConfig::deserialize_ron(txt);
2059 let serialized = config.serialize_ron();
2060 let deserialized = CuConfig::deserialize_ron(&serialized);
2061 let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
2062 assert_eq!(m1_graph.edge_count(), 1);
2063 assert_eq!(m1_graph.node_count(), 2);
2064 let index = 0;
2065 let cnx = m1_graph.get_edge_weight(index).unwrap();
2066 assert_eq!(cnx.src, "src1");
2067 assert_eq!(cnx.dst, "sink");
2068 assert_eq!(cnx.msg, "u32");
2069 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2070 }
2071
2072 #[test]
2073 fn test_keyframe_interval() {
2074 let txt = r#"(
2077 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2078 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2079 logging: ( keyframe_interval: 314 )
2080 )"#;
2081 let config = CuConfig::deserialize_ron(txt);
2082 let logging_config = config.logging.unwrap();
2083 assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
2084 }
2085
2086 #[test]
2087 fn test_default_keyframe_interval() {
2088 let txt = r#"(
2091 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2092 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2093 logging: ( slab_size_mib: 200, section_size_mib: 1024, )
2094 )"#;
2095 let config = CuConfig::deserialize_ron(txt);
2096 let logging_config = config.logging.unwrap();
2097 assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
2098 }
2099}