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