1#[cfg(not(feature = "std"))]
6extern crate alloc;
7
8use ConfigGraphs::{Missions, Simple};
9use core::fmt;
10use core::fmt::Display;
11use cu29_traits::{CuError, CuResult};
12use hashbrown::HashMap;
13pub use petgraph::Direction::Incoming;
14pub use petgraph::Direction::Outgoing;
15use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableDiGraph};
16#[cfg(feature = "std")]
17use petgraph::visit::IntoEdgeReferences;
18use petgraph::visit::{Bfs, EdgeRef};
19use ron::extensions::Extensions;
20use ron::value::Value as RonValue;
21use ron::{Number, Options};
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
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
51#[allow(dead_code)]
53impl Display for ComponentConfig {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 let mut first = true;
56 let ComponentConfig(config) = self;
57 write!(f, "{{")?;
58 for (key, value) in config.iter() {
59 if !first {
60 write!(f, ", ")?;
61 }
62 write!(f, "{key}: {value}")?;
63 first = false;
64 }
65 write!(f, "}}")
66 }
67}
68
69impl ComponentConfig {
71 #[allow(dead_code)]
72 pub fn new() -> Self {
73 ComponentConfig(HashMap::new())
74 }
75
76 #[allow(dead_code)]
77 pub fn get<T: From<Value>>(&self, key: &str) -> Option<T> {
78 let ComponentConfig(config) = self;
79 config.get(key).map(|v| T::from(v.clone()))
80 }
81
82 #[allow(dead_code)]
83 pub fn set<T: Into<Value>>(&mut self, key: &str, value: T) {
84 let ComponentConfig(config) = self;
85 config.insert(key.to_string(), value.into());
86 }
87}
88
89#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
98pub struct Value(RonValue);
99
100macro_rules! impl_from_numeric_for_value {
102 ($($source:ty),* $(,)?) => {
103 $(impl From<$source> for Value {
104 fn from(value: $source) -> Self {
105 Value(RonValue::Number(value.into()))
106 }
107 })*
108 };
109}
110
111impl_from_numeric_for_value!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
113
114impl From<Value> for bool {
115 fn from(value: Value) -> Self {
116 if let Value(RonValue::Bool(v)) = value {
117 v
118 } else {
119 panic!("Expected a Boolean variant but got {value:?}")
120 }
121 }
122}
123macro_rules! impl_from_value_for_int {
124 ($($target:ty),* $(,)?) => {
125 $(
126 impl From<Value> for $target {
127 fn from(value: Value) -> Self {
128 if let Value(RonValue::Number(num)) = value {
129 match num {
130 Number::I8(n) => n as $target,
131 Number::I16(n) => n as $target,
132 Number::I32(n) => n as $target,
133 Number::I64(n) => n as $target,
134 Number::U8(n) => n as $target,
135 Number::U16(n) => n as $target,
136 Number::U32(n) => n as $target,
137 Number::U64(n) => n as $target,
138 Number::F32(_) | Number::F64(_) | Number::__NonExhaustive(_) => {
139 panic!("Expected an integer Number variant but got {num:?}")
140 }
141 }
142 } else {
143 panic!("Expected a Number variant but got {value:?}")
144 }
145 }
146 }
147 )*
148 };
149}
150
151impl_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
152
153impl From<Value> for f64 {
154 fn from(value: Value) -> Self {
155 if let Value(RonValue::Number(num)) = value {
156 num.into_f64()
157 } else {
158 panic!("Expected a Number variant but got {value:?}")
159 }
160 }
161}
162
163impl From<String> for Value {
164 fn from(value: String) -> Self {
165 Value(RonValue::String(value))
166 }
167}
168
169impl From<Value> for String {
170 fn from(value: Value) -> Self {
171 if let Value(RonValue::String(s)) = value {
172 s
173 } else {
174 panic!("Expected a String variant")
175 }
176 }
177}
178
179impl Display for Value {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 let Value(value) = self;
182 match value {
183 RonValue::Number(n) => {
184 let s = match n {
185 Number::I8(n) => n.to_string(),
186 Number::I16(n) => n.to_string(),
187 Number::I32(n) => n.to_string(),
188 Number::I64(n) => n.to_string(),
189 Number::U8(n) => n.to_string(),
190 Number::U16(n) => n.to_string(),
191 Number::U32(n) => n.to_string(),
192 Number::U64(n) => n.to_string(),
193 Number::F32(n) => n.0.to_string(),
194 Number::F64(n) => n.0.to_string(),
195 _ => panic!("Expected a Number variant but got {value:?}"),
196 };
197 write!(f, "{s}")
198 }
199 RonValue::String(s) => write!(f, "{s}"),
200 RonValue::Bool(b) => write!(f, "{b}"),
201 RonValue::Map(m) => write!(f, "{m:?}"),
202 RonValue::Char(c) => write!(f, "{c:?}"),
203 RonValue::Unit => write!(f, "unit"),
204 RonValue::Option(o) => write!(f, "{o:?}"),
205 RonValue::Seq(s) => write!(f, "{s:?}"),
206 RonValue::Bytes(bytes) => write!(f, "{bytes:?}"),
207 }
208 }
209}
210
211#[derive(Serialize, Deserialize, Debug, Clone)]
213pub struct NodeLogging {
214 enabled: bool,
215}
216
217#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
220pub enum Flavor {
221 #[default]
222 Task,
223 Bridge,
224}
225
226#[derive(Serialize, Deserialize, Debug, Clone)]
229pub struct Node {
230 id: String,
232
233 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
235 type_: Option<String>,
236
237 #[serde(skip_serializing_if = "Option::is_none")]
239 config: Option<ComponentConfig>,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
243 resources: Option<HashMap<String, String>>,
244
245 missions: Option<Vec<String>>,
247
248 #[serde(skip_serializing_if = "Option::is_none")]
251 background: Option<bool>,
252
253 #[serde(skip_serializing_if = "Option::is_none")]
259 run_in_sim: Option<bool>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 logging: Option<NodeLogging>,
264
265 #[serde(skip, default)]
267 flavor: Flavor,
268}
269
270impl Node {
271 #[allow(dead_code)]
272 pub fn new(id: &str, ptype: &str) -> Self {
273 Node {
274 id: id.to_string(),
275 type_: Some(ptype.to_string()),
276 config: None,
277 resources: None,
278 missions: None,
279 background: None,
280 run_in_sim: None,
281 logging: None,
282 flavor: Flavor::Task,
283 }
284 }
285
286 #[allow(dead_code)]
287 pub fn new_with_flavor(id: &str, ptype: &str, flavor: Flavor) -> Self {
288 let mut node = Self::new(id, ptype);
289 node.flavor = flavor;
290 node
291 }
292
293 #[allow(dead_code)]
294 pub fn get_id(&self) -> String {
295 self.id.clone()
296 }
297
298 #[allow(dead_code)]
299 pub fn get_type(&self) -> &str {
300 self.type_.as_ref().unwrap()
301 }
302
303 #[allow(dead_code)]
304 pub fn set_type(mut self, name: Option<String>) -> Self {
305 self.type_ = name;
306 self
307 }
308
309 #[allow(dead_code)]
310 pub fn set_resources<I>(&mut self, resources: Option<I>)
311 where
312 I: IntoIterator<Item = (String, String)>,
313 {
314 self.resources = resources.map(|iter| iter.into_iter().collect());
315 }
316
317 #[allow(dead_code)]
318 pub fn is_background(&self) -> bool {
319 self.background.unwrap_or(false)
320 }
321
322 #[allow(dead_code)]
323 pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
324 self.config.as_ref()
325 }
326
327 #[allow(dead_code)]
328 pub fn get_resources(&self) -> Option<&HashMap<String, String>> {
329 self.resources.as_ref()
330 }
331
332 #[allow(dead_code)]
335 pub fn is_run_in_sim(&self) -> bool {
336 self.run_in_sim.unwrap_or(false)
337 }
338
339 #[allow(dead_code)]
340 pub fn is_logging_enabled(&self) -> bool {
341 if let Some(logging) = &self.logging {
342 logging.enabled
343 } else {
344 true
345 }
346 }
347
348 #[allow(dead_code)]
349 pub fn get_param<T: From<Value>>(&self, key: &str) -> Option<T> {
350 let pc = self.config.as_ref()?;
351 let ComponentConfig(pc) = pc;
352 let v = pc.get(key)?;
353 Some(T::from(v.clone()))
354 }
355
356 #[allow(dead_code)]
357 pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
358 if self.config.is_none() {
359 self.config = Some(ComponentConfig(HashMap::new()));
360 }
361 let ComponentConfig(config) = self.config.as_mut().unwrap();
362 config.insert(key.to_string(), value.into());
363 }
364
365 #[allow(dead_code)]
367 pub fn get_flavor(&self) -> Flavor {
368 self.flavor
369 }
370
371 #[allow(dead_code)]
373 pub fn set_flavor(&mut self, flavor: Flavor) {
374 self.flavor = flavor;
375 }
376}
377
378#[derive(Serialize, Deserialize, Debug, Clone)]
380pub enum BridgeChannelConfigRepresentation {
381 Rx {
383 id: String,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 route: Option<String>,
387 #[serde(skip_serializing_if = "Option::is_none")]
389 config: Option<ComponentConfig>,
390 },
391 Tx {
393 id: String,
394 #[serde(skip_serializing_if = "Option::is_none")]
396 route: Option<String>,
397 #[serde(skip_serializing_if = "Option::is_none")]
399 config: Option<ComponentConfig>,
400 },
401}
402
403impl BridgeChannelConfigRepresentation {
404 #[allow(dead_code)]
406 pub fn id(&self) -> &str {
407 match self {
408 BridgeChannelConfigRepresentation::Rx { id, .. }
409 | BridgeChannelConfigRepresentation::Tx { id, .. } => id,
410 }
411 }
412
413 #[allow(dead_code)]
415 pub fn route(&self) -> Option<&str> {
416 match self {
417 BridgeChannelConfigRepresentation::Rx { route, .. }
418 | BridgeChannelConfigRepresentation::Tx { route, .. } => route.as_deref(),
419 }
420 }
421}
422
423enum EndpointRole {
424 Source,
425 Destination,
426}
427
428fn validate_bridge_channel(
429 bridge: &BridgeConfig,
430 channel_id: &str,
431 role: EndpointRole,
432) -> Result<(), String> {
433 let channel = bridge
434 .channels
435 .iter()
436 .find(|ch| ch.id() == channel_id)
437 .ok_or_else(|| {
438 format!(
439 "Bridge '{}' does not declare a channel named '{}'",
440 bridge.id, channel_id
441 )
442 })?;
443
444 match (role, channel) {
445 (EndpointRole::Source, BridgeChannelConfigRepresentation::Rx { .. }) => Ok(()),
446 (EndpointRole::Destination, BridgeChannelConfigRepresentation::Tx { .. }) => Ok(()),
447 (EndpointRole::Source, BridgeChannelConfigRepresentation::Tx { .. }) => Err(format!(
448 "Bridge '{}' channel '{}' is Tx and cannot act as a source",
449 bridge.id, channel_id
450 )),
451 (EndpointRole::Destination, BridgeChannelConfigRepresentation::Rx { .. }) => Err(format!(
452 "Bridge '{}' channel '{}' is Rx and cannot act as a destination",
453 bridge.id, channel_id
454 )),
455 }
456}
457
458#[derive(Serialize, Deserialize, Debug, Clone)]
460pub struct ResourceBundleConfig {
461 pub id: String,
462 #[serde(rename = "provider")]
463 pub provider: String,
464 #[serde(skip_serializing_if = "Option::is_none")]
465 pub config: Option<ComponentConfig>,
466 #[serde(skip_serializing_if = "Option::is_none")]
467 pub missions: Option<Vec<String>>,
468}
469
470#[derive(Serialize, Deserialize, Debug, Clone)]
472pub struct BridgeConfig {
473 pub id: String,
474 #[serde(rename = "type")]
475 pub type_: String,
476 #[serde(skip_serializing_if = "Option::is_none")]
477 pub config: Option<ComponentConfig>,
478 #[serde(skip_serializing_if = "Option::is_none")]
479 pub resources: Option<HashMap<String, String>>,
480 #[serde(skip_serializing_if = "Option::is_none")]
481 pub missions: Option<Vec<String>>,
482 pub channels: Vec<BridgeChannelConfigRepresentation>,
484}
485
486impl BridgeConfig {
487 fn to_node(&self) -> Node {
488 let mut node = Node::new_with_flavor(&self.id, &self.type_, Flavor::Bridge);
489 node.config = self.config.clone();
490 node.resources = self.resources.clone();
491 node.missions = self.missions.clone();
492 node
493 }
494}
495
496fn insert_bridge_node(graph: &mut CuGraph, bridge: &BridgeConfig) -> Result<(), String> {
497 if graph.get_node_id_by_name(bridge.id.as_str()).is_some() {
498 return Err(format!(
499 "Bridge '{}' reuses an existing node id. Bridge ids must be unique.",
500 bridge.id
501 ));
502 }
503 graph
504 .add_node(bridge.to_node())
505 .map(|_| ())
506 .map_err(|e| e.to_string())
507}
508
509#[derive(Serialize, Deserialize, Debug, Clone)]
511struct SerializedCnx {
512 src: String,
513 dst: String,
514 msg: String,
515 missions: Option<Vec<String>>,
516}
517
518#[derive(Debug, Clone)]
520pub struct Cnx {
521 pub src: String,
523 pub dst: String,
525 pub msg: String,
527 pub missions: Option<Vec<String>>,
529 pub src_channel: Option<String>,
531 pub dst_channel: Option<String>,
533}
534
535impl From<&Cnx> for SerializedCnx {
536 fn from(cnx: &Cnx) -> Self {
537 SerializedCnx {
538 src: format_endpoint(&cnx.src, cnx.src_channel.as_deref()),
539 dst: format_endpoint(&cnx.dst, cnx.dst_channel.as_deref()),
540 msg: cnx.msg.clone(),
541 missions: cnx.missions.clone(),
542 }
543 }
544}
545
546fn format_endpoint(node: &str, channel: Option<&str>) -> String {
547 match channel {
548 Some(ch) => format!("{node}/{ch}"),
549 None => node.to_string(),
550 }
551}
552
553fn parse_endpoint(
554 endpoint: &str,
555 role: EndpointRole,
556 bridges: &HashMap<&str, &BridgeConfig>,
557) -> Result<(String, Option<String>), String> {
558 if let Some((node, channel)) = endpoint.split_once('/') {
559 if let Some(bridge) = bridges.get(node) {
560 validate_bridge_channel(bridge, channel, role)?;
561 return Ok((node.to_string(), Some(channel.to_string())));
562 } else {
563 return Err(format!(
564 "Endpoint '{endpoint}' references an unknown bridge '{node}'"
565 ));
566 }
567 }
568
569 if let Some(bridge) = bridges.get(endpoint) {
570 return Err(format!(
571 "Bridge '{}' connections must reference a channel using '{}/<channel>'",
572 bridge.id, bridge.id
573 ));
574 }
575
576 Ok((endpoint.to_string(), None))
577}
578
579fn build_bridge_lookup(bridges: Option<&Vec<BridgeConfig>>) -> HashMap<&str, &BridgeConfig> {
580 let mut map = HashMap::new();
581 if let Some(bridges) = bridges {
582 for bridge in bridges {
583 map.insert(bridge.id.as_str(), bridge);
584 }
585 }
586 map
587}
588
589fn mission_applies(missions: &Option<Vec<String>>, mission_id: &str) -> bool {
590 missions
591 .as_ref()
592 .map(|mission_list| mission_list.iter().any(|m| m == mission_id))
593 .unwrap_or(true)
594}
595
596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599pub enum CuDirection {
600 Outgoing,
601 Incoming,
602}
603
604impl From<CuDirection> for petgraph::Direction {
605 fn from(dir: CuDirection) -> Self {
606 match dir {
607 CuDirection::Outgoing => petgraph::Direction::Outgoing,
608 CuDirection::Incoming => petgraph::Direction::Incoming,
609 }
610 }
611}
612
613#[derive(Default, Debug, Clone)]
614pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
615
616impl CuGraph {
617 #[allow(dead_code)]
618 pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
619 self.0
620 .node_indices()
621 .map(|index| (index.index() as u32, &self.0[index]))
622 .collect()
623 }
624
625 #[allow(dead_code)]
626 pub fn get_neighbor_ids(&self, node_id: NodeId, dir: CuDirection) -> Vec<NodeId> {
627 self.0
628 .neighbors_directed(node_id.into(), dir.into())
629 .map(|petgraph_index| petgraph_index.index() as NodeId)
630 .collect()
631 }
632
633 #[allow(dead_code)]
634 pub fn node_ids(&self) -> Vec<NodeId> {
635 self.0
636 .node_indices()
637 .map(|index| index.index() as NodeId)
638 .collect()
639 }
640
641 #[allow(dead_code)]
642 pub fn edge_id_between(&self, source: NodeId, target: NodeId) -> Option<usize> {
643 self.0
644 .find_edge(source.into(), target.into())
645 .map(|edge| edge.index())
646 }
647
648 #[allow(dead_code)]
649 pub fn edge(&self, edge_id: usize) -> Option<&Cnx> {
650 self.0.edge_weight(EdgeIndex::new(edge_id))
651 }
652
653 #[allow(dead_code)]
654 pub fn edges(&self) -> impl Iterator<Item = &Cnx> {
655 self.0
656 .edge_indices()
657 .filter_map(|edge| self.0.edge_weight(edge))
658 }
659
660 #[allow(dead_code)]
661 pub fn bfs_nodes(&self, start: NodeId) -> Vec<NodeId> {
662 let mut visitor = Bfs::new(&self.0, start.into());
663 let mut nodes = Vec::new();
664 while let Some(node) = visitor.next(&self.0) {
665 nodes.push(node.index() as NodeId);
666 }
667 nodes
668 }
669
670 #[allow(dead_code)]
671 pub fn incoming_neighbor_count(&self, node_id: NodeId) -> usize {
672 self.0.neighbors_directed(node_id.into(), Incoming).count()
673 }
674
675 #[allow(dead_code)]
676 pub fn outgoing_neighbor_count(&self, node_id: NodeId) -> usize {
677 self.0.neighbors_directed(node_id.into(), Outgoing).count()
678 }
679
680 pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
681 self.0.node_indices().collect()
682 }
683
684 pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
685 Ok(self.0.add_node(node).index() as NodeId)
686 }
687
688 #[allow(dead_code)]
689 pub fn connection_exists(&self, source: NodeId, target: NodeId) -> bool {
690 self.0.find_edge(source.into(), target.into()).is_some()
691 }
692
693 pub fn connect_ext(
694 &mut self,
695 source: NodeId,
696 target: NodeId,
697 msg_type: &str,
698 missions: Option<Vec<String>>,
699 src_channel: Option<String>,
700 dst_channel: Option<String>,
701 ) -> CuResult<()> {
702 let (src_id, dst_id) = (
703 self.0
704 .node_weight(source.into())
705 .ok_or("Source node not found")?
706 .id
707 .clone(),
708 self.0
709 .node_weight(target.into())
710 .ok_or("Target node not found")?
711 .id
712 .clone(),
713 );
714
715 let _ = self.0.add_edge(
716 petgraph::stable_graph::NodeIndex::from(source),
717 petgraph::stable_graph::NodeIndex::from(target),
718 Cnx {
719 src: src_id,
720 dst: dst_id,
721 msg: msg_type.to_string(),
722 missions,
723 src_channel,
724 dst_channel,
725 },
726 );
727 Ok(())
728 }
729 #[allow(dead_code)]
733 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
734 self.0.node_weight(node_id.into())
735 }
736
737 #[allow(dead_code)]
738 pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
739 self.0.node_weight(index.into())
740 }
741
742 #[allow(dead_code)]
743 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
744 self.0.node_weight_mut(node_id.into())
745 }
746
747 pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
748 self.0
749 .node_indices()
750 .into_iter()
751 .find(|idx| self.0[*idx].get_id() == name)
752 .map(|i| i.index() as NodeId)
753 }
754
755 #[allow(dead_code)]
756 pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
757 self.0.edge_weight(EdgeIndex::new(index)).cloned()
758 }
759
760 #[allow(dead_code)]
761 pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
762 self.0.node_indices().find_map(|node_index| {
763 if let Some(node) = self.0.node_weight(node_index) {
764 if node.id != node_id {
765 return None;
766 }
767 let edges: Vec<_> = self
768 .0
769 .edges_directed(node_index, Outgoing)
770 .map(|edge| edge.id().index())
771 .collect();
772 if edges.is_empty() {
773 return None;
774 }
775 let cnx = self
776 .0
777 .edge_weight(EdgeIndex::new(edges[0]))
778 .expect("Found an cnx id but could not retrieve it back");
779 return Some(cnx.msg.clone());
780 }
781 None
782 })
783 }
784
785 #[allow(dead_code)]
786 pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
787 self.get_node_input_msg_types(node_id)
788 .and_then(|mut v| v.pop())
789 }
790
791 pub fn get_node_input_msg_types(&self, node_id: &str) -> Option<Vec<String>> {
792 self.0.node_indices().find_map(|node_index| {
793 if let Some(node) = self.0.node_weight(node_index) {
794 if node.id != node_id {
795 return None;
796 }
797 let edges: Vec<_> = self
798 .0
799 .edges_directed(node_index, Incoming)
800 .map(|edge| edge.id().index())
801 .collect();
802 if edges.is_empty() {
803 return None;
804 }
805 let msgs = edges
806 .into_iter()
807 .map(|edge_id| {
808 let cnx = self
809 .0
810 .edge_weight(EdgeIndex::new(edge_id))
811 .expect("Found an cnx id but could not retrieve it back");
812 cnx.msg.clone()
813 })
814 .collect();
815 return Some(msgs);
816 }
817 None
818 })
819 }
820
821 #[allow(dead_code)]
822 pub fn get_connection_msg_type(&self, source: NodeId, target: NodeId) -> Option<&str> {
823 self.0
824 .find_edge(source.into(), target.into())
825 .map(|edge_index| self.0[edge_index].msg.as_str())
826 }
827
828 fn get_edges_by_direction(
830 &self,
831 node_id: NodeId,
832 direction: petgraph::Direction,
833 ) -> CuResult<Vec<usize>> {
834 Ok(self
835 .0
836 .edges_directed(node_id.into(), direction)
837 .map(|edge| edge.id().index())
838 .collect())
839 }
840
841 pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
842 self.get_edges_by_direction(node_id, Outgoing)
843 }
844
845 pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
847 self.get_edges_by_direction(node_id, Incoming)
848 }
849
850 #[allow(dead_code)]
851 pub fn node_count(&self) -> usize {
852 self.0.node_count()
853 }
854
855 #[allow(dead_code)]
856 pub fn edge_count(&self) -> usize {
857 self.0.edge_count()
858 }
859
860 #[allow(dead_code)]
863 pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
864 self.connect_ext(source, target, msg_type, None, None, None)
865 }
866}
867
868impl core::ops::Index<NodeIndex> for CuGraph {
869 type Output = Node;
870
871 fn index(&self, index: NodeIndex) -> &Self::Output {
872 &self.0[index]
873 }
874}
875
876#[derive(Debug, Clone)]
877pub enum ConfigGraphs {
878 Simple(CuGraph),
879 Missions(HashMap<String, CuGraph>),
880}
881
882impl ConfigGraphs {
883 #[allow(dead_code)]
886 pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
887 match self {
888 Simple(graph) => {
889 let mut map = HashMap::new();
890 map.insert("default".to_string(), graph.clone());
891 map
892 }
893 Missions(graphs) => graphs.clone(),
894 }
895 }
896
897 #[allow(dead_code)]
898 pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
899 match self {
900 Simple(graph) => Ok(graph),
901 Missions(graphs) => {
902 if graphs.len() == 1 {
903 Ok(graphs.values().next().unwrap())
904 } else {
905 Err("Cannot get default mission graph from mission config".into())
906 }
907 }
908 }
909 }
910
911 #[allow(dead_code)]
912 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
913 match self {
914 Simple(graph) => {
915 if mission_id.is_none() || mission_id.unwrap() == "default" {
916 Ok(graph)
917 } else {
918 Err("Cannot get mission graph from simple config".into())
919 }
920 }
921 Missions(graphs) => {
922 if let Some(id) = mission_id {
923 graphs
924 .get(id)
925 .ok_or_else(|| format!("Mission {id} not found").into())
926 } else {
927 Err("Mission ID required for mission configs".into())
928 }
929 }
930 }
931 }
932
933 #[allow(dead_code)]
934 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
935 match self {
936 Simple(graph) => {
937 if mission_id.is_none() {
938 Ok(graph)
939 } else {
940 Err("Cannot get mission graph from simple config".into())
941 }
942 }
943 Missions(graphs) => {
944 if let Some(id) = mission_id {
945 graphs
946 .get_mut(id)
947 .ok_or_else(|| format!("Mission {id} not found").into())
948 } else {
949 Err("Mission ID required for mission configs".into())
950 }
951 }
952 }
953 }
954
955 pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
956 match self {
957 Simple(_) => Err("Cannot add mission to simple config".into()),
958 Missions(graphs) => {
959 if graphs.contains_key(mission_id) {
960 Err(format!("Mission {mission_id} already exists").into())
961 } else {
962 let graph = CuGraph::default();
963 graphs.insert(mission_id.to_string(), graph);
964 Ok(graphs.get_mut(mission_id).unwrap())
966 }
967 }
968 }
969 }
970}
971
972#[derive(Debug, Clone)]
978pub struct CuConfig {
979 pub monitor: Option<MonitorConfig>,
981 pub logging: Option<LoggingConfig>,
983 pub runtime: Option<RuntimeConfig>,
985 pub resources: Vec<ResourceBundleConfig>,
987 pub bridges: Vec<BridgeConfig>,
989 pub graphs: ConfigGraphs,
991}
992
993impl CuConfig {
994 #[cfg(feature = "std")]
995 fn ensure_threadpool_bundle(&mut self) {
996 if !self.has_background_tasks() {
997 return;
998 }
999 if self
1000 .resources
1001 .iter()
1002 .any(|bundle| bundle.id == "threadpool")
1003 {
1004 return;
1005 }
1006
1007 let mut config = ComponentConfig::default();
1008 config.set("threads", 2u64);
1009 self.resources.push(ResourceBundleConfig {
1010 id: "threadpool".to_string(),
1011 provider: "cu29::resource::ThreadPoolBundle".to_string(),
1012 config: Some(config),
1013 missions: None,
1014 });
1015 }
1016
1017 #[cfg(feature = "std")]
1018 fn has_background_tasks(&self) -> bool {
1019 match &self.graphs {
1020 ConfigGraphs::Simple(graph) => graph
1021 .get_all_nodes()
1022 .iter()
1023 .any(|(_, node)| node.is_background()),
1024 ConfigGraphs::Missions(graphs) => graphs.values().any(|graph| {
1025 graph
1026 .get_all_nodes()
1027 .iter()
1028 .any(|(_, node)| node.is_background())
1029 }),
1030 }
1031 }
1032}
1033
1034#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1035pub struct MonitorConfig {
1036 #[serde(rename = "type")]
1037 type_: String,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1039 config: Option<ComponentConfig>,
1040}
1041
1042impl MonitorConfig {
1043 #[allow(dead_code)]
1044 pub fn get_type(&self) -> &str {
1045 &self.type_
1046 }
1047
1048 #[allow(dead_code)]
1049 pub fn get_config(&self) -> Option<&ComponentConfig> {
1050 self.config.as_ref()
1051 }
1052}
1053
1054fn default_as_true() -> bool {
1055 true
1056}
1057
1058pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
1059
1060fn default_keyframe_interval() -> Option<u32> {
1061 Some(DEFAULT_KEYFRAME_INTERVAL)
1062}
1063
1064#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1065pub struct LoggingConfig {
1066 #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
1068 pub enable_task_logging: bool,
1069
1070 #[serde(skip_serializing_if = "Option::is_none")]
1072 pub slab_size_mib: Option<u64>,
1073
1074 #[serde(skip_serializing_if = "Option::is_none")]
1076 pub section_size_mib: Option<u64>,
1077
1078 #[serde(
1080 default = "default_keyframe_interval",
1081 skip_serializing_if = "Option::is_none"
1082 )]
1083 pub keyframe_interval: Option<u32>,
1084}
1085
1086#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1087pub struct RuntimeConfig {
1088 #[serde(skip_serializing_if = "Option::is_none")]
1094 pub rate_target_hz: Option<u64>,
1095}
1096
1097#[derive(Serialize, Deserialize, Debug, Clone)]
1099pub struct MissionsConfig {
1100 pub id: String,
1101}
1102
1103#[derive(Serialize, Deserialize, Debug, Clone)]
1105pub struct IncludesConfig {
1106 pub path: String,
1107 pub params: HashMap<String, Value>,
1108 pub missions: Option<Vec<String>>,
1109}
1110
1111#[derive(Serialize, Deserialize, Default)]
1113struct CuConfigRepresentation {
1114 tasks: Option<Vec<Node>>,
1115 resources: Option<Vec<ResourceBundleConfig>>,
1116 bridges: Option<Vec<BridgeConfig>>,
1117 cnx: Option<Vec<SerializedCnx>>,
1118 monitor: Option<MonitorConfig>,
1119 logging: Option<LoggingConfig>,
1120 runtime: Option<RuntimeConfig>,
1121 missions: Option<Vec<MissionsConfig>>,
1122 includes: Option<Vec<IncludesConfig>>,
1123}
1124
1125fn deserialize_config_representation<E>(
1127 representation: &CuConfigRepresentation,
1128) -> Result<CuConfig, E>
1129where
1130 E: From<String>,
1131{
1132 let mut cuconfig = CuConfig::default();
1133 let bridge_lookup = build_bridge_lookup(representation.bridges.as_ref());
1134
1135 if let Some(mission_configs) = &representation.missions {
1136 let mut missions = Missions(HashMap::new());
1138
1139 for mission_config in mission_configs {
1140 let mission_id = mission_config.id.as_str();
1141 let graph = missions
1142 .add_mission(mission_id)
1143 .map_err(|e| E::from(e.to_string()))?;
1144
1145 if let Some(tasks) = &representation.tasks {
1146 for task in tasks {
1147 if let Some(task_missions) = &task.missions {
1148 if task_missions.contains(&mission_id.to_owned()) {
1150 graph
1151 .add_node(task.clone())
1152 .map_err(|e| E::from(e.to_string()))?;
1153 }
1154 } else {
1155 graph
1157 .add_node(task.clone())
1158 .map_err(|e| E::from(e.to_string()))?;
1159 }
1160 }
1161 }
1162
1163 if let Some(bridges) = &representation.bridges {
1164 for bridge in bridges {
1165 if mission_applies(&bridge.missions, mission_id) {
1166 insert_bridge_node(graph, bridge).map_err(E::from)?;
1167 }
1168 }
1169 }
1170
1171 if let Some(cnx) = &representation.cnx {
1172 for c in cnx {
1173 if let Some(cnx_missions) = &c.missions {
1174 if cnx_missions.contains(&mission_id.to_owned()) {
1176 let (src_name, src_channel) =
1177 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1178 .map_err(E::from)?;
1179 let (dst_name, dst_channel) =
1180 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1181 .map_err(E::from)?;
1182 let src =
1183 graph
1184 .get_node_id_by_name(src_name.as_str())
1185 .ok_or_else(|| {
1186 E::from(format!("Source node not found: {}", c.src))
1187 })?;
1188 let dst =
1189 graph
1190 .get_node_id_by_name(dst_name.as_str())
1191 .ok_or_else(|| {
1192 E::from(format!("Destination node not found: {}", c.dst))
1193 })?;
1194 graph
1195 .connect_ext(
1196 src,
1197 dst,
1198 &c.msg,
1199 Some(cnx_missions.clone()),
1200 src_channel,
1201 dst_channel,
1202 )
1203 .map_err(|e| E::from(e.to_string()))?;
1204 }
1205 } else {
1206 let (src_name, src_channel) =
1208 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1209 .map_err(E::from)?;
1210 let (dst_name, dst_channel) =
1211 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1212 .map_err(E::from)?;
1213 let src = graph
1214 .get_node_id_by_name(src_name.as_str())
1215 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1216 let dst =
1217 graph
1218 .get_node_id_by_name(dst_name.as_str())
1219 .ok_or_else(|| {
1220 E::from(format!("Destination node not found: {}", c.dst))
1221 })?;
1222 graph
1223 .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1224 .map_err(|e| E::from(e.to_string()))?;
1225 }
1226 }
1227 }
1228 }
1229 cuconfig.graphs = missions;
1230 } else {
1231 let mut graph = CuGraph::default();
1233
1234 if let Some(tasks) = &representation.tasks {
1235 for task in tasks {
1236 graph
1237 .add_node(task.clone())
1238 .map_err(|e| E::from(e.to_string()))?;
1239 }
1240 }
1241
1242 if let Some(bridges) = &representation.bridges {
1243 for bridge in bridges {
1244 insert_bridge_node(&mut graph, bridge).map_err(E::from)?;
1245 }
1246 }
1247
1248 if let Some(cnx) = &representation.cnx {
1249 for c in cnx {
1250 let (src_name, src_channel) =
1251 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1252 .map_err(E::from)?;
1253 let (dst_name, dst_channel) =
1254 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1255 .map_err(E::from)?;
1256 let src = graph
1257 .get_node_id_by_name(src_name.as_str())
1258 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1259 let dst = graph
1260 .get_node_id_by_name(dst_name.as_str())
1261 .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
1262 graph
1263 .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1264 .map_err(|e| E::from(e.to_string()))?;
1265 }
1266 }
1267 cuconfig.graphs = Simple(graph);
1268 }
1269
1270 cuconfig.monitor = representation.monitor.clone();
1271 cuconfig.logging = representation.logging.clone();
1272 cuconfig.runtime = representation.runtime.clone();
1273 cuconfig.resources = representation.resources.clone().unwrap_or_default();
1274 cuconfig.bridges = representation.bridges.clone().unwrap_or_default();
1275
1276 Ok(cuconfig)
1277}
1278
1279impl<'de> Deserialize<'de> for CuConfig {
1280 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1282 where
1283 D: Deserializer<'de>,
1284 {
1285 let representation =
1286 CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
1287
1288 match deserialize_config_representation::<String>(&representation) {
1290 Ok(config) => Ok(config),
1291 Err(e) => Err(serde::de::Error::custom(e)),
1292 }
1293 }
1294}
1295
1296impl Serialize for CuConfig {
1297 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1299 where
1300 S: Serializer,
1301 {
1302 let bridges = if self.bridges.is_empty() {
1303 None
1304 } else {
1305 Some(self.bridges.clone())
1306 };
1307 let resources = if self.resources.is_empty() {
1308 None
1309 } else {
1310 Some(self.resources.clone())
1311 };
1312 match &self.graphs {
1313 Simple(graph) => {
1314 let tasks: Vec<Node> = graph
1315 .0
1316 .node_indices()
1317 .map(|idx| graph.0[idx].clone())
1318 .filter(|node| node.get_flavor() == Flavor::Task)
1319 .collect();
1320
1321 let cnx: Vec<SerializedCnx> = graph
1322 .0
1323 .edge_indices()
1324 .map(|edge| SerializedCnx::from(&graph.0[edge]))
1325 .collect();
1326
1327 CuConfigRepresentation {
1328 tasks: Some(tasks),
1329 bridges: bridges.clone(),
1330 cnx: Some(cnx),
1331 monitor: self.monitor.clone(),
1332 logging: self.logging.clone(),
1333 runtime: self.runtime.clone(),
1334 resources: resources.clone(),
1335 missions: None,
1336 includes: None,
1337 }
1338 .serialize(serializer)
1339 }
1340 Missions(graphs) => {
1341 let missions = graphs
1342 .keys()
1343 .map(|id| MissionsConfig { id: id.clone() })
1344 .collect();
1345
1346 let mut tasks = Vec::new();
1348 let mut cnx = Vec::new();
1349
1350 for graph in graphs.values() {
1351 for node_idx in graph.node_indices() {
1353 let node = &graph[node_idx];
1354 if node.get_flavor() == Flavor::Task
1355 && !tasks.iter().any(|n: &Node| n.id == node.id)
1356 {
1357 tasks.push(node.clone());
1358 }
1359 }
1360
1361 for edge_idx in graph.0.edge_indices() {
1363 let edge = &graph.0[edge_idx];
1364 let serialized = SerializedCnx::from(edge);
1365 if !cnx.iter().any(|c: &SerializedCnx| {
1366 c.src == serialized.src
1367 && c.dst == serialized.dst
1368 && c.msg == serialized.msg
1369 }) {
1370 cnx.push(serialized);
1371 }
1372 }
1373 }
1374
1375 CuConfigRepresentation {
1376 tasks: Some(tasks),
1377 resources: resources.clone(),
1378 bridges,
1379 cnx: Some(cnx),
1380 monitor: self.monitor.clone(),
1381 logging: self.logging.clone(),
1382 runtime: self.runtime.clone(),
1383 missions: Some(missions),
1384 includes: None,
1385 }
1386 .serialize(serializer)
1387 }
1388 }
1389 }
1390}
1391
1392impl Default for CuConfig {
1393 fn default() -> Self {
1394 CuConfig {
1395 graphs: Simple(CuGraph(StableDiGraph::new())),
1396 monitor: None,
1397 logging: None,
1398 runtime: None,
1399 resources: Vec::new(),
1400 bridges: Vec::new(),
1401 }
1402 }
1403}
1404
1405impl CuConfig {
1408 #[allow(dead_code)]
1409 pub fn new_simple_type() -> Self {
1410 Self::default()
1411 }
1412
1413 #[allow(dead_code)]
1414 pub fn new_mission_type() -> Self {
1415 CuConfig {
1416 graphs: Missions(HashMap::new()),
1417 monitor: None,
1418 logging: None,
1419 runtime: None,
1420 resources: Vec::new(),
1421 bridges: Vec::new(),
1422 }
1423 }
1424
1425 fn get_options() -> Options {
1426 Options::default()
1427 .with_default_extension(Extensions::IMPLICIT_SOME)
1428 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1429 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1430 }
1431
1432 #[allow(dead_code)]
1433 pub fn serialize_ron(&self) -> String {
1434 let ron = Self::get_options();
1435 let pretty = ron::ser::PrettyConfig::default();
1436 ron.to_string_pretty(&self, pretty).unwrap()
1437 }
1438
1439 #[allow(dead_code)]
1440 pub fn deserialize_ron(ron: &str) -> Self {
1441 match Self::get_options().from_str(ron) {
1442 Ok(representation) => Self::deserialize_impl(representation).unwrap_or_else(|e| {
1443 panic!("Error deserializing configuration: {e}");
1444 }),
1445 Err(e) => panic!("Syntax Error in config: {} at position {}", e.code, e.span),
1446 }
1447 }
1448
1449 fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
1450 deserialize_config_representation(&representation)
1451 }
1452
1453 #[cfg(feature = "std")]
1455 #[allow(dead_code)]
1456 pub fn render(
1457 &self,
1458 output: &mut dyn std::io::Write,
1459 mission_id: Option<&str>,
1460 ) -> CuResult<()> {
1461 writeln!(output, "digraph G {{").unwrap();
1462 writeln!(output, " graph [rankdir=LR, nodesep=0.8, ranksep=1.2];").unwrap();
1463 writeln!(output, " node [shape=plain, fontname=\"Noto Sans\"];").unwrap();
1464 writeln!(output, " edge [fontname=\"Noto Sans\"];").unwrap();
1465
1466 let sections = match (&self.graphs, mission_id) {
1467 (Simple(graph), _) => vec![RenderSection { label: None, graph }],
1468 (Missions(graphs), Some(id)) => {
1469 let graph = graphs
1470 .get(id)
1471 .ok_or_else(|| CuError::from(format!("Mission {id} not found")))?;
1472 vec![RenderSection {
1473 label: Some(id.to_string()),
1474 graph,
1475 }]
1476 }
1477 (Missions(graphs), None) => {
1478 let mut missions: Vec<_> = graphs.iter().collect();
1479 missions.sort_by(|a, b| a.0.cmp(b.0));
1480 missions
1481 .into_iter()
1482 .map(|(label, graph)| RenderSection {
1483 label: Some(label.clone()),
1484 graph,
1485 })
1486 .collect()
1487 }
1488 };
1489
1490 for section in sections {
1491 self.render_section(output, section.graph, section.label.as_deref())?;
1492 }
1493
1494 writeln!(output, "}}").unwrap();
1495 Ok(())
1496 }
1497
1498 #[allow(dead_code)]
1499 pub fn get_all_instances_configs(
1500 &self,
1501 mission_id: Option<&str>,
1502 ) -> Vec<Option<&ComponentConfig>> {
1503 let graph = self.graphs.get_graph(mission_id).unwrap();
1504 graph
1505 .get_all_nodes()
1506 .iter()
1507 .map(|(_, node)| node.get_instance_config())
1508 .collect()
1509 }
1510
1511 #[allow(dead_code)]
1512 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1513 self.graphs.get_graph(mission_id)
1514 }
1515
1516 #[allow(dead_code)]
1517 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1518 self.graphs.get_graph_mut(mission_id)
1519 }
1520
1521 #[allow(dead_code)]
1522 pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1523 self.monitor.as_ref()
1524 }
1525
1526 #[allow(dead_code)]
1527 pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
1528 self.runtime.as_ref()
1529 }
1530
1531 pub fn validate_logging_config(&self) -> CuResult<()> {
1534 if let Some(logging) = &self.logging {
1535 return logging.validate();
1536 }
1537 Ok(())
1538 }
1539}
1540
1541#[cfg(feature = "std")]
1542#[derive(Default)]
1543pub(crate) struct PortLookup {
1544 pub inputs: HashMap<String, String>,
1545 pub outputs: HashMap<String, String>,
1546 pub default_input: Option<String>,
1547 pub default_output: Option<String>,
1548}
1549
1550#[cfg(feature = "std")]
1551#[derive(Clone)]
1552pub(crate) struct RenderNode {
1553 pub id: String,
1554 pub type_name: String,
1555 pub flavor: Flavor,
1556 pub inputs: Vec<String>,
1557 pub outputs: Vec<String>,
1558}
1559
1560#[cfg(feature = "std")]
1561#[derive(Clone)]
1562pub(crate) struct RenderConnection {
1563 pub src: String,
1564 pub src_port: Option<String>,
1565 pub dst: String,
1566 pub dst_port: Option<String>,
1567 pub msg: String,
1568}
1569
1570#[cfg(feature = "std")]
1571pub(crate) struct RenderTopology {
1572 pub nodes: Vec<RenderNode>,
1573 pub connections: Vec<RenderConnection>,
1574}
1575
1576#[cfg(feature = "std")]
1577impl RenderTopology {
1578 pub fn sort_connections(&mut self) {
1579 self.connections.sort_by(|a, b| {
1580 a.src
1581 .cmp(&b.src)
1582 .then(a.dst.cmp(&b.dst))
1583 .then(a.msg.cmp(&b.msg))
1584 });
1585 }
1586}
1587
1588#[cfg(feature = "std")]
1589#[allow(dead_code)]
1590struct RenderSection<'a> {
1591 label: Option<String>,
1592 graph: &'a CuGraph,
1593}
1594
1595#[cfg(feature = "std")]
1596impl CuConfig {
1597 #[allow(dead_code)]
1598 fn render_section(
1599 &self,
1600 output: &mut dyn std::io::Write,
1601 graph: &CuGraph,
1602 label: Option<&str>,
1603 ) -> CuResult<()> {
1604 use std::fmt::Write as FmtWrite;
1605
1606 let mut topology = build_render_topology(graph, &self.bridges);
1607 topology.nodes.sort_by(|a, b| a.id.cmp(&b.id));
1608 topology.sort_connections();
1609
1610 let cluster_id = label.map(|lbl| format!("cluster_{}", sanitize_identifier(lbl)));
1611 if let Some(ref cluster_id) = cluster_id {
1612 writeln!(output, " subgraph \"{cluster_id}\" {{").unwrap();
1613 writeln!(
1614 output,
1615 " label=<<B>Mission: {}</B>>;",
1616 encode_text(label.unwrap())
1617 )
1618 .unwrap();
1619 writeln!(
1620 output,
1621 " labelloc=t; labeljust=l; color=\"#bbbbbb\"; style=\"rounded\"; margin=20;"
1622 )
1623 .unwrap();
1624 }
1625 let indent = if cluster_id.is_some() {
1626 " "
1627 } else {
1628 " "
1629 };
1630 let node_prefix = label
1631 .map(|lbl| format!("{}__", sanitize_identifier(lbl)))
1632 .unwrap_or_default();
1633
1634 let mut port_lookup: HashMap<String, PortLookup> = HashMap::new();
1635 let mut id_lookup: HashMap<String, String> = HashMap::new();
1636
1637 for node in &topology.nodes {
1638 let node_idx = graph
1639 .get_node_id_by_name(node.id.as_str())
1640 .ok_or_else(|| CuError::from(format!("Node '{}' missing from graph", node.id)))?;
1641 let node_weight = graph
1642 .get_node(node_idx)
1643 .ok_or_else(|| CuError::from(format!("Node '{}' missing weight", node.id)))?;
1644
1645 let is_src = graph.get_dst_edges(node_idx).unwrap_or_default().is_empty();
1646 let is_sink = graph.get_src_edges(node_idx).unwrap_or_default().is_empty();
1647
1648 let fillcolor = match node.flavor {
1649 Flavor::Bridge => "#faedcd",
1650 Flavor::Task if is_src => "#ddefc7",
1651 Flavor::Task if is_sink => "#cce0ff",
1652 _ => "#f2f2f2",
1653 };
1654
1655 let port_base = format!("{}{}", node_prefix, sanitize_identifier(&node.id));
1656 let (inputs_table, input_map, default_input) =
1657 build_port_table("Inputs", &node.inputs, &port_base, "in");
1658 let (outputs_table, output_map, default_output) =
1659 build_port_table("Outputs", &node.outputs, &port_base, "out");
1660 let config_html = node_weight.config.as_ref().and_then(build_config_table);
1661
1662 let mut label_html = String::new();
1663 write!(
1664 label_html,
1665 "<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"6\" COLOR=\"gray\" BGCOLOR=\"white\">"
1666 )
1667 .unwrap();
1668 write!(
1669 label_html,
1670 "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\" BGCOLOR=\"{fillcolor}\"><FONT POINT-SIZE=\"12\"><B>{}</B></FONT><BR/><FONT COLOR=\"dimgray\">[{}]</FONT></TD></TR>",
1671 encode_text(&node.id),
1672 encode_text(&node.type_name)
1673 )
1674 .unwrap();
1675 write!(
1676 label_html,
1677 "<TR><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{inputs_table}</TD><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{outputs_table}</TD></TR>"
1678 )
1679 .unwrap();
1680
1681 if let Some(config_html) = config_html {
1682 write!(
1683 label_html,
1684 "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\">{config_html}</TD></TR>"
1685 )
1686 .unwrap();
1687 }
1688
1689 label_html.push_str("</TABLE>");
1690
1691 let identifier_raw = if node_prefix.is_empty() {
1692 node.id.clone()
1693 } else {
1694 format!("{node_prefix}{}", node.id)
1695 };
1696 let identifier = escape_dot_id(&identifier_raw);
1697 writeln!(output, "{indent}\"{identifier}\" [label=<{label_html}>];").unwrap();
1698
1699 id_lookup.insert(node.id.clone(), identifier);
1700 port_lookup.insert(
1701 node.id.clone(),
1702 PortLookup {
1703 inputs: input_map,
1704 outputs: output_map,
1705 default_input,
1706 default_output,
1707 },
1708 );
1709 }
1710
1711 for cnx in &topology.connections {
1712 let src_id = id_lookup
1713 .get(&cnx.src)
1714 .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.src)))?;
1715 let dst_id = id_lookup
1716 .get(&cnx.dst)
1717 .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.dst)))?;
1718 let src_suffix = port_lookup
1719 .get(&cnx.src)
1720 .and_then(|lookup| lookup.resolve_output(cnx.src_port.as_deref()))
1721 .map(|port| format!(":\"{port}\":e"))
1722 .unwrap_or_default();
1723 let dst_suffix = port_lookup
1724 .get(&cnx.dst)
1725 .and_then(|lookup| lookup.resolve_input(cnx.dst_port.as_deref()))
1726 .map(|port| format!(":\"{port}\":w"))
1727 .unwrap_or_default();
1728 let msg = encode_text(&cnx.msg);
1729 writeln!(
1730 output,
1731 "{indent}\"{src_id}\"{src_suffix} -> \"{dst_id}\"{dst_suffix} [label=< <B><FONT COLOR=\"gray\">{msg}</FONT></B> >];"
1732 )
1733 .unwrap();
1734 }
1735
1736 if cluster_id.is_some() {
1737 writeln!(output, " }}").unwrap();
1738 }
1739
1740 Ok(())
1741 }
1742}
1743
1744#[cfg(feature = "std")]
1745pub(crate) fn build_render_topology(graph: &CuGraph, bridges: &[BridgeConfig]) -> RenderTopology {
1746 let mut bridge_lookup = HashMap::new();
1747 for bridge in bridges {
1748 bridge_lookup.insert(bridge.id.as_str(), bridge);
1749 }
1750
1751 let mut nodes: Vec<RenderNode> = Vec::new();
1752 let mut node_lookup: HashMap<String, usize> = HashMap::new();
1753 for (_, node) in graph.get_all_nodes() {
1754 let node_id = node.get_id();
1755 let mut inputs = Vec::new();
1756 let mut outputs = Vec::new();
1757 if node.get_flavor() == Flavor::Bridge
1758 && let Some(bridge) = bridge_lookup.get(node_id.as_str())
1759 {
1760 for channel in &bridge.channels {
1761 match channel {
1762 BridgeChannelConfigRepresentation::Rx { id, .. } => outputs.push(id.clone()),
1764 BridgeChannelConfigRepresentation::Tx { id, .. } => inputs.push(id.clone()),
1766 }
1767 }
1768 }
1769
1770 node_lookup.insert(node_id.clone(), nodes.len());
1771 nodes.push(RenderNode {
1772 id: node_id,
1773 type_name: node.get_type().to_string(),
1774 flavor: node.get_flavor(),
1775 inputs,
1776 outputs,
1777 });
1778 }
1779
1780 let mut output_port_lookup: Vec<HashMap<String, String>> = vec![HashMap::new(); nodes.len()];
1781 let mut output_edges: Vec<_> = graph.0.edge_references().collect();
1782 output_edges.sort_by_key(|edge| edge.id().index());
1783 for edge in output_edges {
1784 let cnx = edge.weight();
1785 if let Some(&idx) = node_lookup.get(&cnx.src)
1786 && nodes[idx].flavor == Flavor::Task
1787 && cnx.src_channel.is_none()
1788 {
1789 let port_map = &mut output_port_lookup[idx];
1790 if !port_map.contains_key(&cnx.msg) {
1791 let label = format!("out{}: {}", port_map.len(), cnx.msg);
1792 port_map.insert(cnx.msg.clone(), label.clone());
1793 nodes[idx].outputs.push(label);
1794 }
1795 }
1796 }
1797
1798 let mut auto_input_counts = vec![0usize; nodes.len()];
1799 for edge in graph.0.edge_references() {
1800 let cnx = edge.weight();
1801 if let Some(&idx) = node_lookup.get(&cnx.dst)
1802 && nodes[idx].flavor == Flavor::Task
1803 && cnx.dst_channel.is_none()
1804 {
1805 auto_input_counts[idx] += 1;
1806 }
1807 }
1808
1809 let mut next_auto_input = vec![0usize; nodes.len()];
1810 let mut connections = Vec::new();
1811 for edge in graph.0.edge_references() {
1812 let cnx = edge.weight();
1813 let mut src_port = cnx.src_channel.clone();
1814 let mut dst_port = cnx.dst_channel.clone();
1815
1816 if let Some(&idx) = node_lookup.get(&cnx.src) {
1817 let node = &mut nodes[idx];
1818 if node.flavor == Flavor::Task && src_port.is_none() {
1819 src_port = output_port_lookup[idx].get(&cnx.msg).cloned();
1820 }
1821 }
1822 if let Some(&idx) = node_lookup.get(&cnx.dst) {
1823 let node = &mut nodes[idx];
1824 if node.flavor == Flavor::Task && dst_port.is_none() {
1825 let count = auto_input_counts[idx];
1826 let next = if count <= 1 {
1827 "in".to_string()
1828 } else {
1829 let next = format!("in.{}", next_auto_input[idx]);
1830 next_auto_input[idx] += 1;
1831 next
1832 };
1833 node.inputs.push(next.clone());
1834 dst_port = Some(next);
1835 }
1836 }
1837
1838 connections.push(RenderConnection {
1839 src: cnx.src.clone(),
1840 src_port,
1841 dst: cnx.dst.clone(),
1842 dst_port,
1843 msg: cnx.msg.clone(),
1844 });
1845 }
1846
1847 RenderTopology { nodes, connections }
1848}
1849
1850#[cfg(feature = "std")]
1851impl PortLookup {
1852 pub fn resolve_input(&self, name: Option<&str>) -> Option<&str> {
1853 if let Some(name) = name
1854 && let Some(port) = self.inputs.get(name)
1855 {
1856 return Some(port.as_str());
1857 }
1858 self.default_input.as_deref()
1859 }
1860
1861 pub fn resolve_output(&self, name: Option<&str>) -> Option<&str> {
1862 if let Some(name) = name
1863 && let Some(port) = self.outputs.get(name)
1864 {
1865 return Some(port.as_str());
1866 }
1867 self.default_output.as_deref()
1868 }
1869}
1870
1871#[cfg(feature = "std")]
1872#[allow(dead_code)]
1873fn build_port_table(
1874 title: &str,
1875 names: &[String],
1876 base_id: &str,
1877 prefix: &str,
1878) -> (String, HashMap<String, String>, Option<String>) {
1879 use std::fmt::Write as FmtWrite;
1880
1881 let mut html = String::new();
1882 write!(
1883 html,
1884 "<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">"
1885 )
1886 .unwrap();
1887 write!(
1888 html,
1889 "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT></TD></TR>",
1890 encode_text(title)
1891 )
1892 .unwrap();
1893
1894 let mut lookup = HashMap::new();
1895 let mut default_port = None;
1896
1897 if names.is_empty() {
1898 html.push_str("<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"lightgray\">—</FONT></TD></TR>");
1899 } else {
1900 for (idx, name) in names.iter().enumerate() {
1901 let port_id = format!("{base_id}_{prefix}_{idx}");
1902 write!(
1903 html,
1904 "<TR><TD PORT=\"{port_id}\" ALIGN=\"LEFT\">{}</TD></TR>",
1905 encode_text(name)
1906 )
1907 .unwrap();
1908 lookup.insert(name.clone(), port_id.clone());
1909 if idx == 0 {
1910 default_port = Some(port_id);
1911 }
1912 }
1913 }
1914
1915 html.push_str("</TABLE>");
1916 (html, lookup, default_port)
1917}
1918
1919#[cfg(feature = "std")]
1920#[allow(dead_code)]
1921fn build_config_table(config: &ComponentConfig) -> Option<String> {
1922 use std::fmt::Write as FmtWrite;
1923
1924 if config.0.is_empty() {
1925 return None;
1926 }
1927
1928 let mut entries: Vec<_> = config.0.iter().collect();
1929 entries.sort_by(|a, b| a.0.cmp(b.0));
1930
1931 let mut html = String::new();
1932 html.push_str("<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">");
1933 for (key, value) in entries {
1934 let value_txt = format!("{value}");
1935 write!(
1936 html,
1937 "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT> = {}</TD></TR>",
1938 encode_text(key),
1939 encode_text(&value_txt)
1940 )
1941 .unwrap();
1942 }
1943 html.push_str("</TABLE>");
1944 Some(html)
1945}
1946
1947#[cfg(feature = "std")]
1948#[allow(dead_code)]
1949fn sanitize_identifier(value: &str) -> String {
1950 value
1951 .chars()
1952 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
1953 .collect()
1954}
1955
1956#[cfg(feature = "std")]
1957#[allow(dead_code)]
1958fn escape_dot_id(value: &str) -> String {
1959 let mut escaped = String::with_capacity(value.len());
1960 for ch in value.chars() {
1961 match ch {
1962 '"' => escaped.push_str("\\\""),
1963 '\\' => escaped.push_str("\\\\"),
1964 _ => escaped.push(ch),
1965 }
1966 }
1967 escaped
1968}
1969
1970impl LoggingConfig {
1971 pub fn validate(&self) -> CuResult<()> {
1973 if let Some(section_size_mib) = self.section_size_mib
1974 && let Some(slab_size_mib) = self.slab_size_mib
1975 && section_size_mib > slab_size_mib
1976 {
1977 return Err(CuError::from(format!(
1978 "Section size ({section_size_mib} MiB) cannot be larger than slab size ({slab_size_mib} MiB). Adjust the parameters accordingly."
1979 )));
1980 }
1981
1982 Ok(())
1983 }
1984}
1985
1986#[allow(dead_code)] fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
1988 let mut result = content.to_string();
1989
1990 for (key, value) in params {
1991 let pattern = format!("{{{{{key}}}}}");
1992 result = result.replace(&pattern, &value.to_string());
1993 }
1994
1995 result
1996}
1997
1998#[cfg(feature = "std")]
2000fn process_includes(
2001 file_path: &str,
2002 base_representation: CuConfigRepresentation,
2003 processed_files: &mut Vec<String>,
2004) -> CuResult<CuConfigRepresentation> {
2005 processed_files.push(file_path.to_string());
2007
2008 let mut result = base_representation;
2009
2010 if let Some(includes) = result.includes.take() {
2011 for include in includes {
2012 let include_path = if include.path.starts_with('/') {
2013 include.path.clone()
2014 } else {
2015 let current_dir = std::path::Path::new(file_path)
2016 .parent()
2017 .unwrap_or_else(|| std::path::Path::new(""))
2018 .to_string_lossy()
2019 .to_string();
2020
2021 format!("{}/{}", current_dir, include.path)
2022 };
2023
2024 let include_content = read_to_string(&include_path).map_err(|e| {
2025 CuError::from(format!("Failed to read include file: {include_path}"))
2026 .add_cause(e.to_string().as_str())
2027 })?;
2028
2029 let processed_content = substitute_parameters(&include_content, &include.params);
2030
2031 let mut included_representation: CuConfigRepresentation = match Options::default()
2032 .with_default_extension(Extensions::IMPLICIT_SOME)
2033 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2034 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2035 .from_str(&processed_content)
2036 {
2037 Ok(rep) => rep,
2038 Err(e) => {
2039 return Err(CuError::from(format!(
2040 "Failed to parse include file: {} - Error: {} at position {}",
2041 include_path, e.code, e.span
2042 )));
2043 }
2044 };
2045
2046 included_representation =
2047 process_includes(&include_path, included_representation, processed_files)?;
2048
2049 if let Some(included_tasks) = included_representation.tasks {
2050 if result.tasks.is_none() {
2051 result.tasks = Some(included_tasks);
2052 } else {
2053 let mut tasks = result.tasks.take().unwrap();
2054 for included_task in included_tasks {
2055 if !tasks.iter().any(|t| t.id == included_task.id) {
2056 tasks.push(included_task);
2057 }
2058 }
2059 result.tasks = Some(tasks);
2060 }
2061 }
2062
2063 if let Some(included_bridges) = included_representation.bridges {
2064 if result.bridges.is_none() {
2065 result.bridges = Some(included_bridges);
2066 } else {
2067 let mut bridges = result.bridges.take().unwrap();
2068 for included_bridge in included_bridges {
2069 if !bridges.iter().any(|b| b.id == included_bridge.id) {
2070 bridges.push(included_bridge);
2071 }
2072 }
2073 result.bridges = Some(bridges);
2074 }
2075 }
2076
2077 if let Some(included_resources) = included_representation.resources {
2078 if result.resources.is_none() {
2079 result.resources = Some(included_resources);
2080 } else {
2081 let mut resources = result.resources.take().unwrap();
2082 for included_resource in included_resources {
2083 if !resources.iter().any(|r| r.id == included_resource.id) {
2084 resources.push(included_resource);
2085 }
2086 }
2087 result.resources = Some(resources);
2088 }
2089 }
2090
2091 if let Some(included_cnx) = included_representation.cnx {
2092 if result.cnx.is_none() {
2093 result.cnx = Some(included_cnx);
2094 } else {
2095 let mut cnx = result.cnx.take().unwrap();
2096 for included_c in included_cnx {
2097 if !cnx
2098 .iter()
2099 .any(|c| c.src == included_c.src && c.dst == included_c.dst)
2100 {
2101 cnx.push(included_c);
2102 }
2103 }
2104 result.cnx = Some(cnx);
2105 }
2106 }
2107
2108 if result.monitor.is_none() {
2109 result.monitor = included_representation.monitor;
2110 }
2111
2112 if result.logging.is_none() {
2113 result.logging = included_representation.logging;
2114 }
2115
2116 if result.runtime.is_none() {
2117 result.runtime = included_representation.runtime;
2118 }
2119
2120 if let Some(included_missions) = included_representation.missions {
2121 if result.missions.is_none() {
2122 result.missions = Some(included_missions);
2123 } else {
2124 let mut missions = result.missions.take().unwrap();
2125 for included_mission in included_missions {
2126 if !missions.iter().any(|m| m.id == included_mission.id) {
2127 missions.push(included_mission);
2128 }
2129 }
2130 result.missions = Some(missions);
2131 }
2132 }
2133 }
2134 }
2135
2136 Ok(result)
2137}
2138
2139#[cfg(feature = "std")]
2141pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
2142 let config_content = read_to_string(config_filename).map_err(|e| {
2143 CuError::from(format!(
2144 "Failed to read configuration file: {:?}",
2145 &config_filename
2146 ))
2147 .add_cause(e.to_string().as_str())
2148 })?;
2149 read_configuration_str(config_content, Some(config_filename))
2150}
2151
2152fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
2156 Options::default()
2157 .with_default_extension(Extensions::IMPLICIT_SOME)
2158 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2159 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2160 .from_str(content)
2161 .map_err(|e| {
2162 CuError::from(format!(
2163 "Failed to parse configuration: Error: {} at position {}",
2164 e.code, e.span
2165 ))
2166 })
2167}
2168
2169fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
2172 #[allow(unused_mut)]
2173 let mut cuconfig = CuConfig::deserialize_impl(representation)
2174 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
2175
2176 #[cfg(feature = "std")]
2177 cuconfig.ensure_threadpool_bundle();
2178
2179 cuconfig.validate_logging_config()?;
2180
2181 Ok(cuconfig)
2182}
2183
2184#[allow(unused_variables)]
2185pub fn read_configuration_str(
2186 config_content: String,
2187 file_path: Option<&str>,
2188) -> CuResult<CuConfig> {
2189 let representation = parse_config_string(&config_content)?;
2191
2192 #[cfg(feature = "std")]
2195 let representation = if let Some(path) = file_path {
2196 process_includes(path, representation, &mut Vec::new())?
2197 } else {
2198 representation
2199 };
2200
2201 config_representation_to_config(representation)
2203}
2204
2205#[cfg(test)]
2207mod tests {
2208 use super::*;
2209 #[cfg(not(feature = "std"))]
2210 use alloc::vec;
2211
2212 #[test]
2213 fn test_plain_serialize() {
2214 let mut config = CuConfig::default();
2215 let graph = config.get_graph_mut(None).unwrap();
2216 let n1 = graph
2217 .add_node(Node::new("test1", "package::Plugin1"))
2218 .unwrap();
2219 let n2 = graph
2220 .add_node(Node::new("test2", "package::Plugin2"))
2221 .unwrap();
2222 graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
2223 let serialized = config.serialize_ron();
2224 let deserialized = CuConfig::deserialize_ron(&serialized);
2225 let graph = config.graphs.get_graph(None).unwrap();
2226 let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
2227 assert_eq!(graph.node_count(), deserialized_graph.node_count());
2228 assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
2229 }
2230
2231 #[test]
2232 fn test_serialize_with_params() {
2233 let mut config = CuConfig::default();
2234 let graph = config.get_graph_mut(None).unwrap();
2235 let mut camera = Node::new("copper-camera", "camerapkg::Camera");
2236 camera.set_param::<Value>("resolution-height", 1080.into());
2237 graph.add_node(camera).unwrap();
2238 let serialized = config.serialize_ron();
2239 let config = CuConfig::deserialize_ron(&serialized);
2240 let deserialized = config.get_graph(None).unwrap();
2241 assert_eq!(
2242 deserialized
2243 .get_node(0)
2244 .unwrap()
2245 .get_param::<i32>("resolution-height")
2246 .unwrap(),
2247 1080
2248 );
2249 }
2250
2251 #[test]
2252 #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:9-1:10")]
2253 fn test_deserialization_error() {
2254 let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
2256 CuConfig::deserialize_ron(txt);
2257 }
2258 #[test]
2259 fn test_missions() {
2260 let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
2261 let config = CuConfig::deserialize_ron(txt);
2262 let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
2263 assert!(graph.node_count() == 0);
2264 let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
2265 assert!(graph.node_count() == 0);
2266 }
2267
2268 #[test]
2269 fn test_monitor() {
2270 let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
2271 let config = CuConfig::deserialize_ron(txt);
2272 assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
2273
2274 let txt =
2275 r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
2276 let config = CuConfig::deserialize_ron(txt);
2277 assert_eq!(
2278 config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
2279 4u8.into()
2280 );
2281 }
2282
2283 #[test]
2284 #[cfg(feature = "std")]
2285 fn test_render_topology_multi_input_ports() {
2286 let mut config = CuConfig::default();
2287 let graph = config.get_graph_mut(None).unwrap();
2288 let src1 = graph.add_node(Node::new("src1", "tasks::Source1")).unwrap();
2289 let src2 = graph.add_node(Node::new("src2", "tasks::Source2")).unwrap();
2290 let dst = graph.add_node(Node::new("dst", "tasks::Dst")).unwrap();
2291 graph.connect(src1, dst, "msg::A").unwrap();
2292 graph.connect(src2, dst, "msg::B").unwrap();
2293
2294 let topology = build_render_topology(graph, &[]);
2295 let dst_node = topology
2296 .nodes
2297 .iter()
2298 .find(|node| node.id == "dst")
2299 .expect("missing dst node");
2300 assert_eq!(dst_node.inputs.len(), 2);
2301
2302 let mut dst_ports: Vec<_> = topology
2303 .connections
2304 .iter()
2305 .filter(|cnx| cnx.dst == "dst")
2306 .map(|cnx| cnx.dst_port.as_deref().expect("missing dst port"))
2307 .collect();
2308 dst_ports.sort();
2309 assert_eq!(dst_ports, vec!["in.0", "in.1"]);
2310 }
2311
2312 #[test]
2313 fn test_logging_parameters() {
2314 let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
2316
2317 let config = CuConfig::deserialize_ron(txt);
2318 assert!(config.logging.is_some());
2319 let logging_config = config.logging.unwrap();
2320 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
2321 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
2322 assert!(!logging_config.enable_task_logging);
2323
2324 let txt =
2326 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
2327 let config = CuConfig::deserialize_ron(txt);
2328 assert!(config.logging.is_some());
2329 let logging_config = config.logging.unwrap();
2330 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
2331 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
2332 assert!(logging_config.enable_task_logging);
2333 }
2334
2335 #[test]
2336 fn test_bridge_parsing() {
2337 let txt = r#"
2338 (
2339 tasks: [
2340 (id: "dst", type: "tasks::Destination"),
2341 (id: "src", type: "tasks::Source"),
2342 ],
2343 bridges: [
2344 (
2345 id: "radio",
2346 type: "tasks::SerialBridge",
2347 config: { "path": "/dev/ttyACM0", "baud": 921600 },
2348 channels: [
2349 Rx ( id: "status", route: "sys/status" ),
2350 Tx ( id: "motor", route: "motor/cmd" ),
2351 ],
2352 ),
2353 ],
2354 cnx: [
2355 (src: "radio/status", dst: "dst", msg: "mymsgs::Status"),
2356 (src: "src", dst: "radio/motor", msg: "mymsgs::MotorCmd"),
2357 ],
2358 )
2359 "#;
2360
2361 let config = CuConfig::deserialize_ron(txt);
2362 assert_eq!(config.bridges.len(), 1);
2363 let bridge = &config.bridges[0];
2364 assert_eq!(bridge.id, "radio");
2365 assert_eq!(bridge.channels.len(), 2);
2366 match &bridge.channels[0] {
2367 BridgeChannelConfigRepresentation::Rx { id, route, .. } => {
2368 assert_eq!(id, "status");
2369 assert_eq!(route.as_deref(), Some("sys/status"));
2370 }
2371 _ => panic!("expected Rx channel"),
2372 }
2373 match &bridge.channels[1] {
2374 BridgeChannelConfigRepresentation::Tx { id, route, .. } => {
2375 assert_eq!(id, "motor");
2376 assert_eq!(route.as_deref(), Some("motor/cmd"));
2377 }
2378 _ => panic!("expected Tx channel"),
2379 }
2380 let graph = config.graphs.get_graph(None).unwrap();
2381 let bridge_id = graph
2382 .get_node_id_by_name("radio")
2383 .expect("bridge node missing");
2384 let bridge_node = graph.get_node(bridge_id).unwrap();
2385 assert_eq!(bridge_node.get_flavor(), Flavor::Bridge);
2386
2387 let mut edges = Vec::new();
2389 for edge_idx in graph.0.edge_indices() {
2390 edges.push(graph.0[edge_idx].clone());
2391 }
2392 assert_eq!(edges.len(), 2);
2393 let status_edge = edges
2394 .iter()
2395 .find(|e| e.dst == "dst")
2396 .expect("status edge missing");
2397 assert_eq!(status_edge.src_channel.as_deref(), Some("status"));
2398 assert!(status_edge.dst_channel.is_none());
2399 let motor_edge = edges
2400 .iter()
2401 .find(|e| e.dst_channel.is_some())
2402 .expect("motor edge missing");
2403 assert_eq!(motor_edge.dst_channel.as_deref(), Some("motor"));
2404 }
2405
2406 #[test]
2407 fn test_bridge_roundtrip() {
2408 let mut config = CuConfig::default();
2409 let mut bridge_config = ComponentConfig::default();
2410 bridge_config.set("port", "/dev/ttyACM0".to_string());
2411 config.bridges.push(BridgeConfig {
2412 id: "radio".to_string(),
2413 type_: "tasks::SerialBridge".to_string(),
2414 config: Some(bridge_config),
2415 resources: None,
2416 missions: None,
2417 channels: vec![
2418 BridgeChannelConfigRepresentation::Rx {
2419 id: "status".to_string(),
2420 route: Some("sys/status".to_string()),
2421 config: None,
2422 },
2423 BridgeChannelConfigRepresentation::Tx {
2424 id: "motor".to_string(),
2425 route: Some("motor/cmd".to_string()),
2426 config: None,
2427 },
2428 ],
2429 });
2430
2431 let serialized = config.serialize_ron();
2432 assert!(
2433 serialized.contains("bridges"),
2434 "bridges section missing from serialized config"
2435 );
2436 let deserialized = CuConfig::deserialize_ron(&serialized);
2437 assert_eq!(deserialized.bridges.len(), 1);
2438 let bridge = &deserialized.bridges[0];
2439 assert_eq!(bridge.channels.len(), 2);
2440 assert!(matches!(
2441 bridge.channels[0],
2442 BridgeChannelConfigRepresentation::Rx { .. }
2443 ));
2444 assert!(matches!(
2445 bridge.channels[1],
2446 BridgeChannelConfigRepresentation::Tx { .. }
2447 ));
2448 }
2449
2450 #[test]
2451 fn test_resource_parsing() {
2452 let txt = r#"
2453 (
2454 resources: [
2455 (
2456 id: "fc",
2457 provider: "copper_board_px4::Px4Bundle",
2458 config: { "baud": 921600 },
2459 missions: ["m1"],
2460 ),
2461 (
2462 id: "misc",
2463 provider: "cu29_runtime::StdClockBundle",
2464 ),
2465 ],
2466 )
2467 "#;
2468
2469 let config = CuConfig::deserialize_ron(txt);
2470 assert_eq!(config.resources.len(), 2);
2471 let fc = &config.resources[0];
2472 assert_eq!(fc.id, "fc");
2473 assert_eq!(fc.provider, "copper_board_px4::Px4Bundle");
2474 assert_eq!(fc.missions.as_deref(), Some(&["m1".to_string()][..]));
2475 let baud: u32 = fc
2476 .config
2477 .as_ref()
2478 .and_then(|cfg| cfg.get("baud"))
2479 .expect("missing baud");
2480 assert_eq!(baud, 921_600);
2481 let misc = &config.resources[1];
2482 assert_eq!(misc.id, "misc");
2483 assert_eq!(misc.provider, "cu29_runtime::StdClockBundle");
2484 assert!(misc.config.is_none());
2485 }
2486
2487 #[test]
2488 fn test_resource_roundtrip() {
2489 let mut config = CuConfig::default();
2490 let mut bundle_cfg = ComponentConfig::default();
2491 bundle_cfg.set("path", "/dev/ttyACM0".to_string());
2492 config.resources.push(ResourceBundleConfig {
2493 id: "fc".to_string(),
2494 provider: "copper_board_px4::Px4Bundle".to_string(),
2495 config: Some(bundle_cfg),
2496 missions: Some(vec!["m1".to_string()]),
2497 });
2498
2499 let serialized = config.serialize_ron();
2500 let deserialized = CuConfig::deserialize_ron(&serialized);
2501 assert_eq!(deserialized.resources.len(), 1);
2502 let res = &deserialized.resources[0];
2503 assert_eq!(res.id, "fc");
2504 assert_eq!(res.provider, "copper_board_px4::Px4Bundle");
2505 assert_eq!(res.missions.as_deref(), Some(&["m1".to_string()][..]));
2506 let path: String = res
2507 .config
2508 .as_ref()
2509 .and_then(|cfg| cfg.get("path"))
2510 .expect("missing path");
2511 assert_eq!(path, "/dev/ttyACM0");
2512 }
2513
2514 #[test]
2515 fn test_bridge_channel_config() {
2516 let txt = r#"
2517 (
2518 tasks: [],
2519 bridges: [
2520 (
2521 id: "radio",
2522 type: "tasks::SerialBridge",
2523 channels: [
2524 Rx ( id: "status", route: "sys/status", config: { "filter": "fast" } ),
2525 Tx ( id: "imu", route: "telemetry/imu", config: { "rate": 100 } ),
2526 ],
2527 ),
2528 ],
2529 cnx: [],
2530 )
2531 "#;
2532
2533 let config = CuConfig::deserialize_ron(txt);
2534 let bridge = &config.bridges[0];
2535 match &bridge.channels[0] {
2536 BridgeChannelConfigRepresentation::Rx {
2537 config: Some(cfg), ..
2538 } => {
2539 let val: String = cfg.get("filter").expect("filter missing");
2540 assert_eq!(val, "fast");
2541 }
2542 _ => panic!("expected Rx channel with config"),
2543 }
2544 match &bridge.channels[1] {
2545 BridgeChannelConfigRepresentation::Tx {
2546 config: Some(cfg), ..
2547 } => {
2548 let rate: i32 = cfg.get("rate").expect("rate missing");
2549 assert_eq!(rate, 100);
2550 }
2551 _ => panic!("expected Tx channel with config"),
2552 }
2553 }
2554
2555 #[test]
2556 fn test_task_resources_roundtrip() {
2557 let txt = r#"
2558 (
2559 tasks: [
2560 (
2561 id: "imu",
2562 type: "tasks::ImuDriver",
2563 resources: { "bus": "fc.spi_1", "irq": "fc.gpio_imu" },
2564 ),
2565 ],
2566 cnx: [],
2567 )
2568 "#;
2569
2570 let config = CuConfig::deserialize_ron(txt);
2571 let graph = config.graphs.get_graph(None).unwrap();
2572 let node = graph.get_node(0).expect("missing task node");
2573 let resources = node.get_resources().expect("missing resources map");
2574 assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
2575 assert_eq!(
2576 resources.get("irq").map(String::as_str),
2577 Some("fc.gpio_imu")
2578 );
2579
2580 let serialized = config.serialize_ron();
2581 let deserialized = CuConfig::deserialize_ron(&serialized);
2582 let graph = deserialized.graphs.get_graph(None).unwrap();
2583 let node = graph.get_node(0).expect("missing task node");
2584 let resources = node
2585 .get_resources()
2586 .expect("missing resources map after roundtrip");
2587 assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
2588 assert_eq!(
2589 resources.get("irq").map(String::as_str),
2590 Some("fc.gpio_imu")
2591 );
2592 }
2593
2594 #[test]
2595 fn test_bridge_resources_preserved() {
2596 let mut config = CuConfig::default();
2597 config.resources.push(ResourceBundleConfig {
2598 id: "fc".to_string(),
2599 provider: "board::Bundle".to_string(),
2600 config: None,
2601 missions: None,
2602 });
2603 let bridge_resources = HashMap::from([("serial".to_string(), "fc.serial0".to_string())]);
2604 config.bridges.push(BridgeConfig {
2605 id: "radio".to_string(),
2606 type_: "tasks::SerialBridge".to_string(),
2607 config: None,
2608 resources: Some(bridge_resources),
2609 missions: None,
2610 channels: vec![BridgeChannelConfigRepresentation::Tx {
2611 id: "uplink".to_string(),
2612 route: None,
2613 config: None,
2614 }],
2615 });
2616
2617 let serialized = config.serialize_ron();
2618 let deserialized = CuConfig::deserialize_ron(&serialized);
2619 let graph = deserialized.graphs.get_graph(None).expect("missing graph");
2620 let bridge_id = graph
2621 .get_node_id_by_name("radio")
2622 .expect("bridge node missing");
2623 let node = graph.get_node(bridge_id).expect("missing bridge node");
2624 let resources = node
2625 .get_resources()
2626 .expect("bridge resources were not preserved");
2627 assert_eq!(
2628 resources.get("serial").map(String::as_str),
2629 Some("fc.serial0")
2630 );
2631 }
2632
2633 #[test]
2634 fn test_demo_config_parses() {
2635 let txt = r#"(
2636 resources: [
2637 (
2638 id: "fc",
2639 provider: "crate::resources::RadioBundle",
2640 ),
2641 ],
2642 tasks: [
2643 (id: "thr", type: "tasks::ThrottleControl"),
2644 (id: "tele0", type: "tasks::TelemetrySink0"),
2645 (id: "tele1", type: "tasks::TelemetrySink1"),
2646 (id: "tele2", type: "tasks::TelemetrySink2"),
2647 (id: "tele3", type: "tasks::TelemetrySink3"),
2648 ],
2649 bridges: [
2650 ( id: "crsf",
2651 type: "cu_crsf::CrsfBridge<SerialResource, SerialPortError>",
2652 resources: { "serial": "fc.serial" },
2653 channels: [
2654 Rx ( id: "rc_rx" ), // receiving RC Channels
2655 Tx ( id: "lq_tx" ), // Sending LineQuality back
2656 ],
2657 ),
2658 (
2659 id: "bdshot",
2660 type: "cu_bdshot::RpBdshotBridge",
2661 channels: [
2662 Tx ( id: "esc0_tx" ),
2663 Tx ( id: "esc1_tx" ),
2664 Tx ( id: "esc2_tx" ),
2665 Tx ( id: "esc3_tx" ),
2666 Rx ( id: "esc0_rx" ),
2667 Rx ( id: "esc1_rx" ),
2668 Rx ( id: "esc2_rx" ),
2669 Rx ( id: "esc3_rx" ),
2670 ],
2671 ),
2672 ],
2673 cnx: [
2674 (src: "crsf/rc_rx", dst: "thr", msg: "cu_crsf::messages::RcChannelsPayload"),
2675 (src: "thr", dst: "bdshot/esc0_tx", msg: "cu_bdshot::EscCommand"),
2676 (src: "thr", dst: "bdshot/esc1_tx", msg: "cu_bdshot::EscCommand"),
2677 (src: "thr", dst: "bdshot/esc2_tx", msg: "cu_bdshot::EscCommand"),
2678 (src: "thr", dst: "bdshot/esc3_tx", msg: "cu_bdshot::EscCommand"),
2679 (src: "bdshot/esc0_rx", dst: "tele0", msg: "cu_bdshot::EscTelemetry"),
2680 (src: "bdshot/esc1_rx", dst: "tele1", msg: "cu_bdshot::EscTelemetry"),
2681 (src: "bdshot/esc2_rx", dst: "tele2", msg: "cu_bdshot::EscTelemetry"),
2682 (src: "bdshot/esc3_rx", dst: "tele3", msg: "cu_bdshot::EscTelemetry"),
2683 ],
2684)"#;
2685 let config = CuConfig::deserialize_ron(txt);
2686 assert_eq!(config.resources.len(), 1);
2687 assert_eq!(config.bridges.len(), 2);
2688 }
2689
2690 #[test]
2691 #[should_panic(expected = "channel 'motor' is Tx and cannot act as a source")]
2692 fn test_bridge_tx_cannot_be_source() {
2693 let txt = r#"
2694 (
2695 tasks: [
2696 (id: "dst", type: "tasks::Destination"),
2697 ],
2698 bridges: [
2699 (
2700 id: "radio",
2701 type: "tasks::SerialBridge",
2702 channels: [
2703 Tx ( id: "motor", route: "motor/cmd" ),
2704 ],
2705 ),
2706 ],
2707 cnx: [
2708 (src: "radio/motor", dst: "dst", msg: "mymsgs::MotorCmd"),
2709 ],
2710 )
2711 "#;
2712
2713 CuConfig::deserialize_ron(txt);
2714 }
2715
2716 #[test]
2717 #[should_panic(expected = "channel 'status' is Rx and cannot act as a destination")]
2718 fn test_bridge_rx_cannot_be_destination() {
2719 let txt = r#"
2720 (
2721 tasks: [
2722 (id: "src", type: "tasks::Source"),
2723 ],
2724 bridges: [
2725 (
2726 id: "radio",
2727 type: "tasks::SerialBridge",
2728 channels: [
2729 Rx ( id: "status", route: "sys/status" ),
2730 ],
2731 ),
2732 ],
2733 cnx: [
2734 (src: "src", dst: "radio/status", msg: "mymsgs::Status"),
2735 ],
2736 )
2737 "#;
2738
2739 CuConfig::deserialize_ron(txt);
2740 }
2741
2742 #[test]
2743 fn test_validate_logging_config() {
2744 let txt =
2746 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
2747 let config = CuConfig::deserialize_ron(txt);
2748 assert!(config.validate_logging_config().is_ok());
2749
2750 let txt =
2752 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
2753 let config = CuConfig::deserialize_ron(txt);
2754 assert!(config.validate_logging_config().is_err());
2755 }
2756
2757 #[test]
2759 fn test_deserialization_edge_id_assignment() {
2760 let txt = r#"(
2763 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2764 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
2765 )"#;
2766 let config = CuConfig::deserialize_ron(txt);
2767 let graph = config.graphs.get_graph(None).unwrap();
2768 assert!(config.validate_logging_config().is_ok());
2769
2770 let src1_id = 0;
2772 assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
2773 let src2_id = 1;
2774 assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
2775
2776 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
2779 assert_eq!(src1_edge_id, 1);
2780 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
2781 assert_eq!(src2_edge_id, 0);
2782 }
2783
2784 #[test]
2785 fn test_simple_missions() {
2786 let txt = r#"(
2788 missions: [ (id: "m1"),
2789 (id: "m2"),
2790 ],
2791 tasks: [(id: "src1", type: "a", missions: ["m1"]),
2792 (id: "src2", type: "b", missions: ["m2"]),
2793 (id: "sink", type: "c")],
2794
2795 cnx: [
2796 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2797 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2798 ],
2799 )
2800 "#;
2801
2802 let config = CuConfig::deserialize_ron(txt);
2803 let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
2804 assert_eq!(m1_graph.edge_count(), 1);
2805 assert_eq!(m1_graph.node_count(), 2);
2806 let index = 0;
2807 let cnx = m1_graph.get_edge_weight(index).unwrap();
2808
2809 assert_eq!(cnx.src, "src1");
2810 assert_eq!(cnx.dst, "sink");
2811 assert_eq!(cnx.msg, "u32");
2812 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2813
2814 let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
2815 assert_eq!(m2_graph.edge_count(), 1);
2816 assert_eq!(m2_graph.node_count(), 2);
2817 let index = 0;
2818 let cnx = m2_graph.get_edge_weight(index).unwrap();
2819 assert_eq!(cnx.src, "src2");
2820 assert_eq!(cnx.dst, "sink");
2821 assert_eq!(cnx.msg, "u32");
2822 assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
2823 }
2824 #[test]
2825 fn test_mission_serde() {
2826 let txt = r#"(
2828 missions: [ (id: "m1"),
2829 (id: "m2"),
2830 ],
2831 tasks: [(id: "src1", type: "a", missions: ["m1"]),
2832 (id: "src2", type: "b", missions: ["m2"]),
2833 (id: "sink", type: "c")],
2834
2835 cnx: [
2836 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2837 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2838 ],
2839 )
2840 "#;
2841
2842 let config = CuConfig::deserialize_ron(txt);
2843 let serialized = config.serialize_ron();
2844 let deserialized = CuConfig::deserialize_ron(&serialized);
2845 let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
2846 assert_eq!(m1_graph.edge_count(), 1);
2847 assert_eq!(m1_graph.node_count(), 2);
2848 let index = 0;
2849 let cnx = m1_graph.get_edge_weight(index).unwrap();
2850 assert_eq!(cnx.src, "src1");
2851 assert_eq!(cnx.dst, "sink");
2852 assert_eq!(cnx.msg, "u32");
2853 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2854 }
2855
2856 #[test]
2857 fn test_keyframe_interval() {
2858 let txt = r#"(
2861 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2862 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2863 logging: ( keyframe_interval: 314 )
2864 )"#;
2865 let config = CuConfig::deserialize_ron(txt);
2866 let logging_config = config.logging.unwrap();
2867 assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
2868 }
2869
2870 #[test]
2871 fn test_default_keyframe_interval() {
2872 let txt = r#"(
2875 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2876 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2877 logging: ( slab_size_mib: 200, section_size_mib: 1024, )
2878 )"#;
2879 let config = CuConfig::deserialize_ron(txt);
2880 let logging_config = config.logging.unwrap();
2881 assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
2882 }
2883}