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