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