1#[cfg(not(feature = "std"))]
6extern crate alloc;
7
8use core::fmt;
9use core::fmt::Display;
10use cu29_traits::{CuError, CuResult};
11use hashbrown::HashMap;
12use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableDiGraph};
13use petgraph::visit::EdgeRef;
14pub use petgraph::Direction::Incoming;
15pub use petgraph::Direction::Outgoing;
16use ron::extensions::Extensions;
17use ron::value::Value as RonValue;
18use ron::{Number, Options};
19use serde::{Deserialize, Deserializer, Serialize, Serializer};
20use ConfigGraphs::{Missions, Simple};
21
22#[cfg(not(feature = "std"))]
23mod imp {
24 pub use alloc::borrow::ToOwned;
25 pub use alloc::format;
26 pub use alloc::string::String;
27 pub use alloc::string::ToString;
28 pub use alloc::vec::Vec;
29}
30
31#[cfg(feature = "std")]
32mod imp {
33 pub use html_escape::encode_text;
34 pub use std::fs::read_to_string;
35}
36
37use imp::*;
38
39pub type NodeId = u32;
42
43#[derive(Serialize, Deserialize, Debug, Clone, Default)]
47pub struct ComponentConfig(pub HashMap<String, Value>);
48
49impl Display for ComponentConfig {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 let mut first = true;
52 let ComponentConfig(config) = self;
53 write!(f, "{{")?;
54 for (key, value) in config.iter() {
55 if !first {
56 write!(f, ", ")?;
57 }
58 write!(f, "{key}: {value}")?;
59 first = false;
60 }
61 write!(f, "}}")
62 }
63}
64
65impl ComponentConfig {
67 #[allow(dead_code)]
68 pub fn new() -> Self {
69 ComponentConfig(HashMap::new())
70 }
71
72 #[allow(dead_code)]
73 pub fn get<T: From<Value>>(&self, key: &str) -> Option<T> {
74 let ComponentConfig(config) = self;
75 config.get(key).map(|v| T::from(v.clone()))
76 }
77
78 #[allow(dead_code)]
79 pub fn set<T: Into<Value>>(&mut self, key: &str, value: T) {
80 let ComponentConfig(config) = self;
81 config.insert(key.to_string(), value.into());
82 }
83}
84
85#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
94pub struct Value(RonValue);
95
96macro_rules! impl_from_numeric_for_value {
98 ($($source:ty),* $(,)?) => {
99 $(impl From<$source> for Value {
100 fn from(value: $source) -> Self {
101 Value(RonValue::Number(value.into()))
102 }
103 })*
104 };
105}
106
107impl_from_numeric_for_value!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
109
110impl From<Value> for bool {
111 fn from(value: Value) -> Self {
112 if let Value(RonValue::Bool(v)) = value {
113 v
114 } else {
115 panic!("Expected a Boolean variant but got {value:?}")
116 }
117 }
118}
119macro_rules! impl_from_value_for_int {
120 ($($target:ty),* $(,)?) => {
121 $(
122 impl From<Value> for $target {
123 fn from(value: Value) -> Self {
124 if let Value(RonValue::Number(num)) = value {
125 match num {
126 Number::I8(n) => n as $target,
127 Number::I16(n) => n as $target,
128 Number::I32(n) => n as $target,
129 Number::I64(n) => n as $target,
130 Number::U8(n) => n as $target,
131 Number::U16(n) => n as $target,
132 Number::U32(n) => n as $target,
133 Number::U64(n) => n as $target,
134 Number::F32(_) | Number::F64(_) | Number::__NonExhaustive(_) => {
135 panic!("Expected an integer Number variant but got {num:?}")
136 }
137 }
138 } else {
139 panic!("Expected a Number variant but got {value:?}")
140 }
141 }
142 }
143 )*
144 };
145}
146
147impl_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
148
149impl From<Value> for f64 {
150 fn from(value: Value) -> Self {
151 if let Value(RonValue::Number(num)) = value {
152 num.into_f64()
153 } else {
154 panic!("Expected a Number variant but got {value:?}")
155 }
156 }
157}
158
159impl From<String> for Value {
160 fn from(value: String) -> Self {
161 Value(RonValue::String(value))
162 }
163}
164
165impl From<Value> for String {
166 fn from(value: Value) -> Self {
167 if let Value(RonValue::String(s)) = value {
168 s
169 } else {
170 panic!("Expected a String variant")
171 }
172 }
173}
174
175impl Display for Value {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 let Value(value) = self;
178 match value {
179 RonValue::Number(n) => {
180 let s = match n {
181 Number::I8(n) => n.to_string(),
182 Number::I16(n) => n.to_string(),
183 Number::I32(n) => n.to_string(),
184 Number::I64(n) => n.to_string(),
185 Number::U8(n) => n.to_string(),
186 Number::U16(n) => n.to_string(),
187 Number::U32(n) => n.to_string(),
188 Number::U64(n) => n.to_string(),
189 Number::F32(n) => n.0.to_string(),
190 Number::F64(n) => n.0.to_string(),
191 _ => panic!("Expected a Number variant but got {value:?}"),
192 };
193 write!(f, "{s}")
194 }
195 RonValue::String(s) => write!(f, "{s}"),
196 RonValue::Bool(b) => write!(f, "{b}"),
197 RonValue::Map(m) => write!(f, "{m:?}"),
198 RonValue::Char(c) => write!(f, "{c:?}"),
199 RonValue::Unit => write!(f, "unit"),
200 RonValue::Option(o) => write!(f, "{o:?}"),
201 RonValue::Seq(s) => write!(f, "{s:?}"),
202 RonValue::Bytes(bytes) => write!(f, "{bytes:?}"),
203 }
204 }
205}
206
207#[derive(Serialize, Deserialize, Debug, Clone)]
209pub struct NodeLogging {
210 enabled: bool,
211}
212
213#[derive(Serialize, Deserialize, Debug, Clone)]
216pub struct Node {
217 id: String,
219
220 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
222 type_: Option<String>,
223
224 #[serde(skip_serializing_if = "Option::is_none")]
226 config: Option<ComponentConfig>,
227
228 missions: Option<Vec<String>>,
230
231 #[serde(skip_serializing_if = "Option::is_none")]
234 background: Option<bool>,
235
236 #[serde(skip_serializing_if = "Option::is_none")]
242 run_in_sim: Option<bool>,
243
244 #[serde(skip_serializing_if = "Option::is_none")]
246 logging: Option<NodeLogging>,
247}
248
249impl Node {
250 #[allow(dead_code)]
251 pub fn new(id: &str, ptype: &str) -> Self {
252 Node {
253 id: id.to_string(),
254 type_: Some(ptype.to_string()),
255 config: None,
256 missions: None,
257 background: None,
258 run_in_sim: None,
259 logging: None,
260 }
261 }
262
263 #[allow(dead_code)]
264 pub fn get_id(&self) -> String {
265 self.id.clone()
266 }
267
268 #[allow(dead_code)]
269 pub fn get_type(&self) -> &str {
270 self.type_.as_ref().unwrap()
271 }
272
273 #[allow(dead_code)]
274 pub fn set_type(mut self, name: Option<String>) -> Self {
275 self.type_ = name;
276 self
277 }
278
279 #[allow(dead_code)]
280 pub fn is_background(&self) -> bool {
281 self.background.unwrap_or(false)
282 }
283
284 #[allow(dead_code)]
285 pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
286 self.config.as_ref()
287 }
288
289 #[allow(dead_code)]
292 pub fn is_run_in_sim(&self) -> bool {
293 self.run_in_sim.unwrap_or(false)
294 }
295
296 #[allow(dead_code)]
297 pub fn is_logging_enabled(&self) -> bool {
298 if let Some(logging) = &self.logging {
299 logging.enabled
300 } else {
301 true
302 }
303 }
304
305 #[allow(dead_code)]
306 pub fn get_param<T: From<Value>>(&self, key: &str) -> Option<T> {
307 let pc = self.config.as_ref()?;
308 let ComponentConfig(pc) = pc;
309 let v = pc.get(key)?;
310 Some(T::from(v.clone()))
311 }
312
313 #[allow(dead_code)]
314 pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
315 if self.config.is_none() {
316 self.config = Some(ComponentConfig(HashMap::new()));
317 }
318 let ComponentConfig(config) = self.config.as_mut().unwrap();
319 config.insert(key.to_string(), value.into());
320 }
321}
322
323#[derive(Serialize, Deserialize, Debug, Clone)]
325pub struct Cnx {
326 src: String,
328
329 dst: String,
331
332 pub msg: String,
334
335 pub missions: Option<Vec<String>>,
337}
338
339#[derive(Default, Debug, Clone)]
340pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
341
342impl CuGraph {
343 #[allow(dead_code)]
344 pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
345 self.0
346 .node_indices()
347 .map(|index| (index.index() as u32, &self.0[index]))
348 .collect()
349 }
350
351 pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
352 self.0.node_indices().collect()
353 }
354
355 pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
356 Ok(self.0.add_node(node).index() as NodeId)
357 }
358
359 pub fn connect_ext(
360 &mut self,
361 source: NodeId,
362 target: NodeId,
363 msg_type: &str,
364 missions: Option<Vec<String>>,
365 ) -> CuResult<()> {
366 let (src_id, dst_id) = (
367 self.0
368 .node_weight(source.into())
369 .ok_or("Source node not found")?
370 .id
371 .clone(),
372 self.0
373 .node_weight(target.into())
374 .ok_or("Target node not found")?
375 .id
376 .clone(),
377 );
378
379 let _ = self.0.add_edge(
380 petgraph::stable_graph::NodeIndex::from(source),
381 petgraph::stable_graph::NodeIndex::from(target),
382 Cnx {
383 src: src_id,
384 dst: dst_id,
385 msg: msg_type.to_string(),
386 missions,
387 },
388 );
389 Ok(())
390 }
391 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
395 self.0.node_weight(node_id.into())
396 }
397
398 #[allow(dead_code)]
399 pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
400 self.0.node_weight(index.into())
401 }
402
403 #[allow(dead_code)]
404 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
405 self.0.node_weight_mut(node_id.into())
406 }
407
408 #[allow(dead_code)]
409 pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
410 self.0.edge_weight(EdgeIndex::new(index)).cloned()
411 }
412
413 #[allow(dead_code)]
414 pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
415 self.0.node_indices().find_map(|node_index| {
416 if let Some(node) = self.0.node_weight(node_index) {
417 if node.id != node_id {
418 return None;
419 }
420 let edges: Vec<_> = self
421 .0
422 .edges_directed(node_index, Outgoing)
423 .map(|edge| edge.id().index())
424 .collect();
425 if edges.is_empty() {
426 return None;
427 }
428 let cnx = self
429 .0
430 .edge_weight(EdgeIndex::new(edges[0]))
431 .expect("Found an cnx id but could not retrieve it back");
432 return Some(cnx.msg.clone());
433 }
434 None
435 })
436 }
437
438 #[allow(dead_code)]
439 pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
440 self.0.node_indices().find_map(|node_index| {
441 if let Some(node) = self.0.node_weight(node_index) {
442 if node.id != node_id {
443 return None;
444 }
445 let edges: Vec<_> = self
446 .0
447 .edges_directed(node_index, Incoming)
448 .map(|edge| edge.id().index())
449 .collect();
450 if edges.is_empty() {
451 return None;
452 }
453 let cnx = self
454 .0
455 .edge_weight(EdgeIndex::new(edges[0]))
456 .expect("Found an cnx id but could not retrieve it back");
457 return Some(cnx.msg.clone());
458 }
459 None
460 })
461 }
462
463 fn get_edges_by_direction(
465 &self,
466 node_id: NodeId,
467 direction: petgraph::Direction,
468 ) -> CuResult<Vec<usize>> {
469 Ok(self
470 .0
471 .edges_directed(node_id.into(), direction)
472 .map(|edge| edge.id().index())
473 .collect())
474 }
475
476 pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
477 self.get_edges_by_direction(node_id, Outgoing)
478 }
479
480 pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
482 self.get_edges_by_direction(node_id, Incoming)
483 }
484
485 #[allow(dead_code)]
488 pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
489 self.connect_ext(source, target, msg_type, None)
490 }
491}
492
493impl core::ops::Index<NodeIndex> for CuGraph {
494 type Output = Node;
495
496 fn index(&self, index: NodeIndex) -> &Self::Output {
497 &self.0[index]
498 }
499}
500
501#[derive(Debug, Clone)]
502pub enum ConfigGraphs {
503 Simple(CuGraph),
504 Missions(HashMap<String, CuGraph>),
505}
506
507impl ConfigGraphs {
508 #[allow(dead_code)]
511 pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
512 match self {
513 Simple(graph) => {
514 let mut map = HashMap::new();
515 map.insert("default".to_string(), graph.clone());
516 map
517 }
518 Missions(graphs) => graphs.clone(),
519 }
520 }
521
522 #[allow(dead_code)]
523 pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
524 match self {
525 Simple(graph) => Ok(graph),
526 Missions(graphs) => {
527 if graphs.len() == 1 {
528 Ok(graphs.values().next().unwrap())
529 } else {
530 Err("Cannot get default mission graph from mission config".into())
531 }
532 }
533 }
534 }
535
536 #[allow(dead_code)]
537 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
538 match self {
539 Simple(graph) => {
540 if mission_id.is_none() || mission_id.unwrap() == "default" {
541 Ok(graph)
542 } else {
543 Err("Cannot get mission graph from simple config".into())
544 }
545 }
546 Missions(graphs) => {
547 if let Some(id) = mission_id {
548 graphs
549 .get(id)
550 .ok_or_else(|| format!("Mission {id} not found").into())
551 } else {
552 Err("Mission ID required for mission configs".into())
553 }
554 }
555 }
556 }
557
558 #[allow(dead_code)]
559 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
560 match self {
561 Simple(ref mut graph) => {
562 if mission_id.is_none() {
563 Ok(graph)
564 } else {
565 Err("Cannot get mission graph from simple config".into())
566 }
567 }
568 Missions(ref mut graphs) => {
569 if let Some(id) = mission_id {
570 graphs
571 .get_mut(id)
572 .ok_or_else(|| format!("Mission {id} not found").into())
573 } else {
574 Err("Mission ID required for mission configs".into())
575 }
576 }
577 }
578 }
579
580 pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
581 match self {
582 Simple(_) => Err("Cannot add mission to simple config".into()),
583 Missions(graphs) => {
584 if graphs.contains_key(mission_id) {
585 Err(format!("Mission {mission_id} already exists").into())
586 } else {
587 let graph = CuGraph::default();
588 graphs.insert(mission_id.to_string(), graph);
589 Ok(graphs.get_mut(mission_id).unwrap())
591 }
592 }
593 }
594 }
595}
596
597#[derive(Debug, Clone)]
603pub struct CuConfig {
604 pub monitor: Option<MonitorConfig>,
606 pub logging: Option<LoggingConfig>,
608 pub runtime: Option<RuntimeConfig>,
610 pub graphs: ConfigGraphs,
612}
613
614#[derive(Serialize, Deserialize, Default, Debug, Clone)]
615pub struct MonitorConfig {
616 #[serde(rename = "type")]
617 type_: String,
618 #[serde(skip_serializing_if = "Option::is_none")]
619 config: Option<ComponentConfig>,
620}
621
622impl MonitorConfig {
623 #[allow(dead_code)]
624 pub fn get_type(&self) -> &str {
625 &self.type_
626 }
627
628 #[allow(dead_code)]
629 pub fn get_config(&self) -> Option<&ComponentConfig> {
630 self.config.as_ref()
631 }
632}
633
634fn default_as_true() -> bool {
635 true
636}
637
638pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
639
640fn default_keyframe_interval() -> Option<u32> {
641 Some(DEFAULT_KEYFRAME_INTERVAL)
642}
643
644#[derive(Serialize, Deserialize, Default, Debug, Clone)]
645pub struct LoggingConfig {
646 #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
648 pub enable_task_logging: bool,
649
650 #[serde(skip_serializing_if = "Option::is_none")]
652 pub slab_size_mib: Option<u64>,
653
654 #[serde(skip_serializing_if = "Option::is_none")]
656 pub section_size_mib: Option<u64>,
657
658 #[serde(
660 default = "default_keyframe_interval",
661 skip_serializing_if = "Option::is_none"
662 )]
663 pub keyframe_interval: Option<u32>,
664}
665
666#[derive(Serialize, Deserialize, Default, Debug, Clone)]
667pub struct RuntimeConfig {
668 #[serde(skip_serializing_if = "Option::is_none")]
674 pub rate_target_hz: Option<u64>,
675}
676
677#[derive(Serialize, Deserialize, Debug, Clone)]
679pub struct MissionsConfig {
680 pub id: String,
681}
682
683#[derive(Serialize, Deserialize, Debug, Clone)]
685pub struct IncludesConfig {
686 pub path: String,
687 pub params: HashMap<String, Value>,
688 pub missions: Option<Vec<String>>,
689}
690
691#[derive(Serialize, Deserialize, Default)]
693struct CuConfigRepresentation {
694 tasks: Option<Vec<Node>>,
695 cnx: Option<Vec<Cnx>>,
696 monitor: Option<MonitorConfig>,
697 logging: Option<LoggingConfig>,
698 runtime: Option<RuntimeConfig>,
699 missions: Option<Vec<MissionsConfig>>,
700 includes: Option<Vec<IncludesConfig>>,
701}
702
703fn deserialize_config_representation<E>(
705 representation: &CuConfigRepresentation,
706) -> Result<CuConfig, E>
707where
708 E: From<String>,
709{
710 let mut cuconfig = CuConfig::default();
711
712 if let Some(mission_configs) = &representation.missions {
713 let mut missions = Missions(HashMap::new());
715
716 for mission_config in mission_configs {
717 let mission_id = mission_config.id.as_str();
718 let graph = missions
719 .add_mission(mission_id)
720 .map_err(|e| E::from(e.to_string()))?;
721
722 if let Some(tasks) = &representation.tasks {
723 for task in tasks {
724 if let Some(task_missions) = &task.missions {
725 if task_missions.contains(&mission_id.to_owned()) {
727 graph
728 .add_node(task.clone())
729 .map_err(|e| E::from(e.to_string()))?;
730 }
731 } else {
732 graph
734 .add_node(task.clone())
735 .map_err(|e| E::from(e.to_string()))?;
736 }
737 }
738 }
739
740 if let Some(cnx) = &representation.cnx {
741 for c in cnx {
742 if let Some(cnx_missions) = &c.missions {
743 if cnx_missions.contains(&mission_id.to_owned()) {
745 let src = graph
746 .node_indices()
747 .into_iter()
748 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.src)
749 .ok_or_else(|| {
750 E::from(format!("Source node not found: {}", c.src))
751 })?;
752 let dst = graph
753 .node_indices()
754 .into_iter()
755 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.dst)
756 .ok_or_else(|| {
757 E::from(format!("Destination node not found: {}", c.dst))
758 })?;
759 graph
760 .connect_ext(
761 src.index() as NodeId,
762 dst.index() as NodeId,
763 &c.msg,
764 Some(cnx_missions.clone()),
765 )
766 .map_err(|e| E::from(e.to_string()))?;
767 }
768 } else {
769 let src = graph
771 .node_indices()
772 .into_iter()
773 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.src)
774 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
775 let dst = graph
776 .node_indices()
777 .into_iter()
778 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.dst)
779 .ok_or_else(|| {
780 E::from(format!("Destination node not found: {}", c.dst))
781 })?;
782 graph
783 .connect_ext(src.index() as NodeId, dst.index() as NodeId, &c.msg, None)
784 .map_err(|e| E::from(e.to_string()))?;
785 }
786 }
787 }
788 }
789 cuconfig.graphs = missions;
790 } else {
791 let mut graph = CuGraph::default();
793
794 if let Some(tasks) = &representation.tasks {
795 for task in tasks {
796 graph
797 .add_node(task.clone())
798 .map_err(|e| E::from(e.to_string()))?;
799 }
800 }
801
802 if let Some(cnx) = &representation.cnx {
803 for c in cnx {
804 let src = graph
805 .node_indices()
806 .into_iter()
807 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.src)
808 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
809 let dst = graph
810 .node_indices()
811 .into_iter()
812 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.dst)
813 .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
814 graph
815 .connect_ext(src.index() as NodeId, dst.index() as NodeId, &c.msg, None)
816 .map_err(|e| E::from(e.to_string()))?;
817 }
818 }
819 cuconfig.graphs = Simple(graph);
820 }
821
822 cuconfig.monitor = representation.monitor.clone();
823 cuconfig.logging = representation.logging.clone();
824 cuconfig.runtime = representation.runtime.clone();
825
826 Ok(cuconfig)
827}
828
829impl<'de> Deserialize<'de> for CuConfig {
830 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
832 where
833 D: Deserializer<'de>,
834 {
835 let representation =
836 CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
837
838 match deserialize_config_representation::<String>(&representation) {
840 Ok(config) => Ok(config),
841 Err(e) => Err(serde::de::Error::custom(e)),
842 }
843 }
844}
845
846impl Serialize for CuConfig {
847 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
849 where
850 S: Serializer,
851 {
852 match &self.graphs {
853 Simple(graph) => {
854 let tasks: Vec<Node> = graph
855 .0
856 .node_indices()
857 .map(|idx| graph.0[idx].clone())
858 .collect();
859
860 let cnx: Vec<Cnx> = graph
861 .0
862 .edge_indices()
863 .map(|edge| graph.0[edge].clone())
864 .collect();
865
866 CuConfigRepresentation {
867 tasks: Some(tasks),
868 cnx: Some(cnx),
869 monitor: self.monitor.clone(),
870 logging: self.logging.clone(),
871 runtime: self.runtime.clone(),
872 missions: None,
873 includes: None,
874 }
875 .serialize(serializer)
876 }
877 Missions(graphs) => {
878 let missions = graphs
879 .keys()
880 .map(|id| MissionsConfig { id: id.clone() })
881 .collect();
882
883 let mut tasks = Vec::new();
885 let mut cnx = Vec::new();
886
887 for graph in graphs.values() {
888 for node_idx in graph.node_indices() {
890 let node = &graph[node_idx];
891 if !tasks.iter().any(|n: &Node| n.id == node.id) {
892 tasks.push(node.clone());
893 }
894 }
895
896 for edge_idx in graph.0.edge_indices() {
898 let edge = &graph.0[edge_idx];
899 if !cnx.iter().any(|c: &Cnx| {
900 c.src == edge.src && c.dst == edge.dst && c.msg == edge.msg
901 }) {
902 cnx.push(edge.clone());
903 }
904 }
905 }
906
907 CuConfigRepresentation {
908 tasks: Some(tasks),
909 cnx: Some(cnx),
910 monitor: self.monitor.clone(),
911 logging: self.logging.clone(),
912 runtime: self.runtime.clone(),
913 missions: Some(missions),
914 includes: None,
915 }
916 .serialize(serializer)
917 }
918 }
919 }
920}
921
922impl Default for CuConfig {
923 fn default() -> Self {
924 CuConfig {
925 graphs: Simple(CuGraph(StableDiGraph::new())),
926 monitor: None,
927 logging: None,
928 runtime: None,
929 }
930 }
931}
932
933impl CuConfig {
936 #[allow(dead_code)]
937 pub fn new_simple_type() -> Self {
938 Self::default()
939 }
940
941 #[allow(dead_code)]
942 pub fn new_mission_type() -> Self {
943 CuConfig {
944 graphs: Missions(HashMap::new()),
945 monitor: None,
946 logging: None,
947 runtime: None,
948 }
949 }
950
951 fn get_options() -> Options {
952 Options::default()
953 .with_default_extension(Extensions::IMPLICIT_SOME)
954 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
955 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
956 }
957
958 #[allow(dead_code)]
959 pub fn serialize_ron(&self) -> String {
960 let ron = Self::get_options();
961 let pretty = ron::ser::PrettyConfig::default();
962 ron.to_string_pretty(&self, pretty).unwrap()
963 }
964
965 #[allow(dead_code)]
966 pub fn deserialize_ron(ron: &str) -> Self {
967 match Self::get_options().from_str(ron) {
968 Ok(representation) => Self::deserialize_impl(representation).unwrap_or_else(|e| {
969 panic!("Error deserializing configuration: {e}");
970 }),
971 Err(e) => panic!("Syntax Error in config: {} at position {}", e.code, e.span),
972 }
973 }
974
975 fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
976 deserialize_config_representation(&representation)
977 }
978
979 #[cfg(feature = "std")]
981 pub fn render(
982 &self,
983 output: &mut dyn std::io::Write,
984 mission_id: Option<&str>,
985 ) -> CuResult<()> {
986 writeln!(output, "digraph G {{").unwrap();
987
988 let graph = self.get_graph(mission_id)?;
989
990 for index in graph.node_indices() {
991 let node = &graph[index];
992 let config_str = match &node.config {
993 Some(config) => {
994 let config_str = config
995 .0
996 .iter()
997 .map(|(k, v)| format!("<B>{k}</B> = {v}<BR ALIGN=\"LEFT\"/>"))
998 .collect::<Vec<String>>()
999 .join("\n");
1000 format!("____________<BR/><BR ALIGN=\"LEFT\"/>{config_str}")
1001 }
1002 None => String::new(),
1003 };
1004 writeln!(output, "{} [", index.index()).unwrap();
1005 writeln!(output, "shape=box,").unwrap();
1006 writeln!(output, "style=\"rounded, filled\",").unwrap();
1007 writeln!(output, "fontname=\"Noto Sans\"").unwrap();
1008
1009 let is_src = graph
1010 .get_dst_edges(index.index() as NodeId)
1011 .unwrap_or_default()
1012 .is_empty();
1013 let is_sink = graph
1014 .get_src_edges(index.index() as NodeId)
1015 .unwrap_or_default()
1016 .is_empty();
1017 if is_src {
1018 writeln!(output, "fillcolor=lightgreen,").unwrap();
1019 } else if is_sink {
1020 writeln!(output, "fillcolor=lightblue,").unwrap();
1021 } else {
1022 writeln!(output, "fillcolor=lightgrey,").unwrap();
1023 }
1024 writeln!(output, "color=grey,").unwrap();
1025
1026 writeln!(output, "labeljust=l,").unwrap();
1027 writeln!(
1028 output,
1029 "label=< <FONT COLOR=\"red\"><B>{}</B></FONT> <FONT COLOR=\"dimgray\">[{}]</FONT><BR ALIGN=\"LEFT\"/>{} >",
1030 node.id,
1031 node.get_type(),
1032 config_str
1033 )
1034 .unwrap();
1035
1036 writeln!(output, "];").unwrap();
1037 }
1038 for edge in graph.0.edge_indices() {
1039 let (src, dst) = graph.0.edge_endpoints(edge).unwrap();
1040
1041 let cnx = &graph.0[edge];
1042 let msg = encode_text(&cnx.msg);
1043 writeln!(
1044 output,
1045 "{} -> {} [label=< <B><FONT COLOR=\"gray\">{}</FONT></B> >];",
1046 src.index(),
1047 dst.index(),
1048 msg
1049 )
1050 .unwrap();
1051 }
1052 writeln!(output, "}}").unwrap();
1053 Ok(())
1054 }
1055
1056 #[allow(dead_code)]
1057 pub fn get_all_instances_configs(
1058 &self,
1059 mission_id: Option<&str>,
1060 ) -> Vec<Option<&ComponentConfig>> {
1061 let graph = self.graphs.get_graph(mission_id).unwrap();
1062 graph
1063 .get_all_nodes()
1064 .iter()
1065 .map(|(_, node)| node.get_instance_config())
1066 .collect()
1067 }
1068
1069 #[allow(dead_code)]
1070 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1071 self.graphs.get_graph(mission_id)
1072 }
1073
1074 #[allow(dead_code)]
1075 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1076 self.graphs.get_graph_mut(mission_id)
1077 }
1078
1079 #[allow(dead_code)]
1080 pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1081 self.monitor.as_ref()
1082 }
1083
1084 #[allow(dead_code)]
1085 pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
1086 self.runtime.as_ref()
1087 }
1088
1089 pub fn validate_logging_config(&self) -> CuResult<()> {
1092 if let Some(logging) = &self.logging {
1093 return logging.validate();
1094 }
1095 Ok(())
1096 }
1097}
1098
1099impl LoggingConfig {
1100 pub fn validate(&self) -> CuResult<()> {
1102 if let Some(section_size_mib) = self.section_size_mib {
1103 if let Some(slab_size_mib) = self.slab_size_mib {
1104 if section_size_mib > slab_size_mib {
1105 return Err(CuError::from(format!("Section size ({section_size_mib} MiB) cannot be larger than slab size ({slab_size_mib} MiB). Adjust the parameters accordingly.")));
1106 }
1107 }
1108 }
1109
1110 Ok(())
1111 }
1112}
1113
1114#[allow(dead_code)] fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
1116 let mut result = content.to_string();
1117
1118 for (key, value) in params {
1119 let pattern = format!("{{{{{key}}}}}");
1120 result = result.replace(&pattern, &value.to_string());
1121 }
1122
1123 result
1124}
1125
1126#[cfg(feature = "std")]
1128fn process_includes(
1129 file_path: &str,
1130 base_representation: CuConfigRepresentation,
1131 processed_files: &mut Vec<String>,
1132) -> CuResult<CuConfigRepresentation> {
1133 processed_files.push(file_path.to_string());
1135
1136 let mut result = base_representation;
1137
1138 if let Some(includes) = result.includes.take() {
1139 for include in includes {
1140 let include_path = if include.path.starts_with('/') {
1141 include.path.clone()
1142 } else {
1143 let current_dir = std::path::Path::new(file_path)
1144 .parent()
1145 .unwrap_or_else(|| std::path::Path::new(""))
1146 .to_string_lossy()
1147 .to_string();
1148
1149 format!("{}/{}", current_dir, include.path)
1150 };
1151
1152 let include_content = read_to_string(&include_path).map_err(|e| {
1153 CuError::from(format!("Failed to read include file: {include_path}"))
1154 .add_cause(e.to_string().as_str())
1155 })?;
1156
1157 let processed_content = substitute_parameters(&include_content, &include.params);
1158
1159 let mut included_representation: CuConfigRepresentation = match Options::default()
1160 .with_default_extension(Extensions::IMPLICIT_SOME)
1161 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1162 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1163 .from_str(&processed_content)
1164 {
1165 Ok(rep) => rep,
1166 Err(e) => {
1167 return Err(CuError::from(format!(
1168 "Failed to parse include file: {} - Error: {} at position {}",
1169 include_path, e.code, e.span
1170 )));
1171 }
1172 };
1173
1174 included_representation =
1175 process_includes(&include_path, included_representation, processed_files)?;
1176
1177 if let Some(included_tasks) = included_representation.tasks {
1178 if result.tasks.is_none() {
1179 result.tasks = Some(included_tasks);
1180 } else {
1181 let mut tasks = result.tasks.take().unwrap();
1182 for included_task in included_tasks {
1183 if !tasks.iter().any(|t| t.id == included_task.id) {
1184 tasks.push(included_task);
1185 }
1186 }
1187 result.tasks = Some(tasks);
1188 }
1189 }
1190
1191 if let Some(included_cnx) = included_representation.cnx {
1192 if result.cnx.is_none() {
1193 result.cnx = Some(included_cnx);
1194 } else {
1195 let mut cnx = result.cnx.take().unwrap();
1196 for included_c in included_cnx {
1197 if !cnx
1198 .iter()
1199 .any(|c| c.src == included_c.src && c.dst == included_c.dst)
1200 {
1201 cnx.push(included_c);
1202 }
1203 }
1204 result.cnx = Some(cnx);
1205 }
1206 }
1207
1208 if result.monitor.is_none() {
1209 result.monitor = included_representation.monitor;
1210 }
1211
1212 if result.logging.is_none() {
1213 result.logging = included_representation.logging;
1214 }
1215
1216 if result.runtime.is_none() {
1217 result.runtime = included_representation.runtime;
1218 }
1219
1220 if let Some(included_missions) = included_representation.missions {
1221 if result.missions.is_none() {
1222 result.missions = Some(included_missions);
1223 } else {
1224 let mut missions = result.missions.take().unwrap();
1225 for included_mission in included_missions {
1226 if !missions.iter().any(|m| m.id == included_mission.id) {
1227 missions.push(included_mission);
1228 }
1229 }
1230 result.missions = Some(missions);
1231 }
1232 }
1233 }
1234 }
1235
1236 Ok(result)
1237}
1238
1239#[cfg(feature = "std")]
1241pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
1242 let config_content = read_to_string(config_filename).map_err(|e| {
1243 CuError::from(format!(
1244 "Failed to read configuration file: {:?}",
1245 &config_filename
1246 ))
1247 .add_cause(e.to_string().as_str())
1248 })?;
1249 read_configuration_str(config_content, Some(config_filename))
1250}
1251
1252fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
1256 Options::default()
1257 .with_default_extension(Extensions::IMPLICIT_SOME)
1258 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1259 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1260 .from_str(content)
1261 .map_err(|e| {
1262 CuError::from(format!(
1263 "Failed to parse configuration: Error: {} at position {}",
1264 e.code, e.span
1265 ))
1266 })
1267}
1268
1269fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
1272 let cuconfig = CuConfig::deserialize_impl(representation)
1273 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
1274
1275 cuconfig.validate_logging_config()?;
1276
1277 Ok(cuconfig)
1278}
1279
1280#[allow(unused_variables)]
1281pub fn read_configuration_str(
1282 config_content: String,
1283 file_path: Option<&str>,
1284) -> CuResult<CuConfig> {
1285 let representation = parse_config_string(&config_content)?;
1287
1288 #[cfg(feature = "std")]
1291 let representation = if let Some(path) = file_path {
1292 process_includes(path, representation, &mut Vec::new())?
1293 } else {
1294 representation
1295 };
1296
1297 config_representation_to_config(representation)
1299}
1300
1301#[cfg(test)]
1303mod tests {
1304 use super::*;
1305 #[cfg(not(feature = "std"))]
1306 use alloc::vec;
1307
1308 #[test]
1309 fn test_plain_serialize() {
1310 let mut config = CuConfig::default();
1311 let graph = config.get_graph_mut(None).unwrap();
1312 let n1 = graph
1313 .add_node(Node::new("test1", "package::Plugin1"))
1314 .unwrap();
1315 let n2 = graph
1316 .add_node(Node::new("test2", "package::Plugin2"))
1317 .unwrap();
1318 graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
1319 let serialized = config.serialize_ron();
1320 let deserialized = CuConfig::deserialize_ron(&serialized);
1321 let graph = config.graphs.get_graph(None).unwrap();
1322 let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
1323 assert_eq!(graph.0.node_count(), deserialized_graph.0.node_count());
1324 assert_eq!(graph.0.edge_count(), deserialized_graph.0.edge_count());
1325 }
1326
1327 #[test]
1328 fn test_serialize_with_params() {
1329 let mut config = CuConfig::default();
1330 let graph = config.get_graph_mut(None).unwrap();
1331 let mut camera = Node::new("copper-camera", "camerapkg::Camera");
1332 camera.set_param::<Value>("resolution-height", 1080.into());
1333 graph.add_node(camera).unwrap();
1334 let serialized = config.serialize_ron();
1335 let config = CuConfig::deserialize_ron(&serialized);
1336 let deserialized = config.get_graph(None).unwrap();
1337 assert_eq!(
1338 deserialized
1339 .get_node(0)
1340 .unwrap()
1341 .get_param::<i32>("resolution-height")
1342 .unwrap(),
1343 1080
1344 );
1345 }
1346
1347 #[test]
1348 #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:9-1:10")]
1349 fn test_deserialization_error() {
1350 let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1352 CuConfig::deserialize_ron(txt);
1353 }
1354 #[test]
1355 fn test_missions() {
1356 let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
1357 let config = CuConfig::deserialize_ron(txt);
1358 let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
1359 assert!(graph.0.node_count() == 0);
1360 let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
1361 assert!(graph.0.node_count() == 0);
1362 }
1363
1364 #[test]
1365 fn test_monitor() {
1366 let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1367 let config = CuConfig::deserialize_ron(txt);
1368 assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
1369
1370 let txt =
1371 r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
1372 let config = CuConfig::deserialize_ron(txt);
1373 assert_eq!(
1374 config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
1375 4u8.into()
1376 );
1377 }
1378
1379 #[test]
1380 fn test_logging_parameters() {
1381 let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
1383
1384 let config = CuConfig::deserialize_ron(txt);
1385 assert!(config.logging.is_some());
1386 let logging_config = config.logging.unwrap();
1387 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1388 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1389 assert!(!logging_config.enable_task_logging);
1390
1391 let txt =
1393 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
1394 let config = CuConfig::deserialize_ron(txt);
1395 assert!(config.logging.is_some());
1396 let logging_config = config.logging.unwrap();
1397 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1398 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1399 assert!(logging_config.enable_task_logging);
1400 }
1401
1402 #[test]
1403 fn test_validate_logging_config() {
1404 let txt =
1406 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
1407 let config = CuConfig::deserialize_ron(txt);
1408 assert!(config.validate_logging_config().is_ok());
1409
1410 let txt =
1412 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
1413 let config = CuConfig::deserialize_ron(txt);
1414 assert!(config.validate_logging_config().is_err());
1415 }
1416
1417 #[test]
1419 fn test_deserialization_edge_id_assignment() {
1420 let txt = r#"(
1423 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1424 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
1425 )"#;
1426 let config = CuConfig::deserialize_ron(txt);
1427 let graph = config.graphs.get_graph(None).unwrap();
1428 assert!(config.validate_logging_config().is_ok());
1429
1430 let src1_id = 0;
1432 assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
1433 let src2_id = 1;
1434 assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
1435
1436 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
1439 assert_eq!(src1_edge_id, 1);
1440 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
1441 assert_eq!(src2_edge_id, 0);
1442 }
1443
1444 #[test]
1445 fn test_simple_missions() {
1446 let txt = r#"(
1448 missions: [ (id: "m1"),
1449 (id: "m2"),
1450 ],
1451 tasks: [(id: "src1", type: "a", missions: ["m1"]),
1452 (id: "src2", type: "b", missions: ["m2"]),
1453 (id: "sink", type: "c")],
1454
1455 cnx: [
1456 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
1457 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
1458 ],
1459 )
1460 "#;
1461
1462 let config = CuConfig::deserialize_ron(txt);
1463 let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
1464 assert_eq!(m1_graph.0.edge_count(), 1);
1465 assert_eq!(m1_graph.0.node_count(), 2);
1466 let index = EdgeIndex::new(0);
1467 let cnx = m1_graph.0.edge_weight(index).unwrap();
1468
1469 assert_eq!(cnx.src, "src1");
1470 assert_eq!(cnx.dst, "sink");
1471 assert_eq!(cnx.msg, "u32");
1472 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
1473
1474 let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
1475 assert_eq!(m2_graph.0.edge_count(), 1);
1476 assert_eq!(m2_graph.0.node_count(), 2);
1477 let index = EdgeIndex::new(0);
1478 let cnx = m2_graph.0.edge_weight(index).unwrap();
1479 assert_eq!(cnx.src, "src2");
1480 assert_eq!(cnx.dst, "sink");
1481 assert_eq!(cnx.msg, "u32");
1482 assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
1483 }
1484 #[test]
1485 fn test_mission_serde() {
1486 let txt = r#"(
1488 missions: [ (id: "m1"),
1489 (id: "m2"),
1490 ],
1491 tasks: [(id: "src1", type: "a", missions: ["m1"]),
1492 (id: "src2", type: "b", missions: ["m2"]),
1493 (id: "sink", type: "c")],
1494
1495 cnx: [
1496 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
1497 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
1498 ],
1499 )
1500 "#;
1501
1502 let config = CuConfig::deserialize_ron(txt);
1503 let serialized = config.serialize_ron();
1504 let deserialized = CuConfig::deserialize_ron(&serialized);
1505 let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
1506 assert_eq!(m1_graph.0.edge_count(), 1);
1507 assert_eq!(m1_graph.0.node_count(), 2);
1508 let index = EdgeIndex::new(0);
1509 let cnx = m1_graph.0.edge_weight(index).unwrap();
1510 assert_eq!(cnx.src, "src1");
1511 assert_eq!(cnx.dst, "sink");
1512 assert_eq!(cnx.msg, "u32");
1513 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
1514 }
1515
1516 #[test]
1517 fn test_keyframe_interval() {
1518 let txt = r#"(
1521 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1522 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
1523 logging: ( keyframe_interval: 314 )
1524 )"#;
1525 let config = CuConfig::deserialize_ron(txt);
1526 let logging_config = config.logging.unwrap();
1527 assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
1528 }
1529
1530 #[test]
1531 fn test_default_keyframe_interval() {
1532 let txt = r#"(
1535 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1536 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
1537 logging: ( slab_size_mib: 200, section_size_mib: 1024, )
1538 )"#;
1539 let config = CuConfig::deserialize_ron(txt);
1540 let logging_config = config.logging.unwrap();
1541 assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
1542 }
1543}