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(_) => {
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)]
193pub struct Node {
194 id: String,
195
196 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
197 type_: Option<String>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
200 config: Option<ComponentConfig>,
201
202 missions: Option<Vec<String>>,
203}
204
205impl Node {
206 #[allow(dead_code)]
207 pub fn new(id: &str, ptype: &str) -> Self {
208 Node {
209 id: id.to_string(),
210 type_: Some(ptype.to_string()),
211 config: None,
213 missions: None,
214 }
215 }
216
217 #[allow(dead_code)]
218 pub fn get_id(&self) -> String {
219 self.id.clone()
220 }
221
222 #[allow(dead_code)]
223 pub fn set_type(mut self, name: Option<String>) -> Self {
224 self.type_ = name;
225 self
226 }
227
228 #[allow(dead_code)]
229 pub fn get_type(&self) -> &str {
230 self.type_.as_ref().unwrap()
231 }
232
233 #[allow(dead_code)]
234 pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
235 self.config.as_ref()
236 }
237
238 #[allow(dead_code)]
239 pub fn get_param<T: From<Value>>(&self, key: &str) -> Option<T> {
240 let pc = self.config.as_ref()?;
241 let ComponentConfig(pc) = pc;
242 let v = pc.get(key)?;
243 Some(T::from(v.clone()))
244 }
245
246 #[allow(dead_code)]
247 pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
248 if self.config.is_none() {
249 self.config = Some(ComponentConfig(HashMap::new()));
250 }
251 let ComponentConfig(config) = self.config.as_mut().unwrap();
252 config.insert(key.to_string(), value.into());
253 }
254}
255
256#[derive(Serialize, Deserialize, Debug, Clone)]
258pub struct Cnx {
259 src: String,
261
262 dst: String,
264
265 pub msg: String,
267
268 pub missions: Option<Vec<String>>,
270
271 pub store: Option<bool>,
273}
274
275#[derive(Default, Debug, Clone)]
276pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
277
278impl CuGraph {
279 #[allow(dead_code)]
280 pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
281 self.0
282 .node_indices()
283 .map(|index| (index.index() as u32, &self.0[index]))
284 .collect()
285 }
286
287 pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
288 self.0.node_indices().collect()
289 }
290
291 pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
292 Ok(self.0.add_node(node).index() as NodeId)
293 }
294
295 pub fn connect_ext(
296 &mut self,
297 source: NodeId,
298 target: NodeId,
299 msg_type: &str,
300 store: Option<bool>,
301 missions: Option<Vec<String>>,
302 ) -> CuResult<()> {
303 let (src_id, dst_id) = (
304 self.0
305 .node_weight(source.into())
306 .ok_or("Source node not found")?
307 .id
308 .clone(),
309 self.0
310 .node_weight(target.into())
311 .ok_or("Target node not found")?
312 .id
313 .clone(),
314 );
315
316 let _ = self.0.add_edge(
317 petgraph::stable_graph::NodeIndex::from(source),
318 petgraph::stable_graph::NodeIndex::from(target),
319 Cnx {
320 src: src_id,
321 dst: dst_id,
322 msg: msg_type.to_string(),
323 missions,
324 store,
325 },
326 );
327 Ok(())
328 }
329 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
333 self.0.node_weight(node_id.into())
334 }
335
336 #[allow(dead_code)]
337 pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
338 self.0.node_weight(index.into())
339 }
340
341 #[allow(dead_code)]
342 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
343 self.0.node_weight_mut(node_id.into())
344 }
345
346 #[allow(dead_code)]
347 pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
348 self.0.edge_weight(EdgeIndex::new(index)).cloned()
349 }
350
351 #[allow(dead_code)]
352 pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
353 self.0.node_indices().find_map(|node_index| {
354 if let Some(node) = self.0.node_weight(node_index) {
355 if node.id != node_id {
356 return None;
357 }
358 let edges: Vec<_> = self
359 .0
360 .edges_directed(node_index, Outgoing)
361 .map(|edge| edge.id().index())
362 .collect();
363 if edges.is_empty() {
364 panic!("A CuSrcTask is configured with no task connected to it.")
365 }
366 let cnx = self
367 .0
368 .edge_weight(EdgeIndex::new(edges[0]))
369 .expect("Found an cnx id but could not retrieve it back");
370 return Some(cnx.msg.clone());
371 }
372 None
373 })
374 }
375
376 #[allow(dead_code)]
377 pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
378 self.0.node_indices().find_map(|node_index| {
379 if let Some(node) = self.0.node_weight(node_index) {
380 if node.id != node_id {
381 return None;
382 }
383 let edges: Vec<_> = self
384 .0
385 .edges_directed(node_index, Incoming)
386 .map(|edge| edge.id().index())
387 .collect();
388 if edges.is_empty() {
389 return None;
390 }
391 let cnx = self
392 .0
393 .edge_weight(EdgeIndex::new(edges[0]))
394 .expect("Found an cnx id but could not retrieve it back");
395 return Some(cnx.msg.clone());
396 }
397 None
398 })
399 }
400
401 fn get_edges_by_direction(
403 &self,
404 node_id: NodeId,
405 direction: petgraph::Direction,
406 ) -> CuResult<Vec<usize>> {
407 Ok(self
408 .0
409 .edges_directed(node_id.into(), direction)
410 .map(|edge| edge.id().index())
411 .collect())
412 }
413
414 pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
415 self.get_edges_by_direction(node_id, Outgoing)
416 }
417
418 pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
420 self.get_edges_by_direction(node_id, Incoming)
421 }
422
423 #[allow(dead_code)]
426 pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
427 self.connect_ext(source, target, msg_type, None, None)
428 }
429}
430
431impl std::ops::Index<NodeIndex> for CuGraph {
432 type Output = Node;
433
434 fn index(&self, index: NodeIndex) -> &Self::Output {
435 &self.0[index]
436 }
437}
438
439#[derive(Debug, Clone)]
440pub enum ConfigGraphs {
441 Simple(CuGraph),
442 Missions(HashMap<String, CuGraph>),
443}
444
445impl ConfigGraphs {
446 #[allow(dead_code)]
449 pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
450 match self {
451 Simple(graph) => {
452 let mut map = HashMap::new();
453 map.insert("default".to_string(), graph.clone());
454 map
455 }
456 Missions(graphs) => graphs.clone(),
457 }
458 }
459
460 #[allow(dead_code)]
461 pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
462 match self {
463 Simple(graph) => Ok(graph),
464 Missions(graphs) => {
465 if graphs.len() == 1 {
466 Ok(graphs.values().next().unwrap())
467 } else {
468 Err("Cannot get default mission graph from mission config".into())
469 }
470 }
471 }
472 }
473
474 #[allow(dead_code)]
475 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
476 match self {
477 Simple(graph) => {
478 if mission_id.is_none() || mission_id.unwrap() == "default" {
479 Ok(graph)
480 } else {
481 Err("Cannot get mission graph from simple config".into())
482 }
483 }
484 Missions(graphs) => {
485 if let Some(id) = mission_id {
486 graphs
487 .get(id)
488 .ok_or_else(|| format!("Mission {id} not found").into())
489 } else {
490 Err("Mission ID required for mission configs".into())
491 }
492 }
493 }
494 }
495
496 #[allow(dead_code)]
497 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
498 match self {
499 Simple(ref mut graph) => {
500 if mission_id.is_none() {
501 Ok(graph)
502 } else {
503 Err("Cannot get mission graph from simple config".into())
504 }
505 }
506 Missions(ref mut graphs) => {
507 if let Some(id) = mission_id {
508 graphs
509 .get_mut(id)
510 .ok_or_else(|| format!("Mission {id} not found").into())
511 } else {
512 Err("Mission ID required for mission configs".into())
513 }
514 }
515 }
516 }
517
518 pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
519 match self {
520 Simple(_) => Err("Cannot add mission to simple config".into()),
521 Missions(graphs) => {
522 if graphs.contains_key(mission_id) {
523 Err(format!("Mission {mission_id} already exists").into())
524 } else {
525 let graph = CuGraph::default();
526 graphs.insert(mission_id.to_string(), graph);
527 Ok(graphs.get_mut(mission_id).unwrap())
529 }
530 }
531 }
532 }
533}
534
535#[derive(Debug, Clone)]
541pub struct CuConfig {
542 pub monitor: Option<MonitorConfig>,
544 pub logging: Option<LoggingConfig>,
546 pub graphs: ConfigGraphs,
548}
549
550#[derive(Serialize, Deserialize, Default, Debug, Clone)]
551pub struct MonitorConfig {
552 #[serde(rename = "type")]
553 type_: String,
554 #[serde(skip_serializing_if = "Option::is_none")]
555 config: Option<ComponentConfig>,
556}
557
558impl MonitorConfig {
559 #[allow(dead_code)]
560 pub fn get_type(&self) -> &str {
561 &self.type_
562 }
563
564 #[allow(dead_code)]
565 pub fn get_config(&self) -> Option<&ComponentConfig> {
566 self.config.as_ref()
567 }
568}
569
570fn default_as_true() -> bool {
571 true
572}
573
574pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
575
576fn default_keyframe_interval() -> Option<u32> {
577 Some(DEFAULT_KEYFRAME_INTERVAL)
578}
579
580#[derive(Serialize, Deserialize, Default, Debug, Clone)]
581pub struct LoggingConfig {
582 #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
584 pub enable_task_logging: bool,
585
586 #[serde(skip_serializing_if = "Option::is_none")]
588 pub slab_size_mib: Option<u64>,
589
590 #[serde(skip_serializing_if = "Option::is_none")]
592 pub section_size_mib: Option<u64>,
593
594 #[serde(
596 default = "default_keyframe_interval",
597 skip_serializing_if = "Option::is_none"
598 )]
599 pub keyframe_interval: Option<u32>,
600}
601
602#[derive(Serialize, Deserialize, Debug, Clone)]
604pub struct MissionsConfig {
605 pub id: String,
606}
607
608#[derive(Serialize, Deserialize, Debug, Clone)]
610pub struct IncludesConfig {
611 pub path: String,
612 pub params: HashMap<String, Value>,
613 pub missions: Option<Vec<String>>,
614}
615
616#[derive(Serialize, Deserialize, Default)]
618struct CuConfigRepresentation {
619 tasks: Option<Vec<Node>>,
620 cnx: Option<Vec<Cnx>>,
621 monitor: Option<MonitorConfig>,
622 logging: Option<LoggingConfig>,
623 missions: Option<Vec<MissionsConfig>>,
624 includes: Option<Vec<IncludesConfig>>,
625}
626
627fn deserialize_config_representation<E>(
629 representation: &CuConfigRepresentation,
630) -> Result<CuConfig, E>
631where
632 E: From<String>,
633{
634 let mut cuconfig = CuConfig::default();
635
636 if let Some(mission_configs) = &representation.missions {
637 let mut missions = Missions(HashMap::new());
639
640 for mission_config in mission_configs {
641 let mission_id = mission_config.id.as_str();
642 let graph = missions
643 .add_mission(mission_id)
644 .map_err(|e| E::from(e.to_string()))?;
645
646 if let Some(tasks) = &representation.tasks {
647 for task in tasks {
648 if let Some(task_missions) = &task.missions {
649 if task_missions.contains(&mission_id.to_owned()) {
651 graph
652 .add_node(task.clone())
653 .map_err(|e| E::from(e.to_string()))?;
654 }
655 } else {
656 graph
658 .add_node(task.clone())
659 .map_err(|e| E::from(e.to_string()))?;
660 }
661 }
662 }
663
664 if let Some(cnx) = &representation.cnx {
665 for c in cnx {
666 if let Some(cnx_missions) = &c.missions {
667 if cnx_missions.contains(&mission_id.to_owned()) {
669 let src = graph
670 .node_indices()
671 .into_iter()
672 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.src)
673 .ok_or_else(|| {
674 E::from(format!("Source node not found: {}", c.src))
675 })?;
676 let dst = graph
677 .node_indices()
678 .into_iter()
679 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.dst)
680 .ok_or_else(|| {
681 E::from(format!("Destination node not found: {}", c.dst))
682 })?;
683 graph
684 .connect_ext(
685 src.index() as NodeId,
686 dst.index() as NodeId,
687 &c.msg,
688 c.store,
689 Some(cnx_missions.clone()),
690 )
691 .map_err(|e| E::from(e.to_string()))?;
692 }
693 } else {
694 let src = graph
696 .node_indices()
697 .into_iter()
698 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.src)
699 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
700 let dst = graph
701 .node_indices()
702 .into_iter()
703 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.dst)
704 .ok_or_else(|| {
705 E::from(format!("Destination node not found: {}", c.dst))
706 })?;
707 graph
708 .connect_ext(
709 src.index() as NodeId,
710 dst.index() as NodeId,
711 &c.msg,
712 c.store,
713 None,
714 )
715 .map_err(|e| E::from(e.to_string()))?;
716 }
717 }
718 }
719 }
720 cuconfig.graphs = missions;
721 } else {
722 let mut graph = CuGraph::default();
724
725 if let Some(tasks) = &representation.tasks {
726 for task in tasks {
727 graph
728 .add_node(task.clone())
729 .map_err(|e| E::from(e.to_string()))?;
730 }
731 }
732
733 if let Some(cnx) = &representation.cnx {
734 for c in cnx {
735 let src = graph
736 .node_indices()
737 .into_iter()
738 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.src)
739 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
740 let dst = graph
741 .node_indices()
742 .into_iter()
743 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.dst)
744 .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
745 graph
746 .connect_ext(
747 src.index() as NodeId,
748 dst.index() as NodeId,
749 &c.msg,
750 c.store,
751 None,
752 )
753 .map_err(|e| E::from(e.to_string()))?;
754 }
755 }
756 cuconfig.graphs = Simple(graph);
757 }
758
759 cuconfig.monitor = representation.monitor.clone();
760 cuconfig.logging = representation.logging.clone();
761
762 Ok(cuconfig)
763}
764
765impl<'de> Deserialize<'de> for CuConfig {
766 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
768 where
769 D: Deserializer<'de>,
770 {
771 let representation =
772 CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
773
774 match deserialize_config_representation::<String>(&representation) {
776 Ok(config) => Ok(config),
777 Err(e) => Err(serde::de::Error::custom(e)),
778 }
779 }
780}
781
782impl Serialize for CuConfig {
783 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
785 where
786 S: Serializer,
787 {
788 match &self.graphs {
789 Simple(graph) => {
790 let tasks: Vec<Node> = graph
791 .0
792 .node_indices()
793 .map(|idx| graph.0[idx].clone())
794 .collect();
795
796 let cnx: Vec<Cnx> = graph
797 .0
798 .edge_indices()
799 .map(|edge| graph.0[edge].clone())
800 .collect();
801
802 CuConfigRepresentation {
803 tasks: Some(tasks),
804 cnx: Some(cnx),
805 monitor: self.monitor.clone(),
806 logging: self.logging.clone(),
807 missions: None,
808 includes: None,
809 }
810 .serialize(serializer)
811 }
812 Missions(graphs) => {
813 let missions = graphs
814 .keys()
815 .map(|id| MissionsConfig { id: id.clone() })
816 .collect();
817
818 let mut tasks = Vec::new();
820 let mut cnx = Vec::new();
821
822 for graph in graphs.values() {
823 for node_idx in graph.node_indices() {
825 let node = &graph[node_idx];
826 if !tasks.iter().any(|n: &Node| n.id == node.id) {
827 tasks.push(node.clone());
828 }
829 }
830
831 for edge_idx in graph.0.edge_indices() {
833 let edge = &graph.0[edge_idx];
834 if !cnx.iter().any(|c: &Cnx| {
835 c.src == edge.src && c.dst == edge.dst && c.msg == edge.msg
836 }) {
837 cnx.push(edge.clone());
838 }
839 }
840 }
841
842 CuConfigRepresentation {
843 tasks: Some(tasks),
844 cnx: Some(cnx),
845 monitor: self.monitor.clone(),
846 logging: self.logging.clone(),
847 missions: Some(missions),
848 includes: None,
849 }
850 .serialize(serializer)
851 }
852 }
853 }
854}
855
856impl Default for CuConfig {
857 fn default() -> Self {
858 CuConfig {
859 graphs: Simple(CuGraph(StableDiGraph::new())),
860 monitor: None,
861 logging: None,
862 }
863 }
864}
865
866impl CuConfig {
869 #[allow(dead_code)]
870 pub fn new_simple_type() -> Self {
871 Self::default()
872 }
873
874 #[allow(dead_code)]
875 pub fn new_mission_type() -> Self {
876 CuConfig {
877 graphs: Missions(HashMap::new()),
878 monitor: None,
879 logging: None,
880 }
881 }
882
883 fn get_options() -> Options {
884 Options::default()
885 .with_default_extension(Extensions::IMPLICIT_SOME)
886 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
887 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
888 }
889
890 #[allow(dead_code)]
891 pub fn serialize_ron(&self) -> String {
892 let ron = Self::get_options();
893 let pretty = ron::ser::PrettyConfig::default();
894 ron.to_string_pretty(&self, pretty).unwrap()
895 }
896
897 #[allow(dead_code)]
898 pub fn deserialize_ron(ron: &str) -> Self {
899 match Self::get_options().from_str(ron) {
900 Ok(representation) => Self::deserialize_impl(representation).unwrap_or_else(|e| {
901 panic!("Error deserializing configuration: {e}");
902 }),
903 Err(e) => panic!(
904 "Syntax Error in config: {} at position {}",
905 e.code, e.position
906 ),
907 }
908 }
909
910 fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
911 deserialize_config_representation(&representation)
912 }
913
914 pub fn render(
916 &self,
917 output: &mut dyn std::io::Write,
918 mission_id: Option<&str>,
919 ) -> CuResult<()> {
920 writeln!(output, "digraph G {{").unwrap();
921
922 let graph = self.get_graph(mission_id)?;
923
924 for index in graph.node_indices() {
925 let node = &graph[index];
926 let config_str = match &node.config {
927 Some(config) => {
928 let config_str = config
929 .0
930 .iter()
931 .map(|(k, v)| format!("<B>{k}</B> = {v}<BR ALIGN=\"LEFT\"/>"))
932 .collect::<Vec<String>>()
933 .join("\n");
934 format!("____________<BR/><BR ALIGN=\"LEFT\"/>{config_str}")
935 }
936 None => String::new(),
937 };
938 writeln!(output, "{} [", index.index()).unwrap();
939 writeln!(output, "shape=box,").unwrap();
940 writeln!(output, "style=\"rounded, filled\",").unwrap();
941 writeln!(output, "fontname=\"Noto Sans\"").unwrap();
942
943 let is_src = graph
944 .get_dst_edges(index.index() as NodeId)
945 .unwrap_or_default()
946 .is_empty();
947 let is_sink = graph
948 .get_src_edges(index.index() as NodeId)
949 .unwrap_or_default()
950 .is_empty();
951 if is_src {
952 writeln!(output, "fillcolor=lightgreen,").unwrap();
953 } else if is_sink {
954 writeln!(output, "fillcolor=lightblue,").unwrap();
955 } else {
956 writeln!(output, "fillcolor=lightgrey,").unwrap();
957 }
958 writeln!(output, "color=grey,").unwrap();
959
960 writeln!(output, "labeljust=l,").unwrap();
961 writeln!(
962 output,
963 "label=< <FONT COLOR=\"red\"><B>{}</B></FONT> <FONT COLOR=\"dimgray\">[{}]</FONT><BR ALIGN=\"LEFT\"/>{} >",
964 node.id,
965 node.get_type(),
966 config_str
967 )
968 .unwrap();
969
970 writeln!(output, "];").unwrap();
971 }
972 for edge in graph.0.edge_indices() {
973 let (src, dst) = graph.0.edge_endpoints(edge).unwrap();
974
975 let cnx = &graph.0[edge];
976 let msg = encode_text(&cnx.msg);
977 writeln!(
978 output,
979 "{} -> {} [label=< <B><FONT COLOR=\"gray\">{}</FONT></B> >];",
980 src.index(),
981 dst.index(),
982 msg
983 )
984 .unwrap();
985 }
986 writeln!(output, "}}").unwrap();
987 Ok(())
988 }
989
990 #[allow(dead_code)]
991 pub fn get_all_instances_configs(
992 &self,
993 mission_id: Option<&str>,
994 ) -> Vec<Option<&ComponentConfig>> {
995 let graph = self.graphs.get_graph(mission_id).unwrap();
996 graph
997 .get_all_nodes()
998 .iter()
999 .map(|(_, node)| node.get_instance_config())
1000 .collect()
1001 }
1002
1003 #[allow(dead_code)]
1004 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1005 self.graphs.get_graph(mission_id)
1006 }
1007
1008 #[allow(dead_code)]
1009 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1010 self.graphs.get_graph_mut(mission_id)
1011 }
1012
1013 #[allow(dead_code)]
1014 pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1015 self.monitor.as_ref()
1016 }
1017
1018 pub fn validate_logging_config(&self) -> CuResult<()> {
1021 if let Some(logging) = &self.logging {
1022 return logging.validate();
1023 }
1024 Ok(())
1025 }
1026}
1027
1028impl LoggingConfig {
1029 pub fn validate(&self) -> CuResult<()> {
1031 if let Some(section_size_mib) = self.section_size_mib {
1032 if let Some(slab_size_mib) = self.slab_size_mib {
1033 if section_size_mib > slab_size_mib {
1034 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.")));
1035 }
1036 }
1037 }
1038
1039 Ok(())
1040 }
1041}
1042
1043fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
1044 let mut result = content.to_string();
1045
1046 for (key, value) in params {
1047 let pattern = format!("{{{{{key}}}}}");
1048 result = result.replace(&pattern, &value.to_string());
1049 }
1050
1051 result
1052}
1053
1054fn process_includes(
1056 file_path: &str,
1057 base_representation: CuConfigRepresentation,
1058 processed_files: &mut Vec<String>,
1059) -> CuResult<CuConfigRepresentation> {
1060 processed_files.push(file_path.to_string());
1062
1063 let mut result = base_representation;
1064
1065 if let Some(includes) = result.includes.take() {
1066 for include in includes {
1067 let include_path = if include.path.starts_with('/') {
1068 include.path.clone()
1069 } else {
1070 let current_dir = std::path::Path::new(file_path)
1071 .parent()
1072 .unwrap_or_else(|| std::path::Path::new(""))
1073 .to_string_lossy()
1074 .to_string();
1075
1076 format!("{}/{}", current_dir, include.path)
1077 };
1078
1079 let include_content = read_to_string(&include_path).map_err(|e| {
1080 CuError::from(format!("Failed to read include file: {include_path}"))
1081 .add_cause(e.to_string().as_str())
1082 })?;
1083
1084 let processed_content = substitute_parameters(&include_content, &include.params);
1085
1086 let mut included_representation: CuConfigRepresentation = match Options::default()
1087 .with_default_extension(Extensions::IMPLICIT_SOME)
1088 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1089 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1090 .from_str(&processed_content)
1091 {
1092 Ok(rep) => rep,
1093 Err(e) => {
1094 return Err(CuError::from(format!(
1095 "Failed to parse include file: {} - Error: {} at position {}",
1096 include_path, e.code, e.position
1097 )));
1098 }
1099 };
1100
1101 included_representation =
1102 process_includes(&include_path, included_representation, processed_files)?;
1103
1104 if let Some(included_tasks) = included_representation.tasks {
1105 if result.tasks.is_none() {
1106 result.tasks = Some(included_tasks);
1107 } else {
1108 let mut tasks = result.tasks.take().unwrap();
1109 for included_task in included_tasks {
1110 if !tasks.iter().any(|t| t.id == included_task.id) {
1111 tasks.push(included_task);
1112 }
1113 }
1114 result.tasks = Some(tasks);
1115 }
1116 }
1117
1118 if let Some(included_cnx) = included_representation.cnx {
1119 if result.cnx.is_none() {
1120 result.cnx = Some(included_cnx);
1121 } else {
1122 let mut cnx = result.cnx.take().unwrap();
1123 for included_c in included_cnx {
1124 if !cnx
1125 .iter()
1126 .any(|c| c.src == included_c.src && c.dst == included_c.dst)
1127 {
1128 cnx.push(included_c);
1129 }
1130 }
1131 result.cnx = Some(cnx);
1132 }
1133 }
1134
1135 if result.monitor.is_none() {
1136 result.monitor = included_representation.monitor;
1137 }
1138
1139 if result.logging.is_none() {
1140 result.logging = included_representation.logging;
1141 }
1142
1143 if let Some(included_missions) = included_representation.missions {
1144 if result.missions.is_none() {
1145 result.missions = Some(included_missions);
1146 } else {
1147 let mut missions = result.missions.take().unwrap();
1148 for included_mission in included_missions {
1149 if !missions.iter().any(|m| m.id == included_mission.id) {
1150 missions.push(included_mission);
1151 }
1152 }
1153 result.missions = Some(missions);
1154 }
1155 }
1156 }
1157 }
1158
1159 Ok(result)
1160}
1161
1162pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
1164 let config_content = read_to_string(config_filename).map_err(|e| {
1165 CuError::from(format!(
1166 "Failed to read configuration file: {:?}",
1167 &config_filename
1168 ))
1169 .add_cause(e.to_string().as_str())
1170 })?;
1171 read_configuration_str(config_content, Some(config_filename))
1172}
1173
1174fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
1178 Options::default()
1179 .with_default_extension(Extensions::IMPLICIT_SOME)
1180 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1181 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1182 .from_str(content)
1183 .map_err(|e| {
1184 CuError::from(format!(
1185 "Failed to parse configuration: Error: {} at position {}",
1186 e.code, e.position
1187 ))
1188 })
1189}
1190
1191fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
1194 let cuconfig = CuConfig::deserialize_impl(representation)
1195 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
1196
1197 cuconfig.validate_logging_config()?;
1198
1199 Ok(cuconfig)
1200}
1201
1202pub fn read_configuration_str(
1203 config_content: String,
1204 file_path: Option<&str>,
1205) -> CuResult<CuConfig> {
1206 let representation = parse_config_string(&config_content)?;
1208
1209 let processed_representation = if let Some(path) = file_path {
1211 process_includes(path, representation, &mut Vec::new())?
1212 } else {
1213 representation
1214 };
1215
1216 config_representation_to_config(processed_representation)
1218}
1219
1220#[cfg(test)]
1222mod tests {
1223 use super::*;
1224
1225 #[test]
1226 fn test_plain_serialize() {
1227 let mut config = CuConfig::default();
1228 let graph = config.get_graph_mut(None).unwrap();
1229 let n1 = graph
1230 .add_node(Node::new("test1", "package::Plugin1"))
1231 .unwrap();
1232 let n2 = graph
1233 .add_node(Node::new("test2", "package::Plugin2"))
1234 .unwrap();
1235 graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
1236 let serialized = config.serialize_ron();
1237 let deserialized = CuConfig::deserialize_ron(&serialized);
1238 let graph = config.graphs.get_graph(None).unwrap();
1239 let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
1240 assert_eq!(graph.0.node_count(), deserialized_graph.0.node_count());
1241 assert_eq!(graph.0.edge_count(), deserialized_graph.0.edge_count());
1242 }
1243
1244 #[test]
1245 fn test_serialize_with_params() {
1246 let mut config = CuConfig::default();
1247 let graph = config.get_graph_mut(None).unwrap();
1248 let mut camera = Node::new("copper-camera", "camerapkg::Camera");
1249 camera.set_param::<Value>("resolution-height", 1080.into());
1250 graph.add_node(camera).unwrap();
1251 let serialized = config.serialize_ron();
1252 let config = CuConfig::deserialize_ron(&serialized);
1253 let deserialized = config.get_graph(None).unwrap();
1254 assert_eq!(
1255 deserialized
1256 .get_node(0)
1257 .unwrap()
1258 .get_param::<i32>("resolution-height")
1259 .unwrap(),
1260 1080
1261 );
1262 }
1263
1264 #[test]
1265 #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:10")]
1266 fn test_deserialization_error() {
1267 let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1269 CuConfig::deserialize_ron(txt);
1270 }
1271 #[test]
1272 fn test_missions() {
1273 let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
1274 let config = CuConfig::deserialize_ron(txt);
1275 let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
1276 assert!(graph.0.node_count() == 0);
1277 let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
1278 assert!(graph.0.node_count() == 0);
1279 }
1280
1281 #[test]
1282 fn test_monitor() {
1283 let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1284 let config = CuConfig::deserialize_ron(txt);
1285 assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
1286
1287 let txt =
1288 r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
1289 let config = CuConfig::deserialize_ron(txt);
1290 assert_eq!(
1291 config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
1292 4u8.into()
1293 );
1294 }
1295
1296 #[test]
1297 fn test_logging_parameters() {
1298 let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
1300
1301 let config = CuConfig::deserialize_ron(txt);
1302 assert!(config.logging.is_some());
1303 let logging_config = config.logging.unwrap();
1304 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1305 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1306 assert!(!logging_config.enable_task_logging);
1307
1308 let txt =
1310 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
1311 let config = CuConfig::deserialize_ron(txt);
1312 assert!(config.logging.is_some());
1313 let logging_config = config.logging.unwrap();
1314 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1315 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1316 assert!(logging_config.enable_task_logging);
1317 }
1318
1319 #[test]
1320 fn test_validate_logging_config() {
1321 let txt =
1323 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
1324 let config = CuConfig::deserialize_ron(txt);
1325 assert!(config.validate_logging_config().is_ok());
1326
1327 let txt =
1329 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
1330 let config = CuConfig::deserialize_ron(txt);
1331 assert!(config.validate_logging_config().is_err());
1332 }
1333
1334 #[test]
1336 fn test_deserialization_edge_id_assignment() {
1337 let txt = r#"(
1340 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1341 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
1342 )"#;
1343 let config = CuConfig::deserialize_ron(txt);
1344 let graph = config.graphs.get_graph(None).unwrap();
1345 assert!(config.validate_logging_config().is_ok());
1346
1347 let src1_id = 0;
1349 assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
1350 let src2_id = 1;
1351 assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
1352
1353 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
1356 assert_eq!(src1_edge_id, 1);
1357 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
1358 assert_eq!(src2_edge_id, 0);
1359 }
1360
1361 #[test]
1362 fn test_simple_missions() {
1363 let txt = r#"(
1365 missions: [ (id: "m1"),
1366 (id: "m2"),
1367 ],
1368 tasks: [(id: "src1", type: "a", missions: ["m1"]),
1369 (id: "src2", type: "b", missions: ["m2"]),
1370 (id: "sink", type: "c")],
1371
1372 cnx: [
1373 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
1374 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
1375 ],
1376 )
1377 "#;
1378
1379 let config = CuConfig::deserialize_ron(txt);
1380 let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
1381 assert_eq!(m1_graph.0.edge_count(), 1);
1382 assert_eq!(m1_graph.0.node_count(), 2);
1383 let index = EdgeIndex::new(0);
1384 let cnx = m1_graph.0.edge_weight(index).unwrap();
1385
1386 assert_eq!(cnx.src, "src1");
1387 assert_eq!(cnx.dst, "sink");
1388 assert_eq!(cnx.msg, "u32");
1389 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
1390
1391 let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
1392 assert_eq!(m2_graph.0.edge_count(), 1);
1393 assert_eq!(m2_graph.0.node_count(), 2);
1394 let index = EdgeIndex::new(0);
1395 let cnx = m2_graph.0.edge_weight(index).unwrap();
1396 assert_eq!(cnx.src, "src2");
1397 assert_eq!(cnx.dst, "sink");
1398 assert_eq!(cnx.msg, "u32");
1399 assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
1400 }
1401 #[test]
1402 fn test_mission_serde() {
1403 let txt = r#"(
1405 missions: [ (id: "m1"),
1406 (id: "m2"),
1407 ],
1408 tasks: [(id: "src1", type: "a", missions: ["m1"]),
1409 (id: "src2", type: "b", missions: ["m2"]),
1410 (id: "sink", type: "c")],
1411
1412 cnx: [
1413 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
1414 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
1415 ],
1416 )
1417 "#;
1418
1419 let config = CuConfig::deserialize_ron(txt);
1420 let serialized = config.serialize_ron();
1421 let deserialized = CuConfig::deserialize_ron(&serialized);
1422 let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
1423 assert_eq!(m1_graph.0.edge_count(), 1);
1424 assert_eq!(m1_graph.0.node_count(), 2);
1425 let index = EdgeIndex::new(0);
1426 let cnx = m1_graph.0.edge_weight(index).unwrap();
1427 assert_eq!(cnx.src, "src1");
1428 assert_eq!(cnx.dst, "sink");
1429 assert_eq!(cnx.msg, "u32");
1430 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
1431 }
1432
1433 #[test]
1434 fn test_keyframe_interval() {
1435 let txt = r#"(
1438 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1439 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
1440 logging: ( keyframe_interval: 314 )
1441 )"#;
1442 let config = CuConfig::deserialize_ron(txt);
1443 let logging_config = config.logging.unwrap();
1444 assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
1445 }
1446
1447 #[test]
1448 fn test_default_keyframe_interval() {
1449 let txt = r#"(
1452 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1453 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
1454 logging: ( slab_size_mib: 200, section_size_mib: 1024, )
1455 )"#;
1456 let config = CuConfig::deserialize_ron(txt);
1457 let logging_config = config.logging.unwrap();
1458 assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
1459 }
1460}