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