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