1#[cfg(not(feature = "std"))]
6extern crate alloc;
7
8use ConfigGraphs::{Missions, Simple};
9use core::any::type_name;
10use core::fmt;
11use core::fmt::Display;
12use cu29_traits::{CuError, CuResult};
13use cu29_value::Value as CuValue;
14use hashbrown::HashMap;
15pub use petgraph::Direction::Incoming;
16pub use petgraph::Direction::Outgoing;
17use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableDiGraph};
18#[cfg(feature = "std")]
19use petgraph::visit::IntoEdgeReferences;
20use petgraph::visit::{Bfs, EdgeRef};
21use ron::extensions::Extensions;
22use ron::value::Value as RonValue;
23use ron::{Number, Options};
24use serde::de::DeserializeOwned;
25use serde::{Deserialize, Deserializer, Serialize, Serializer};
26
27#[cfg(not(feature = "std"))]
28use alloc::boxed::Box;
29#[cfg(not(feature = "std"))]
30use alloc::collections::BTreeMap;
31#[cfg(feature = "std")]
32use std::collections::BTreeMap;
33
34#[cfg(not(feature = "std"))]
35mod imp {
36 pub use alloc::borrow::ToOwned;
37 pub use alloc::format;
38 pub use alloc::string::String;
39 pub use alloc::string::ToString;
40 pub use alloc::vec::Vec;
41}
42
43#[cfg(feature = "std")]
44mod imp {
45 pub use html_escape::encode_text;
46 pub use std::fs::read_to_string;
47}
48
49use imp::*;
50
51pub type NodeId = u32;
54
55#[derive(Serialize, Deserialize, Debug, Clone, Default)]
59pub struct ComponentConfig(pub HashMap<String, Value>);
60
61#[allow(dead_code)]
63impl Display for ComponentConfig {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 let mut first = true;
66 let ComponentConfig(config) = self;
67 write!(f, "{{")?;
68 for (key, value) in config.iter() {
69 if !first {
70 write!(f, ", ")?;
71 }
72 write!(f, "{key}: {value}")?;
73 first = false;
74 }
75 write!(f, "}}")
76 }
77}
78
79impl ComponentConfig {
81 #[allow(dead_code)]
82 pub fn new() -> Self {
83 ComponentConfig(HashMap::new())
84 }
85
86 #[allow(dead_code)]
87 pub fn get<T>(&self, key: &str) -> Result<Option<T>, ConfigError>
88 where
89 T: for<'a> TryFrom<&'a Value, Error = ConfigError>,
90 {
91 let ComponentConfig(config) = self;
92 match config.get(key) {
93 Some(value) => T::try_from(value).map(Some),
94 None => Ok(None),
95 }
96 }
97
98 #[allow(dead_code)]
99 pub fn get_value<T>(&self, key: &str) -> Result<Option<T>, ConfigError>
113 where
114 T: DeserializeOwned,
115 {
116 let ComponentConfig(config) = self;
117 let Some(value) = config.get(key) else {
118 return Ok(None);
119 };
120 let cu_value = ron_value_to_cu_value(&value.0).map_err(|err| err.with_key(key))?;
121 cu_value
122 .deserialize_into::<T>()
123 .map(Some)
124 .map_err(|err| ConfigError {
125 message: format!(
126 "Config key '{key}' failed to deserialize as {}: {err}",
127 type_name::<T>()
128 ),
129 })
130 }
131
132 #[allow(dead_code)]
133 pub fn set<T: Into<Value>>(&mut self, key: &str, value: T) {
134 let ComponentConfig(config) = self;
135 config.insert(key.to_string(), value.into());
136 }
137}
138
139fn ron_value_to_cu_value(value: &RonValue) -> Result<CuValue, ConfigError> {
140 match value {
141 RonValue::Bool(v) => Ok(CuValue::Bool(*v)),
142 RonValue::Char(v) => Ok(CuValue::Char(*v)),
143 RonValue::String(v) => Ok(CuValue::String(v.clone())),
144 RonValue::Bytes(v) => Ok(CuValue::Bytes(v.clone())),
145 RonValue::Unit => Ok(CuValue::Unit),
146 RonValue::Option(v) => {
147 let mapped = match v {
148 Some(inner) => Some(Box::new(ron_value_to_cu_value(inner)?)),
149 None => None,
150 };
151 Ok(CuValue::Option(mapped))
152 }
153 RonValue::Seq(seq) => {
154 let mut mapped = Vec::with_capacity(seq.len());
155 for item in seq {
156 mapped.push(ron_value_to_cu_value(item)?);
157 }
158 Ok(CuValue::Seq(mapped))
159 }
160 RonValue::Map(map) => {
161 let mut mapped = BTreeMap::new();
162 for (key, value) in map.iter() {
163 let mapped_key = ron_value_to_cu_value(key)?;
164 let mapped_value = ron_value_to_cu_value(value)?;
165 mapped.insert(mapped_key, mapped_value);
166 }
167 Ok(CuValue::Map(mapped))
168 }
169 RonValue::Number(num) => match num {
170 Number::I8(v) => Ok(CuValue::I8(*v)),
171 Number::I16(v) => Ok(CuValue::I16(*v)),
172 Number::I32(v) => Ok(CuValue::I32(*v)),
173 Number::I64(v) => Ok(CuValue::I64(*v)),
174 Number::U8(v) => Ok(CuValue::U8(*v)),
175 Number::U16(v) => Ok(CuValue::U16(*v)),
176 Number::U32(v) => Ok(CuValue::U32(*v)),
177 Number::U64(v) => Ok(CuValue::U64(*v)),
178 Number::F32(v) => Ok(CuValue::F32(v.0)),
179 Number::F64(v) => Ok(CuValue::F64(v.0)),
180 Number::__NonExhaustive(_) => Err(ConfigError {
181 message: "Unsupported RON number variant".to_string(),
182 }),
183 },
184 }
185}
186
187#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
196pub struct Value(RonValue);
197
198#[derive(Debug, Clone, PartialEq)]
199pub struct ConfigError {
200 message: String,
201}
202
203impl ConfigError {
204 fn type_mismatch(expected: &'static str, value: &Value) -> Self {
205 ConfigError {
206 message: format!("Expected {expected} but got {value:?}"),
207 }
208 }
209
210 fn with_key(self, key: &str) -> Self {
211 ConfigError {
212 message: format!("Config key '{key}': {}", self.message),
213 }
214 }
215}
216
217impl Display for ConfigError {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 write!(f, "{}", self.message)
220 }
221}
222
223#[cfg(feature = "std")]
224impl std::error::Error for ConfigError {}
225
226#[cfg(not(feature = "std"))]
227impl core::error::Error for ConfigError {}
228
229impl From<ConfigError> for CuError {
230 fn from(err: ConfigError) -> Self {
231 CuError::from(err.to_string())
232 }
233}
234
235macro_rules! impl_from_numeric_for_value {
237 ($($source:ty),* $(,)?) => {
238 $(impl From<$source> for Value {
239 fn from(value: $source) -> Self {
240 Value(RonValue::Number(value.into()))
241 }
242 })*
243 };
244}
245
246impl_from_numeric_for_value!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
248
249impl TryFrom<&Value> for bool {
250 type Error = ConfigError;
251
252 fn try_from(value: &Value) -> Result<Self, Self::Error> {
253 if let Value(RonValue::Bool(v)) = value {
254 Ok(*v)
255 } else {
256 Err(ConfigError::type_mismatch("bool", value))
257 }
258 }
259}
260
261impl From<Value> for bool {
262 fn from(value: Value) -> Self {
263 if let Value(RonValue::Bool(v)) = value {
264 v
265 } else {
266 panic!("Expected a Boolean variant but got {value:?}")
267 }
268 }
269}
270macro_rules! impl_from_value_for_int {
271 ($($target:ty),* $(,)?) => {
272 $(
273 impl From<Value> for $target {
274 fn from(value: Value) -> Self {
275 if let Value(RonValue::Number(num)) = value {
276 match num {
277 Number::I8(n) => n as $target,
278 Number::I16(n) => n as $target,
279 Number::I32(n) => n as $target,
280 Number::I64(n) => n as $target,
281 Number::U8(n) => n as $target,
282 Number::U16(n) => n as $target,
283 Number::U32(n) => n as $target,
284 Number::U64(n) => n as $target,
285 Number::F32(_) | Number::F64(_) | Number::__NonExhaustive(_) => {
286 panic!("Expected an integer Number variant but got {num:?}")
287 }
288 }
289 } else {
290 panic!("Expected a Number variant but got {value:?}")
291 }
292 }
293 }
294 )*
295 };
296}
297
298impl_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
299
300macro_rules! impl_try_from_value_for_int {
301 ($($target:ty),* $(,)?) => {
302 $(
303 impl TryFrom<&Value> for $target {
304 type Error = ConfigError;
305
306 fn try_from(value: &Value) -> Result<Self, Self::Error> {
307 if let Value(RonValue::Number(num)) = value {
308 match num {
309 Number::I8(n) => Ok(*n as $target),
310 Number::I16(n) => Ok(*n as $target),
311 Number::I32(n) => Ok(*n as $target),
312 Number::I64(n) => Ok(*n as $target),
313 Number::U8(n) => Ok(*n as $target),
314 Number::U16(n) => Ok(*n as $target),
315 Number::U32(n) => Ok(*n as $target),
316 Number::U64(n) => Ok(*n as $target),
317 Number::F32(_) | Number::F64(_) | Number::__NonExhaustive(_) => {
318 Err(ConfigError::type_mismatch("integer", value))
319 }
320 }
321 } else {
322 Err(ConfigError::type_mismatch("integer", value))
323 }
324 }
325 }
326 )*
327 };
328}
329
330impl_try_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
331
332impl TryFrom<&Value> for f64 {
333 type Error = ConfigError;
334
335 fn try_from(value: &Value) -> Result<Self, Self::Error> {
336 if let Value(RonValue::Number(num)) = value {
337 let number = match num {
338 Number::I8(n) => *n as f64,
339 Number::I16(n) => *n as f64,
340 Number::I32(n) => *n as f64,
341 Number::I64(n) => *n as f64,
342 Number::U8(n) => *n as f64,
343 Number::U16(n) => *n as f64,
344 Number::U32(n) => *n as f64,
345 Number::U64(n) => *n as f64,
346 Number::F32(n) => n.0 as f64,
347 Number::F64(n) => n.0,
348 Number::__NonExhaustive(_) => {
349 return Err(ConfigError::type_mismatch("number", value));
350 }
351 };
352 Ok(number)
353 } else {
354 Err(ConfigError::type_mismatch("number", value))
355 }
356 }
357}
358
359impl From<Value> for f64 {
360 fn from(value: Value) -> Self {
361 if let Value(RonValue::Number(num)) = value {
362 num.into_f64()
363 } else {
364 panic!("Expected a Number variant but got {value:?}")
365 }
366 }
367}
368
369impl From<String> for Value {
370 fn from(value: String) -> Self {
371 Value(RonValue::String(value))
372 }
373}
374
375impl TryFrom<&Value> for String {
376 type Error = ConfigError;
377
378 fn try_from(value: &Value) -> Result<Self, Self::Error> {
379 if let Value(RonValue::String(s)) = value {
380 Ok(s.clone())
381 } else {
382 Err(ConfigError::type_mismatch("string", value))
383 }
384 }
385}
386
387impl From<Value> for String {
388 fn from(value: Value) -> Self {
389 if let Value(RonValue::String(s)) = value {
390 s
391 } else {
392 panic!("Expected a String variant")
393 }
394 }
395}
396
397impl Display for Value {
398 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399 let Value(value) = self;
400 match value {
401 RonValue::Number(n) => {
402 let s = match n {
403 Number::I8(n) => n.to_string(),
404 Number::I16(n) => n.to_string(),
405 Number::I32(n) => n.to_string(),
406 Number::I64(n) => n.to_string(),
407 Number::U8(n) => n.to_string(),
408 Number::U16(n) => n.to_string(),
409 Number::U32(n) => n.to_string(),
410 Number::U64(n) => n.to_string(),
411 Number::F32(n) => n.0.to_string(),
412 Number::F64(n) => n.0.to_string(),
413 _ => panic!("Expected a Number variant but got {value:?}"),
414 };
415 write!(f, "{s}")
416 }
417 RonValue::String(s) => write!(f, "{s}"),
418 RonValue::Bool(b) => write!(f, "{b}"),
419 RonValue::Map(m) => write!(f, "{m:?}"),
420 RonValue::Char(c) => write!(f, "{c:?}"),
421 RonValue::Unit => write!(f, "unit"),
422 RonValue::Option(o) => write!(f, "{o:?}"),
423 RonValue::Seq(s) => write!(f, "{s:?}"),
424 RonValue::Bytes(bytes) => write!(f, "{bytes:?}"),
425 }
426 }
427}
428
429#[derive(Serialize, Deserialize, Debug, Clone)]
431pub struct NodeLogging {
432 enabled: bool,
433}
434
435#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
438pub enum Flavor {
439 #[default]
440 Task,
441 Bridge,
442}
443
444#[derive(Serialize, Deserialize, Debug, Clone)]
447pub struct Node {
448 id: String,
450
451 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
453 type_: Option<String>,
454
455 #[serde(skip_serializing_if = "Option::is_none")]
457 config: Option<ComponentConfig>,
458
459 #[serde(skip_serializing_if = "Option::is_none")]
461 resources: Option<HashMap<String, String>>,
462
463 missions: Option<Vec<String>>,
465
466 #[serde(skip_serializing_if = "Option::is_none")]
469 background: Option<bool>,
470
471 #[serde(skip_serializing_if = "Option::is_none")]
477 run_in_sim: Option<bool>,
478
479 #[serde(skip_serializing_if = "Option::is_none")]
481 logging: Option<NodeLogging>,
482
483 #[serde(skip, default)]
485 flavor: Flavor,
486}
487
488impl Node {
489 #[allow(dead_code)]
490 pub fn new(id: &str, ptype: &str) -> Self {
491 Node {
492 id: id.to_string(),
493 type_: Some(ptype.to_string()),
494 config: None,
495 resources: None,
496 missions: None,
497 background: None,
498 run_in_sim: None,
499 logging: None,
500 flavor: Flavor::Task,
501 }
502 }
503
504 #[allow(dead_code)]
505 pub fn new_with_flavor(id: &str, ptype: &str, flavor: Flavor) -> Self {
506 let mut node = Self::new(id, ptype);
507 node.flavor = flavor;
508 node
509 }
510
511 #[allow(dead_code)]
512 pub fn get_id(&self) -> String {
513 self.id.clone()
514 }
515
516 #[allow(dead_code)]
517 pub fn get_type(&self) -> &str {
518 self.type_.as_ref().unwrap()
519 }
520
521 #[allow(dead_code)]
522 pub fn set_type(mut self, name: Option<String>) -> Self {
523 self.type_ = name;
524 self
525 }
526
527 #[allow(dead_code)]
528 pub fn set_resources<I>(&mut self, resources: Option<I>)
529 where
530 I: IntoIterator<Item = (String, String)>,
531 {
532 self.resources = resources.map(|iter| iter.into_iter().collect());
533 }
534
535 #[allow(dead_code)]
536 pub fn is_background(&self) -> bool {
537 self.background.unwrap_or(false)
538 }
539
540 #[allow(dead_code)]
541 pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
542 self.config.as_ref()
543 }
544
545 #[allow(dead_code)]
546 pub fn get_resources(&self) -> Option<&HashMap<String, String>> {
547 self.resources.as_ref()
548 }
549
550 #[allow(dead_code)]
553 pub fn is_run_in_sim(&self) -> bool {
554 self.run_in_sim.unwrap_or(false)
555 }
556
557 #[allow(dead_code)]
558 pub fn is_logging_enabled(&self) -> bool {
559 if let Some(logging) = &self.logging {
560 logging.enabled
561 } else {
562 true
563 }
564 }
565
566 #[allow(dead_code)]
567 pub fn get_param<T>(&self, key: &str) -> Result<Option<T>, ConfigError>
568 where
569 T: for<'a> TryFrom<&'a Value, Error = ConfigError>,
570 {
571 let pc = match self.config.as_ref() {
572 Some(pc) => pc,
573 None => return Ok(None),
574 };
575 let ComponentConfig(pc) = pc;
576 match pc.get(key) {
577 Some(v) => T::try_from(v).map(Some),
578 None => Ok(None),
579 }
580 }
581
582 #[allow(dead_code)]
583 pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
584 if self.config.is_none() {
585 self.config = Some(ComponentConfig(HashMap::new()));
586 }
587 let ComponentConfig(config) = self.config.as_mut().unwrap();
588 config.insert(key.to_string(), value.into());
589 }
590
591 #[allow(dead_code)]
593 pub fn get_flavor(&self) -> Flavor {
594 self.flavor
595 }
596
597 #[allow(dead_code)]
599 pub fn set_flavor(&mut self, flavor: Flavor) {
600 self.flavor = flavor;
601 }
602}
603
604#[derive(Serialize, Deserialize, Debug, Clone)]
606pub enum BridgeChannelConfigRepresentation {
607 Rx {
609 id: String,
610 #[serde(skip_serializing_if = "Option::is_none")]
612 route: Option<String>,
613 #[serde(skip_serializing_if = "Option::is_none")]
615 config: Option<ComponentConfig>,
616 },
617 Tx {
619 id: String,
620 #[serde(skip_serializing_if = "Option::is_none")]
622 route: Option<String>,
623 #[serde(skip_serializing_if = "Option::is_none")]
625 config: Option<ComponentConfig>,
626 },
627}
628
629impl BridgeChannelConfigRepresentation {
630 #[allow(dead_code)]
632 pub fn id(&self) -> &str {
633 match self {
634 BridgeChannelConfigRepresentation::Rx { id, .. }
635 | BridgeChannelConfigRepresentation::Tx { id, .. } => id,
636 }
637 }
638
639 #[allow(dead_code)]
641 pub fn route(&self) -> Option<&str> {
642 match self {
643 BridgeChannelConfigRepresentation::Rx { route, .. }
644 | BridgeChannelConfigRepresentation::Tx { route, .. } => route.as_deref(),
645 }
646 }
647}
648
649enum EndpointRole {
650 Source,
651 Destination,
652}
653
654fn validate_bridge_channel(
655 bridge: &BridgeConfig,
656 channel_id: &str,
657 role: EndpointRole,
658) -> Result<(), String> {
659 let channel = bridge
660 .channels
661 .iter()
662 .find(|ch| ch.id() == channel_id)
663 .ok_or_else(|| {
664 format!(
665 "Bridge '{}' does not declare a channel named '{}'",
666 bridge.id, channel_id
667 )
668 })?;
669
670 match (role, channel) {
671 (EndpointRole::Source, BridgeChannelConfigRepresentation::Rx { .. }) => Ok(()),
672 (EndpointRole::Destination, BridgeChannelConfigRepresentation::Tx { .. }) => Ok(()),
673 (EndpointRole::Source, BridgeChannelConfigRepresentation::Tx { .. }) => Err(format!(
674 "Bridge '{}' channel '{}' is Tx and cannot act as a source",
675 bridge.id, channel_id
676 )),
677 (EndpointRole::Destination, BridgeChannelConfigRepresentation::Rx { .. }) => Err(format!(
678 "Bridge '{}' channel '{}' is Rx and cannot act as a destination",
679 bridge.id, channel_id
680 )),
681 }
682}
683
684#[derive(Serialize, Deserialize, Debug, Clone)]
686pub struct ResourceBundleConfig {
687 pub id: String,
688 #[serde(rename = "provider")]
689 pub provider: String,
690 #[serde(skip_serializing_if = "Option::is_none")]
691 pub config: Option<ComponentConfig>,
692 #[serde(skip_serializing_if = "Option::is_none")]
693 pub missions: Option<Vec<String>>,
694}
695
696#[derive(Serialize, Deserialize, Debug, Clone)]
698pub struct BridgeConfig {
699 pub id: String,
700 #[serde(rename = "type")]
701 pub type_: String,
702 #[serde(skip_serializing_if = "Option::is_none")]
703 pub config: Option<ComponentConfig>,
704 #[serde(skip_serializing_if = "Option::is_none")]
705 pub resources: Option<HashMap<String, String>>,
706 #[serde(skip_serializing_if = "Option::is_none")]
707 pub missions: Option<Vec<String>>,
708 pub channels: Vec<BridgeChannelConfigRepresentation>,
710}
711
712impl BridgeConfig {
713 fn to_node(&self) -> Node {
714 let mut node = Node::new_with_flavor(&self.id, &self.type_, Flavor::Bridge);
715 node.config = self.config.clone();
716 node.resources = self.resources.clone();
717 node.missions = self.missions.clone();
718 node
719 }
720}
721
722fn insert_bridge_node(graph: &mut CuGraph, bridge: &BridgeConfig) -> Result<(), String> {
723 if graph.get_node_id_by_name(bridge.id.as_str()).is_some() {
724 return Err(format!(
725 "Bridge '{}' reuses an existing node id. Bridge ids must be unique.",
726 bridge.id
727 ));
728 }
729 graph
730 .add_node(bridge.to_node())
731 .map(|_| ())
732 .map_err(|e| e.to_string())
733}
734
735#[derive(Serialize, Deserialize, Debug, Clone)]
737struct SerializedCnx {
738 src: String,
739 dst: String,
740 msg: String,
741 missions: Option<Vec<String>>,
742}
743
744#[derive(Debug, Clone)]
746pub struct Cnx {
747 pub src: String,
749 pub dst: String,
751 pub msg: String,
753 pub missions: Option<Vec<String>>,
755 pub src_channel: Option<String>,
757 pub dst_channel: Option<String>,
759}
760
761impl From<&Cnx> for SerializedCnx {
762 fn from(cnx: &Cnx) -> Self {
763 SerializedCnx {
764 src: format_endpoint(&cnx.src, cnx.src_channel.as_deref()),
765 dst: format_endpoint(&cnx.dst, cnx.dst_channel.as_deref()),
766 msg: cnx.msg.clone(),
767 missions: cnx.missions.clone(),
768 }
769 }
770}
771
772fn format_endpoint(node: &str, channel: Option<&str>) -> String {
773 match channel {
774 Some(ch) => format!("{node}/{ch}"),
775 None => node.to_string(),
776 }
777}
778
779fn parse_endpoint(
780 endpoint: &str,
781 role: EndpointRole,
782 bridges: &HashMap<&str, &BridgeConfig>,
783) -> Result<(String, Option<String>), String> {
784 if let Some((node, channel)) = endpoint.split_once('/') {
785 if let Some(bridge) = bridges.get(node) {
786 validate_bridge_channel(bridge, channel, role)?;
787 return Ok((node.to_string(), Some(channel.to_string())));
788 } else {
789 return Err(format!(
790 "Endpoint '{endpoint}' references an unknown bridge '{node}'"
791 ));
792 }
793 }
794
795 if let Some(bridge) = bridges.get(endpoint) {
796 return Err(format!(
797 "Bridge '{}' connections must reference a channel using '{}/<channel>'",
798 bridge.id, bridge.id
799 ));
800 }
801
802 Ok((endpoint.to_string(), None))
803}
804
805fn build_bridge_lookup(bridges: Option<&Vec<BridgeConfig>>) -> HashMap<&str, &BridgeConfig> {
806 let mut map = HashMap::new();
807 if let Some(bridges) = bridges {
808 for bridge in bridges {
809 map.insert(bridge.id.as_str(), bridge);
810 }
811 }
812 map
813}
814
815fn mission_applies(missions: &Option<Vec<String>>, mission_id: &str) -> bool {
816 missions
817 .as_ref()
818 .map(|mission_list| mission_list.iter().any(|m| m == mission_id))
819 .unwrap_or(true)
820}
821
822#[derive(Debug, Clone, Copy, PartialEq, Eq)]
825pub enum CuDirection {
826 Outgoing,
827 Incoming,
828}
829
830impl From<CuDirection> for petgraph::Direction {
831 fn from(dir: CuDirection) -> Self {
832 match dir {
833 CuDirection::Outgoing => petgraph::Direction::Outgoing,
834 CuDirection::Incoming => petgraph::Direction::Incoming,
835 }
836 }
837}
838
839#[derive(Default, Debug, Clone)]
840pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
841
842impl CuGraph {
843 #[allow(dead_code)]
844 pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
845 self.0
846 .node_indices()
847 .map(|index| (index.index() as u32, &self.0[index]))
848 .collect()
849 }
850
851 #[allow(dead_code)]
852 pub fn get_neighbor_ids(&self, node_id: NodeId, dir: CuDirection) -> Vec<NodeId> {
853 self.0
854 .neighbors_directed(node_id.into(), dir.into())
855 .map(|petgraph_index| petgraph_index.index() as NodeId)
856 .collect()
857 }
858
859 #[allow(dead_code)]
860 pub fn node_ids(&self) -> Vec<NodeId> {
861 self.0
862 .node_indices()
863 .map(|index| index.index() as NodeId)
864 .collect()
865 }
866
867 #[allow(dead_code)]
868 pub fn edge_id_between(&self, source: NodeId, target: NodeId) -> Option<usize> {
869 self.0
870 .find_edge(source.into(), target.into())
871 .map(|edge| edge.index())
872 }
873
874 #[allow(dead_code)]
875 pub fn edge(&self, edge_id: usize) -> Option<&Cnx> {
876 self.0.edge_weight(EdgeIndex::new(edge_id))
877 }
878
879 #[allow(dead_code)]
880 pub fn edges(&self) -> impl Iterator<Item = &Cnx> {
881 self.0
882 .edge_indices()
883 .filter_map(|edge| self.0.edge_weight(edge))
884 }
885
886 #[allow(dead_code)]
887 pub fn bfs_nodes(&self, start: NodeId) -> Vec<NodeId> {
888 let mut visitor = Bfs::new(&self.0, start.into());
889 let mut nodes = Vec::new();
890 while let Some(node) = visitor.next(&self.0) {
891 nodes.push(node.index() as NodeId);
892 }
893 nodes
894 }
895
896 #[allow(dead_code)]
897 pub fn incoming_neighbor_count(&self, node_id: NodeId) -> usize {
898 self.0.neighbors_directed(node_id.into(), Incoming).count()
899 }
900
901 #[allow(dead_code)]
902 pub fn outgoing_neighbor_count(&self, node_id: NodeId) -> usize {
903 self.0.neighbors_directed(node_id.into(), Outgoing).count()
904 }
905
906 pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
907 self.0.node_indices().collect()
908 }
909
910 pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
911 Ok(self.0.add_node(node).index() as NodeId)
912 }
913
914 #[allow(dead_code)]
915 pub fn connection_exists(&self, source: NodeId, target: NodeId) -> bool {
916 self.0.find_edge(source.into(), target.into()).is_some()
917 }
918
919 pub fn connect_ext(
920 &mut self,
921 source: NodeId,
922 target: NodeId,
923 msg_type: &str,
924 missions: Option<Vec<String>>,
925 src_channel: Option<String>,
926 dst_channel: Option<String>,
927 ) -> CuResult<()> {
928 let (src_id, dst_id) = (
929 self.0
930 .node_weight(source.into())
931 .ok_or("Source node not found")?
932 .id
933 .clone(),
934 self.0
935 .node_weight(target.into())
936 .ok_or("Target node not found")?
937 .id
938 .clone(),
939 );
940
941 let _ = self.0.add_edge(
942 petgraph::stable_graph::NodeIndex::from(source),
943 petgraph::stable_graph::NodeIndex::from(target),
944 Cnx {
945 src: src_id,
946 dst: dst_id,
947 msg: msg_type.to_string(),
948 missions,
949 src_channel,
950 dst_channel,
951 },
952 );
953 Ok(())
954 }
955 #[allow(dead_code)]
959 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
960 self.0.node_weight(node_id.into())
961 }
962
963 #[allow(dead_code)]
964 pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
965 self.0.node_weight(index.into())
966 }
967
968 #[allow(dead_code)]
969 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
970 self.0.node_weight_mut(node_id.into())
971 }
972
973 pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
974 self.0
975 .node_indices()
976 .into_iter()
977 .find(|idx| self.0[*idx].get_id() == name)
978 .map(|i| i.index() as NodeId)
979 }
980
981 #[allow(dead_code)]
982 pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
983 self.0.edge_weight(EdgeIndex::new(index)).cloned()
984 }
985
986 #[allow(dead_code)]
987 pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
988 self.0.node_indices().find_map(|node_index| {
989 if let Some(node) = self.0.node_weight(node_index) {
990 if node.id != node_id {
991 return None;
992 }
993 let edges: Vec<_> = self
994 .0
995 .edges_directed(node_index, Outgoing)
996 .map(|edge| edge.id().index())
997 .collect();
998 if edges.is_empty() {
999 return None;
1000 }
1001 let cnx = self
1002 .0
1003 .edge_weight(EdgeIndex::new(edges[0]))
1004 .expect("Found an cnx id but could not retrieve it back");
1005 return Some(cnx.msg.clone());
1006 }
1007 None
1008 })
1009 }
1010
1011 #[allow(dead_code)]
1012 pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
1013 self.get_node_input_msg_types(node_id)
1014 .and_then(|mut v| v.pop())
1015 }
1016
1017 pub fn get_node_input_msg_types(&self, node_id: &str) -> Option<Vec<String>> {
1018 self.0.node_indices().find_map(|node_index| {
1019 if let Some(node) = self.0.node_weight(node_index) {
1020 if node.id != node_id {
1021 return None;
1022 }
1023 let edges: Vec<_> = self
1024 .0
1025 .edges_directed(node_index, Incoming)
1026 .map(|edge| edge.id().index())
1027 .collect();
1028 if edges.is_empty() {
1029 return None;
1030 }
1031 let msgs = edges
1032 .into_iter()
1033 .map(|edge_id| {
1034 let cnx = self
1035 .0
1036 .edge_weight(EdgeIndex::new(edge_id))
1037 .expect("Found an cnx id but could not retrieve it back");
1038 cnx.msg.clone()
1039 })
1040 .collect();
1041 return Some(msgs);
1042 }
1043 None
1044 })
1045 }
1046
1047 #[allow(dead_code)]
1048 pub fn get_connection_msg_type(&self, source: NodeId, target: NodeId) -> Option<&str> {
1049 self.0
1050 .find_edge(source.into(), target.into())
1051 .map(|edge_index| self.0[edge_index].msg.as_str())
1052 }
1053
1054 fn get_edges_by_direction(
1056 &self,
1057 node_id: NodeId,
1058 direction: petgraph::Direction,
1059 ) -> CuResult<Vec<usize>> {
1060 Ok(self
1061 .0
1062 .edges_directed(node_id.into(), direction)
1063 .map(|edge| edge.id().index())
1064 .collect())
1065 }
1066
1067 pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
1068 self.get_edges_by_direction(node_id, Outgoing)
1069 }
1070
1071 pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
1073 self.get_edges_by_direction(node_id, Incoming)
1074 }
1075
1076 #[allow(dead_code)]
1077 pub fn node_count(&self) -> usize {
1078 self.0.node_count()
1079 }
1080
1081 #[allow(dead_code)]
1082 pub fn edge_count(&self) -> usize {
1083 self.0.edge_count()
1084 }
1085
1086 #[allow(dead_code)]
1089 pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
1090 self.connect_ext(source, target, msg_type, None, None, None)
1091 }
1092}
1093
1094impl core::ops::Index<NodeIndex> for CuGraph {
1095 type Output = Node;
1096
1097 fn index(&self, index: NodeIndex) -> &Self::Output {
1098 &self.0[index]
1099 }
1100}
1101
1102#[derive(Debug, Clone)]
1103pub enum ConfigGraphs {
1104 Simple(CuGraph),
1105 Missions(HashMap<String, CuGraph>),
1106}
1107
1108impl ConfigGraphs {
1109 #[allow(dead_code)]
1112 pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
1113 match self {
1114 Simple(graph) => {
1115 let mut map = HashMap::new();
1116 map.insert("default".to_string(), graph.clone());
1117 map
1118 }
1119 Missions(graphs) => graphs.clone(),
1120 }
1121 }
1122
1123 #[allow(dead_code)]
1124 pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
1125 match self {
1126 Simple(graph) => Ok(graph),
1127 Missions(graphs) => {
1128 if graphs.len() == 1 {
1129 Ok(graphs.values().next().unwrap())
1130 } else {
1131 Err("Cannot get default mission graph from mission config".into())
1132 }
1133 }
1134 }
1135 }
1136
1137 #[allow(dead_code)]
1138 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1139 match self {
1140 Simple(graph) => {
1141 if mission_id.is_none() || mission_id.unwrap() == "default" {
1142 Ok(graph)
1143 } else {
1144 Err("Cannot get mission graph from simple config".into())
1145 }
1146 }
1147 Missions(graphs) => {
1148 if let Some(id) = mission_id {
1149 graphs
1150 .get(id)
1151 .ok_or_else(|| format!("Mission {id} not found").into())
1152 } else {
1153 Err("Mission ID required for mission configs".into())
1154 }
1155 }
1156 }
1157 }
1158
1159 #[allow(dead_code)]
1160 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1161 match self {
1162 Simple(graph) => {
1163 if mission_id.is_none() {
1164 Ok(graph)
1165 } else {
1166 Err("Cannot get mission graph from simple config".into())
1167 }
1168 }
1169 Missions(graphs) => {
1170 if let Some(id) = mission_id {
1171 graphs
1172 .get_mut(id)
1173 .ok_or_else(|| format!("Mission {id} not found").into())
1174 } else {
1175 Err("Mission ID required for mission configs".into())
1176 }
1177 }
1178 }
1179 }
1180
1181 pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
1182 match self {
1183 Simple(_) => Err("Cannot add mission to simple config".into()),
1184 Missions(graphs) => {
1185 if graphs.contains_key(mission_id) {
1186 Err(format!("Mission {mission_id} already exists").into())
1187 } else {
1188 let graph = CuGraph::default();
1189 graphs.insert(mission_id.to_string(), graph);
1190 Ok(graphs.get_mut(mission_id).unwrap())
1192 }
1193 }
1194 }
1195 }
1196}
1197
1198#[derive(Debug, Clone)]
1204pub struct CuConfig {
1205 pub monitor: Option<MonitorConfig>,
1207 pub logging: Option<LoggingConfig>,
1209 pub runtime: Option<RuntimeConfig>,
1211 pub resources: Vec<ResourceBundleConfig>,
1213 pub bridges: Vec<BridgeConfig>,
1215 pub graphs: ConfigGraphs,
1217}
1218
1219impl CuConfig {
1220 #[cfg(feature = "std")]
1221 fn ensure_threadpool_bundle(&mut self) {
1222 if !self.has_background_tasks() {
1223 return;
1224 }
1225 if self
1226 .resources
1227 .iter()
1228 .any(|bundle| bundle.id == "threadpool")
1229 {
1230 return;
1231 }
1232
1233 let mut config = ComponentConfig::default();
1234 config.set("threads", 2u64);
1235 self.resources.push(ResourceBundleConfig {
1236 id: "threadpool".to_string(),
1237 provider: "cu29::resource::ThreadPoolBundle".to_string(),
1238 config: Some(config),
1239 missions: None,
1240 });
1241 }
1242
1243 #[cfg(feature = "std")]
1244 fn has_background_tasks(&self) -> bool {
1245 match &self.graphs {
1246 ConfigGraphs::Simple(graph) => graph
1247 .get_all_nodes()
1248 .iter()
1249 .any(|(_, node)| node.is_background()),
1250 ConfigGraphs::Missions(graphs) => graphs.values().any(|graph| {
1251 graph
1252 .get_all_nodes()
1253 .iter()
1254 .any(|(_, node)| node.is_background())
1255 }),
1256 }
1257 }
1258}
1259
1260#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1261pub struct MonitorConfig {
1262 #[serde(rename = "type")]
1263 type_: String,
1264 #[serde(skip_serializing_if = "Option::is_none")]
1265 config: Option<ComponentConfig>,
1266}
1267
1268impl MonitorConfig {
1269 #[allow(dead_code)]
1270 pub fn get_type(&self) -> &str {
1271 &self.type_
1272 }
1273
1274 #[allow(dead_code)]
1275 pub fn get_config(&self) -> Option<&ComponentConfig> {
1276 self.config.as_ref()
1277 }
1278}
1279
1280fn default_as_true() -> bool {
1281 true
1282}
1283
1284pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
1285
1286fn default_keyframe_interval() -> Option<u32> {
1287 Some(DEFAULT_KEYFRAME_INTERVAL)
1288}
1289
1290#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1291pub struct LoggingConfig {
1292 #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
1294 pub enable_task_logging: bool,
1295
1296 #[serde(skip_serializing_if = "Option::is_none")]
1298 pub slab_size_mib: Option<u64>,
1299
1300 #[serde(skip_serializing_if = "Option::is_none")]
1302 pub section_size_mib: Option<u64>,
1303
1304 #[serde(
1306 default = "default_keyframe_interval",
1307 skip_serializing_if = "Option::is_none"
1308 )]
1309 pub keyframe_interval: Option<u32>,
1310}
1311
1312#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1313pub struct RuntimeConfig {
1314 #[serde(skip_serializing_if = "Option::is_none")]
1320 pub rate_target_hz: Option<u64>,
1321}
1322
1323#[derive(Serialize, Deserialize, Debug, Clone)]
1325pub struct MissionsConfig {
1326 pub id: String,
1327}
1328
1329#[derive(Serialize, Deserialize, Debug, Clone)]
1331pub struct IncludesConfig {
1332 pub path: String,
1333 pub params: HashMap<String, Value>,
1334 pub missions: Option<Vec<String>>,
1335}
1336
1337#[derive(Serialize, Deserialize, Default)]
1339struct CuConfigRepresentation {
1340 tasks: Option<Vec<Node>>,
1341 resources: Option<Vec<ResourceBundleConfig>>,
1342 bridges: Option<Vec<BridgeConfig>>,
1343 cnx: Option<Vec<SerializedCnx>>,
1344 monitor: Option<MonitorConfig>,
1345 logging: Option<LoggingConfig>,
1346 runtime: Option<RuntimeConfig>,
1347 missions: Option<Vec<MissionsConfig>>,
1348 includes: Option<Vec<IncludesConfig>>,
1349}
1350
1351fn deserialize_config_representation<E>(
1353 representation: &CuConfigRepresentation,
1354) -> Result<CuConfig, E>
1355where
1356 E: From<String>,
1357{
1358 let mut cuconfig = CuConfig::default();
1359 let bridge_lookup = build_bridge_lookup(representation.bridges.as_ref());
1360
1361 if let Some(mission_configs) = &representation.missions {
1362 let mut missions = Missions(HashMap::new());
1364
1365 for mission_config in mission_configs {
1366 let mission_id = mission_config.id.as_str();
1367 let graph = missions
1368 .add_mission(mission_id)
1369 .map_err(|e| E::from(e.to_string()))?;
1370
1371 if let Some(tasks) = &representation.tasks {
1372 for task in tasks {
1373 if let Some(task_missions) = &task.missions {
1374 if task_missions.contains(&mission_id.to_owned()) {
1376 graph
1377 .add_node(task.clone())
1378 .map_err(|e| E::from(e.to_string()))?;
1379 }
1380 } else {
1381 graph
1383 .add_node(task.clone())
1384 .map_err(|e| E::from(e.to_string()))?;
1385 }
1386 }
1387 }
1388
1389 if let Some(bridges) = &representation.bridges {
1390 for bridge in bridges {
1391 if mission_applies(&bridge.missions, mission_id) {
1392 insert_bridge_node(graph, bridge).map_err(E::from)?;
1393 }
1394 }
1395 }
1396
1397 if let Some(cnx) = &representation.cnx {
1398 for c in cnx {
1399 if let Some(cnx_missions) = &c.missions {
1400 if cnx_missions.contains(&mission_id.to_owned()) {
1402 let (src_name, src_channel) =
1403 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1404 .map_err(E::from)?;
1405 let (dst_name, dst_channel) =
1406 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1407 .map_err(E::from)?;
1408 let src =
1409 graph
1410 .get_node_id_by_name(src_name.as_str())
1411 .ok_or_else(|| {
1412 E::from(format!("Source node not found: {}", c.src))
1413 })?;
1414 let dst =
1415 graph
1416 .get_node_id_by_name(dst_name.as_str())
1417 .ok_or_else(|| {
1418 E::from(format!("Destination node not found: {}", c.dst))
1419 })?;
1420 graph
1421 .connect_ext(
1422 src,
1423 dst,
1424 &c.msg,
1425 Some(cnx_missions.clone()),
1426 src_channel,
1427 dst_channel,
1428 )
1429 .map_err(|e| E::from(e.to_string()))?;
1430 }
1431 } else {
1432 let (src_name, src_channel) =
1434 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1435 .map_err(E::from)?;
1436 let (dst_name, dst_channel) =
1437 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1438 .map_err(E::from)?;
1439 let src = graph
1440 .get_node_id_by_name(src_name.as_str())
1441 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1442 let dst =
1443 graph
1444 .get_node_id_by_name(dst_name.as_str())
1445 .ok_or_else(|| {
1446 E::from(format!("Destination node not found: {}", c.dst))
1447 })?;
1448 graph
1449 .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1450 .map_err(|e| E::from(e.to_string()))?;
1451 }
1452 }
1453 }
1454 }
1455 cuconfig.graphs = missions;
1456 } else {
1457 let mut graph = CuGraph::default();
1459
1460 if let Some(tasks) = &representation.tasks {
1461 for task in tasks {
1462 graph
1463 .add_node(task.clone())
1464 .map_err(|e| E::from(e.to_string()))?;
1465 }
1466 }
1467
1468 if let Some(bridges) = &representation.bridges {
1469 for bridge in bridges {
1470 insert_bridge_node(&mut graph, bridge).map_err(E::from)?;
1471 }
1472 }
1473
1474 if let Some(cnx) = &representation.cnx {
1475 for c in cnx {
1476 let (src_name, src_channel) =
1477 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1478 .map_err(E::from)?;
1479 let (dst_name, dst_channel) =
1480 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1481 .map_err(E::from)?;
1482 let src = graph
1483 .get_node_id_by_name(src_name.as_str())
1484 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1485 let dst = graph
1486 .get_node_id_by_name(dst_name.as_str())
1487 .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
1488 graph
1489 .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1490 .map_err(|e| E::from(e.to_string()))?;
1491 }
1492 }
1493 cuconfig.graphs = Simple(graph);
1494 }
1495
1496 cuconfig.monitor = representation.monitor.clone();
1497 cuconfig.logging = representation.logging.clone();
1498 cuconfig.runtime = representation.runtime.clone();
1499 cuconfig.resources = representation.resources.clone().unwrap_or_default();
1500 cuconfig.bridges = representation.bridges.clone().unwrap_or_default();
1501
1502 Ok(cuconfig)
1503}
1504
1505impl<'de> Deserialize<'de> for CuConfig {
1506 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1508 where
1509 D: Deserializer<'de>,
1510 {
1511 let representation =
1512 CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
1513
1514 match deserialize_config_representation::<String>(&representation) {
1516 Ok(config) => Ok(config),
1517 Err(e) => Err(serde::de::Error::custom(e)),
1518 }
1519 }
1520}
1521
1522impl Serialize for CuConfig {
1523 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1525 where
1526 S: Serializer,
1527 {
1528 let bridges = if self.bridges.is_empty() {
1529 None
1530 } else {
1531 Some(self.bridges.clone())
1532 };
1533 let resources = if self.resources.is_empty() {
1534 None
1535 } else {
1536 Some(self.resources.clone())
1537 };
1538 match &self.graphs {
1539 Simple(graph) => {
1540 let tasks: Vec<Node> = graph
1541 .0
1542 .node_indices()
1543 .map(|idx| graph.0[idx].clone())
1544 .filter(|node| node.get_flavor() == Flavor::Task)
1545 .collect();
1546
1547 let cnx: Vec<SerializedCnx> = graph
1548 .0
1549 .edge_indices()
1550 .map(|edge| SerializedCnx::from(&graph.0[edge]))
1551 .collect();
1552
1553 CuConfigRepresentation {
1554 tasks: Some(tasks),
1555 bridges: bridges.clone(),
1556 cnx: Some(cnx),
1557 monitor: self.monitor.clone(),
1558 logging: self.logging.clone(),
1559 runtime: self.runtime.clone(),
1560 resources: resources.clone(),
1561 missions: None,
1562 includes: None,
1563 }
1564 .serialize(serializer)
1565 }
1566 Missions(graphs) => {
1567 let missions = graphs
1568 .keys()
1569 .map(|id| MissionsConfig { id: id.clone() })
1570 .collect();
1571
1572 let mut tasks = Vec::new();
1574 let mut cnx = Vec::new();
1575
1576 for graph in graphs.values() {
1577 for node_idx in graph.node_indices() {
1579 let node = &graph[node_idx];
1580 if node.get_flavor() == Flavor::Task
1581 && !tasks.iter().any(|n: &Node| n.id == node.id)
1582 {
1583 tasks.push(node.clone());
1584 }
1585 }
1586
1587 for edge_idx in graph.0.edge_indices() {
1589 let edge = &graph.0[edge_idx];
1590 let serialized = SerializedCnx::from(edge);
1591 if !cnx.iter().any(|c: &SerializedCnx| {
1592 c.src == serialized.src
1593 && c.dst == serialized.dst
1594 && c.msg == serialized.msg
1595 }) {
1596 cnx.push(serialized);
1597 }
1598 }
1599 }
1600
1601 CuConfigRepresentation {
1602 tasks: Some(tasks),
1603 resources: resources.clone(),
1604 bridges,
1605 cnx: Some(cnx),
1606 monitor: self.monitor.clone(),
1607 logging: self.logging.clone(),
1608 runtime: self.runtime.clone(),
1609 missions: Some(missions),
1610 includes: None,
1611 }
1612 .serialize(serializer)
1613 }
1614 }
1615 }
1616}
1617
1618impl Default for CuConfig {
1619 fn default() -> Self {
1620 CuConfig {
1621 graphs: Simple(CuGraph(StableDiGraph::new())),
1622 monitor: None,
1623 logging: None,
1624 runtime: None,
1625 resources: Vec::new(),
1626 bridges: Vec::new(),
1627 }
1628 }
1629}
1630
1631impl CuConfig {
1634 #[allow(dead_code)]
1635 pub fn new_simple_type() -> Self {
1636 Self::default()
1637 }
1638
1639 #[allow(dead_code)]
1640 pub fn new_mission_type() -> Self {
1641 CuConfig {
1642 graphs: Missions(HashMap::new()),
1643 monitor: None,
1644 logging: None,
1645 runtime: None,
1646 resources: Vec::new(),
1647 bridges: Vec::new(),
1648 }
1649 }
1650
1651 fn get_options() -> Options {
1652 Options::default()
1653 .with_default_extension(Extensions::IMPLICIT_SOME)
1654 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1655 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1656 }
1657
1658 #[allow(dead_code)]
1659 pub fn serialize_ron(&self) -> CuResult<String> {
1660 let ron = Self::get_options();
1661 let pretty = ron::ser::PrettyConfig::default();
1662 ron.to_string_pretty(&self, pretty)
1663 .map_err(|e| CuError::from(format!("Error serializing configuration: {e}")))
1664 }
1665
1666 #[allow(dead_code)]
1667 pub fn deserialize_ron(ron: &str) -> CuResult<Self> {
1668 let representation = Self::get_options().from_str(ron).map_err(|e| {
1669 CuError::from(format!(
1670 "Syntax Error in config: {} at position {}",
1671 e.code, e.span
1672 ))
1673 })?;
1674 Self::deserialize_impl(representation)
1675 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))
1676 }
1677
1678 fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
1679 deserialize_config_representation(&representation)
1680 }
1681
1682 #[cfg(feature = "std")]
1684 #[allow(dead_code)]
1685 pub fn render(
1686 &self,
1687 output: &mut dyn std::io::Write,
1688 mission_id: Option<&str>,
1689 ) -> CuResult<()> {
1690 writeln!(output, "digraph G {{")
1691 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1692 writeln!(output, " graph [rankdir=LR, nodesep=0.8, ranksep=1.2];")
1693 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1694 writeln!(output, " node [shape=plain, fontname=\"Noto Sans\"];")
1695 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1696 writeln!(output, " edge [fontname=\"Noto Sans\"];")
1697 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1698
1699 let sections = match (&self.graphs, mission_id) {
1700 (Simple(graph), _) => vec![RenderSection { label: None, graph }],
1701 (Missions(graphs), Some(id)) => {
1702 let graph = graphs
1703 .get(id)
1704 .ok_or_else(|| CuError::from(format!("Mission {id} not found")))?;
1705 vec![RenderSection {
1706 label: Some(id.to_string()),
1707 graph,
1708 }]
1709 }
1710 (Missions(graphs), None) => {
1711 let mut missions: Vec<_> = graphs.iter().collect();
1712 missions.sort_by(|a, b| a.0.cmp(b.0));
1713 missions
1714 .into_iter()
1715 .map(|(label, graph)| RenderSection {
1716 label: Some(label.clone()),
1717 graph,
1718 })
1719 .collect()
1720 }
1721 };
1722
1723 for section in sections {
1724 self.render_section(output, section.graph, section.label.as_deref())?;
1725 }
1726
1727 writeln!(output, "}}")
1728 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1729 Ok(())
1730 }
1731
1732 #[allow(dead_code)]
1733 pub fn get_all_instances_configs(
1734 &self,
1735 mission_id: Option<&str>,
1736 ) -> Vec<Option<&ComponentConfig>> {
1737 let graph = self.graphs.get_graph(mission_id).unwrap();
1738 graph
1739 .get_all_nodes()
1740 .iter()
1741 .map(|(_, node)| node.get_instance_config())
1742 .collect()
1743 }
1744
1745 #[allow(dead_code)]
1746 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1747 self.graphs.get_graph(mission_id)
1748 }
1749
1750 #[allow(dead_code)]
1751 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1752 self.graphs.get_graph_mut(mission_id)
1753 }
1754
1755 #[allow(dead_code)]
1756 pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1757 self.monitor.as_ref()
1758 }
1759
1760 #[allow(dead_code)]
1761 pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
1762 self.runtime.as_ref()
1763 }
1764
1765 pub fn validate_logging_config(&self) -> CuResult<()> {
1768 if let Some(logging) = &self.logging {
1769 return logging.validate();
1770 }
1771 Ok(())
1772 }
1773}
1774
1775#[cfg(feature = "std")]
1776#[derive(Default)]
1777pub(crate) struct PortLookup {
1778 pub inputs: HashMap<String, String>,
1779 pub outputs: HashMap<String, String>,
1780 pub default_input: Option<String>,
1781 pub default_output: Option<String>,
1782}
1783
1784#[cfg(feature = "std")]
1785#[derive(Clone)]
1786pub(crate) struct RenderNode {
1787 pub id: String,
1788 pub type_name: String,
1789 pub flavor: Flavor,
1790 pub inputs: Vec<String>,
1791 pub outputs: Vec<String>,
1792}
1793
1794#[cfg(feature = "std")]
1795#[derive(Clone)]
1796pub(crate) struct RenderConnection {
1797 pub src: String,
1798 pub src_port: Option<String>,
1799 #[allow(dead_code)]
1800 pub src_channel: Option<String>,
1801 pub dst: String,
1802 pub dst_port: Option<String>,
1803 #[allow(dead_code)]
1804 pub dst_channel: Option<String>,
1805 pub msg: String,
1806}
1807
1808#[cfg(feature = "std")]
1809pub(crate) struct RenderTopology {
1810 pub nodes: Vec<RenderNode>,
1811 pub connections: Vec<RenderConnection>,
1812}
1813
1814#[cfg(feature = "std")]
1815impl RenderTopology {
1816 pub fn sort_connections(&mut self) {
1817 self.connections.sort_by(|a, b| {
1818 a.src
1819 .cmp(&b.src)
1820 .then(a.dst.cmp(&b.dst))
1821 .then(a.msg.cmp(&b.msg))
1822 });
1823 }
1824}
1825
1826#[cfg(feature = "std")]
1827#[allow(dead_code)]
1828struct RenderSection<'a> {
1829 label: Option<String>,
1830 graph: &'a CuGraph,
1831}
1832
1833#[cfg(feature = "std")]
1834impl CuConfig {
1835 #[allow(dead_code)]
1836 fn render_section(
1837 &self,
1838 output: &mut dyn std::io::Write,
1839 graph: &CuGraph,
1840 label: Option<&str>,
1841 ) -> CuResult<()> {
1842 use std::fmt::Write as FmtWrite;
1843
1844 let mut topology = build_render_topology(graph, &self.bridges);
1845 topology.nodes.sort_by(|a, b| a.id.cmp(&b.id));
1846 topology.sort_connections();
1847
1848 let cluster_id = label.map(|lbl| format!("cluster_{}", sanitize_identifier(lbl)));
1849 if let Some(ref cluster_id) = cluster_id {
1850 writeln!(output, " subgraph \"{cluster_id}\" {{")
1851 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1852 writeln!(
1853 output,
1854 " label=<<B>Mission: {}</B>>;",
1855 encode_text(label.unwrap())
1856 )
1857 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1858 writeln!(
1859 output,
1860 " labelloc=t; labeljust=l; color=\"#bbbbbb\"; style=\"rounded\"; margin=20;"
1861 )
1862 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1863 }
1864 let indent = if cluster_id.is_some() {
1865 " "
1866 } else {
1867 " "
1868 };
1869 let node_prefix = label
1870 .map(|lbl| format!("{}__", sanitize_identifier(lbl)))
1871 .unwrap_or_default();
1872
1873 let mut port_lookup: HashMap<String, PortLookup> = HashMap::new();
1874 let mut id_lookup: HashMap<String, String> = HashMap::new();
1875
1876 for node in &topology.nodes {
1877 let node_idx = graph
1878 .get_node_id_by_name(node.id.as_str())
1879 .ok_or_else(|| CuError::from(format!("Node '{}' missing from graph", node.id)))?;
1880 let node_weight = graph
1881 .get_node(node_idx)
1882 .ok_or_else(|| CuError::from(format!("Node '{}' missing weight", node.id)))?;
1883
1884 let is_src = graph.get_dst_edges(node_idx).unwrap_or_default().is_empty();
1885 let is_sink = graph.get_src_edges(node_idx).unwrap_or_default().is_empty();
1886
1887 let fillcolor = match node.flavor {
1888 Flavor::Bridge => "#faedcd",
1889 Flavor::Task if is_src => "#ddefc7",
1890 Flavor::Task if is_sink => "#cce0ff",
1891 _ => "#f2f2f2",
1892 };
1893
1894 let port_base = format!("{}{}", node_prefix, sanitize_identifier(&node.id));
1895 let (inputs_table, input_map, default_input) =
1896 build_port_table("Inputs", &node.inputs, &port_base, "in");
1897 let (outputs_table, output_map, default_output) =
1898 build_port_table("Outputs", &node.outputs, &port_base, "out");
1899 let config_html = node_weight.config.as_ref().and_then(build_config_table);
1900
1901 let mut label_html = String::new();
1902 write!(
1903 label_html,
1904 "<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"6\" COLOR=\"gray\" BGCOLOR=\"white\">"
1905 )
1906 .unwrap();
1907 write!(
1908 label_html,
1909 "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\" BGCOLOR=\"{fillcolor}\"><FONT POINT-SIZE=\"12\"><B>{}</B></FONT><BR/><FONT COLOR=\"dimgray\">[{}]</FONT></TD></TR>",
1910 encode_text(&node.id),
1911 encode_text(&node.type_name)
1912 )
1913 .unwrap();
1914 write!(
1915 label_html,
1916 "<TR><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{inputs_table}</TD><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{outputs_table}</TD></TR>"
1917 )
1918 .unwrap();
1919
1920 if let Some(config_html) = config_html {
1921 write!(
1922 label_html,
1923 "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\">{config_html}</TD></TR>"
1924 )
1925 .unwrap();
1926 }
1927
1928 label_html.push_str("</TABLE>");
1929
1930 let identifier_raw = if node_prefix.is_empty() {
1931 node.id.clone()
1932 } else {
1933 format!("{node_prefix}{}", node.id)
1934 };
1935 let identifier = escape_dot_id(&identifier_raw);
1936 writeln!(output, "{indent}\"{identifier}\" [label=<{label_html}>];")
1937 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1938
1939 id_lookup.insert(node.id.clone(), identifier);
1940 port_lookup.insert(
1941 node.id.clone(),
1942 PortLookup {
1943 inputs: input_map,
1944 outputs: output_map,
1945 default_input,
1946 default_output,
1947 },
1948 );
1949 }
1950
1951 for cnx in &topology.connections {
1952 let src_id = id_lookup
1953 .get(&cnx.src)
1954 .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.src)))?;
1955 let dst_id = id_lookup
1956 .get(&cnx.dst)
1957 .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.dst)))?;
1958 let src_suffix = port_lookup
1959 .get(&cnx.src)
1960 .and_then(|lookup| lookup.resolve_output(cnx.src_port.as_deref()))
1961 .map(|port| format!(":\"{port}\":e"))
1962 .unwrap_or_default();
1963 let dst_suffix = port_lookup
1964 .get(&cnx.dst)
1965 .and_then(|lookup| lookup.resolve_input(cnx.dst_port.as_deref()))
1966 .map(|port| format!(":\"{port}\":w"))
1967 .unwrap_or_default();
1968 let msg = encode_text(&cnx.msg);
1969 writeln!(
1970 output,
1971 "{indent}\"{src_id}\"{src_suffix} -> \"{dst_id}\"{dst_suffix} [label=< <B><FONT COLOR=\"gray\">{msg}</FONT></B> >];"
1972 )
1973 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1974 }
1975
1976 if cluster_id.is_some() {
1977 writeln!(output, " }}")
1978 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
1979 }
1980
1981 Ok(())
1982 }
1983}
1984
1985#[cfg(feature = "std")]
1986pub(crate) fn build_render_topology(graph: &CuGraph, bridges: &[BridgeConfig]) -> RenderTopology {
1987 let mut bridge_lookup = HashMap::new();
1988 for bridge in bridges {
1989 bridge_lookup.insert(bridge.id.as_str(), bridge);
1990 }
1991
1992 let mut nodes: Vec<RenderNode> = Vec::new();
1993 let mut node_lookup: HashMap<String, usize> = HashMap::new();
1994 for (_, node) in graph.get_all_nodes() {
1995 let node_id = node.get_id();
1996 let mut inputs = Vec::new();
1997 let mut outputs = Vec::new();
1998 if node.get_flavor() == Flavor::Bridge
1999 && let Some(bridge) = bridge_lookup.get(node_id.as_str())
2000 {
2001 for channel in &bridge.channels {
2002 match channel {
2003 BridgeChannelConfigRepresentation::Rx { id, .. } => outputs.push(id.clone()),
2005 BridgeChannelConfigRepresentation::Tx { id, .. } => inputs.push(id.clone()),
2007 }
2008 }
2009 }
2010
2011 node_lookup.insert(node_id.clone(), nodes.len());
2012 nodes.push(RenderNode {
2013 id: node_id,
2014 type_name: node.get_type().to_string(),
2015 flavor: node.get_flavor(),
2016 inputs,
2017 outputs,
2018 });
2019 }
2020
2021 let mut output_port_lookup: Vec<HashMap<String, String>> = vec![HashMap::new(); nodes.len()];
2022 let mut output_edges: Vec<_> = graph.0.edge_references().collect();
2023 output_edges.sort_by_key(|edge| edge.id().index());
2024 for edge in output_edges {
2025 let cnx = edge.weight();
2026 if let Some(&idx) = node_lookup.get(&cnx.src)
2027 && nodes[idx].flavor == Flavor::Task
2028 && cnx.src_channel.is_none()
2029 {
2030 let port_map = &mut output_port_lookup[idx];
2031 if !port_map.contains_key(&cnx.msg) {
2032 let label = format!("out{}: {}", port_map.len(), cnx.msg);
2033 port_map.insert(cnx.msg.clone(), label.clone());
2034 nodes[idx].outputs.push(label);
2035 }
2036 }
2037 }
2038
2039 let mut auto_input_counts = vec![0usize; nodes.len()];
2040 for edge in graph.0.edge_references() {
2041 let cnx = edge.weight();
2042 if let Some(&idx) = node_lookup.get(&cnx.dst)
2043 && nodes[idx].flavor == Flavor::Task
2044 && cnx.dst_channel.is_none()
2045 {
2046 auto_input_counts[idx] += 1;
2047 }
2048 }
2049
2050 let mut next_auto_input = vec![0usize; nodes.len()];
2051 let mut connections = Vec::new();
2052 for edge in graph.0.edge_references() {
2053 let cnx = edge.weight();
2054 let mut src_port = cnx.src_channel.clone();
2055 let mut dst_port = cnx.dst_channel.clone();
2056
2057 if let Some(&idx) = node_lookup.get(&cnx.src) {
2058 let node = &mut nodes[idx];
2059 if node.flavor == Flavor::Task && src_port.is_none() {
2060 src_port = output_port_lookup[idx].get(&cnx.msg).cloned();
2061 }
2062 }
2063 if let Some(&idx) = node_lookup.get(&cnx.dst) {
2064 let node = &mut nodes[idx];
2065 if node.flavor == Flavor::Task && dst_port.is_none() {
2066 let count = auto_input_counts[idx];
2067 let next = if count <= 1 {
2068 "in".to_string()
2069 } else {
2070 let next = format!("in.{}", next_auto_input[idx]);
2071 next_auto_input[idx] += 1;
2072 next
2073 };
2074 node.inputs.push(next.clone());
2075 dst_port = Some(next);
2076 }
2077 }
2078
2079 connections.push(RenderConnection {
2080 src: cnx.src.clone(),
2081 src_port,
2082 src_channel: cnx.src_channel.clone(),
2083 dst: cnx.dst.clone(),
2084 dst_port,
2085 dst_channel: cnx.dst_channel.clone(),
2086 msg: cnx.msg.clone(),
2087 });
2088 }
2089
2090 RenderTopology { nodes, connections }
2091}
2092
2093#[cfg(feature = "std")]
2094impl PortLookup {
2095 pub fn resolve_input(&self, name: Option<&str>) -> Option<&str> {
2096 if let Some(name) = name
2097 && let Some(port) = self.inputs.get(name)
2098 {
2099 return Some(port.as_str());
2100 }
2101 self.default_input.as_deref()
2102 }
2103
2104 pub fn resolve_output(&self, name: Option<&str>) -> Option<&str> {
2105 if let Some(name) = name
2106 && let Some(port) = self.outputs.get(name)
2107 {
2108 return Some(port.as_str());
2109 }
2110 self.default_output.as_deref()
2111 }
2112}
2113
2114#[cfg(feature = "std")]
2115#[allow(dead_code)]
2116fn build_port_table(
2117 title: &str,
2118 names: &[String],
2119 base_id: &str,
2120 prefix: &str,
2121) -> (String, HashMap<String, String>, Option<String>) {
2122 use std::fmt::Write as FmtWrite;
2123
2124 let mut html = String::new();
2125 write!(
2126 html,
2127 "<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">"
2128 )
2129 .unwrap();
2130 write!(
2131 html,
2132 "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT></TD></TR>",
2133 encode_text(title)
2134 )
2135 .unwrap();
2136
2137 let mut lookup = HashMap::new();
2138 let mut default_port = None;
2139
2140 if names.is_empty() {
2141 html.push_str("<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"lightgray\">—</FONT></TD></TR>");
2142 } else {
2143 for (idx, name) in names.iter().enumerate() {
2144 let port_id = format!("{base_id}_{prefix}_{idx}");
2145 write!(
2146 html,
2147 "<TR><TD PORT=\"{port_id}\" ALIGN=\"LEFT\">{}</TD></TR>",
2148 encode_text(name)
2149 )
2150 .unwrap();
2151 lookup.insert(name.clone(), port_id.clone());
2152 if idx == 0 {
2153 default_port = Some(port_id);
2154 }
2155 }
2156 }
2157
2158 html.push_str("</TABLE>");
2159 (html, lookup, default_port)
2160}
2161
2162#[cfg(feature = "std")]
2163#[allow(dead_code)]
2164fn build_config_table(config: &ComponentConfig) -> Option<String> {
2165 use std::fmt::Write as FmtWrite;
2166
2167 if config.0.is_empty() {
2168 return None;
2169 }
2170
2171 let mut entries: Vec<_> = config.0.iter().collect();
2172 entries.sort_by(|a, b| a.0.cmp(b.0));
2173
2174 let mut html = String::new();
2175 html.push_str("<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">");
2176 for (key, value) in entries {
2177 let value_txt = format!("{value}");
2178 write!(
2179 html,
2180 "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT> = {}</TD></TR>",
2181 encode_text(key),
2182 encode_text(&value_txt)
2183 )
2184 .unwrap();
2185 }
2186 html.push_str("</TABLE>");
2187 Some(html)
2188}
2189
2190#[cfg(feature = "std")]
2191#[allow(dead_code)]
2192fn sanitize_identifier(value: &str) -> String {
2193 value
2194 .chars()
2195 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
2196 .collect()
2197}
2198
2199#[cfg(feature = "std")]
2200#[allow(dead_code)]
2201fn escape_dot_id(value: &str) -> String {
2202 let mut escaped = String::with_capacity(value.len());
2203 for ch in value.chars() {
2204 match ch {
2205 '"' => escaped.push_str("\\\""),
2206 '\\' => escaped.push_str("\\\\"),
2207 _ => escaped.push(ch),
2208 }
2209 }
2210 escaped
2211}
2212
2213impl LoggingConfig {
2214 pub fn validate(&self) -> CuResult<()> {
2216 if let Some(section_size_mib) = self.section_size_mib
2217 && let Some(slab_size_mib) = self.slab_size_mib
2218 && section_size_mib > slab_size_mib
2219 {
2220 return Err(CuError::from(format!(
2221 "Section size ({section_size_mib} MiB) cannot be larger than slab size ({slab_size_mib} MiB). Adjust the parameters accordingly."
2222 )));
2223 }
2224
2225 Ok(())
2226 }
2227}
2228
2229#[allow(dead_code)] fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
2231 let mut result = content.to_string();
2232
2233 for (key, value) in params {
2234 let pattern = format!("{{{{{key}}}}}");
2235 result = result.replace(&pattern, &value.to_string());
2236 }
2237
2238 result
2239}
2240
2241#[cfg(feature = "std")]
2243fn process_includes(
2244 file_path: &str,
2245 base_representation: CuConfigRepresentation,
2246 processed_files: &mut Vec<String>,
2247) -> CuResult<CuConfigRepresentation> {
2248 processed_files.push(file_path.to_string());
2250
2251 let mut result = base_representation;
2252
2253 if let Some(includes) = result.includes.take() {
2254 for include in includes {
2255 let include_path = if include.path.starts_with('/') {
2256 include.path.clone()
2257 } else {
2258 let current_dir = std::path::Path::new(file_path)
2259 .parent()
2260 .unwrap_or_else(|| std::path::Path::new(""))
2261 .to_string_lossy()
2262 .to_string();
2263
2264 format!("{}/{}", current_dir, include.path)
2265 };
2266
2267 let include_content = read_to_string(&include_path).map_err(|e| {
2268 CuError::from(format!("Failed to read include file: {include_path}"))
2269 .add_cause(e.to_string().as_str())
2270 })?;
2271
2272 let processed_content = substitute_parameters(&include_content, &include.params);
2273
2274 let mut included_representation: CuConfigRepresentation = match Options::default()
2275 .with_default_extension(Extensions::IMPLICIT_SOME)
2276 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2277 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2278 .from_str(&processed_content)
2279 {
2280 Ok(rep) => rep,
2281 Err(e) => {
2282 return Err(CuError::from(format!(
2283 "Failed to parse include file: {} - Error: {} at position {}",
2284 include_path, e.code, e.span
2285 )));
2286 }
2287 };
2288
2289 included_representation =
2290 process_includes(&include_path, included_representation, processed_files)?;
2291
2292 if let Some(included_tasks) = included_representation.tasks {
2293 if result.tasks.is_none() {
2294 result.tasks = Some(included_tasks);
2295 } else {
2296 let mut tasks = result.tasks.take().unwrap();
2297 for included_task in included_tasks {
2298 if !tasks.iter().any(|t| t.id == included_task.id) {
2299 tasks.push(included_task);
2300 }
2301 }
2302 result.tasks = Some(tasks);
2303 }
2304 }
2305
2306 if let Some(included_bridges) = included_representation.bridges {
2307 if result.bridges.is_none() {
2308 result.bridges = Some(included_bridges);
2309 } else {
2310 let mut bridges = result.bridges.take().unwrap();
2311 for included_bridge in included_bridges {
2312 if !bridges.iter().any(|b| b.id == included_bridge.id) {
2313 bridges.push(included_bridge);
2314 }
2315 }
2316 result.bridges = Some(bridges);
2317 }
2318 }
2319
2320 if let Some(included_resources) = included_representation.resources {
2321 if result.resources.is_none() {
2322 result.resources = Some(included_resources);
2323 } else {
2324 let mut resources = result.resources.take().unwrap();
2325 for included_resource in included_resources {
2326 if !resources.iter().any(|r| r.id == included_resource.id) {
2327 resources.push(included_resource);
2328 }
2329 }
2330 result.resources = Some(resources);
2331 }
2332 }
2333
2334 if let Some(included_cnx) = included_representation.cnx {
2335 if result.cnx.is_none() {
2336 result.cnx = Some(included_cnx);
2337 } else {
2338 let mut cnx = result.cnx.take().unwrap();
2339 for included_c in included_cnx {
2340 if !cnx
2341 .iter()
2342 .any(|c| c.src == included_c.src && c.dst == included_c.dst)
2343 {
2344 cnx.push(included_c);
2345 }
2346 }
2347 result.cnx = Some(cnx);
2348 }
2349 }
2350
2351 if result.monitor.is_none() {
2352 result.monitor = included_representation.monitor;
2353 }
2354
2355 if result.logging.is_none() {
2356 result.logging = included_representation.logging;
2357 }
2358
2359 if result.runtime.is_none() {
2360 result.runtime = included_representation.runtime;
2361 }
2362
2363 if let Some(included_missions) = included_representation.missions {
2364 if result.missions.is_none() {
2365 result.missions = Some(included_missions);
2366 } else {
2367 let mut missions = result.missions.take().unwrap();
2368 for included_mission in included_missions {
2369 if !missions.iter().any(|m| m.id == included_mission.id) {
2370 missions.push(included_mission);
2371 }
2372 }
2373 result.missions = Some(missions);
2374 }
2375 }
2376 }
2377 }
2378
2379 Ok(result)
2380}
2381
2382#[cfg(feature = "std")]
2384pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
2385 let config_content = read_to_string(config_filename).map_err(|e| {
2386 CuError::from(format!(
2387 "Failed to read configuration file: {:?}",
2388 &config_filename
2389 ))
2390 .add_cause(e.to_string().as_str())
2391 })?;
2392 read_configuration_str(config_content, Some(config_filename))
2393}
2394
2395fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
2399 Options::default()
2400 .with_default_extension(Extensions::IMPLICIT_SOME)
2401 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2402 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2403 .from_str(content)
2404 .map_err(|e| {
2405 CuError::from(format!(
2406 "Failed to parse configuration: Error: {} at position {}",
2407 e.code, e.span
2408 ))
2409 })
2410}
2411
2412fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
2415 #[allow(unused_mut)]
2416 let mut cuconfig = CuConfig::deserialize_impl(representation)
2417 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
2418
2419 #[cfg(feature = "std")]
2420 cuconfig.ensure_threadpool_bundle();
2421
2422 cuconfig.validate_logging_config()?;
2423
2424 Ok(cuconfig)
2425}
2426
2427#[allow(unused_variables)]
2428pub fn read_configuration_str(
2429 config_content: String,
2430 file_path: Option<&str>,
2431) -> CuResult<CuConfig> {
2432 let representation = parse_config_string(&config_content)?;
2434
2435 #[cfg(feature = "std")]
2438 let representation = if let Some(path) = file_path {
2439 process_includes(path, representation, &mut Vec::new())?
2440 } else {
2441 representation
2442 };
2443
2444 config_representation_to_config(representation)
2446}
2447
2448#[cfg(test)]
2450mod tests {
2451 use super::*;
2452 #[cfg(not(feature = "std"))]
2453 use alloc::vec;
2454 use serde::Deserialize;
2455
2456 #[test]
2457 fn test_plain_serialize() {
2458 let mut config = CuConfig::default();
2459 let graph = config.get_graph_mut(None).unwrap();
2460 let n1 = graph
2461 .add_node(Node::new("test1", "package::Plugin1"))
2462 .unwrap();
2463 let n2 = graph
2464 .add_node(Node::new("test2", "package::Plugin2"))
2465 .unwrap();
2466 graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
2467 let serialized = config.serialize_ron().unwrap();
2468 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
2469 let graph = config.graphs.get_graph(None).unwrap();
2470 let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
2471 assert_eq!(graph.node_count(), deserialized_graph.node_count());
2472 assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
2473 }
2474
2475 #[test]
2476 fn test_serialize_with_params() {
2477 let mut config = CuConfig::default();
2478 let graph = config.get_graph_mut(None).unwrap();
2479 let mut camera = Node::new("copper-camera", "camerapkg::Camera");
2480 camera.set_param::<Value>("resolution-height", 1080.into());
2481 graph.add_node(camera).unwrap();
2482 let serialized = config.serialize_ron().unwrap();
2483 let config = CuConfig::deserialize_ron(&serialized).unwrap();
2484 let deserialized = config.get_graph(None).unwrap();
2485 let resolution = deserialized
2486 .get_node(0)
2487 .unwrap()
2488 .get_param::<i32>("resolution-height")
2489 .expect("resolution-height lookup failed");
2490 assert_eq!(resolution, Some(1080));
2491 }
2492
2493 #[derive(Debug, Deserialize, PartialEq)]
2494 struct InnerSettings {
2495 threshold: u32,
2496 flags: Option<bool>,
2497 }
2498
2499 #[derive(Debug, Deserialize, PartialEq)]
2500 struct SettingsConfig {
2501 gain: f32,
2502 matrix: [[f32; 3]; 3],
2503 inner: InnerSettings,
2504 tags: Vec<String>,
2505 }
2506
2507 #[test]
2508 fn test_component_config_get_value_structured() {
2509 let txt = r#"
2510 (
2511 tasks: [
2512 (
2513 id: "task",
2514 type: "pkg::Task",
2515 config: {
2516 "settings": {
2517 "gain": 1.5,
2518 "matrix": [
2519 [1.0, 0.0, 0.0],
2520 [0.0, 1.0, 0.0],
2521 [0.0, 0.0, 1.0],
2522 ],
2523 "inner": { "threshold": 42, "flags": Some(true) },
2524 "tags": ["alpha", "beta"],
2525 },
2526 },
2527 ),
2528 ],
2529 cnx: [],
2530 )
2531 "#;
2532 let config = CuConfig::deserialize_ron(txt).unwrap();
2533 let graph = config.graphs.get_graph(None).unwrap();
2534 let node = graph.get_node(0).unwrap();
2535 let component = node.get_instance_config().expect("missing config");
2536 let settings = component
2537 .get_value::<SettingsConfig>("settings")
2538 .expect("settings lookup failed")
2539 .expect("missing settings");
2540 let expected = SettingsConfig {
2541 gain: 1.5,
2542 matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
2543 inner: InnerSettings {
2544 threshold: 42,
2545 flags: Some(true),
2546 },
2547 tags: vec!["alpha".to_string(), "beta".to_string()],
2548 };
2549 assert_eq!(settings, expected);
2550 }
2551
2552 #[test]
2553 fn test_component_config_get_value_scalar_compatibility() {
2554 let txt = r#"
2555 (
2556 tasks: [
2557 (id: "task", type: "pkg::Task", config: { "scalar": 7 }),
2558 ],
2559 cnx: [],
2560 )
2561 "#;
2562 let config = CuConfig::deserialize_ron(txt).unwrap();
2563 let graph = config.graphs.get_graph(None).unwrap();
2564 let node = graph.get_node(0).unwrap();
2565 let component = node.get_instance_config().expect("missing config");
2566 let scalar = component
2567 .get::<u32>("scalar")
2568 .expect("scalar lookup failed");
2569 assert_eq!(scalar, Some(7));
2570 }
2571
2572 #[test]
2573 fn test_component_config_get_value_mixed_usage() {
2574 let txt = r#"
2575 (
2576 tasks: [
2577 (
2578 id: "task",
2579 type: "pkg::Task",
2580 config: {
2581 "scalar": 12,
2582 "settings": {
2583 "gain": 2.5,
2584 "matrix": [
2585 [1.0, 2.0, 3.0],
2586 [4.0, 5.0, 6.0],
2587 [7.0, 8.0, 9.0],
2588 ],
2589 "inner": { "threshold": 7, "flags": None },
2590 "tags": ["gamma"],
2591 },
2592 },
2593 ),
2594 ],
2595 cnx: [],
2596 )
2597 "#;
2598 let config = CuConfig::deserialize_ron(txt).unwrap();
2599 let graph = config.graphs.get_graph(None).unwrap();
2600 let node = graph.get_node(0).unwrap();
2601 let component = node.get_instance_config().expect("missing config");
2602 let scalar = component
2603 .get::<u32>("scalar")
2604 .expect("scalar lookup failed");
2605 let settings = component
2606 .get_value::<SettingsConfig>("settings")
2607 .expect("settings lookup failed");
2608 assert_eq!(scalar, Some(12));
2609 assert!(settings.is_some());
2610 }
2611
2612 #[test]
2613 fn test_component_config_get_value_error_includes_key() {
2614 let txt = r#"
2615 (
2616 tasks: [
2617 (
2618 id: "task",
2619 type: "pkg::Task",
2620 config: { "settings": { "gain": 1.0 } },
2621 ),
2622 ],
2623 cnx: [],
2624 )
2625 "#;
2626 let config = CuConfig::deserialize_ron(txt).unwrap();
2627 let graph = config.graphs.get_graph(None).unwrap();
2628 let node = graph.get_node(0).unwrap();
2629 let component = node.get_instance_config().expect("missing config");
2630 let err = component
2631 .get_value::<u32>("settings")
2632 .expect_err("expected type mismatch");
2633 assert!(err.to_string().contains("settings"));
2634 }
2635
2636 #[test]
2637 fn test_deserialization_error() {
2638 let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
2640 let err = CuConfig::deserialize_ron(txt).expect_err("expected deserialization error");
2641 assert!(
2642 err.to_string()
2643 .contains("Syntax Error in config: Expected opening `[` at position 1:9-1:10")
2644 );
2645 }
2646 #[test]
2647 fn test_missions() {
2648 let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
2649 let config = CuConfig::deserialize_ron(txt).unwrap();
2650 let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
2651 assert!(graph.node_count() == 0);
2652 let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
2653 assert!(graph.node_count() == 0);
2654 }
2655
2656 #[test]
2657 fn test_monitor() {
2658 let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
2659 let config = CuConfig::deserialize_ron(txt).unwrap();
2660 assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
2661
2662 let txt =
2663 r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
2664 let config = CuConfig::deserialize_ron(txt).unwrap();
2665 assert_eq!(
2666 config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
2667 4u8.into()
2668 );
2669 }
2670
2671 #[test]
2672 #[cfg(feature = "std")]
2673 fn test_render_topology_multi_input_ports() {
2674 let mut config = CuConfig::default();
2675 let graph = config.get_graph_mut(None).unwrap();
2676 let src1 = graph.add_node(Node::new("src1", "tasks::Source1")).unwrap();
2677 let src2 = graph.add_node(Node::new("src2", "tasks::Source2")).unwrap();
2678 let dst = graph.add_node(Node::new("dst", "tasks::Dst")).unwrap();
2679 graph.connect(src1, dst, "msg::A").unwrap();
2680 graph.connect(src2, dst, "msg::B").unwrap();
2681
2682 let topology = build_render_topology(graph, &[]);
2683 let dst_node = topology
2684 .nodes
2685 .iter()
2686 .find(|node| node.id == "dst")
2687 .expect("missing dst node");
2688 assert_eq!(dst_node.inputs.len(), 2);
2689
2690 let mut dst_ports: Vec<_> = topology
2691 .connections
2692 .iter()
2693 .filter(|cnx| cnx.dst == "dst")
2694 .map(|cnx| cnx.dst_port.as_deref().expect("missing dst port"))
2695 .collect();
2696 dst_ports.sort();
2697 assert_eq!(dst_ports, vec!["in.0", "in.1"]);
2698 }
2699
2700 #[test]
2701 fn test_logging_parameters() {
2702 let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
2704
2705 let config = CuConfig::deserialize_ron(txt).unwrap();
2706 assert!(config.logging.is_some());
2707 let logging_config = config.logging.unwrap();
2708 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
2709 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
2710 assert!(!logging_config.enable_task_logging);
2711
2712 let txt =
2714 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
2715 let config = CuConfig::deserialize_ron(txt).unwrap();
2716 assert!(config.logging.is_some());
2717 let logging_config = config.logging.unwrap();
2718 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
2719 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
2720 assert!(logging_config.enable_task_logging);
2721 }
2722
2723 #[test]
2724 fn test_bridge_parsing() {
2725 let txt = r#"
2726 (
2727 tasks: [
2728 (id: "dst", type: "tasks::Destination"),
2729 (id: "src", type: "tasks::Source"),
2730 ],
2731 bridges: [
2732 (
2733 id: "radio",
2734 type: "tasks::SerialBridge",
2735 config: { "path": "/dev/ttyACM0", "baud": 921600 },
2736 channels: [
2737 Rx ( id: "status", route: "sys/status" ),
2738 Tx ( id: "motor", route: "motor/cmd" ),
2739 ],
2740 ),
2741 ],
2742 cnx: [
2743 (src: "radio/status", dst: "dst", msg: "mymsgs::Status"),
2744 (src: "src", dst: "radio/motor", msg: "mymsgs::MotorCmd"),
2745 ],
2746 )
2747 "#;
2748
2749 let config = CuConfig::deserialize_ron(txt).unwrap();
2750 assert_eq!(config.bridges.len(), 1);
2751 let bridge = &config.bridges[0];
2752 assert_eq!(bridge.id, "radio");
2753 assert_eq!(bridge.channels.len(), 2);
2754 match &bridge.channels[0] {
2755 BridgeChannelConfigRepresentation::Rx { id, route, .. } => {
2756 assert_eq!(id, "status");
2757 assert_eq!(route.as_deref(), Some("sys/status"));
2758 }
2759 _ => panic!("expected Rx channel"),
2760 }
2761 match &bridge.channels[1] {
2762 BridgeChannelConfigRepresentation::Tx { id, route, .. } => {
2763 assert_eq!(id, "motor");
2764 assert_eq!(route.as_deref(), Some("motor/cmd"));
2765 }
2766 _ => panic!("expected Tx channel"),
2767 }
2768 let graph = config.graphs.get_graph(None).unwrap();
2769 let bridge_id = graph
2770 .get_node_id_by_name("radio")
2771 .expect("bridge node missing");
2772 let bridge_node = graph.get_node(bridge_id).unwrap();
2773 assert_eq!(bridge_node.get_flavor(), Flavor::Bridge);
2774
2775 let mut edges = Vec::new();
2777 for edge_idx in graph.0.edge_indices() {
2778 edges.push(graph.0[edge_idx].clone());
2779 }
2780 assert_eq!(edges.len(), 2);
2781 let status_edge = edges
2782 .iter()
2783 .find(|e| e.dst == "dst")
2784 .expect("status edge missing");
2785 assert_eq!(status_edge.src_channel.as_deref(), Some("status"));
2786 assert!(status_edge.dst_channel.is_none());
2787 let motor_edge = edges
2788 .iter()
2789 .find(|e| e.dst_channel.is_some())
2790 .expect("motor edge missing");
2791 assert_eq!(motor_edge.dst_channel.as_deref(), Some("motor"));
2792 }
2793
2794 #[test]
2795 fn test_bridge_roundtrip() {
2796 let mut config = CuConfig::default();
2797 let mut bridge_config = ComponentConfig::default();
2798 bridge_config.set("port", "/dev/ttyACM0".to_string());
2799 config.bridges.push(BridgeConfig {
2800 id: "radio".to_string(),
2801 type_: "tasks::SerialBridge".to_string(),
2802 config: Some(bridge_config),
2803 resources: None,
2804 missions: None,
2805 channels: vec![
2806 BridgeChannelConfigRepresentation::Rx {
2807 id: "status".to_string(),
2808 route: Some("sys/status".to_string()),
2809 config: None,
2810 },
2811 BridgeChannelConfigRepresentation::Tx {
2812 id: "motor".to_string(),
2813 route: Some("motor/cmd".to_string()),
2814 config: None,
2815 },
2816 ],
2817 });
2818
2819 let serialized = config.serialize_ron().unwrap();
2820 assert!(
2821 serialized.contains("bridges"),
2822 "bridges section missing from serialized config"
2823 );
2824 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
2825 assert_eq!(deserialized.bridges.len(), 1);
2826 let bridge = &deserialized.bridges[0];
2827 assert_eq!(bridge.channels.len(), 2);
2828 assert!(matches!(
2829 bridge.channels[0],
2830 BridgeChannelConfigRepresentation::Rx { .. }
2831 ));
2832 assert!(matches!(
2833 bridge.channels[1],
2834 BridgeChannelConfigRepresentation::Tx { .. }
2835 ));
2836 }
2837
2838 #[test]
2839 fn test_resource_parsing() {
2840 let txt = r#"
2841 (
2842 resources: [
2843 (
2844 id: "fc",
2845 provider: "copper_board_px4::Px4Bundle",
2846 config: { "baud": 921600 },
2847 missions: ["m1"],
2848 ),
2849 (
2850 id: "misc",
2851 provider: "cu29_runtime::StdClockBundle",
2852 ),
2853 ],
2854 )
2855 "#;
2856
2857 let config = CuConfig::deserialize_ron(txt).unwrap();
2858 assert_eq!(config.resources.len(), 2);
2859 let fc = &config.resources[0];
2860 assert_eq!(fc.id, "fc");
2861 assert_eq!(fc.provider, "copper_board_px4::Px4Bundle");
2862 assert_eq!(fc.missions.as_deref(), Some(&["m1".to_string()][..]));
2863 let baud: u32 = fc
2864 .config
2865 .as_ref()
2866 .expect("missing config")
2867 .get::<u32>("baud")
2868 .expect("baud lookup failed")
2869 .expect("missing baud");
2870 assert_eq!(baud, 921_600);
2871 let misc = &config.resources[1];
2872 assert_eq!(misc.id, "misc");
2873 assert_eq!(misc.provider, "cu29_runtime::StdClockBundle");
2874 assert!(misc.config.is_none());
2875 }
2876
2877 #[test]
2878 fn test_resource_roundtrip() {
2879 let mut config = CuConfig::default();
2880 let mut bundle_cfg = ComponentConfig::default();
2881 bundle_cfg.set("path", "/dev/ttyACM0".to_string());
2882 config.resources.push(ResourceBundleConfig {
2883 id: "fc".to_string(),
2884 provider: "copper_board_px4::Px4Bundle".to_string(),
2885 config: Some(bundle_cfg),
2886 missions: Some(vec!["m1".to_string()]),
2887 });
2888
2889 let serialized = config.serialize_ron().unwrap();
2890 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
2891 assert_eq!(deserialized.resources.len(), 1);
2892 let res = &deserialized.resources[0];
2893 assert_eq!(res.id, "fc");
2894 assert_eq!(res.provider, "copper_board_px4::Px4Bundle");
2895 assert_eq!(res.missions.as_deref(), Some(&["m1".to_string()][..]));
2896 let path: String = res
2897 .config
2898 .as_ref()
2899 .expect("missing config")
2900 .get::<String>("path")
2901 .expect("path lookup failed")
2902 .expect("missing path");
2903 assert_eq!(path, "/dev/ttyACM0");
2904 }
2905
2906 #[test]
2907 fn test_bridge_channel_config() {
2908 let txt = r#"
2909 (
2910 tasks: [],
2911 bridges: [
2912 (
2913 id: "radio",
2914 type: "tasks::SerialBridge",
2915 channels: [
2916 Rx ( id: "status", route: "sys/status", config: { "filter": "fast" } ),
2917 Tx ( id: "imu", route: "telemetry/imu", config: { "rate": 100 } ),
2918 ],
2919 ),
2920 ],
2921 cnx: [],
2922 )
2923 "#;
2924
2925 let config = CuConfig::deserialize_ron(txt).unwrap();
2926 let bridge = &config.bridges[0];
2927 match &bridge.channels[0] {
2928 BridgeChannelConfigRepresentation::Rx {
2929 config: Some(cfg), ..
2930 } => {
2931 let val = cfg
2932 .get::<String>("filter")
2933 .expect("filter lookup failed")
2934 .expect("filter missing");
2935 assert_eq!(val, "fast");
2936 }
2937 _ => panic!("expected Rx channel with config"),
2938 }
2939 match &bridge.channels[1] {
2940 BridgeChannelConfigRepresentation::Tx {
2941 config: Some(cfg), ..
2942 } => {
2943 let rate = cfg
2944 .get::<i32>("rate")
2945 .expect("rate lookup failed")
2946 .expect("rate missing");
2947 assert_eq!(rate, 100);
2948 }
2949 _ => panic!("expected Tx channel with config"),
2950 }
2951 }
2952
2953 #[test]
2954 fn test_task_resources_roundtrip() {
2955 let txt = r#"
2956 (
2957 tasks: [
2958 (
2959 id: "imu",
2960 type: "tasks::ImuDriver",
2961 resources: { "bus": "fc.spi_1", "irq": "fc.gpio_imu" },
2962 ),
2963 ],
2964 cnx: [],
2965 )
2966 "#;
2967
2968 let config = CuConfig::deserialize_ron(txt).unwrap();
2969 let graph = config.graphs.get_graph(None).unwrap();
2970 let node = graph.get_node(0).expect("missing task node");
2971 let resources = node.get_resources().expect("missing resources map");
2972 assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
2973 assert_eq!(
2974 resources.get("irq").map(String::as_str),
2975 Some("fc.gpio_imu")
2976 );
2977
2978 let serialized = config.serialize_ron().unwrap();
2979 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
2980 let graph = deserialized.graphs.get_graph(None).unwrap();
2981 let node = graph.get_node(0).expect("missing task node");
2982 let resources = node
2983 .get_resources()
2984 .expect("missing resources map after roundtrip");
2985 assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
2986 assert_eq!(
2987 resources.get("irq").map(String::as_str),
2988 Some("fc.gpio_imu")
2989 );
2990 }
2991
2992 #[test]
2993 fn test_bridge_resources_preserved() {
2994 let mut config = CuConfig::default();
2995 config.resources.push(ResourceBundleConfig {
2996 id: "fc".to_string(),
2997 provider: "board::Bundle".to_string(),
2998 config: None,
2999 missions: None,
3000 });
3001 let bridge_resources = HashMap::from([("serial".to_string(), "fc.serial0".to_string())]);
3002 config.bridges.push(BridgeConfig {
3003 id: "radio".to_string(),
3004 type_: "tasks::SerialBridge".to_string(),
3005 config: None,
3006 resources: Some(bridge_resources),
3007 missions: None,
3008 channels: vec![BridgeChannelConfigRepresentation::Tx {
3009 id: "uplink".to_string(),
3010 route: None,
3011 config: None,
3012 }],
3013 });
3014
3015 let serialized = config.serialize_ron().unwrap();
3016 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3017 let graph = deserialized.graphs.get_graph(None).expect("missing graph");
3018 let bridge_id = graph
3019 .get_node_id_by_name("radio")
3020 .expect("bridge node missing");
3021 let node = graph.get_node(bridge_id).expect("missing bridge node");
3022 let resources = node
3023 .get_resources()
3024 .expect("bridge resources were not preserved");
3025 assert_eq!(
3026 resources.get("serial").map(String::as_str),
3027 Some("fc.serial0")
3028 );
3029 }
3030
3031 #[test]
3032 fn test_demo_config_parses() {
3033 let txt = r#"(
3034 resources: [
3035 (
3036 id: "fc",
3037 provider: "crate::resources::RadioBundle",
3038 ),
3039 ],
3040 tasks: [
3041 (id: "thr", type: "tasks::ThrottleControl"),
3042 (id: "tele0", type: "tasks::TelemetrySink0"),
3043 (id: "tele1", type: "tasks::TelemetrySink1"),
3044 (id: "tele2", type: "tasks::TelemetrySink2"),
3045 (id: "tele3", type: "tasks::TelemetrySink3"),
3046 ],
3047 bridges: [
3048 ( id: "crsf",
3049 type: "cu_crsf::CrsfBridge<SerialResource, SerialPortError>",
3050 resources: { "serial": "fc.serial" },
3051 channels: [
3052 Rx ( id: "rc_rx" ), // receiving RC Channels
3053 Tx ( id: "lq_tx" ), // Sending LineQuality back
3054 ],
3055 ),
3056 (
3057 id: "bdshot",
3058 type: "cu_bdshot::RpBdshotBridge",
3059 channels: [
3060 Tx ( id: "esc0_tx" ),
3061 Tx ( id: "esc1_tx" ),
3062 Tx ( id: "esc2_tx" ),
3063 Tx ( id: "esc3_tx" ),
3064 Rx ( id: "esc0_rx" ),
3065 Rx ( id: "esc1_rx" ),
3066 Rx ( id: "esc2_rx" ),
3067 Rx ( id: "esc3_rx" ),
3068 ],
3069 ),
3070 ],
3071 cnx: [
3072 (src: "crsf/rc_rx", dst: "thr", msg: "cu_crsf::messages::RcChannelsPayload"),
3073 (src: "thr", dst: "bdshot/esc0_tx", msg: "cu_bdshot::EscCommand"),
3074 (src: "thr", dst: "bdshot/esc1_tx", msg: "cu_bdshot::EscCommand"),
3075 (src: "thr", dst: "bdshot/esc2_tx", msg: "cu_bdshot::EscCommand"),
3076 (src: "thr", dst: "bdshot/esc3_tx", msg: "cu_bdshot::EscCommand"),
3077 (src: "bdshot/esc0_rx", dst: "tele0", msg: "cu_bdshot::EscTelemetry"),
3078 (src: "bdshot/esc1_rx", dst: "tele1", msg: "cu_bdshot::EscTelemetry"),
3079 (src: "bdshot/esc2_rx", dst: "tele2", msg: "cu_bdshot::EscTelemetry"),
3080 (src: "bdshot/esc3_rx", dst: "tele3", msg: "cu_bdshot::EscTelemetry"),
3081 ],
3082)"#;
3083 let config = CuConfig::deserialize_ron(txt).unwrap();
3084 assert_eq!(config.resources.len(), 1);
3085 assert_eq!(config.bridges.len(), 2);
3086 }
3087
3088 #[test]
3089 fn test_bridge_tx_cannot_be_source() {
3090 let txt = r#"
3091 (
3092 tasks: [
3093 (id: "dst", type: "tasks::Destination"),
3094 ],
3095 bridges: [
3096 (
3097 id: "radio",
3098 type: "tasks::SerialBridge",
3099 channels: [
3100 Tx ( id: "motor", route: "motor/cmd" ),
3101 ],
3102 ),
3103 ],
3104 cnx: [
3105 (src: "radio/motor", dst: "dst", msg: "mymsgs::MotorCmd"),
3106 ],
3107 )
3108 "#;
3109
3110 let err = CuConfig::deserialize_ron(txt).expect_err("expected bridge source error");
3111 assert!(
3112 err.to_string()
3113 .contains("channel 'motor' is Tx and cannot act as a source")
3114 );
3115 }
3116
3117 #[test]
3118 fn test_bridge_rx_cannot_be_destination() {
3119 let txt = r#"
3120 (
3121 tasks: [
3122 (id: "src", type: "tasks::Source"),
3123 ],
3124 bridges: [
3125 (
3126 id: "radio",
3127 type: "tasks::SerialBridge",
3128 channels: [
3129 Rx ( id: "status", route: "sys/status" ),
3130 ],
3131 ),
3132 ],
3133 cnx: [
3134 (src: "src", dst: "radio/status", msg: "mymsgs::Status"),
3135 ],
3136 )
3137 "#;
3138
3139 let err = CuConfig::deserialize_ron(txt).expect_err("expected bridge destination error");
3140 assert!(
3141 err.to_string()
3142 .contains("channel 'status' is Rx and cannot act as a destination")
3143 );
3144 }
3145
3146 #[test]
3147 fn test_validate_logging_config() {
3148 let txt =
3150 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
3151 let config = CuConfig::deserialize_ron(txt).unwrap();
3152 assert!(config.validate_logging_config().is_ok());
3153
3154 let txt =
3156 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
3157 let config = CuConfig::deserialize_ron(txt).unwrap();
3158 assert!(config.validate_logging_config().is_err());
3159 }
3160
3161 #[test]
3163 fn test_deserialization_edge_id_assignment() {
3164 let txt = r#"(
3167 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
3168 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
3169 )"#;
3170 let config = CuConfig::deserialize_ron(txt).unwrap();
3171 let graph = config.graphs.get_graph(None).unwrap();
3172 assert!(config.validate_logging_config().is_ok());
3173
3174 let src1_id = 0;
3176 assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
3177 let src2_id = 1;
3178 assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
3179
3180 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
3183 assert_eq!(src1_edge_id, 1);
3184 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
3185 assert_eq!(src2_edge_id, 0);
3186 }
3187
3188 #[test]
3189 fn test_simple_missions() {
3190 let txt = r#"(
3192 missions: [ (id: "m1"),
3193 (id: "m2"),
3194 ],
3195 tasks: [(id: "src1", type: "a", missions: ["m1"]),
3196 (id: "src2", type: "b", missions: ["m2"]),
3197 (id: "sink", type: "c")],
3198
3199 cnx: [
3200 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
3201 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
3202 ],
3203 )
3204 "#;
3205
3206 let config = CuConfig::deserialize_ron(txt).unwrap();
3207 let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
3208 assert_eq!(m1_graph.edge_count(), 1);
3209 assert_eq!(m1_graph.node_count(), 2);
3210 let index = 0;
3211 let cnx = m1_graph.get_edge_weight(index).unwrap();
3212
3213 assert_eq!(cnx.src, "src1");
3214 assert_eq!(cnx.dst, "sink");
3215 assert_eq!(cnx.msg, "u32");
3216 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
3217
3218 let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
3219 assert_eq!(m2_graph.edge_count(), 1);
3220 assert_eq!(m2_graph.node_count(), 2);
3221 let index = 0;
3222 let cnx = m2_graph.get_edge_weight(index).unwrap();
3223 assert_eq!(cnx.src, "src2");
3224 assert_eq!(cnx.dst, "sink");
3225 assert_eq!(cnx.msg, "u32");
3226 assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
3227 }
3228 #[test]
3229 fn test_mission_serde() {
3230 let txt = r#"(
3232 missions: [ (id: "m1"),
3233 (id: "m2"),
3234 ],
3235 tasks: [(id: "src1", type: "a", missions: ["m1"]),
3236 (id: "src2", type: "b", missions: ["m2"]),
3237 (id: "sink", type: "c")],
3238
3239 cnx: [
3240 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
3241 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
3242 ],
3243 )
3244 "#;
3245
3246 let config = CuConfig::deserialize_ron(txt).unwrap();
3247 let serialized = config.serialize_ron().unwrap();
3248 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3249 let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
3250 assert_eq!(m1_graph.edge_count(), 1);
3251 assert_eq!(m1_graph.node_count(), 2);
3252 let index = 0;
3253 let cnx = m1_graph.get_edge_weight(index).unwrap();
3254 assert_eq!(cnx.src, "src1");
3255 assert_eq!(cnx.dst, "sink");
3256 assert_eq!(cnx.msg, "u32");
3257 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
3258 }
3259
3260 #[test]
3261 fn test_keyframe_interval() {
3262 let txt = r#"(
3265 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
3266 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
3267 logging: ( keyframe_interval: 314 )
3268 )"#;
3269 let config = CuConfig::deserialize_ron(txt).unwrap();
3270 let logging_config = config.logging.unwrap();
3271 assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
3272 }
3273
3274 #[test]
3275 fn test_default_keyframe_interval() {
3276 let txt = r#"(
3279 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
3280 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
3281 logging: ( slab_size_mib: 200, section_size_mib: 1024, )
3282 )"#;
3283 let config = CuConfig::deserialize_ron(txt).unwrap();
3284 let logging_config = config.logging.unwrap();
3285 assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
3286 }
3287}