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