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};
16use petgraph::visit::EdgeRef;
17#[cfg(feature = "std")]
18use petgraph::visit::IntoEdgeReferences;
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 incoming_neighbor_count(&self, node_id: NodeId) -> usize {
635 self.0.neighbors_directed(node_id.into(), Incoming).count()
636 }
637
638 #[allow(dead_code)]
639 pub fn outgoing_neighbor_count(&self, node_id: NodeId) -> usize {
640 self.0.neighbors_directed(node_id.into(), Outgoing).count()
641 }
642
643 pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
644 self.0.node_indices().collect()
645 }
646
647 pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
648 Ok(self.0.add_node(node).index() as NodeId)
649 }
650
651 #[allow(dead_code)]
652 pub fn connection_exists(&self, source: NodeId, target: NodeId) -> bool {
653 self.0.find_edge(source.into(), target.into()).is_some()
654 }
655
656 pub fn connect_ext(
657 &mut self,
658 source: NodeId,
659 target: NodeId,
660 msg_type: &str,
661 missions: Option<Vec<String>>,
662 src_channel: Option<String>,
663 dst_channel: Option<String>,
664 ) -> CuResult<()> {
665 let (src_id, dst_id) = (
666 self.0
667 .node_weight(source.into())
668 .ok_or("Source node not found")?
669 .id
670 .clone(),
671 self.0
672 .node_weight(target.into())
673 .ok_or("Target node not found")?
674 .id
675 .clone(),
676 );
677
678 let _ = self.0.add_edge(
679 petgraph::stable_graph::NodeIndex::from(source),
680 petgraph::stable_graph::NodeIndex::from(target),
681 Cnx {
682 src: src_id,
683 dst: dst_id,
684 msg: msg_type.to_string(),
685 missions,
686 src_channel,
687 dst_channel,
688 },
689 );
690 Ok(())
691 }
692 #[allow(dead_code)]
696 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
697 self.0.node_weight(node_id.into())
698 }
699
700 #[allow(dead_code)]
701 pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
702 self.0.node_weight(index.into())
703 }
704
705 #[allow(dead_code)]
706 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
707 self.0.node_weight_mut(node_id.into())
708 }
709
710 pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
711 self.0
712 .node_indices()
713 .into_iter()
714 .find(|idx| self.0[*idx].get_id() == name)
715 .map(|i| i.index() as NodeId)
716 }
717
718 #[allow(dead_code)]
719 pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
720 self.0.edge_weight(EdgeIndex::new(index)).cloned()
721 }
722
723 #[allow(dead_code)]
724 pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
725 self.0.node_indices().find_map(|node_index| {
726 if let Some(node) = self.0.node_weight(node_index) {
727 if node.id != node_id {
728 return None;
729 }
730 let edges: Vec<_> = self
731 .0
732 .edges_directed(node_index, Outgoing)
733 .map(|edge| edge.id().index())
734 .collect();
735 if edges.is_empty() {
736 return None;
737 }
738 let cnx = self
739 .0
740 .edge_weight(EdgeIndex::new(edges[0]))
741 .expect("Found an cnx id but could not retrieve it back");
742 return Some(cnx.msg.clone());
743 }
744 None
745 })
746 }
747
748 #[allow(dead_code)]
749 pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
750 self.get_node_input_msg_types(node_id)
751 .and_then(|mut v| v.pop())
752 }
753
754 pub fn get_node_input_msg_types(&self, node_id: &str) -> Option<Vec<String>> {
755 self.0.node_indices().find_map(|node_index| {
756 if let Some(node) = self.0.node_weight(node_index) {
757 if node.id != node_id {
758 return None;
759 }
760 let edges: Vec<_> = self
761 .0
762 .edges_directed(node_index, Incoming)
763 .map(|edge| edge.id().index())
764 .collect();
765 if edges.is_empty() {
766 return None;
767 }
768 let msgs = edges
769 .into_iter()
770 .map(|edge_id| {
771 let cnx = self
772 .0
773 .edge_weight(EdgeIndex::new(edge_id))
774 .expect("Found an cnx id but could not retrieve it back");
775 cnx.msg.clone()
776 })
777 .collect();
778 return Some(msgs);
779 }
780 None
781 })
782 }
783
784 #[allow(dead_code)]
785 pub fn get_connection_msg_type(&self, source: NodeId, target: NodeId) -> Option<&str> {
786 self.0
787 .find_edge(source.into(), target.into())
788 .map(|edge_index| self.0[edge_index].msg.as_str())
789 }
790
791 fn get_edges_by_direction(
793 &self,
794 node_id: NodeId,
795 direction: petgraph::Direction,
796 ) -> CuResult<Vec<usize>> {
797 Ok(self
798 .0
799 .edges_directed(node_id.into(), direction)
800 .map(|edge| edge.id().index())
801 .collect())
802 }
803
804 pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
805 self.get_edges_by_direction(node_id, Outgoing)
806 }
807
808 pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
810 self.get_edges_by_direction(node_id, Incoming)
811 }
812
813 #[allow(dead_code)]
814 pub fn node_count(&self) -> usize {
815 self.0.node_count()
816 }
817
818 #[allow(dead_code)]
819 pub fn edge_count(&self) -> usize {
820 self.0.edge_count()
821 }
822
823 #[allow(dead_code)]
826 pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
827 self.connect_ext(source, target, msg_type, None, None, None)
828 }
829}
830
831impl core::ops::Index<NodeIndex> for CuGraph {
832 type Output = Node;
833
834 fn index(&self, index: NodeIndex) -> &Self::Output {
835 &self.0[index]
836 }
837}
838
839#[derive(Debug, Clone)]
840pub enum ConfigGraphs {
841 Simple(CuGraph),
842 Missions(HashMap<String, CuGraph>),
843}
844
845impl ConfigGraphs {
846 #[allow(dead_code)]
849 pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
850 match self {
851 Simple(graph) => {
852 let mut map = HashMap::new();
853 map.insert("default".to_string(), graph.clone());
854 map
855 }
856 Missions(graphs) => graphs.clone(),
857 }
858 }
859
860 #[allow(dead_code)]
861 pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
862 match self {
863 Simple(graph) => Ok(graph),
864 Missions(graphs) => {
865 if graphs.len() == 1 {
866 Ok(graphs.values().next().unwrap())
867 } else {
868 Err("Cannot get default mission graph from mission config".into())
869 }
870 }
871 }
872 }
873
874 #[allow(dead_code)]
875 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
876 match self {
877 Simple(graph) => {
878 if mission_id.is_none() || mission_id.unwrap() == "default" {
879 Ok(graph)
880 } else {
881 Err("Cannot get mission graph from simple config".into())
882 }
883 }
884 Missions(graphs) => {
885 if let Some(id) = mission_id {
886 graphs
887 .get(id)
888 .ok_or_else(|| format!("Mission {id} not found").into())
889 } else {
890 Err("Mission ID required for mission configs".into())
891 }
892 }
893 }
894 }
895
896 #[allow(dead_code)]
897 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
898 match self {
899 Simple(graph) => {
900 if mission_id.is_none() {
901 Ok(graph)
902 } else {
903 Err("Cannot get mission graph from simple config".into())
904 }
905 }
906 Missions(graphs) => {
907 if let Some(id) = mission_id {
908 graphs
909 .get_mut(id)
910 .ok_or_else(|| format!("Mission {id} not found").into())
911 } else {
912 Err("Mission ID required for mission configs".into())
913 }
914 }
915 }
916 }
917
918 pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
919 match self {
920 Simple(_) => Err("Cannot add mission to simple config".into()),
921 Missions(graphs) => {
922 if graphs.contains_key(mission_id) {
923 Err(format!("Mission {mission_id} already exists").into())
924 } else {
925 let graph = CuGraph::default();
926 graphs.insert(mission_id.to_string(), graph);
927 Ok(graphs.get_mut(mission_id).unwrap())
929 }
930 }
931 }
932 }
933}
934
935#[derive(Debug, Clone)]
941pub struct CuConfig {
942 pub monitor: Option<MonitorConfig>,
944 pub logging: Option<LoggingConfig>,
946 pub runtime: Option<RuntimeConfig>,
948 pub resources: Vec<ResourceBundleConfig>,
950 pub bridges: Vec<BridgeConfig>,
952 pub graphs: ConfigGraphs,
954}
955
956impl CuConfig {
957 #[cfg(feature = "std")]
958 fn ensure_threadpool_bundle(&mut self) {
959 if self
960 .resources
961 .iter()
962 .any(|bundle| bundle.id == "threadpool")
963 {
964 return;
965 }
966
967 let mut config = ComponentConfig::default();
968 config.set("threads", 2u64);
969 self.resources.push(ResourceBundleConfig {
970 id: "threadpool".to_string(),
971 provider: "cu29::resource::ThreadPoolBundle".to_string(),
972 config: Some(config),
973 missions: None,
974 });
975 }
976}
977
978#[derive(Serialize, Deserialize, Default, Debug, Clone)]
979pub struct MonitorConfig {
980 #[serde(rename = "type")]
981 type_: String,
982 #[serde(skip_serializing_if = "Option::is_none")]
983 config: Option<ComponentConfig>,
984}
985
986impl MonitorConfig {
987 #[allow(dead_code)]
988 pub fn get_type(&self) -> &str {
989 &self.type_
990 }
991
992 #[allow(dead_code)]
993 pub fn get_config(&self) -> Option<&ComponentConfig> {
994 self.config.as_ref()
995 }
996}
997
998fn default_as_true() -> bool {
999 true
1000}
1001
1002pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
1003
1004fn default_keyframe_interval() -> Option<u32> {
1005 Some(DEFAULT_KEYFRAME_INTERVAL)
1006}
1007
1008#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1009pub struct LoggingConfig {
1010 #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
1012 pub enable_task_logging: bool,
1013
1014 #[serde(skip_serializing_if = "Option::is_none")]
1016 pub slab_size_mib: Option<u64>,
1017
1018 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub section_size_mib: Option<u64>,
1021
1022 #[serde(
1024 default = "default_keyframe_interval",
1025 skip_serializing_if = "Option::is_none"
1026 )]
1027 pub keyframe_interval: Option<u32>,
1028}
1029
1030#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1031pub struct RuntimeConfig {
1032 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub rate_target_hz: Option<u64>,
1039}
1040
1041#[derive(Serialize, Deserialize, Debug, Clone)]
1043pub struct MissionsConfig {
1044 pub id: String,
1045}
1046
1047#[derive(Serialize, Deserialize, Debug, Clone)]
1049pub struct IncludesConfig {
1050 pub path: String,
1051 pub params: HashMap<String, Value>,
1052 pub missions: Option<Vec<String>>,
1053}
1054
1055#[derive(Serialize, Deserialize, Default)]
1057struct CuConfigRepresentation {
1058 tasks: Option<Vec<Node>>,
1059 resources: Option<Vec<ResourceBundleConfig>>,
1060 bridges: Option<Vec<BridgeConfig>>,
1061 cnx: Option<Vec<SerializedCnx>>,
1062 monitor: Option<MonitorConfig>,
1063 logging: Option<LoggingConfig>,
1064 runtime: Option<RuntimeConfig>,
1065 missions: Option<Vec<MissionsConfig>>,
1066 includes: Option<Vec<IncludesConfig>>,
1067}
1068
1069fn deserialize_config_representation<E>(
1071 representation: &CuConfigRepresentation,
1072) -> Result<CuConfig, E>
1073where
1074 E: From<String>,
1075{
1076 let mut cuconfig = CuConfig::default();
1077 let bridge_lookup = build_bridge_lookup(representation.bridges.as_ref());
1078
1079 if let Some(mission_configs) = &representation.missions {
1080 let mut missions = Missions(HashMap::new());
1082
1083 for mission_config in mission_configs {
1084 let mission_id = mission_config.id.as_str();
1085 let graph = missions
1086 .add_mission(mission_id)
1087 .map_err(|e| E::from(e.to_string()))?;
1088
1089 if let Some(tasks) = &representation.tasks {
1090 for task in tasks {
1091 if let Some(task_missions) = &task.missions {
1092 if task_missions.contains(&mission_id.to_owned()) {
1094 graph
1095 .add_node(task.clone())
1096 .map_err(|e| E::from(e.to_string()))?;
1097 }
1098 } else {
1099 graph
1101 .add_node(task.clone())
1102 .map_err(|e| E::from(e.to_string()))?;
1103 }
1104 }
1105 }
1106
1107 if let Some(bridges) = &representation.bridges {
1108 for bridge in bridges {
1109 if mission_applies(&bridge.missions, mission_id) {
1110 insert_bridge_node(graph, bridge).map_err(E::from)?;
1111 }
1112 }
1113 }
1114
1115 if let Some(cnx) = &representation.cnx {
1116 for c in cnx {
1117 if let Some(cnx_missions) = &c.missions {
1118 if cnx_missions.contains(&mission_id.to_owned()) {
1120 let (src_name, src_channel) =
1121 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1122 .map_err(E::from)?;
1123 let (dst_name, dst_channel) =
1124 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1125 .map_err(E::from)?;
1126 let src =
1127 graph
1128 .get_node_id_by_name(src_name.as_str())
1129 .ok_or_else(|| {
1130 E::from(format!("Source node not found: {}", c.src))
1131 })?;
1132 let dst =
1133 graph
1134 .get_node_id_by_name(dst_name.as_str())
1135 .ok_or_else(|| {
1136 E::from(format!("Destination node not found: {}", c.dst))
1137 })?;
1138 graph
1139 .connect_ext(
1140 src,
1141 dst,
1142 &c.msg,
1143 Some(cnx_missions.clone()),
1144 src_channel,
1145 dst_channel,
1146 )
1147 .map_err(|e| E::from(e.to_string()))?;
1148 }
1149 } else {
1150 let (src_name, src_channel) =
1152 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1153 .map_err(E::from)?;
1154 let (dst_name, dst_channel) =
1155 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1156 .map_err(E::from)?;
1157 let src = graph
1158 .get_node_id_by_name(src_name.as_str())
1159 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1160 let dst =
1161 graph
1162 .get_node_id_by_name(dst_name.as_str())
1163 .ok_or_else(|| {
1164 E::from(format!("Destination node not found: {}", c.dst))
1165 })?;
1166 graph
1167 .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1168 .map_err(|e| E::from(e.to_string()))?;
1169 }
1170 }
1171 }
1172 }
1173 cuconfig.graphs = missions;
1174 } else {
1175 let mut graph = CuGraph::default();
1177
1178 if let Some(tasks) = &representation.tasks {
1179 for task in tasks {
1180 graph
1181 .add_node(task.clone())
1182 .map_err(|e| E::from(e.to_string()))?;
1183 }
1184 }
1185
1186 if let Some(bridges) = &representation.bridges {
1187 for bridge in bridges {
1188 insert_bridge_node(&mut graph, bridge).map_err(E::from)?;
1189 }
1190 }
1191
1192 if let Some(cnx) = &representation.cnx {
1193 for c in cnx {
1194 let (src_name, src_channel) =
1195 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1196 .map_err(E::from)?;
1197 let (dst_name, dst_channel) =
1198 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1199 .map_err(E::from)?;
1200 let src = graph
1201 .get_node_id_by_name(src_name.as_str())
1202 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1203 let dst = graph
1204 .get_node_id_by_name(dst_name.as_str())
1205 .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
1206 graph
1207 .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1208 .map_err(|e| E::from(e.to_string()))?;
1209 }
1210 }
1211 cuconfig.graphs = Simple(graph);
1212 }
1213
1214 cuconfig.monitor = representation.monitor.clone();
1215 cuconfig.logging = representation.logging.clone();
1216 cuconfig.runtime = representation.runtime.clone();
1217 cuconfig.resources = representation.resources.clone().unwrap_or_default();
1218 cuconfig.bridges = representation.bridges.clone().unwrap_or_default();
1219
1220 Ok(cuconfig)
1221}
1222
1223impl<'de> Deserialize<'de> for CuConfig {
1224 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1226 where
1227 D: Deserializer<'de>,
1228 {
1229 let representation =
1230 CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
1231
1232 match deserialize_config_representation::<String>(&representation) {
1234 Ok(config) => Ok(config),
1235 Err(e) => Err(serde::de::Error::custom(e)),
1236 }
1237 }
1238}
1239
1240impl Serialize for CuConfig {
1241 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1243 where
1244 S: Serializer,
1245 {
1246 let bridges = if self.bridges.is_empty() {
1247 None
1248 } else {
1249 Some(self.bridges.clone())
1250 };
1251 let resources = if self.resources.is_empty() {
1252 None
1253 } else {
1254 Some(self.resources.clone())
1255 };
1256 match &self.graphs {
1257 Simple(graph) => {
1258 let tasks: Vec<Node> = graph
1259 .0
1260 .node_indices()
1261 .map(|idx| graph.0[idx].clone())
1262 .filter(|node| node.get_flavor() == Flavor::Task)
1263 .collect();
1264
1265 let cnx: Vec<SerializedCnx> = graph
1266 .0
1267 .edge_indices()
1268 .map(|edge| SerializedCnx::from(&graph.0[edge]))
1269 .collect();
1270
1271 CuConfigRepresentation {
1272 tasks: Some(tasks),
1273 bridges: bridges.clone(),
1274 cnx: Some(cnx),
1275 monitor: self.monitor.clone(),
1276 logging: self.logging.clone(),
1277 runtime: self.runtime.clone(),
1278 resources: resources.clone(),
1279 missions: None,
1280 includes: None,
1281 }
1282 .serialize(serializer)
1283 }
1284 Missions(graphs) => {
1285 let missions = graphs
1286 .keys()
1287 .map(|id| MissionsConfig { id: id.clone() })
1288 .collect();
1289
1290 let mut tasks = Vec::new();
1292 let mut cnx = Vec::new();
1293
1294 for graph in graphs.values() {
1295 for node_idx in graph.node_indices() {
1297 let node = &graph[node_idx];
1298 if node.get_flavor() == Flavor::Task
1299 && !tasks.iter().any(|n: &Node| n.id == node.id)
1300 {
1301 tasks.push(node.clone());
1302 }
1303 }
1304
1305 for edge_idx in graph.0.edge_indices() {
1307 let edge = &graph.0[edge_idx];
1308 let serialized = SerializedCnx::from(edge);
1309 if !cnx.iter().any(|c: &SerializedCnx| {
1310 c.src == serialized.src
1311 && c.dst == serialized.dst
1312 && c.msg == serialized.msg
1313 }) {
1314 cnx.push(serialized);
1315 }
1316 }
1317 }
1318
1319 CuConfigRepresentation {
1320 tasks: Some(tasks),
1321 resources: resources.clone(),
1322 bridges,
1323 cnx: Some(cnx),
1324 monitor: self.monitor.clone(),
1325 logging: self.logging.clone(),
1326 runtime: self.runtime.clone(),
1327 missions: Some(missions),
1328 includes: None,
1329 }
1330 .serialize(serializer)
1331 }
1332 }
1333 }
1334}
1335
1336impl Default for CuConfig {
1337 fn default() -> Self {
1338 CuConfig {
1339 graphs: Simple(CuGraph(StableDiGraph::new())),
1340 monitor: None,
1341 logging: None,
1342 runtime: None,
1343 resources: Vec::new(),
1344 bridges: Vec::new(),
1345 }
1346 }
1347}
1348
1349impl CuConfig {
1352 #[allow(dead_code)]
1353 pub fn new_simple_type() -> Self {
1354 Self::default()
1355 }
1356
1357 #[allow(dead_code)]
1358 pub fn new_mission_type() -> Self {
1359 CuConfig {
1360 graphs: Missions(HashMap::new()),
1361 monitor: None,
1362 logging: None,
1363 runtime: None,
1364 resources: Vec::new(),
1365 bridges: Vec::new(),
1366 }
1367 }
1368
1369 fn get_options() -> Options {
1370 Options::default()
1371 .with_default_extension(Extensions::IMPLICIT_SOME)
1372 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1373 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1374 }
1375
1376 #[allow(dead_code)]
1377 pub fn serialize_ron(&self) -> String {
1378 let ron = Self::get_options();
1379 let pretty = ron::ser::PrettyConfig::default();
1380 ron.to_string_pretty(&self, pretty).unwrap()
1381 }
1382
1383 #[allow(dead_code)]
1384 pub fn deserialize_ron(ron: &str) -> Self {
1385 match Self::get_options().from_str(ron) {
1386 Ok(representation) => Self::deserialize_impl(representation).unwrap_or_else(|e| {
1387 panic!("Error deserializing configuration: {e}");
1388 }),
1389 Err(e) => panic!("Syntax Error in config: {} at position {}", e.code, e.span),
1390 }
1391 }
1392
1393 fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
1394 deserialize_config_representation(&representation)
1395 }
1396
1397 #[cfg(feature = "std")]
1399 pub fn render(
1400 &self,
1401 output: &mut dyn std::io::Write,
1402 mission_id: Option<&str>,
1403 ) -> CuResult<()> {
1404 writeln!(output, "digraph G {{").unwrap();
1405 writeln!(output, " graph [rankdir=LR, nodesep=0.8, ranksep=1.2];").unwrap();
1406 writeln!(output, " node [shape=plain, fontname=\"Noto Sans\"];").unwrap();
1407 writeln!(output, " edge [fontname=\"Noto Sans\"];").unwrap();
1408
1409 let sections = match (&self.graphs, mission_id) {
1410 (Simple(graph), _) => vec![RenderSection { label: None, graph }],
1411 (Missions(graphs), Some(id)) => {
1412 let graph = graphs
1413 .get(id)
1414 .ok_or_else(|| CuError::from(format!("Mission {id} not found")))?;
1415 vec![RenderSection {
1416 label: Some(id.to_string()),
1417 graph,
1418 }]
1419 }
1420 (Missions(graphs), None) => {
1421 let mut missions: Vec<_> = graphs.iter().collect();
1422 missions.sort_by(|a, b| a.0.cmp(b.0));
1423 missions
1424 .into_iter()
1425 .map(|(label, graph)| RenderSection {
1426 label: Some(label.clone()),
1427 graph,
1428 })
1429 .collect()
1430 }
1431 };
1432
1433 for section in sections {
1434 self.render_section(output, section.graph, section.label.as_deref())?;
1435 }
1436
1437 writeln!(output, "}}").unwrap();
1438 Ok(())
1439 }
1440
1441 #[allow(dead_code)]
1442 pub fn get_all_instances_configs(
1443 &self,
1444 mission_id: Option<&str>,
1445 ) -> Vec<Option<&ComponentConfig>> {
1446 let graph = self.graphs.get_graph(mission_id).unwrap();
1447 graph
1448 .get_all_nodes()
1449 .iter()
1450 .map(|(_, node)| node.get_instance_config())
1451 .collect()
1452 }
1453
1454 #[allow(dead_code)]
1455 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1456 self.graphs.get_graph(mission_id)
1457 }
1458
1459 #[allow(dead_code)]
1460 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1461 self.graphs.get_graph_mut(mission_id)
1462 }
1463
1464 #[allow(dead_code)]
1465 pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1466 self.monitor.as_ref()
1467 }
1468
1469 #[allow(dead_code)]
1470 pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
1471 self.runtime.as_ref()
1472 }
1473
1474 pub fn validate_logging_config(&self) -> CuResult<()> {
1477 if let Some(logging) = &self.logging {
1478 return logging.validate();
1479 }
1480 Ok(())
1481 }
1482}
1483
1484#[cfg(feature = "std")]
1485struct PortLookup {
1486 inputs: HashMap<String, String>,
1487 outputs: HashMap<String, String>,
1488 default_input: Option<String>,
1489 default_output: Option<String>,
1490}
1491
1492#[cfg(feature = "std")]
1493#[derive(Clone)]
1494struct RenderNode {
1495 id: String,
1496 type_name: String,
1497 flavor: Flavor,
1498 inputs: Vec<String>,
1499 outputs: Vec<String>,
1500}
1501
1502#[cfg(feature = "std")]
1503#[derive(Clone)]
1504struct RenderConnection {
1505 src: String,
1506 src_port: Option<String>,
1507 dst: String,
1508 dst_port: Option<String>,
1509 msg: String,
1510}
1511
1512#[cfg(feature = "std")]
1513struct RenderTopology {
1514 nodes: Vec<RenderNode>,
1515 connections: Vec<RenderConnection>,
1516}
1517
1518#[cfg(feature = "std")]
1519struct RenderSection<'a> {
1520 label: Option<String>,
1521 graph: &'a CuGraph,
1522}
1523
1524#[cfg(feature = "std")]
1525impl CuConfig {
1526 fn render_section(
1527 &self,
1528 output: &mut dyn std::io::Write,
1529 graph: &CuGraph,
1530 label: Option<&str>,
1531 ) -> CuResult<()> {
1532 use std::fmt::Write as FmtWrite;
1533
1534 let mut topology = build_render_topology(graph, &self.bridges);
1535 topology.nodes.sort_by(|a, b| a.id.cmp(&b.id));
1536 topology.connections.sort_by(|a, b| {
1537 a.src
1538 .cmp(&b.src)
1539 .then(a.dst.cmp(&b.dst))
1540 .then(a.msg.cmp(&b.msg))
1541 });
1542
1543 let cluster_id = label.map(|lbl| format!("cluster_{}", sanitize_identifier(lbl)));
1544 if let Some(ref cluster_id) = cluster_id {
1545 writeln!(output, " subgraph \"{cluster_id}\" {{").unwrap();
1546 writeln!(
1547 output,
1548 " label=<<B>Mission: {}</B>>;",
1549 encode_text(label.unwrap())
1550 )
1551 .unwrap();
1552 writeln!(
1553 output,
1554 " labelloc=t; labeljust=l; color=\"#bbbbbb\"; style=\"rounded\"; margin=20;"
1555 )
1556 .unwrap();
1557 }
1558 let indent = if cluster_id.is_some() {
1559 " "
1560 } else {
1561 " "
1562 };
1563 let node_prefix = label
1564 .map(|lbl| format!("{}__", sanitize_identifier(lbl)))
1565 .unwrap_or_default();
1566
1567 let mut port_lookup: HashMap<String, PortLookup> = HashMap::new();
1568 let mut id_lookup: HashMap<String, String> = HashMap::new();
1569
1570 for node in &topology.nodes {
1571 let node_idx = graph
1572 .get_node_id_by_name(node.id.as_str())
1573 .ok_or_else(|| CuError::from(format!("Node '{}' missing from graph", node.id)))?;
1574 let node_weight = graph
1575 .get_node(node_idx)
1576 .ok_or_else(|| CuError::from(format!("Node '{}' missing weight", node.id)))?;
1577
1578 let is_src = graph.get_dst_edges(node_idx).unwrap_or_default().is_empty();
1579 let is_sink = graph.get_src_edges(node_idx).unwrap_or_default().is_empty();
1580
1581 let fillcolor = match node.flavor {
1582 Flavor::Bridge => "#faedcd",
1583 Flavor::Task if is_src => "#ddefc7",
1584 Flavor::Task if is_sink => "#cce0ff",
1585 _ => "#f2f2f2",
1586 };
1587
1588 let port_base = format!("{}{}", node_prefix, sanitize_identifier(&node.id));
1589 let (inputs_table, input_map, default_input) =
1590 build_port_table("Inputs", &node.inputs, &port_base, "in");
1591 let (outputs_table, output_map, default_output) =
1592 build_port_table("Outputs", &node.outputs, &port_base, "out");
1593 let config_html = node_weight.config.as_ref().and_then(build_config_table);
1594
1595 let mut label_html = String::new();
1596 write!(
1597 label_html,
1598 "<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"6\" COLOR=\"gray\" BGCOLOR=\"white\">"
1599 )
1600 .unwrap();
1601 write!(
1602 label_html,
1603 "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\" BGCOLOR=\"{fillcolor}\"><FONT POINT-SIZE=\"12\"><B>{}</B></FONT><BR/><FONT COLOR=\"dimgray\">[{}]</FONT></TD></TR>",
1604 encode_text(&node.id),
1605 encode_text(&node.type_name)
1606 )
1607 .unwrap();
1608 write!(
1609 label_html,
1610 "<TR><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{inputs_table}</TD><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{outputs_table}</TD></TR>"
1611 )
1612 .unwrap();
1613
1614 if let Some(config_html) = config_html {
1615 write!(
1616 label_html,
1617 "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\">{config_html}</TD></TR>"
1618 )
1619 .unwrap();
1620 }
1621
1622 label_html.push_str("</TABLE>");
1623
1624 let identifier_raw = if node_prefix.is_empty() {
1625 node.id.clone()
1626 } else {
1627 format!("{node_prefix}{}", node.id)
1628 };
1629 let identifier = escape_dot_id(&identifier_raw);
1630 writeln!(output, "{indent}\"{identifier}\" [label=<{label_html}>];").unwrap();
1631
1632 id_lookup.insert(node.id.clone(), identifier);
1633 port_lookup.insert(
1634 node.id.clone(),
1635 PortLookup {
1636 inputs: input_map,
1637 outputs: output_map,
1638 default_input,
1639 default_output,
1640 },
1641 );
1642 }
1643
1644 for cnx in &topology.connections {
1645 let src_id = id_lookup
1646 .get(&cnx.src)
1647 .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.src)))?;
1648 let dst_id = id_lookup
1649 .get(&cnx.dst)
1650 .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.dst)))?;
1651 let src_suffix = port_lookup
1652 .get(&cnx.src)
1653 .and_then(|lookup| lookup.resolve_output(cnx.src_port.as_deref()))
1654 .map(|port| format!(":\"{port}\":e"))
1655 .unwrap_or_default();
1656 let dst_suffix = port_lookup
1657 .get(&cnx.dst)
1658 .and_then(|lookup| lookup.resolve_input(cnx.dst_port.as_deref()))
1659 .map(|port| format!(":\"{port}\":w"))
1660 .unwrap_or_default();
1661 let msg = encode_text(&cnx.msg);
1662 writeln!(
1663 output,
1664 "{indent}\"{src_id}\"{src_suffix} -> \"{dst_id}\"{dst_suffix} [label=< <B><FONT COLOR=\"gray\">{msg}</FONT></B> >];"
1665 )
1666 .unwrap();
1667 }
1668
1669 if cluster_id.is_some() {
1670 writeln!(output, " }}").unwrap();
1671 }
1672
1673 Ok(())
1674 }
1675}
1676
1677#[cfg(feature = "std")]
1678fn build_render_topology(graph: &CuGraph, bridges: &[BridgeConfig]) -> RenderTopology {
1679 let mut bridge_lookup = HashMap::new();
1680 for bridge in bridges {
1681 bridge_lookup.insert(bridge.id.as_str(), bridge);
1682 }
1683
1684 let mut nodes: HashMap<String, RenderNode> = HashMap::new();
1685 for (_, node) in graph.get_all_nodes() {
1686 let node_id = node.get_id();
1687 let mut inputs = Vec::new();
1688 let mut outputs = Vec::new();
1689 if node.get_flavor() == Flavor::Bridge
1690 && let Some(bridge) = bridge_lookup.get(node_id.as_str())
1691 {
1692 for channel in &bridge.channels {
1693 match channel {
1694 BridgeChannelConfigRepresentation::Rx { id, .. } => outputs.push(id.clone()),
1696 BridgeChannelConfigRepresentation::Tx { id, .. } => inputs.push(id.clone()),
1698 }
1699 }
1700 }
1701
1702 nodes.insert(
1703 node_id.clone(),
1704 RenderNode {
1705 id: node_id,
1706 type_name: node.get_type().to_string(),
1707 flavor: node.get_flavor(),
1708 inputs,
1709 outputs,
1710 },
1711 );
1712 }
1713
1714 let mut connections = Vec::new();
1715 for edge in graph.0.edge_references() {
1716 let cnx = edge.weight();
1717 if let Some(node) = nodes.get_mut(&cnx.src)
1718 && node.flavor == Flavor::Task
1719 && cnx.src_channel.is_none()
1720 && node.outputs.is_empty()
1721 {
1722 node.outputs.push("out0".to_string());
1723 }
1724 if let Some(node) = nodes.get_mut(&cnx.dst)
1725 && node.flavor == Flavor::Task
1726 && cnx.dst_channel.is_none()
1727 {
1728 let next = format!("in{}", node.inputs.len());
1729 node.inputs.push(next);
1730 }
1731
1732 connections.push(RenderConnection {
1733 src: cnx.src.clone(),
1734 src_port: cnx.src_channel.clone(),
1735 dst: cnx.dst.clone(),
1736 dst_port: cnx.dst_channel.clone(),
1737 msg: cnx.msg.clone(),
1738 });
1739 }
1740
1741 RenderTopology {
1742 nodes: nodes.into_values().collect(),
1743 connections,
1744 }
1745}
1746
1747#[cfg(feature = "std")]
1748impl PortLookup {
1749 fn resolve_input(&self, name: Option<&str>) -> Option<&str> {
1750 if let Some(name) = name
1751 && let Some(port) = self.inputs.get(name)
1752 {
1753 return Some(port.as_str());
1754 }
1755 self.default_input.as_deref()
1756 }
1757
1758 fn resolve_output(&self, name: Option<&str>) -> Option<&str> {
1759 if let Some(name) = name
1760 && let Some(port) = self.outputs.get(name)
1761 {
1762 return Some(port.as_str());
1763 }
1764 self.default_output.as_deref()
1765 }
1766}
1767
1768#[cfg(feature = "std")]
1769fn build_port_table(
1770 title: &str,
1771 names: &[String],
1772 base_id: &str,
1773 prefix: &str,
1774) -> (String, HashMap<String, String>, Option<String>) {
1775 use std::fmt::Write as FmtWrite;
1776
1777 let mut html = String::new();
1778 write!(
1779 html,
1780 "<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">"
1781 )
1782 .unwrap();
1783 write!(
1784 html,
1785 "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT></TD></TR>",
1786 encode_text(title)
1787 )
1788 .unwrap();
1789
1790 let mut lookup = HashMap::new();
1791 let mut default_port = None;
1792
1793 if names.is_empty() {
1794 html.push_str("<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"lightgray\">—</FONT></TD></TR>");
1795 } else {
1796 for (idx, name) in names.iter().enumerate() {
1797 let port_id = format!("{base_id}_{prefix}_{idx}");
1798 write!(
1799 html,
1800 "<TR><TD PORT=\"{port_id}\" ALIGN=\"LEFT\">{}</TD></TR>",
1801 encode_text(name)
1802 )
1803 .unwrap();
1804 lookup.insert(name.clone(), port_id.clone());
1805 if idx == 0 {
1806 default_port = Some(port_id);
1807 }
1808 }
1809 }
1810
1811 html.push_str("</TABLE>");
1812 (html, lookup, default_port)
1813}
1814
1815#[cfg(feature = "std")]
1816fn build_config_table(config: &ComponentConfig) -> Option<String> {
1817 use std::fmt::Write as FmtWrite;
1818
1819 if config.0.is_empty() {
1820 return None;
1821 }
1822
1823 let mut entries: Vec<_> = config.0.iter().collect();
1824 entries.sort_by(|a, b| a.0.cmp(b.0));
1825
1826 let mut html = String::new();
1827 html.push_str("<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">");
1828 for (key, value) in entries {
1829 let value_txt = format!("{value}");
1830 write!(
1831 html,
1832 "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT> = {}</TD></TR>",
1833 encode_text(key),
1834 encode_text(&value_txt)
1835 )
1836 .unwrap();
1837 }
1838 html.push_str("</TABLE>");
1839 Some(html)
1840}
1841
1842#[cfg(feature = "std")]
1843fn sanitize_identifier(value: &str) -> String {
1844 value
1845 .chars()
1846 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
1847 .collect()
1848}
1849
1850#[cfg(feature = "std")]
1851fn escape_dot_id(value: &str) -> String {
1852 let mut escaped = String::with_capacity(value.len());
1853 for ch in value.chars() {
1854 match ch {
1855 '"' => escaped.push_str("\\\""),
1856 '\\' => escaped.push_str("\\\\"),
1857 _ => escaped.push(ch),
1858 }
1859 }
1860 escaped
1861}
1862
1863impl LoggingConfig {
1864 pub fn validate(&self) -> CuResult<()> {
1866 if let Some(section_size_mib) = self.section_size_mib
1867 && let Some(slab_size_mib) = self.slab_size_mib
1868 && section_size_mib > slab_size_mib
1869 {
1870 return Err(CuError::from(format!(
1871 "Section size ({section_size_mib} MiB) cannot be larger than slab size ({slab_size_mib} MiB). Adjust the parameters accordingly."
1872 )));
1873 }
1874
1875 Ok(())
1876 }
1877}
1878
1879#[allow(dead_code)] fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
1881 let mut result = content.to_string();
1882
1883 for (key, value) in params {
1884 let pattern = format!("{{{{{key}}}}}");
1885 result = result.replace(&pattern, &value.to_string());
1886 }
1887
1888 result
1889}
1890
1891#[cfg(feature = "std")]
1893fn process_includes(
1894 file_path: &str,
1895 base_representation: CuConfigRepresentation,
1896 processed_files: &mut Vec<String>,
1897) -> CuResult<CuConfigRepresentation> {
1898 processed_files.push(file_path.to_string());
1900
1901 let mut result = base_representation;
1902
1903 if let Some(includes) = result.includes.take() {
1904 for include in includes {
1905 let include_path = if include.path.starts_with('/') {
1906 include.path.clone()
1907 } else {
1908 let current_dir = std::path::Path::new(file_path)
1909 .parent()
1910 .unwrap_or_else(|| std::path::Path::new(""))
1911 .to_string_lossy()
1912 .to_string();
1913
1914 format!("{}/{}", current_dir, include.path)
1915 };
1916
1917 let include_content = read_to_string(&include_path).map_err(|e| {
1918 CuError::from(format!("Failed to read include file: {include_path}"))
1919 .add_cause(e.to_string().as_str())
1920 })?;
1921
1922 let processed_content = substitute_parameters(&include_content, &include.params);
1923
1924 let mut included_representation: CuConfigRepresentation = match Options::default()
1925 .with_default_extension(Extensions::IMPLICIT_SOME)
1926 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1927 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1928 .from_str(&processed_content)
1929 {
1930 Ok(rep) => rep,
1931 Err(e) => {
1932 return Err(CuError::from(format!(
1933 "Failed to parse include file: {} - Error: {} at position {}",
1934 include_path, e.code, e.span
1935 )));
1936 }
1937 };
1938
1939 included_representation =
1940 process_includes(&include_path, included_representation, processed_files)?;
1941
1942 if let Some(included_tasks) = included_representation.tasks {
1943 if result.tasks.is_none() {
1944 result.tasks = Some(included_tasks);
1945 } else {
1946 let mut tasks = result.tasks.take().unwrap();
1947 for included_task in included_tasks {
1948 if !tasks.iter().any(|t| t.id == included_task.id) {
1949 tasks.push(included_task);
1950 }
1951 }
1952 result.tasks = Some(tasks);
1953 }
1954 }
1955
1956 if let Some(included_bridges) = included_representation.bridges {
1957 if result.bridges.is_none() {
1958 result.bridges = Some(included_bridges);
1959 } else {
1960 let mut bridges = result.bridges.take().unwrap();
1961 for included_bridge in included_bridges {
1962 if !bridges.iter().any(|b| b.id == included_bridge.id) {
1963 bridges.push(included_bridge);
1964 }
1965 }
1966 result.bridges = Some(bridges);
1967 }
1968 }
1969
1970 if let Some(included_resources) = included_representation.resources {
1971 if result.resources.is_none() {
1972 result.resources = Some(included_resources);
1973 } else {
1974 let mut resources = result.resources.take().unwrap();
1975 for included_resource in included_resources {
1976 if !resources.iter().any(|r| r.id == included_resource.id) {
1977 resources.push(included_resource);
1978 }
1979 }
1980 result.resources = Some(resources);
1981 }
1982 }
1983
1984 if let Some(included_cnx) = included_representation.cnx {
1985 if result.cnx.is_none() {
1986 result.cnx = Some(included_cnx);
1987 } else {
1988 let mut cnx = result.cnx.take().unwrap();
1989 for included_c in included_cnx {
1990 if !cnx
1991 .iter()
1992 .any(|c| c.src == included_c.src && c.dst == included_c.dst)
1993 {
1994 cnx.push(included_c);
1995 }
1996 }
1997 result.cnx = Some(cnx);
1998 }
1999 }
2000
2001 if result.monitor.is_none() {
2002 result.monitor = included_representation.monitor;
2003 }
2004
2005 if result.logging.is_none() {
2006 result.logging = included_representation.logging;
2007 }
2008
2009 if result.runtime.is_none() {
2010 result.runtime = included_representation.runtime;
2011 }
2012
2013 if let Some(included_missions) = included_representation.missions {
2014 if result.missions.is_none() {
2015 result.missions = Some(included_missions);
2016 } else {
2017 let mut missions = result.missions.take().unwrap();
2018 for included_mission in included_missions {
2019 if !missions.iter().any(|m| m.id == included_mission.id) {
2020 missions.push(included_mission);
2021 }
2022 }
2023 result.missions = Some(missions);
2024 }
2025 }
2026 }
2027 }
2028
2029 Ok(result)
2030}
2031
2032#[cfg(feature = "std")]
2034pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
2035 let config_content = read_to_string(config_filename).map_err(|e| {
2036 CuError::from(format!(
2037 "Failed to read configuration file: {:?}",
2038 &config_filename
2039 ))
2040 .add_cause(e.to_string().as_str())
2041 })?;
2042 read_configuration_str(config_content, Some(config_filename))
2043}
2044
2045fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
2049 Options::default()
2050 .with_default_extension(Extensions::IMPLICIT_SOME)
2051 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2052 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2053 .from_str(content)
2054 .map_err(|e| {
2055 CuError::from(format!(
2056 "Failed to parse configuration: Error: {} at position {}",
2057 e.code, e.span
2058 ))
2059 })
2060}
2061
2062fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
2065 #[allow(unused_mut)]
2066 let mut cuconfig = CuConfig::deserialize_impl(representation)
2067 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
2068
2069 #[cfg(feature = "std")]
2070 cuconfig.ensure_threadpool_bundle();
2071
2072 cuconfig.validate_logging_config()?;
2073
2074 Ok(cuconfig)
2075}
2076
2077#[allow(unused_variables)]
2078pub fn read_configuration_str(
2079 config_content: String,
2080 file_path: Option<&str>,
2081) -> CuResult<CuConfig> {
2082 let representation = parse_config_string(&config_content)?;
2084
2085 #[cfg(feature = "std")]
2088 let representation = if let Some(path) = file_path {
2089 process_includes(path, representation, &mut Vec::new())?
2090 } else {
2091 representation
2092 };
2093
2094 config_representation_to_config(representation)
2096}
2097
2098#[cfg(test)]
2100mod tests {
2101 use super::*;
2102 #[cfg(not(feature = "std"))]
2103 use alloc::vec;
2104
2105 #[test]
2106 fn test_plain_serialize() {
2107 let mut config = CuConfig::default();
2108 let graph = config.get_graph_mut(None).unwrap();
2109 let n1 = graph
2110 .add_node(Node::new("test1", "package::Plugin1"))
2111 .unwrap();
2112 let n2 = graph
2113 .add_node(Node::new("test2", "package::Plugin2"))
2114 .unwrap();
2115 graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
2116 let serialized = config.serialize_ron();
2117 let deserialized = CuConfig::deserialize_ron(&serialized);
2118 let graph = config.graphs.get_graph(None).unwrap();
2119 let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
2120 assert_eq!(graph.node_count(), deserialized_graph.node_count());
2121 assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
2122 }
2123
2124 #[test]
2125 fn test_serialize_with_params() {
2126 let mut config = CuConfig::default();
2127 let graph = config.get_graph_mut(None).unwrap();
2128 let mut camera = Node::new("copper-camera", "camerapkg::Camera");
2129 camera.set_param::<Value>("resolution-height", 1080.into());
2130 graph.add_node(camera).unwrap();
2131 let serialized = config.serialize_ron();
2132 let config = CuConfig::deserialize_ron(&serialized);
2133 let deserialized = config.get_graph(None).unwrap();
2134 assert_eq!(
2135 deserialized
2136 .get_node(0)
2137 .unwrap()
2138 .get_param::<i32>("resolution-height")
2139 .unwrap(),
2140 1080
2141 );
2142 }
2143
2144 #[test]
2145 #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:9-1:10")]
2146 fn test_deserialization_error() {
2147 let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
2149 CuConfig::deserialize_ron(txt);
2150 }
2151 #[test]
2152 fn test_missions() {
2153 let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
2154 let config = CuConfig::deserialize_ron(txt);
2155 let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
2156 assert!(graph.node_count() == 0);
2157 let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
2158 assert!(graph.node_count() == 0);
2159 }
2160
2161 #[test]
2162 fn test_monitor() {
2163 let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
2164 let config = CuConfig::deserialize_ron(txt);
2165 assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
2166
2167 let txt =
2168 r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
2169 let config = CuConfig::deserialize_ron(txt);
2170 assert_eq!(
2171 config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
2172 4u8.into()
2173 );
2174 }
2175
2176 #[test]
2177 fn test_logging_parameters() {
2178 let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
2180
2181 let config = CuConfig::deserialize_ron(txt);
2182 assert!(config.logging.is_some());
2183 let logging_config = config.logging.unwrap();
2184 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
2185 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
2186 assert!(!logging_config.enable_task_logging);
2187
2188 let txt =
2190 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
2191 let config = CuConfig::deserialize_ron(txt);
2192 assert!(config.logging.is_some());
2193 let logging_config = config.logging.unwrap();
2194 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
2195 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
2196 assert!(logging_config.enable_task_logging);
2197 }
2198
2199 #[test]
2200 fn test_bridge_parsing() {
2201 let txt = r#"
2202 (
2203 tasks: [
2204 (id: "dst", type: "tasks::Destination"),
2205 (id: "src", type: "tasks::Source"),
2206 ],
2207 bridges: [
2208 (
2209 id: "radio",
2210 type: "tasks::SerialBridge",
2211 config: { "path": "/dev/ttyACM0", "baud": 921600 },
2212 channels: [
2213 Rx ( id: "status", route: "sys/status" ),
2214 Tx ( id: "motor", route: "motor/cmd" ),
2215 ],
2216 ),
2217 ],
2218 cnx: [
2219 (src: "radio/status", dst: "dst", msg: "mymsgs::Status"),
2220 (src: "src", dst: "radio/motor", msg: "mymsgs::MotorCmd"),
2221 ],
2222 )
2223 "#;
2224
2225 let config = CuConfig::deserialize_ron(txt);
2226 assert_eq!(config.bridges.len(), 1);
2227 let bridge = &config.bridges[0];
2228 assert_eq!(bridge.id, "radio");
2229 assert_eq!(bridge.channels.len(), 2);
2230 match &bridge.channels[0] {
2231 BridgeChannelConfigRepresentation::Rx { id, route, .. } => {
2232 assert_eq!(id, "status");
2233 assert_eq!(route.as_deref(), Some("sys/status"));
2234 }
2235 _ => panic!("expected Rx channel"),
2236 }
2237 match &bridge.channels[1] {
2238 BridgeChannelConfigRepresentation::Tx { id, route, .. } => {
2239 assert_eq!(id, "motor");
2240 assert_eq!(route.as_deref(), Some("motor/cmd"));
2241 }
2242 _ => panic!("expected Tx channel"),
2243 }
2244 let graph = config.graphs.get_graph(None).unwrap();
2245 let bridge_id = graph
2246 .get_node_id_by_name("radio")
2247 .expect("bridge node missing");
2248 let bridge_node = graph.get_node(bridge_id).unwrap();
2249 assert_eq!(bridge_node.get_flavor(), Flavor::Bridge);
2250
2251 let mut edges = Vec::new();
2253 for edge_idx in graph.0.edge_indices() {
2254 edges.push(graph.0[edge_idx].clone());
2255 }
2256 assert_eq!(edges.len(), 2);
2257 let status_edge = edges
2258 .iter()
2259 .find(|e| e.dst == "dst")
2260 .expect("status edge missing");
2261 assert_eq!(status_edge.src_channel.as_deref(), Some("status"));
2262 assert!(status_edge.dst_channel.is_none());
2263 let motor_edge = edges
2264 .iter()
2265 .find(|e| e.dst_channel.is_some())
2266 .expect("motor edge missing");
2267 assert_eq!(motor_edge.dst_channel.as_deref(), Some("motor"));
2268 }
2269
2270 #[test]
2271 fn test_bridge_roundtrip() {
2272 let mut config = CuConfig::default();
2273 let mut bridge_config = ComponentConfig::default();
2274 bridge_config.set("port", "/dev/ttyACM0".to_string());
2275 config.bridges.push(BridgeConfig {
2276 id: "radio".to_string(),
2277 type_: "tasks::SerialBridge".to_string(),
2278 config: Some(bridge_config),
2279 resources: None,
2280 missions: None,
2281 channels: vec![
2282 BridgeChannelConfigRepresentation::Rx {
2283 id: "status".to_string(),
2284 route: Some("sys/status".to_string()),
2285 config: None,
2286 },
2287 BridgeChannelConfigRepresentation::Tx {
2288 id: "motor".to_string(),
2289 route: Some("motor/cmd".to_string()),
2290 config: None,
2291 },
2292 ],
2293 });
2294
2295 let serialized = config.serialize_ron();
2296 assert!(
2297 serialized.contains("bridges"),
2298 "bridges section missing from serialized config"
2299 );
2300 let deserialized = CuConfig::deserialize_ron(&serialized);
2301 assert_eq!(deserialized.bridges.len(), 1);
2302 let bridge = &deserialized.bridges[0];
2303 assert_eq!(bridge.channels.len(), 2);
2304 assert!(matches!(
2305 bridge.channels[0],
2306 BridgeChannelConfigRepresentation::Rx { .. }
2307 ));
2308 assert!(matches!(
2309 bridge.channels[1],
2310 BridgeChannelConfigRepresentation::Tx { .. }
2311 ));
2312 }
2313
2314 #[test]
2315 fn test_resource_parsing() {
2316 let txt = r#"
2317 (
2318 resources: [
2319 (
2320 id: "fc",
2321 provider: "copper_board_px4::Px4Bundle",
2322 config: { "baud": 921600 },
2323 missions: ["m1"],
2324 ),
2325 (
2326 id: "misc",
2327 provider: "cu29_runtime::StdClockBundle",
2328 ),
2329 ],
2330 )
2331 "#;
2332
2333 let config = CuConfig::deserialize_ron(txt);
2334 assert_eq!(config.resources.len(), 2);
2335 let fc = &config.resources[0];
2336 assert_eq!(fc.id, "fc");
2337 assert_eq!(fc.provider, "copper_board_px4::Px4Bundle");
2338 assert_eq!(fc.missions.as_deref(), Some(&["m1".to_string()][..]));
2339 let baud: u32 = fc
2340 .config
2341 .as_ref()
2342 .and_then(|cfg| cfg.get("baud"))
2343 .expect("missing baud");
2344 assert_eq!(baud, 921_600);
2345 let misc = &config.resources[1];
2346 assert_eq!(misc.id, "misc");
2347 assert_eq!(misc.provider, "cu29_runtime::StdClockBundle");
2348 assert!(misc.config.is_none());
2349 }
2350
2351 #[test]
2352 fn test_resource_roundtrip() {
2353 let mut config = CuConfig::default();
2354 let mut bundle_cfg = ComponentConfig::default();
2355 bundle_cfg.set("path", "/dev/ttyACM0".to_string());
2356 config.resources.push(ResourceBundleConfig {
2357 id: "fc".to_string(),
2358 provider: "copper_board_px4::Px4Bundle".to_string(),
2359 config: Some(bundle_cfg),
2360 missions: Some(vec!["m1".to_string()]),
2361 });
2362
2363 let serialized = config.serialize_ron();
2364 let deserialized = CuConfig::deserialize_ron(&serialized);
2365 assert_eq!(deserialized.resources.len(), 1);
2366 let res = &deserialized.resources[0];
2367 assert_eq!(res.id, "fc");
2368 assert_eq!(res.provider, "copper_board_px4::Px4Bundle");
2369 assert_eq!(res.missions.as_deref(), Some(&["m1".to_string()][..]));
2370 let path: String = res
2371 .config
2372 .as_ref()
2373 .and_then(|cfg| cfg.get("path"))
2374 .expect("missing path");
2375 assert_eq!(path, "/dev/ttyACM0");
2376 }
2377
2378 #[test]
2379 fn test_bridge_channel_config() {
2380 let txt = r#"
2381 (
2382 tasks: [],
2383 bridges: [
2384 (
2385 id: "radio",
2386 type: "tasks::SerialBridge",
2387 channels: [
2388 Rx ( id: "status", route: "sys/status", config: { "filter": "fast" } ),
2389 Tx ( id: "imu", route: "telemetry/imu", config: { "rate": 100 } ),
2390 ],
2391 ),
2392 ],
2393 cnx: [],
2394 )
2395 "#;
2396
2397 let config = CuConfig::deserialize_ron(txt);
2398 let bridge = &config.bridges[0];
2399 match &bridge.channels[0] {
2400 BridgeChannelConfigRepresentation::Rx {
2401 config: Some(cfg), ..
2402 } => {
2403 let val: String = cfg.get("filter").expect("filter missing");
2404 assert_eq!(val, "fast");
2405 }
2406 _ => panic!("expected Rx channel with config"),
2407 }
2408 match &bridge.channels[1] {
2409 BridgeChannelConfigRepresentation::Tx {
2410 config: Some(cfg), ..
2411 } => {
2412 let rate: i32 = cfg.get("rate").expect("rate missing");
2413 assert_eq!(rate, 100);
2414 }
2415 _ => panic!("expected Tx channel with config"),
2416 }
2417 }
2418
2419 #[test]
2420 fn test_task_resources_roundtrip() {
2421 let txt = r#"
2422 (
2423 tasks: [
2424 (
2425 id: "imu",
2426 type: "tasks::ImuDriver",
2427 resources: { "bus": "fc.spi_1", "irq": "fc.gpio_imu" },
2428 ),
2429 ],
2430 cnx: [],
2431 )
2432 "#;
2433
2434 let config = CuConfig::deserialize_ron(txt);
2435 let graph = config.graphs.get_graph(None).unwrap();
2436 let node = graph.get_node(0).expect("missing task node");
2437 let resources = node.get_resources().expect("missing resources map");
2438 assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
2439 assert_eq!(
2440 resources.get("irq").map(String::as_str),
2441 Some("fc.gpio_imu")
2442 );
2443
2444 let serialized = config.serialize_ron();
2445 let deserialized = CuConfig::deserialize_ron(&serialized);
2446 let graph = deserialized.graphs.get_graph(None).unwrap();
2447 let node = graph.get_node(0).expect("missing task node");
2448 let resources = node
2449 .get_resources()
2450 .expect("missing resources map after roundtrip");
2451 assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
2452 assert_eq!(
2453 resources.get("irq").map(String::as_str),
2454 Some("fc.gpio_imu")
2455 );
2456 }
2457
2458 #[test]
2459 fn test_bridge_resources_preserved() {
2460 let mut config = CuConfig::default();
2461 config.resources.push(ResourceBundleConfig {
2462 id: "fc".to_string(),
2463 provider: "board::Bundle".to_string(),
2464 config: None,
2465 missions: None,
2466 });
2467 let bridge_resources = HashMap::from([("serial".to_string(), "fc.serial0".to_string())]);
2468 config.bridges.push(BridgeConfig {
2469 id: "radio".to_string(),
2470 type_: "tasks::SerialBridge".to_string(),
2471 config: None,
2472 resources: Some(bridge_resources),
2473 missions: None,
2474 channels: vec![BridgeChannelConfigRepresentation::Tx {
2475 id: "uplink".to_string(),
2476 route: None,
2477 config: None,
2478 }],
2479 });
2480
2481 let serialized = config.serialize_ron();
2482 let deserialized = CuConfig::deserialize_ron(&serialized);
2483 let graph = deserialized.graphs.get_graph(None).expect("missing graph");
2484 let bridge_id = graph
2485 .get_node_id_by_name("radio")
2486 .expect("bridge node missing");
2487 let node = graph.get_node(bridge_id).expect("missing bridge node");
2488 let resources = node
2489 .get_resources()
2490 .expect("bridge resources were not preserved");
2491 assert_eq!(
2492 resources.get("serial").map(String::as_str),
2493 Some("fc.serial0")
2494 );
2495 }
2496
2497 #[test]
2498 fn test_demo_config_parses() {
2499 let txt = r#"(
2500 resources: [
2501 (
2502 id: "fc",
2503 provider: "crate::resources::RadioBundle",
2504 ),
2505 ],
2506 tasks: [
2507 (id: "thr", type: "tasks::ThrottleControl"),
2508 (id: "tele0", type: "tasks::TelemetrySink0"),
2509 (id: "tele1", type: "tasks::TelemetrySink1"),
2510 (id: "tele2", type: "tasks::TelemetrySink2"),
2511 (id: "tele3", type: "tasks::TelemetrySink3"),
2512 ],
2513 bridges: [
2514 ( id: "crsf",
2515 type: "cu_crsf::CrsfBridge<SerialResource, SerialPortError>",
2516 resources: { "serial": "fc.serial" },
2517 channels: [
2518 Rx ( id: "rc_rx" ), // receiving RC Channels
2519 Tx ( id: "lq_tx" ), // Sending LineQuality back
2520 ],
2521 ),
2522 (
2523 id: "bdshot",
2524 type: "cu_bdshot::RpBdshotBridge",
2525 channels: [
2526 Tx ( id: "esc0_tx" ),
2527 Tx ( id: "esc1_tx" ),
2528 Tx ( id: "esc2_tx" ),
2529 Tx ( id: "esc3_tx" ),
2530 Rx ( id: "esc0_rx" ),
2531 Rx ( id: "esc1_rx" ),
2532 Rx ( id: "esc2_rx" ),
2533 Rx ( id: "esc3_rx" ),
2534 ],
2535 ),
2536 ],
2537 cnx: [
2538 (src: "crsf/rc_rx", dst: "thr", msg: "cu_crsf::messages::RcChannelsPayload"),
2539 (src: "thr", dst: "bdshot/esc0_tx", msg: "cu_bdshot::EscCommand"),
2540 (src: "thr", dst: "bdshot/esc1_tx", msg: "cu_bdshot::EscCommand"),
2541 (src: "thr", dst: "bdshot/esc2_tx", msg: "cu_bdshot::EscCommand"),
2542 (src: "thr", dst: "bdshot/esc3_tx", msg: "cu_bdshot::EscCommand"),
2543 (src: "bdshot/esc0_rx", dst: "tele0", msg: "cu_bdshot::EscTelemetry"),
2544 (src: "bdshot/esc1_rx", dst: "tele1", msg: "cu_bdshot::EscTelemetry"),
2545 (src: "bdshot/esc2_rx", dst: "tele2", msg: "cu_bdshot::EscTelemetry"),
2546 (src: "bdshot/esc3_rx", dst: "tele3", msg: "cu_bdshot::EscTelemetry"),
2547 ],
2548)"#;
2549 let config = CuConfig::deserialize_ron(txt);
2550 assert_eq!(config.resources.len(), 1);
2551 assert_eq!(config.bridges.len(), 2);
2552 }
2553
2554 #[test]
2555 #[should_panic(expected = "channel 'motor' is Tx and cannot act as a source")]
2556 fn test_bridge_tx_cannot_be_source() {
2557 let txt = r#"
2558 (
2559 tasks: [
2560 (id: "dst", type: "tasks::Destination"),
2561 ],
2562 bridges: [
2563 (
2564 id: "radio",
2565 type: "tasks::SerialBridge",
2566 channels: [
2567 Tx ( id: "motor", route: "motor/cmd" ),
2568 ],
2569 ),
2570 ],
2571 cnx: [
2572 (src: "radio/motor", dst: "dst", msg: "mymsgs::MotorCmd"),
2573 ],
2574 )
2575 "#;
2576
2577 CuConfig::deserialize_ron(txt);
2578 }
2579
2580 #[test]
2581 #[should_panic(expected = "channel 'status' is Rx and cannot act as a destination")]
2582 fn test_bridge_rx_cannot_be_destination() {
2583 let txt = r#"
2584 (
2585 tasks: [
2586 (id: "src", type: "tasks::Source"),
2587 ],
2588 bridges: [
2589 (
2590 id: "radio",
2591 type: "tasks::SerialBridge",
2592 channels: [
2593 Rx ( id: "status", route: "sys/status" ),
2594 ],
2595 ),
2596 ],
2597 cnx: [
2598 (src: "src", dst: "radio/status", msg: "mymsgs::Status"),
2599 ],
2600 )
2601 "#;
2602
2603 CuConfig::deserialize_ron(txt);
2604 }
2605
2606 #[test]
2607 fn test_validate_logging_config() {
2608 let txt =
2610 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
2611 let config = CuConfig::deserialize_ron(txt);
2612 assert!(config.validate_logging_config().is_ok());
2613
2614 let txt =
2616 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
2617 let config = CuConfig::deserialize_ron(txt);
2618 assert!(config.validate_logging_config().is_err());
2619 }
2620
2621 #[test]
2623 fn test_deserialization_edge_id_assignment() {
2624 let txt = r#"(
2627 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2628 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
2629 )"#;
2630 let config = CuConfig::deserialize_ron(txt);
2631 let graph = config.graphs.get_graph(None).unwrap();
2632 assert!(config.validate_logging_config().is_ok());
2633
2634 let src1_id = 0;
2636 assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
2637 let src2_id = 1;
2638 assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
2639
2640 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
2643 assert_eq!(src1_edge_id, 1);
2644 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
2645 assert_eq!(src2_edge_id, 0);
2646 }
2647
2648 #[test]
2649 fn test_simple_missions() {
2650 let txt = r#"(
2652 missions: [ (id: "m1"),
2653 (id: "m2"),
2654 ],
2655 tasks: [(id: "src1", type: "a", missions: ["m1"]),
2656 (id: "src2", type: "b", missions: ["m2"]),
2657 (id: "sink", type: "c")],
2658
2659 cnx: [
2660 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2661 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2662 ],
2663 )
2664 "#;
2665
2666 let config = CuConfig::deserialize_ron(txt);
2667 let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
2668 assert_eq!(m1_graph.edge_count(), 1);
2669 assert_eq!(m1_graph.node_count(), 2);
2670 let index = 0;
2671 let cnx = m1_graph.get_edge_weight(index).unwrap();
2672
2673 assert_eq!(cnx.src, "src1");
2674 assert_eq!(cnx.dst, "sink");
2675 assert_eq!(cnx.msg, "u32");
2676 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2677
2678 let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
2679 assert_eq!(m2_graph.edge_count(), 1);
2680 assert_eq!(m2_graph.node_count(), 2);
2681 let index = 0;
2682 let cnx = m2_graph.get_edge_weight(index).unwrap();
2683 assert_eq!(cnx.src, "src2");
2684 assert_eq!(cnx.dst, "sink");
2685 assert_eq!(cnx.msg, "u32");
2686 assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
2687 }
2688 #[test]
2689 fn test_mission_serde() {
2690 let txt = r#"(
2692 missions: [ (id: "m1"),
2693 (id: "m2"),
2694 ],
2695 tasks: [(id: "src1", type: "a", missions: ["m1"]),
2696 (id: "src2", type: "b", missions: ["m2"]),
2697 (id: "sink", type: "c")],
2698
2699 cnx: [
2700 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2701 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2702 ],
2703 )
2704 "#;
2705
2706 let config = CuConfig::deserialize_ron(txt);
2707 let serialized = config.serialize_ron();
2708 let deserialized = CuConfig::deserialize_ron(&serialized);
2709 let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
2710 assert_eq!(m1_graph.edge_count(), 1);
2711 assert_eq!(m1_graph.node_count(), 2);
2712 let index = 0;
2713 let cnx = m1_graph.get_edge_weight(index).unwrap();
2714 assert_eq!(cnx.src, "src1");
2715 assert_eq!(cnx.dst, "sink");
2716 assert_eq!(cnx.msg, "u32");
2717 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2718 }
2719
2720 #[test]
2721 fn test_keyframe_interval() {
2722 let txt = r#"(
2725 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2726 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2727 logging: ( keyframe_interval: 314 )
2728 )"#;
2729 let config = CuConfig::deserialize_ron(txt);
2730 let logging_config = config.logging.unwrap();
2731 assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
2732 }
2733
2734 #[test]
2735 fn test_default_keyframe_interval() {
2736 let txt = r#"(
2739 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2740 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2741 logging: ( slab_size_mib: 200, section_size_mib: 1024, )
2742 )"#;
2743 let config = CuConfig::deserialize_ron(txt);
2744 let logging_config = config.logging.unwrap();
2745 assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
2746 }
2747}