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 #[allow(dead_code)]
142 pub fn merge_from(&mut self, other: &ComponentConfig) {
143 let ComponentConfig(config) = self;
144 for (key, value) in &other.0 {
145 config.insert(key.clone(), value.clone());
146 }
147 }
148}
149
150fn ron_value_to_cu_value(value: &RonValue) -> Result<CuValue, ConfigError> {
151 match value {
152 RonValue::Bool(v) => Ok(CuValue::Bool(*v)),
153 RonValue::Char(v) => Ok(CuValue::Char(*v)),
154 RonValue::String(v) => Ok(CuValue::String(v.clone())),
155 RonValue::Bytes(v) => Ok(CuValue::Bytes(v.clone())),
156 RonValue::Unit => Ok(CuValue::Unit),
157 RonValue::Option(v) => {
158 let mapped = match v {
159 Some(inner) => Some(Box::new(ron_value_to_cu_value(inner)?)),
160 None => None,
161 };
162 Ok(CuValue::Option(mapped))
163 }
164 RonValue::Seq(seq) => {
165 let mut mapped = Vec::with_capacity(seq.len());
166 for item in seq {
167 mapped.push(ron_value_to_cu_value(item)?);
168 }
169 Ok(CuValue::Seq(mapped))
170 }
171 RonValue::Map(map) => {
172 let mut mapped = BTreeMap::new();
173 for (key, value) in map.iter() {
174 let mapped_key = ron_value_to_cu_value(key)?;
175 let mapped_value = ron_value_to_cu_value(value)?;
176 mapped.insert(mapped_key, mapped_value);
177 }
178 Ok(CuValue::Map(mapped))
179 }
180 RonValue::Number(num) => match num {
181 Number::I8(v) => Ok(CuValue::I8(*v)),
182 Number::I16(v) => Ok(CuValue::I16(*v)),
183 Number::I32(v) => Ok(CuValue::I32(*v)),
184 Number::I64(v) => Ok(CuValue::I64(*v)),
185 Number::U8(v) => Ok(CuValue::U8(*v)),
186 Number::U16(v) => Ok(CuValue::U16(*v)),
187 Number::U32(v) => Ok(CuValue::U32(*v)),
188 Number::U64(v) => Ok(CuValue::U64(*v)),
189 Number::F32(v) => Ok(CuValue::F32(v.0)),
190 Number::F64(v) => Ok(CuValue::F64(v.0)),
191 Number::__NonExhaustive(_) => Err(ConfigError {
192 message: "Unsupported RON number variant".to_string(),
193 }),
194 },
195 }
196}
197
198#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
207pub struct Value(RonValue);
208
209#[derive(Debug, Clone, PartialEq)]
210pub struct ConfigError {
211 message: String,
212}
213
214impl ConfigError {
215 fn type_mismatch(expected: &'static str, value: &Value) -> Self {
216 ConfigError {
217 message: format!("Expected {expected} but got {value:?}"),
218 }
219 }
220
221 fn with_key(self, key: &str) -> Self {
222 ConfigError {
223 message: format!("Config key '{key}': {}", self.message),
224 }
225 }
226}
227
228impl Display for ConfigError {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 write!(f, "{}", self.message)
231 }
232}
233
234#[cfg(feature = "std")]
235impl std::error::Error for ConfigError {}
236
237#[cfg(not(feature = "std"))]
238impl core::error::Error for ConfigError {}
239
240impl From<ConfigError> for CuError {
241 fn from(err: ConfigError) -> Self {
242 CuError::from(err.to_string())
243 }
244}
245
246macro_rules! impl_from_numeric_for_value {
248 ($($source:ty),* $(,)?) => {
249 $(impl From<$source> for Value {
250 fn from(value: $source) -> Self {
251 Value(RonValue::Number(value.into()))
252 }
253 })*
254 };
255}
256
257impl_from_numeric_for_value!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
259
260impl TryFrom<&Value> for bool {
261 type Error = ConfigError;
262
263 fn try_from(value: &Value) -> Result<Self, Self::Error> {
264 if let Value(RonValue::Bool(v)) = value {
265 Ok(*v)
266 } else {
267 Err(ConfigError::type_mismatch("bool", value))
268 }
269 }
270}
271
272impl From<Value> for bool {
273 fn from(value: Value) -> Self {
274 if let Value(RonValue::Bool(v)) = value {
275 v
276 } else {
277 panic!("Expected a Boolean variant but got {value:?}")
278 }
279 }
280}
281macro_rules! impl_from_value_for_int {
282 ($($target:ty),* $(,)?) => {
283 $(
284 impl From<Value> for $target {
285 fn from(value: Value) -> Self {
286 if let Value(RonValue::Number(num)) = value {
287 match num {
288 Number::I8(n) => n as $target,
289 Number::I16(n) => n as $target,
290 Number::I32(n) => n as $target,
291 Number::I64(n) => n as $target,
292 Number::U8(n) => n as $target,
293 Number::U16(n) => n as $target,
294 Number::U32(n) => n as $target,
295 Number::U64(n) => n as $target,
296 Number::F32(_) | Number::F64(_) | Number::__NonExhaustive(_) => {
297 panic!("Expected an integer Number variant but got {num:?}")
298 }
299 }
300 } else {
301 panic!("Expected a Number variant but got {value:?}")
302 }
303 }
304 }
305 )*
306 };
307}
308
309impl_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
310
311macro_rules! impl_try_from_value_for_int {
312 ($($target:ty),* $(,)?) => {
313 $(
314 impl TryFrom<&Value> for $target {
315 type Error = ConfigError;
316
317 fn try_from(value: &Value) -> Result<Self, Self::Error> {
318 if let Value(RonValue::Number(num)) = value {
319 match num {
320 Number::I8(n) => Ok(*n as $target),
321 Number::I16(n) => Ok(*n as $target),
322 Number::I32(n) => Ok(*n as $target),
323 Number::I64(n) => Ok(*n as $target),
324 Number::U8(n) => Ok(*n as $target),
325 Number::U16(n) => Ok(*n as $target),
326 Number::U32(n) => Ok(*n as $target),
327 Number::U64(n) => Ok(*n as $target),
328 Number::F32(_) | Number::F64(_) | Number::__NonExhaustive(_) => {
329 Err(ConfigError::type_mismatch("integer", value))
330 }
331 }
332 } else {
333 Err(ConfigError::type_mismatch("integer", value))
334 }
335 }
336 }
337 )*
338 };
339}
340
341impl_try_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
342
343impl TryFrom<&Value> for f64 {
344 type Error = ConfigError;
345
346 fn try_from(value: &Value) -> Result<Self, Self::Error> {
347 if let Value(RonValue::Number(num)) = value {
348 let number = match num {
349 Number::I8(n) => *n as f64,
350 Number::I16(n) => *n as f64,
351 Number::I32(n) => *n as f64,
352 Number::I64(n) => *n as f64,
353 Number::U8(n) => *n as f64,
354 Number::U16(n) => *n as f64,
355 Number::U32(n) => *n as f64,
356 Number::U64(n) => *n as f64,
357 Number::F32(n) => n.0 as f64,
358 Number::F64(n) => n.0,
359 Number::__NonExhaustive(_) => {
360 return Err(ConfigError::type_mismatch("number", value));
361 }
362 };
363 Ok(number)
364 } else {
365 Err(ConfigError::type_mismatch("number", value))
366 }
367 }
368}
369
370impl From<Value> for f64 {
371 fn from(value: Value) -> Self {
372 if let Value(RonValue::Number(num)) = value {
373 num.into_f64()
374 } else {
375 panic!("Expected a Number variant but got {value:?}")
376 }
377 }
378}
379
380impl From<String> for Value {
381 fn from(value: String) -> Self {
382 Value(RonValue::String(value))
383 }
384}
385
386impl TryFrom<&Value> for String {
387 type Error = ConfigError;
388
389 fn try_from(value: &Value) -> Result<Self, Self::Error> {
390 if let Value(RonValue::String(s)) = value {
391 Ok(s.clone())
392 } else {
393 Err(ConfigError::type_mismatch("string", value))
394 }
395 }
396}
397
398impl From<Value> for String {
399 fn from(value: Value) -> Self {
400 if let Value(RonValue::String(s)) = value {
401 s
402 } else {
403 panic!("Expected a String variant")
404 }
405 }
406}
407
408impl Display for Value {
409 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410 let Value(value) = self;
411 match value {
412 RonValue::Number(n) => {
413 let s = match n {
414 Number::I8(n) => n.to_string(),
415 Number::I16(n) => n.to_string(),
416 Number::I32(n) => n.to_string(),
417 Number::I64(n) => n.to_string(),
418 Number::U8(n) => n.to_string(),
419 Number::U16(n) => n.to_string(),
420 Number::U32(n) => n.to_string(),
421 Number::U64(n) => n.to_string(),
422 Number::F32(n) => n.0.to_string(),
423 Number::F64(n) => n.0.to_string(),
424 _ => panic!("Expected a Number variant but got {value:?}"),
425 };
426 write!(f, "{s}")
427 }
428 RonValue::String(s) => write!(f, "{s}"),
429 RonValue::Bool(b) => write!(f, "{b}"),
430 RonValue::Map(m) => write!(f, "{m:?}"),
431 RonValue::Char(c) => write!(f, "{c:?}"),
432 RonValue::Unit => write!(f, "unit"),
433 RonValue::Option(o) => write!(f, "{o:?}"),
434 RonValue::Seq(s) => write!(f, "{s:?}"),
435 RonValue::Bytes(bytes) => write!(f, "{bytes:?}"),
436 }
437 }
438}
439
440#[derive(Serialize, Deserialize, Debug, Clone)]
442pub struct NodeLogging {
443 enabled: bool,
444}
445
446#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
449pub enum Flavor {
450 #[default]
451 Task,
452 Bridge,
453}
454
455#[derive(Serialize, Deserialize, Debug, Clone)]
458pub struct Node {
459 id: String,
461
462 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
464 type_: Option<String>,
465
466 #[serde(skip_serializing_if = "Option::is_none")]
468 config: Option<ComponentConfig>,
469
470 #[serde(skip_serializing_if = "Option::is_none")]
472 resources: Option<HashMap<String, String>>,
473
474 missions: Option<Vec<String>>,
476
477 #[serde(skip_serializing_if = "Option::is_none")]
480 background: Option<bool>,
481
482 #[serde(skip_serializing_if = "Option::is_none")]
488 run_in_sim: Option<bool>,
489
490 #[serde(skip_serializing_if = "Option::is_none")]
492 logging: Option<NodeLogging>,
493
494 #[serde(skip, default)]
496 flavor: Flavor,
497 #[serde(skip, default)]
499 nc_outputs: Vec<String>,
500 #[serde(skip, default)]
502 nc_output_orders: Vec<usize>,
503}
504
505impl Node {
506 #[allow(dead_code)]
507 pub fn new(id: &str, ptype: &str) -> Self {
508 Node {
509 id: id.to_string(),
510 type_: Some(ptype.to_string()),
511 config: None,
512 resources: None,
513 missions: None,
514 background: None,
515 run_in_sim: None,
516 logging: None,
517 flavor: Flavor::Task,
518 nc_outputs: Vec::new(),
519 nc_output_orders: Vec::new(),
520 }
521 }
522
523 #[allow(dead_code)]
524 pub fn new_with_flavor(id: &str, ptype: &str, flavor: Flavor) -> Self {
525 let mut node = Self::new(id, ptype);
526 node.flavor = flavor;
527 node
528 }
529
530 #[allow(dead_code)]
531 pub fn get_id(&self) -> String {
532 self.id.clone()
533 }
534
535 #[allow(dead_code)]
536 pub fn get_type(&self) -> &str {
537 self.type_.as_ref().unwrap()
538 }
539
540 #[allow(dead_code)]
541 pub fn set_type(mut self, name: Option<String>) -> Self {
542 self.type_ = name;
543 self
544 }
545
546 #[allow(dead_code)]
547 pub fn set_resources<I>(&mut self, resources: Option<I>)
548 where
549 I: IntoIterator<Item = (String, String)>,
550 {
551 self.resources = resources.map(|iter| iter.into_iter().collect());
552 }
553
554 #[allow(dead_code)]
555 pub fn is_background(&self) -> bool {
556 self.background.unwrap_or(false)
557 }
558
559 #[allow(dead_code)]
560 pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
561 self.config.as_ref()
562 }
563
564 #[allow(dead_code)]
565 pub fn get_resources(&self) -> Option<&HashMap<String, String>> {
566 self.resources.as_ref()
567 }
568
569 #[allow(dead_code)]
572 pub fn is_run_in_sim(&self) -> bool {
573 self.run_in_sim.unwrap_or(false)
574 }
575
576 #[allow(dead_code)]
577 pub fn is_logging_enabled(&self) -> bool {
578 if let Some(logging) = &self.logging {
579 logging.enabled
580 } else {
581 true
582 }
583 }
584
585 #[allow(dead_code)]
586 pub fn get_param<T>(&self, key: &str) -> Result<Option<T>, ConfigError>
587 where
588 T: for<'a> TryFrom<&'a Value, Error = ConfigError>,
589 {
590 let pc = match self.config.as_ref() {
591 Some(pc) => pc,
592 None => return Ok(None),
593 };
594 let ComponentConfig(pc) = pc;
595 match pc.get(key) {
596 Some(v) => T::try_from(v).map(Some),
597 None => Ok(None),
598 }
599 }
600
601 #[allow(dead_code)]
602 pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
603 if self.config.is_none() {
604 self.config = Some(ComponentConfig(HashMap::new()));
605 }
606 let ComponentConfig(config) = self.config.as_mut().unwrap();
607 config.insert(key.to_string(), value.into());
608 }
609
610 #[allow(dead_code)]
612 pub fn get_flavor(&self) -> Flavor {
613 self.flavor
614 }
615
616 #[allow(dead_code)]
618 pub fn set_flavor(&mut self, flavor: Flavor) {
619 self.flavor = flavor;
620 }
621
622 #[allow(dead_code)]
624 pub fn add_nc_output(&mut self, msg_type: &str, order: usize) {
625 if let Some(pos) = self
626 .nc_outputs
627 .iter()
628 .position(|existing| existing == msg_type)
629 {
630 if order < self.nc_output_orders[pos] {
631 self.nc_output_orders[pos] = order;
632 }
633 return;
634 }
635 self.nc_outputs.push(msg_type.to_string());
636 self.nc_output_orders.push(order);
637 }
638
639 #[allow(dead_code)]
641 pub fn nc_outputs(&self) -> &[String] {
642 &self.nc_outputs
643 }
644
645 #[allow(dead_code)]
647 pub fn nc_outputs_with_order(&self) -> impl Iterator<Item = (&String, usize)> {
648 self.nc_outputs
649 .iter()
650 .zip(self.nc_output_orders.iter().copied())
651 }
652}
653
654#[derive(Serialize, Deserialize, Debug, Clone)]
656pub enum BridgeChannelConfigRepresentation {
657 Rx {
659 id: String,
660 #[serde(skip_serializing_if = "Option::is_none")]
662 route: Option<String>,
663 #[serde(skip_serializing_if = "Option::is_none")]
665 config: Option<ComponentConfig>,
666 },
667 Tx {
669 id: String,
670 #[serde(skip_serializing_if = "Option::is_none")]
672 route: Option<String>,
673 #[serde(skip_serializing_if = "Option::is_none")]
675 config: Option<ComponentConfig>,
676 },
677}
678
679impl BridgeChannelConfigRepresentation {
680 #[allow(dead_code)]
682 pub fn id(&self) -> &str {
683 match self {
684 BridgeChannelConfigRepresentation::Rx { id, .. }
685 | BridgeChannelConfigRepresentation::Tx { id, .. } => id,
686 }
687 }
688
689 #[allow(dead_code)]
691 pub fn route(&self) -> Option<&str> {
692 match self {
693 BridgeChannelConfigRepresentation::Rx { route, .. }
694 | BridgeChannelConfigRepresentation::Tx { route, .. } => route.as_deref(),
695 }
696 }
697}
698
699enum EndpointRole {
700 Source,
701 Destination,
702}
703
704fn validate_bridge_channel(
705 bridge: &BridgeConfig,
706 channel_id: &str,
707 role: EndpointRole,
708) -> Result<(), String> {
709 let channel = bridge
710 .channels
711 .iter()
712 .find(|ch| ch.id() == channel_id)
713 .ok_or_else(|| {
714 format!(
715 "Bridge '{}' does not declare a channel named '{}'",
716 bridge.id, channel_id
717 )
718 })?;
719
720 match (role, channel) {
721 (EndpointRole::Source, BridgeChannelConfigRepresentation::Rx { .. }) => Ok(()),
722 (EndpointRole::Destination, BridgeChannelConfigRepresentation::Tx { .. }) => Ok(()),
723 (EndpointRole::Source, BridgeChannelConfigRepresentation::Tx { .. }) => Err(format!(
724 "Bridge '{}' channel '{}' is Tx and cannot act as a source",
725 bridge.id, channel_id
726 )),
727 (EndpointRole::Destination, BridgeChannelConfigRepresentation::Rx { .. }) => Err(format!(
728 "Bridge '{}' channel '{}' is Rx and cannot act as a destination",
729 bridge.id, channel_id
730 )),
731 }
732}
733
734#[derive(Serialize, Deserialize, Debug, Clone)]
736pub struct ResourceBundleConfig {
737 pub id: String,
738 #[serde(rename = "provider")]
739 pub provider: String,
740 #[serde(skip_serializing_if = "Option::is_none")]
741 pub config: Option<ComponentConfig>,
742 #[serde(skip_serializing_if = "Option::is_none")]
743 pub missions: Option<Vec<String>>,
744}
745
746#[derive(Serialize, Deserialize, Debug, Clone)]
748pub struct BridgeConfig {
749 pub id: String,
750 #[serde(rename = "type")]
751 pub type_: String,
752 #[serde(skip_serializing_if = "Option::is_none")]
753 pub config: Option<ComponentConfig>,
754 #[serde(skip_serializing_if = "Option::is_none")]
755 pub resources: Option<HashMap<String, String>>,
756 #[serde(skip_serializing_if = "Option::is_none")]
757 pub missions: Option<Vec<String>>,
758 #[serde(skip_serializing_if = "Option::is_none")]
763 pub run_in_sim: Option<bool>,
764 pub channels: Vec<BridgeChannelConfigRepresentation>,
766}
767
768impl BridgeConfig {
769 #[allow(dead_code)]
771 pub fn is_run_in_sim(&self) -> bool {
772 self.run_in_sim.unwrap_or(true)
773 }
774
775 fn to_node(&self) -> Node {
776 let mut node = Node::new_with_flavor(&self.id, &self.type_, Flavor::Bridge);
777 node.config = self.config.clone();
778 node.resources = self.resources.clone();
779 node.missions = self.missions.clone();
780 node
781 }
782}
783
784fn insert_bridge_node(graph: &mut CuGraph, bridge: &BridgeConfig) -> Result<(), String> {
785 if graph.get_node_id_by_name(bridge.id.as_str()).is_some() {
786 return Err(format!(
787 "Bridge '{}' reuses an existing node id. Bridge ids must be unique.",
788 bridge.id
789 ));
790 }
791 graph
792 .add_node(bridge.to_node())
793 .map(|_| ())
794 .map_err(|e| e.to_string())
795}
796
797#[derive(Serialize, Deserialize, Debug, Clone)]
799struct SerializedCnx {
800 src: String,
801 dst: String,
802 msg: String,
803 missions: Option<Vec<String>>,
804}
805
806pub const NC_ENDPOINT: &str = "__nc__";
808
809#[derive(Debug, Clone)]
811pub struct Cnx {
812 pub src: String,
814 pub dst: String,
816 pub msg: String,
818 pub missions: Option<Vec<String>>,
820 pub src_channel: Option<String>,
822 pub dst_channel: Option<String>,
824 pub order: usize,
826}
827
828impl From<&Cnx> for SerializedCnx {
829 fn from(cnx: &Cnx) -> Self {
830 SerializedCnx {
831 src: format_endpoint(&cnx.src, cnx.src_channel.as_deref()),
832 dst: format_endpoint(&cnx.dst, cnx.dst_channel.as_deref()),
833 msg: cnx.msg.clone(),
834 missions: cnx.missions.clone(),
835 }
836 }
837}
838
839fn format_endpoint(node: &str, channel: Option<&str>) -> String {
840 match channel {
841 Some(ch) => format!("{node}/{ch}"),
842 None => node.to_string(),
843 }
844}
845
846fn parse_endpoint(
847 endpoint: &str,
848 role: EndpointRole,
849 bridges: &HashMap<&str, &BridgeConfig>,
850) -> Result<(String, Option<String>), String> {
851 if let Some((node, channel)) = endpoint.split_once('/') {
852 if let Some(bridge) = bridges.get(node) {
853 validate_bridge_channel(bridge, channel, role)?;
854 return Ok((node.to_string(), Some(channel.to_string())));
855 } else {
856 return Err(format!(
857 "Endpoint '{endpoint}' references an unknown bridge '{node}'"
858 ));
859 }
860 }
861
862 if let Some(bridge) = bridges.get(endpoint) {
863 return Err(format!(
864 "Bridge '{}' connections must reference a channel using '{}/<channel>'",
865 bridge.id, bridge.id
866 ));
867 }
868
869 Ok((endpoint.to_string(), None))
870}
871
872fn build_bridge_lookup(bridges: Option<&Vec<BridgeConfig>>) -> HashMap<&str, &BridgeConfig> {
873 let mut map = HashMap::new();
874 if let Some(bridges) = bridges {
875 for bridge in bridges {
876 map.insert(bridge.id.as_str(), bridge);
877 }
878 }
879 map
880}
881
882fn mission_applies(missions: &Option<Vec<String>>, mission_id: &str) -> bool {
883 missions
884 .as_ref()
885 .map(|mission_list| mission_list.iter().any(|m| m == mission_id))
886 .unwrap_or(true)
887}
888
889fn merge_connection_missions(existing: &mut Option<Vec<String>>, incoming: &Option<Vec<String>>) {
890 if incoming.is_none() {
891 *existing = None;
892 return;
893 }
894 if existing.is_none() {
895 return;
896 }
897
898 if let (Some(existing_missions), Some(incoming_missions)) =
899 (existing.as_mut(), incoming.as_ref())
900 {
901 for mission in incoming_missions {
902 if !existing_missions
903 .iter()
904 .any(|existing_mission| existing_mission == mission)
905 {
906 existing_missions.push(mission.clone());
907 }
908 }
909 existing_missions.sort();
910 existing_missions.dedup();
911 }
912}
913
914fn register_nc_output<E>(
915 graph: &mut CuGraph,
916 src_endpoint: &str,
917 msg_type: &str,
918 order: usize,
919 bridge_lookup: &HashMap<&str, &BridgeConfig>,
920) -> Result<(), E>
921where
922 E: From<String>,
923{
924 let (src_name, src_channel) =
925 parse_endpoint(src_endpoint, EndpointRole::Source, bridge_lookup).map_err(E::from)?;
926 if src_channel.is_some() {
927 return Err(E::from(format!(
928 "NC destination '{}' does not support bridge channels in source endpoint '{}'",
929 NC_ENDPOINT, src_endpoint
930 )));
931 }
932
933 let src = graph
934 .get_node_id_by_name(src_name.as_str())
935 .ok_or_else(|| E::from(format!("Source node not found: {src_endpoint}")))?;
936 let src_node = graph
937 .get_node_mut(src)
938 .ok_or_else(|| E::from(format!("Source node id {src} not found for NC output")))?;
939 if src_node.get_flavor() != Flavor::Task {
940 return Err(E::from(format!(
941 "NC destination '{}' is only supported for task outputs (source '{}')",
942 NC_ENDPOINT, src_endpoint
943 )));
944 }
945 src_node.add_nc_output(msg_type, order);
946 Ok(())
947}
948
949#[derive(Debug, Clone, Copy, PartialEq, Eq)]
952pub enum CuDirection {
953 Outgoing,
954 Incoming,
955}
956
957impl From<CuDirection> for petgraph::Direction {
958 fn from(dir: CuDirection) -> Self {
959 match dir {
960 CuDirection::Outgoing => petgraph::Direction::Outgoing,
961 CuDirection::Incoming => petgraph::Direction::Incoming,
962 }
963 }
964}
965
966#[derive(Default, Debug, Clone)]
967pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
968
969impl CuGraph {
970 #[allow(dead_code)]
971 pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
972 self.0
973 .node_indices()
974 .map(|index| (index.index() as u32, &self.0[index]))
975 .collect()
976 }
977
978 #[allow(dead_code)]
979 pub fn get_neighbor_ids(&self, node_id: NodeId, dir: CuDirection) -> Vec<NodeId> {
980 self.0
981 .neighbors_directed(node_id.into(), dir.into())
982 .map(|petgraph_index| petgraph_index.index() as NodeId)
983 .collect()
984 }
985
986 #[allow(dead_code)]
987 pub fn node_ids(&self) -> Vec<NodeId> {
988 self.0
989 .node_indices()
990 .map(|index| index.index() as NodeId)
991 .collect()
992 }
993
994 #[allow(dead_code)]
995 pub fn edge_id_between(&self, source: NodeId, target: NodeId) -> Option<usize> {
996 self.0
997 .find_edge(source.into(), target.into())
998 .map(|edge| edge.index())
999 }
1000
1001 #[allow(dead_code)]
1002 pub fn edge(&self, edge_id: usize) -> Option<&Cnx> {
1003 self.0.edge_weight(EdgeIndex::new(edge_id))
1004 }
1005
1006 #[allow(dead_code)]
1007 pub fn edges(&self) -> impl Iterator<Item = &Cnx> {
1008 self.0
1009 .edge_indices()
1010 .filter_map(|edge| self.0.edge_weight(edge))
1011 }
1012
1013 #[allow(dead_code)]
1014 pub fn bfs_nodes(&self, start: NodeId) -> Vec<NodeId> {
1015 let mut visitor = Bfs::new(&self.0, start.into());
1016 let mut nodes = Vec::new();
1017 while let Some(node) = visitor.next(&self.0) {
1018 nodes.push(node.index() as NodeId);
1019 }
1020 nodes
1021 }
1022
1023 #[allow(dead_code)]
1024 pub fn incoming_neighbor_count(&self, node_id: NodeId) -> usize {
1025 self.0.neighbors_directed(node_id.into(), Incoming).count()
1026 }
1027
1028 #[allow(dead_code)]
1029 pub fn outgoing_neighbor_count(&self, node_id: NodeId) -> usize {
1030 self.0.neighbors_directed(node_id.into(), Outgoing).count()
1031 }
1032
1033 pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
1034 self.0.node_indices().collect()
1035 }
1036
1037 pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
1038 Ok(self.0.add_node(node).index() as NodeId)
1039 }
1040
1041 #[allow(dead_code)]
1042 pub fn connection_exists(&self, source: NodeId, target: NodeId) -> bool {
1043 self.0.find_edge(source.into(), target.into()).is_some()
1044 }
1045
1046 pub fn connect_ext(
1047 &mut self,
1048 source: NodeId,
1049 target: NodeId,
1050 msg_type: &str,
1051 missions: Option<Vec<String>>,
1052 src_channel: Option<String>,
1053 dst_channel: Option<String>,
1054 ) -> CuResult<()> {
1055 self.connect_ext_with_order(
1056 source,
1057 target,
1058 msg_type,
1059 missions,
1060 src_channel,
1061 dst_channel,
1062 usize::MAX,
1063 )
1064 }
1065
1066 #[allow(clippy::too_many_arguments)]
1067 pub fn connect_ext_with_order(
1068 &mut self,
1069 source: NodeId,
1070 target: NodeId,
1071 msg_type: &str,
1072 missions: Option<Vec<String>>,
1073 src_channel: Option<String>,
1074 dst_channel: Option<String>,
1075 order: usize,
1076 ) -> CuResult<()> {
1077 let (src_id, dst_id) = (
1078 self.0
1079 .node_weight(source.into())
1080 .ok_or("Source node not found")?
1081 .id
1082 .clone(),
1083 self.0
1084 .node_weight(target.into())
1085 .ok_or("Target node not found")?
1086 .id
1087 .clone(),
1088 );
1089
1090 let _ = self.0.add_edge(
1091 petgraph::stable_graph::NodeIndex::from(source),
1092 petgraph::stable_graph::NodeIndex::from(target),
1093 Cnx {
1094 src: src_id,
1095 dst: dst_id,
1096 msg: msg_type.to_string(),
1097 missions,
1098 src_channel,
1099 dst_channel,
1100 order,
1101 },
1102 );
1103 Ok(())
1104 }
1105 #[allow(dead_code)]
1109 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
1110 self.0.node_weight(node_id.into())
1111 }
1112
1113 #[allow(dead_code)]
1114 pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
1115 self.0.node_weight(index.into())
1116 }
1117
1118 #[allow(dead_code)]
1119 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
1120 self.0.node_weight_mut(node_id.into())
1121 }
1122
1123 pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
1124 self.0
1125 .node_indices()
1126 .into_iter()
1127 .find(|idx| self.0[*idx].get_id() == name)
1128 .map(|i| i.index() as NodeId)
1129 }
1130
1131 #[allow(dead_code)]
1132 pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
1133 self.0.edge_weight(EdgeIndex::new(index)).cloned()
1134 }
1135
1136 #[allow(dead_code)]
1137 pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
1138 self.0.node_indices().find_map(|node_index| {
1139 if let Some(node) = self.0.node_weight(node_index) {
1140 if node.id != node_id {
1141 return None;
1142 }
1143 let edges: Vec<_> = self
1144 .0
1145 .edges_directed(node_index, Outgoing)
1146 .map(|edge| edge.id().index())
1147 .collect();
1148 if edges.is_empty() {
1149 return None;
1150 }
1151 let cnx = self
1152 .0
1153 .edge_weight(EdgeIndex::new(edges[0]))
1154 .expect("Found an cnx id but could not retrieve it back");
1155 return Some(cnx.msg.clone());
1156 }
1157 None
1158 })
1159 }
1160
1161 #[allow(dead_code)]
1162 pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
1163 self.get_node_input_msg_types(node_id)
1164 .and_then(|mut v| v.pop())
1165 }
1166
1167 pub fn get_node_input_msg_types(&self, node_id: &str) -> Option<Vec<String>> {
1168 self.0.node_indices().find_map(|node_index| {
1169 if let Some(node) = self.0.node_weight(node_index) {
1170 if node.id != node_id {
1171 return None;
1172 }
1173 let edges: Vec<_> = self
1174 .0
1175 .edges_directed(node_index, Incoming)
1176 .map(|edge| edge.id().index())
1177 .collect();
1178 if edges.is_empty() {
1179 return None;
1180 }
1181 let msgs = edges
1182 .into_iter()
1183 .map(|edge_id| {
1184 let cnx = self
1185 .0
1186 .edge_weight(EdgeIndex::new(edge_id))
1187 .expect("Found an cnx id but could not retrieve it back");
1188 cnx.msg.clone()
1189 })
1190 .collect();
1191 return Some(msgs);
1192 }
1193 None
1194 })
1195 }
1196
1197 #[allow(dead_code)]
1198 pub fn get_connection_msg_type(&self, source: NodeId, target: NodeId) -> Option<&str> {
1199 self.0
1200 .find_edge(source.into(), target.into())
1201 .map(|edge_index| self.0[edge_index].msg.as_str())
1202 }
1203
1204 fn get_edges_by_direction(
1206 &self,
1207 node_id: NodeId,
1208 direction: petgraph::Direction,
1209 ) -> CuResult<Vec<usize>> {
1210 Ok(self
1211 .0
1212 .edges_directed(node_id.into(), direction)
1213 .map(|edge| edge.id().index())
1214 .collect())
1215 }
1216
1217 pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
1218 self.get_edges_by_direction(node_id, Outgoing)
1219 }
1220
1221 pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
1223 self.get_edges_by_direction(node_id, Incoming)
1224 }
1225
1226 #[allow(dead_code)]
1227 pub fn node_count(&self) -> usize {
1228 self.0.node_count()
1229 }
1230
1231 #[allow(dead_code)]
1232 pub fn edge_count(&self) -> usize {
1233 self.0.edge_count()
1234 }
1235
1236 #[allow(dead_code)]
1239 pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
1240 self.connect_ext(source, target, msg_type, None, None, None)
1241 }
1242}
1243
1244impl core::ops::Index<NodeIndex> for CuGraph {
1245 type Output = Node;
1246
1247 fn index(&self, index: NodeIndex) -> &Self::Output {
1248 &self.0[index]
1249 }
1250}
1251
1252#[derive(Debug, Clone)]
1253pub enum ConfigGraphs {
1254 Simple(CuGraph),
1255 Missions(HashMap<String, CuGraph>),
1256}
1257
1258impl ConfigGraphs {
1259 #[allow(dead_code)]
1262 pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
1263 match self {
1264 Simple(graph) => HashMap::from([(DEFAULT_MISSION_ID.to_string(), graph.clone())]),
1265 Missions(graphs) => graphs.clone(),
1266 }
1267 }
1268
1269 #[allow(dead_code)]
1270 pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
1271 match self {
1272 Simple(graph) => Ok(graph),
1273 Missions(graphs) => {
1274 if graphs.len() == 1 {
1275 Ok(graphs.values().next().unwrap())
1276 } else {
1277 Err("Cannot get default mission graph from mission config".into())
1278 }
1279 }
1280 }
1281 }
1282
1283 #[allow(dead_code)]
1284 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1285 match self {
1286 Simple(graph) => match mission_id {
1287 None | Some(DEFAULT_MISSION_ID) => Ok(graph),
1288 Some(_) => Err("Cannot get mission graph from simple config".into()),
1289 },
1290 Missions(graphs) => {
1291 let id = mission_id
1292 .ok_or_else(|| "Mission ID required for mission configs".to_string())?;
1293 graphs
1294 .get(id)
1295 .ok_or_else(|| format!("Mission {id} not found").into())
1296 }
1297 }
1298 }
1299
1300 #[allow(dead_code)]
1301 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1302 match self {
1303 Simple(graph) => match mission_id {
1304 None => Ok(graph),
1305 Some(_) => Err("Cannot get mission graph from simple config".into()),
1306 },
1307 Missions(graphs) => {
1308 let id = mission_id
1309 .ok_or_else(|| "Mission ID required for mission configs".to_string())?;
1310 graphs
1311 .get_mut(id)
1312 .ok_or_else(|| format!("Mission {id} not found").into())
1313 }
1314 }
1315 }
1316
1317 pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
1318 match self {
1319 Simple(_) => Err("Cannot add mission to simple config".into()),
1320 Missions(graphs) => match graphs.entry(mission_id.to_string()) {
1321 hashbrown::hash_map::Entry::Occupied(_) => {
1322 Err(format!("Mission {mission_id} already exists").into())
1323 }
1324 hashbrown::hash_map::Entry::Vacant(entry) => Ok(entry.insert(CuGraph::default())),
1325 },
1326 }
1327 }
1328}
1329
1330#[derive(Debug, Clone)]
1336pub struct CuConfig {
1337 pub monitors: Vec<MonitorConfig>,
1339 pub logging: Option<LoggingConfig>,
1341 pub runtime: Option<RuntimeConfig>,
1343 pub resources: Vec<ResourceBundleConfig>,
1345 pub bridges: Vec<BridgeConfig>,
1347 pub graphs: ConfigGraphs,
1349}
1350
1351impl CuConfig {
1352 #[cfg(feature = "std")]
1353 fn ensure_threadpool_bundle(&mut self) {
1354 if !self.has_background_tasks() {
1355 return;
1356 }
1357 if self
1358 .resources
1359 .iter()
1360 .any(|bundle| bundle.id == "threadpool")
1361 {
1362 return;
1363 }
1364
1365 let mut config = ComponentConfig::default();
1366 config.set("threads", 2u64);
1367 self.resources.push(ResourceBundleConfig {
1368 id: "threadpool".to_string(),
1369 provider: "cu29::resource::ThreadPoolBundle".to_string(),
1370 config: Some(config),
1371 missions: None,
1372 });
1373 }
1374
1375 #[cfg(feature = "std")]
1376 fn has_background_tasks(&self) -> bool {
1377 match &self.graphs {
1378 ConfigGraphs::Simple(graph) => graph
1379 .get_all_nodes()
1380 .iter()
1381 .any(|(_, node)| node.is_background()),
1382 ConfigGraphs::Missions(graphs) => graphs.values().any(|graph| {
1383 graph
1384 .get_all_nodes()
1385 .iter()
1386 .any(|(_, node)| node.is_background())
1387 }),
1388 }
1389 }
1390}
1391
1392#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1393pub struct MonitorConfig {
1394 #[serde(rename = "type")]
1395 type_: String,
1396 #[serde(skip_serializing_if = "Option::is_none")]
1397 config: Option<ComponentConfig>,
1398}
1399
1400impl MonitorConfig {
1401 #[allow(dead_code)]
1402 pub fn get_type(&self) -> &str {
1403 &self.type_
1404 }
1405
1406 #[allow(dead_code)]
1407 pub fn get_config(&self) -> Option<&ComponentConfig> {
1408 self.config.as_ref()
1409 }
1410}
1411
1412fn default_as_true() -> bool {
1413 true
1414}
1415
1416pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
1417
1418fn default_keyframe_interval() -> Option<u32> {
1419 Some(DEFAULT_KEYFRAME_INTERVAL)
1420}
1421
1422#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1423pub struct LoggingConfig {
1424 #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
1426 pub enable_task_logging: bool,
1427
1428 #[serde(skip_serializing_if = "Option::is_none")]
1433 pub copperlist_count: Option<usize>,
1434
1435 #[serde(skip_serializing_if = "Option::is_none")]
1437 pub slab_size_mib: Option<u64>,
1438
1439 #[serde(skip_serializing_if = "Option::is_none")]
1441 pub section_size_mib: Option<u64>,
1442
1443 #[serde(
1445 default = "default_keyframe_interval",
1446 skip_serializing_if = "Option::is_none"
1447 )]
1448 pub keyframe_interval: Option<u32>,
1449}
1450
1451#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1452pub struct RuntimeConfig {
1453 #[serde(skip_serializing_if = "Option::is_none")]
1459 pub rate_target_hz: Option<u64>,
1460}
1461
1462pub const MAX_RATE_TARGET_HZ: u64 = 1_000_000_000;
1467
1468#[derive(Serialize, Deserialize, Debug, Clone)]
1470pub struct MissionsConfig {
1471 pub id: String,
1472}
1473
1474#[derive(Serialize, Deserialize, Debug, Clone)]
1476pub struct IncludesConfig {
1477 pub path: String,
1478 pub params: HashMap<String, Value>,
1479 pub missions: Option<Vec<String>>,
1480}
1481
1482#[cfg(feature = "std")]
1484#[allow(dead_code)]
1485#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1486pub struct MultiCopperSubsystemConfig {
1487 pub id: String,
1488 pub config: String,
1489}
1490
1491#[cfg(feature = "std")]
1493#[allow(dead_code)]
1494#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1495pub struct MultiCopperInterconnectConfig {
1496 pub from: String,
1497 pub to: String,
1498 pub msg: String,
1499}
1500
1501#[cfg(feature = "std")]
1503#[allow(dead_code)]
1504#[derive(Serialize, Deserialize, Debug, Clone)]
1505pub struct InstanceConfigSetOperation {
1506 pub path: String,
1507 pub value: ComponentConfig,
1508}
1509
1510#[cfg(feature = "std")]
1512#[allow(dead_code)]
1513#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1514pub struct MultiCopperEndpoint {
1515 pub subsystem_id: String,
1516 pub bridge_id: String,
1517 pub channel_id: String,
1518}
1519
1520#[cfg(feature = "std")]
1521impl Display for MultiCopperEndpoint {
1522 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1523 write!(
1524 f,
1525 "{}/{}/{}",
1526 self.subsystem_id, self.bridge_id, self.channel_id
1527 )
1528 }
1529}
1530
1531#[cfg(feature = "std")]
1533#[allow(dead_code)]
1534#[derive(Debug, Clone)]
1535pub struct MultiCopperSubsystem {
1536 pub id: String,
1537 pub subsystem_code: u16,
1538 pub config_path: String,
1539 pub config: CuConfig,
1540}
1541
1542#[cfg(feature = "std")]
1544#[allow(dead_code)]
1545#[derive(Debug, Clone, PartialEq, Eq)]
1546pub struct MultiCopperInterconnect {
1547 pub from: MultiCopperEndpoint,
1548 pub to: MultiCopperEndpoint,
1549 pub msg: String,
1550 pub bridge_type: String,
1551}
1552
1553#[cfg(feature = "std")]
1555#[allow(dead_code)]
1556#[derive(Debug, Clone)]
1557pub struct MultiCopperConfig {
1558 pub subsystems: Vec<MultiCopperSubsystem>,
1559 pub interconnects: Vec<MultiCopperInterconnect>,
1560 pub instance_overrides_root: Option<String>,
1561}
1562
1563#[cfg(feature = "std")]
1564impl MultiCopperConfig {
1565 #[allow(dead_code)]
1566 pub fn subsystem(&self, id: &str) -> Option<&MultiCopperSubsystem> {
1567 self.subsystems.iter().find(|subsystem| subsystem.id == id)
1568 }
1569
1570 #[allow(dead_code)]
1571 pub fn resolve_subsystem_config_for_instance(
1572 &self,
1573 subsystem_id: &str,
1574 instance_id: u32,
1575 ) -> CuResult<CuConfig> {
1576 let subsystem = self.subsystem(subsystem_id).ok_or_else(|| {
1577 CuError::from(format!(
1578 "Multi-Copper config does not define subsystem '{}'.",
1579 subsystem_id
1580 ))
1581 })?;
1582 let mut config = subsystem.config.clone();
1583
1584 let Some(root) = &self.instance_overrides_root else {
1585 return Ok(config);
1586 };
1587
1588 let override_path = std::path::Path::new(root)
1589 .join(instance_id.to_string())
1590 .join(format!("{subsystem_id}.ron"));
1591 if !override_path.exists() {
1592 return Ok(config);
1593 }
1594
1595 apply_instance_overrides_from_file(&mut config, &override_path)?;
1596 Ok(config)
1597 }
1598}
1599
1600#[cfg(feature = "std")]
1601#[allow(dead_code)]
1602#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1603struct MultiCopperConfigRepresentation {
1604 subsystems: Vec<MultiCopperSubsystemConfig>,
1605 interconnects: Vec<MultiCopperInterconnectConfig>,
1606 instance_overrides_root: Option<String>,
1607}
1608
1609#[cfg(feature = "std")]
1610#[derive(Serialize, Deserialize, Debug, Clone, Default)]
1611struct InstanceConfigOverridesRepresentation {
1612 #[serde(default)]
1613 set: Vec<InstanceConfigSetOperation>,
1614}
1615
1616#[cfg(feature = "std")]
1617#[allow(dead_code)]
1618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1619enum MultiCopperChannelDirection {
1620 Rx,
1621 Tx,
1622}
1623
1624#[cfg(feature = "std")]
1625#[allow(dead_code)]
1626#[derive(Debug, Clone)]
1627struct MultiCopperChannelContract {
1628 bridge_type: String,
1629 direction: MultiCopperChannelDirection,
1630 msg: Option<String>,
1631}
1632
1633#[cfg(feature = "std")]
1634#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1635enum InstanceConfigTargetKind {
1636 Task,
1637 Resource,
1638 Bridge,
1639}
1640
1641#[derive(Serialize, Deserialize, Default)]
1643struct CuConfigRepresentation {
1644 tasks: Option<Vec<Node>>,
1645 resources: Option<Vec<ResourceBundleConfig>>,
1646 bridges: Option<Vec<BridgeConfig>>,
1647 cnx: Option<Vec<SerializedCnx>>,
1648 #[serde(
1649 default,
1650 alias = "monitor",
1651 deserialize_with = "deserialize_monitor_configs"
1652 )]
1653 monitors: Option<Vec<MonitorConfig>>,
1654 logging: Option<LoggingConfig>,
1655 runtime: Option<RuntimeConfig>,
1656 missions: Option<Vec<MissionsConfig>>,
1657 includes: Option<Vec<IncludesConfig>>,
1658}
1659
1660#[derive(Deserialize)]
1661#[serde(untagged)]
1662enum OneOrManyMonitorConfig {
1663 One(MonitorConfig),
1664 Many(Vec<MonitorConfig>),
1665}
1666
1667fn deserialize_monitor_configs<'de, D>(
1668 deserializer: D,
1669) -> Result<Option<Vec<MonitorConfig>>, D::Error>
1670where
1671 D: Deserializer<'de>,
1672{
1673 let parsed = Option::<OneOrManyMonitorConfig>::deserialize(deserializer)?;
1674 Ok(parsed.map(|value| match value {
1675 OneOrManyMonitorConfig::One(single) => vec![single],
1676 OneOrManyMonitorConfig::Many(many) => many,
1677 }))
1678}
1679
1680fn deserialize_config_representation<E>(
1682 representation: &CuConfigRepresentation,
1683) -> Result<CuConfig, E>
1684where
1685 E: From<String>,
1686{
1687 let mut cuconfig = CuConfig::default();
1688 let bridge_lookup = build_bridge_lookup(representation.bridges.as_ref());
1689
1690 if let Some(mission_configs) = &representation.missions {
1691 let mut missions = Missions(HashMap::new());
1693
1694 for mission_config in mission_configs {
1695 let mission_id = mission_config.id.as_str();
1696 let graph = missions
1697 .add_mission(mission_id)
1698 .map_err(|e| E::from(e.to_string()))?;
1699
1700 if let Some(tasks) = &representation.tasks {
1701 for task in tasks {
1702 if let Some(task_missions) = &task.missions {
1703 if task_missions.contains(&mission_id.to_owned()) {
1705 graph
1706 .add_node(task.clone())
1707 .map_err(|e| E::from(e.to_string()))?;
1708 }
1709 } else {
1710 graph
1712 .add_node(task.clone())
1713 .map_err(|e| E::from(e.to_string()))?;
1714 }
1715 }
1716 }
1717
1718 if let Some(bridges) = &representation.bridges {
1719 for bridge in bridges {
1720 if mission_applies(&bridge.missions, mission_id) {
1721 insert_bridge_node(graph, bridge).map_err(E::from)?;
1722 }
1723 }
1724 }
1725
1726 if let Some(cnx) = &representation.cnx {
1727 for (connection_order, c) in cnx.iter().enumerate() {
1728 if let Some(cnx_missions) = &c.missions {
1729 if cnx_missions.contains(&mission_id.to_owned()) {
1731 if c.dst == NC_ENDPOINT {
1732 register_nc_output::<E>(
1733 graph,
1734 &c.src,
1735 &c.msg,
1736 connection_order,
1737 &bridge_lookup,
1738 )?;
1739 continue;
1740 }
1741 let (src_name, src_channel) =
1742 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1743 .map_err(E::from)?;
1744 let (dst_name, dst_channel) =
1745 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1746 .map_err(E::from)?;
1747 let src =
1748 graph
1749 .get_node_id_by_name(src_name.as_str())
1750 .ok_or_else(|| {
1751 E::from(format!("Source node not found: {}", c.src))
1752 })?;
1753 let dst =
1754 graph
1755 .get_node_id_by_name(dst_name.as_str())
1756 .ok_or_else(|| {
1757 E::from(format!("Destination node not found: {}", c.dst))
1758 })?;
1759 graph
1760 .connect_ext_with_order(
1761 src,
1762 dst,
1763 &c.msg,
1764 Some(cnx_missions.clone()),
1765 src_channel,
1766 dst_channel,
1767 connection_order,
1768 )
1769 .map_err(|e| E::from(e.to_string()))?;
1770 }
1771 } else {
1772 if c.dst == NC_ENDPOINT {
1774 register_nc_output::<E>(
1775 graph,
1776 &c.src,
1777 &c.msg,
1778 connection_order,
1779 &bridge_lookup,
1780 )?;
1781 continue;
1782 }
1783 let (src_name, src_channel) =
1784 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1785 .map_err(E::from)?;
1786 let (dst_name, dst_channel) =
1787 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1788 .map_err(E::from)?;
1789 let src = graph
1790 .get_node_id_by_name(src_name.as_str())
1791 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1792 let dst =
1793 graph
1794 .get_node_id_by_name(dst_name.as_str())
1795 .ok_or_else(|| {
1796 E::from(format!("Destination node not found: {}", c.dst))
1797 })?;
1798 graph
1799 .connect_ext_with_order(
1800 src,
1801 dst,
1802 &c.msg,
1803 None,
1804 src_channel,
1805 dst_channel,
1806 connection_order,
1807 )
1808 .map_err(|e| E::from(e.to_string()))?;
1809 }
1810 }
1811 }
1812 }
1813 cuconfig.graphs = missions;
1814 } else {
1815 let mut graph = CuGraph::default();
1817
1818 if let Some(tasks) = &representation.tasks {
1819 for task in tasks {
1820 graph
1821 .add_node(task.clone())
1822 .map_err(|e| E::from(e.to_string()))?;
1823 }
1824 }
1825
1826 if let Some(bridges) = &representation.bridges {
1827 for bridge in bridges {
1828 insert_bridge_node(&mut graph, bridge).map_err(E::from)?;
1829 }
1830 }
1831
1832 if let Some(cnx) = &representation.cnx {
1833 for (connection_order, c) in cnx.iter().enumerate() {
1834 if c.dst == NC_ENDPOINT {
1835 register_nc_output::<E>(
1836 &mut graph,
1837 &c.src,
1838 &c.msg,
1839 connection_order,
1840 &bridge_lookup,
1841 )?;
1842 continue;
1843 }
1844 let (src_name, src_channel) =
1845 parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1846 .map_err(E::from)?;
1847 let (dst_name, dst_channel) =
1848 parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1849 .map_err(E::from)?;
1850 let src = graph
1851 .get_node_id_by_name(src_name.as_str())
1852 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1853 let dst = graph
1854 .get_node_id_by_name(dst_name.as_str())
1855 .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
1856 graph
1857 .connect_ext_with_order(
1858 src,
1859 dst,
1860 &c.msg,
1861 None,
1862 src_channel,
1863 dst_channel,
1864 connection_order,
1865 )
1866 .map_err(|e| E::from(e.to_string()))?;
1867 }
1868 }
1869 cuconfig.graphs = Simple(graph);
1870 }
1871
1872 cuconfig.monitors = representation.monitors.clone().unwrap_or_default();
1873 cuconfig.logging = representation.logging.clone();
1874 cuconfig.runtime = representation.runtime.clone();
1875 cuconfig.resources = representation.resources.clone().unwrap_or_default();
1876 cuconfig.bridges = representation.bridges.clone().unwrap_or_default();
1877
1878 Ok(cuconfig)
1879}
1880
1881impl<'de> Deserialize<'de> for CuConfig {
1882 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1884 where
1885 D: Deserializer<'de>,
1886 {
1887 let representation =
1888 CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
1889
1890 match deserialize_config_representation::<String>(&representation) {
1892 Ok(config) => Ok(config),
1893 Err(e) => Err(serde::de::Error::custom(e)),
1894 }
1895 }
1896}
1897
1898impl Serialize for CuConfig {
1899 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1901 where
1902 S: Serializer,
1903 {
1904 let bridges = if self.bridges.is_empty() {
1905 None
1906 } else {
1907 Some(self.bridges.clone())
1908 };
1909 let resources = if self.resources.is_empty() {
1910 None
1911 } else {
1912 Some(self.resources.clone())
1913 };
1914 let monitors = (!self.monitors.is_empty()).then_some(self.monitors.clone());
1915 match &self.graphs {
1916 Simple(graph) => {
1917 let tasks: Vec<Node> = graph
1918 .0
1919 .node_indices()
1920 .map(|idx| graph.0[idx].clone())
1921 .filter(|node| node.get_flavor() == Flavor::Task)
1922 .collect();
1923
1924 let mut ordered_cnx: Vec<(usize, SerializedCnx)> = graph
1925 .0
1926 .edge_indices()
1927 .map(|edge_idx| {
1928 let edge = &graph.0[edge_idx];
1929 let order = if edge.order == usize::MAX {
1930 edge_idx.index()
1931 } else {
1932 edge.order
1933 };
1934 (order, SerializedCnx::from(edge))
1935 })
1936 .collect();
1937 for node_idx in graph.0.node_indices() {
1938 let node = &graph.0[node_idx];
1939 if node.get_flavor() != Flavor::Task {
1940 continue;
1941 }
1942 for (msg, order) in node.nc_outputs_with_order() {
1943 ordered_cnx.push((
1944 order,
1945 SerializedCnx {
1946 src: node.get_id(),
1947 dst: NC_ENDPOINT.to_string(),
1948 msg: msg.clone(),
1949 missions: None,
1950 },
1951 ));
1952 }
1953 }
1954 ordered_cnx.sort_by(|(order_a, cnx_a), (order_b, cnx_b)| {
1955 order_a
1956 .cmp(order_b)
1957 .then_with(|| cnx_a.src.cmp(&cnx_b.src))
1958 .then_with(|| cnx_a.dst.cmp(&cnx_b.dst))
1959 .then_with(|| cnx_a.msg.cmp(&cnx_b.msg))
1960 });
1961 let cnx: Vec<SerializedCnx> = ordered_cnx
1962 .into_iter()
1963 .map(|(_, serialized)| serialized)
1964 .collect();
1965
1966 CuConfigRepresentation {
1967 tasks: Some(tasks),
1968 bridges: bridges.clone(),
1969 cnx: Some(cnx),
1970 monitors: monitors.clone(),
1971 logging: self.logging.clone(),
1972 runtime: self.runtime.clone(),
1973 resources: resources.clone(),
1974 missions: None,
1975 includes: None,
1976 }
1977 .serialize(serializer)
1978 }
1979 Missions(graphs) => {
1980 let missions = graphs
1981 .keys()
1982 .map(|id| MissionsConfig { id: id.clone() })
1983 .collect();
1984
1985 let mut tasks = Vec::new();
1987 let mut ordered_cnx: Vec<(usize, SerializedCnx)> = Vec::new();
1988
1989 for (mission_id, graph) in graphs {
1990 for node_idx in graph.node_indices() {
1992 let node = &graph[node_idx];
1993 if node.get_flavor() == Flavor::Task
1994 && !tasks.iter().any(|n: &Node| n.id == node.id)
1995 {
1996 tasks.push(node.clone());
1997 }
1998 }
1999
2000 for edge_idx in graph.0.edge_indices() {
2002 let edge = &graph.0[edge_idx];
2003 let order = if edge.order == usize::MAX {
2004 edge_idx.index()
2005 } else {
2006 edge.order
2007 };
2008 let serialized = SerializedCnx::from(edge);
2009 if let Some((existing_order, existing_serialized)) =
2010 ordered_cnx.iter_mut().find(|(_, c)| {
2011 c.src == serialized.src
2012 && c.dst == serialized.dst
2013 && c.msg == serialized.msg
2014 })
2015 {
2016 if order < *existing_order {
2017 *existing_order = order;
2018 }
2019 merge_connection_missions(
2020 &mut existing_serialized.missions,
2021 &serialized.missions,
2022 );
2023 } else {
2024 ordered_cnx.push((order, serialized));
2025 }
2026 }
2027 for node_idx in graph.0.node_indices() {
2028 let node = &graph.0[node_idx];
2029 if node.get_flavor() != Flavor::Task {
2030 continue;
2031 }
2032 for (msg, order) in node.nc_outputs_with_order() {
2033 let serialized = SerializedCnx {
2034 src: node.get_id(),
2035 dst: NC_ENDPOINT.to_string(),
2036 msg: msg.clone(),
2037 missions: Some(vec![mission_id.clone()]),
2038 };
2039 if let Some((existing_order, existing_serialized)) =
2040 ordered_cnx.iter_mut().find(|(_, c)| {
2041 c.src == serialized.src
2042 && c.dst == serialized.dst
2043 && c.msg == serialized.msg
2044 })
2045 {
2046 if order < *existing_order {
2047 *existing_order = order;
2048 }
2049 merge_connection_missions(
2050 &mut existing_serialized.missions,
2051 &serialized.missions,
2052 );
2053 } else {
2054 ordered_cnx.push((order, serialized));
2055 }
2056 }
2057 }
2058 }
2059 ordered_cnx.sort_by(|(order_a, cnx_a), (order_b, cnx_b)| {
2060 order_a
2061 .cmp(order_b)
2062 .then_with(|| cnx_a.src.cmp(&cnx_b.src))
2063 .then_with(|| cnx_a.dst.cmp(&cnx_b.dst))
2064 .then_with(|| cnx_a.msg.cmp(&cnx_b.msg))
2065 });
2066 let cnx: Vec<SerializedCnx> = ordered_cnx
2067 .into_iter()
2068 .map(|(_, serialized)| serialized)
2069 .collect();
2070
2071 CuConfigRepresentation {
2072 tasks: Some(tasks),
2073 resources: resources.clone(),
2074 bridges,
2075 cnx: Some(cnx),
2076 monitors,
2077 logging: self.logging.clone(),
2078 runtime: self.runtime.clone(),
2079 missions: Some(missions),
2080 includes: None,
2081 }
2082 .serialize(serializer)
2083 }
2084 }
2085 }
2086}
2087
2088impl Default for CuConfig {
2089 fn default() -> Self {
2090 CuConfig {
2091 graphs: Simple(CuGraph(StableDiGraph::new())),
2092 monitors: Vec::new(),
2093 logging: None,
2094 runtime: None,
2095 resources: Vec::new(),
2096 bridges: Vec::new(),
2097 }
2098 }
2099}
2100
2101impl CuConfig {
2104 #[allow(dead_code)]
2105 pub fn new_simple_type() -> Self {
2106 Self::default()
2107 }
2108
2109 #[allow(dead_code)]
2110 pub fn new_mission_type() -> Self {
2111 CuConfig {
2112 graphs: Missions(HashMap::new()),
2113 monitors: Vec::new(),
2114 logging: None,
2115 runtime: None,
2116 resources: Vec::new(),
2117 bridges: Vec::new(),
2118 }
2119 }
2120
2121 fn get_options() -> Options {
2122 Options::default()
2123 .with_default_extension(Extensions::IMPLICIT_SOME)
2124 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2125 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2126 }
2127
2128 #[allow(dead_code)]
2129 pub fn serialize_ron(&self) -> CuResult<String> {
2130 let ron = Self::get_options();
2131 let pretty = ron::ser::PrettyConfig::default();
2132 ron.to_string_pretty(&self, pretty)
2133 .map_err(|e| CuError::from(format!("Error serializing configuration: {e}")))
2134 }
2135
2136 #[allow(dead_code)]
2137 pub fn deserialize_ron(ron: &str) -> CuResult<Self> {
2138 let representation = Self::get_options().from_str(ron).map_err(|e| {
2139 CuError::from(format!(
2140 "Syntax Error in config: {} at position {}",
2141 e.code, e.span
2142 ))
2143 })?;
2144 Self::deserialize_impl(representation)
2145 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))
2146 }
2147
2148 fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
2149 deserialize_config_representation(&representation)
2150 }
2151
2152 #[cfg(feature = "std")]
2154 #[allow(dead_code)]
2155 pub fn render(
2156 &self,
2157 output: &mut dyn std::io::Write,
2158 mission_id: Option<&str>,
2159 ) -> CuResult<()> {
2160 writeln!(output, "digraph G {{")
2161 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2162 writeln!(output, " graph [rankdir=LR, nodesep=0.8, ranksep=1.2];")
2163 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2164 writeln!(output, " node [shape=plain, fontname=\"Noto Sans\"];")
2165 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2166 writeln!(output, " edge [fontname=\"Noto Sans\"];")
2167 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2168
2169 let sections = match (&self.graphs, mission_id) {
2170 (Simple(graph), _) => vec![RenderSection { label: None, graph }],
2171 (Missions(graphs), Some(id)) => {
2172 let graph = graphs
2173 .get(id)
2174 .ok_or_else(|| CuError::from(format!("Mission {id} not found")))?;
2175 vec![RenderSection {
2176 label: Some(id.to_string()),
2177 graph,
2178 }]
2179 }
2180 (Missions(graphs), None) => {
2181 let mut missions: Vec<_> = graphs.iter().collect();
2182 missions.sort_by(|a, b| a.0.cmp(b.0));
2183 missions
2184 .into_iter()
2185 .map(|(label, graph)| RenderSection {
2186 label: Some(label.clone()),
2187 graph,
2188 })
2189 .collect()
2190 }
2191 };
2192
2193 for section in sections {
2194 self.render_section(output, section.graph, section.label.as_deref())?;
2195 }
2196
2197 writeln!(output, "}}")
2198 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2199 Ok(())
2200 }
2201
2202 #[allow(dead_code)]
2203 pub fn get_all_instances_configs(
2204 &self,
2205 mission_id: Option<&str>,
2206 ) -> Vec<Option<&ComponentConfig>> {
2207 let graph = self.graphs.get_graph(mission_id).unwrap();
2208 graph
2209 .get_all_nodes()
2210 .iter()
2211 .map(|(_, node)| node.get_instance_config())
2212 .collect()
2213 }
2214
2215 #[allow(dead_code)]
2216 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
2217 self.graphs.get_graph(mission_id)
2218 }
2219
2220 #[allow(dead_code)]
2221 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
2222 self.graphs.get_graph_mut(mission_id)
2223 }
2224
2225 #[allow(dead_code)]
2226 pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
2227 self.monitors.first()
2228 }
2229
2230 #[allow(dead_code)]
2231 pub fn get_monitor_configs(&self) -> &[MonitorConfig] {
2232 &self.monitors
2233 }
2234
2235 #[allow(dead_code)]
2236 pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
2237 self.runtime.as_ref()
2238 }
2239
2240 pub fn validate_logging_config(&self) -> CuResult<()> {
2243 if let Some(logging) = &self.logging {
2244 return logging.validate();
2245 }
2246 Ok(())
2247 }
2248
2249 pub fn validate_runtime_config(&self) -> CuResult<()> {
2251 if let Some(runtime) = &self.runtime {
2252 return runtime.validate();
2253 }
2254 Ok(())
2255 }
2256}
2257
2258#[cfg(feature = "std")]
2259#[derive(Default)]
2260pub(crate) struct PortLookup {
2261 pub inputs: HashMap<String, String>,
2262 pub outputs: HashMap<String, String>,
2263 pub default_input: Option<String>,
2264 pub default_output: Option<String>,
2265}
2266
2267#[cfg(feature = "std")]
2268#[derive(Clone)]
2269pub(crate) struct RenderNode {
2270 pub id: String,
2271 pub type_name: String,
2272 pub flavor: Flavor,
2273 pub inputs: Vec<String>,
2274 pub outputs: Vec<String>,
2275}
2276
2277#[cfg(feature = "std")]
2278#[derive(Clone)]
2279pub(crate) struct RenderConnection {
2280 pub src: String,
2281 pub src_port: Option<String>,
2282 #[allow(dead_code)]
2283 pub src_channel: Option<String>,
2284 pub dst: String,
2285 pub dst_port: Option<String>,
2286 #[allow(dead_code)]
2287 pub dst_channel: Option<String>,
2288 pub msg: String,
2289}
2290
2291#[cfg(feature = "std")]
2292pub(crate) struct RenderTopology {
2293 pub nodes: Vec<RenderNode>,
2294 pub connections: Vec<RenderConnection>,
2295}
2296
2297#[cfg(feature = "std")]
2298impl RenderTopology {
2299 pub fn sort_connections(&mut self) {
2300 self.connections.sort_by(|a, b| {
2301 a.src
2302 .cmp(&b.src)
2303 .then(a.dst.cmp(&b.dst))
2304 .then(a.msg.cmp(&b.msg))
2305 });
2306 }
2307}
2308
2309#[cfg(feature = "std")]
2310#[allow(dead_code)]
2311struct RenderSection<'a> {
2312 label: Option<String>,
2313 graph: &'a CuGraph,
2314}
2315
2316#[cfg(feature = "std")]
2317impl CuConfig {
2318 #[allow(dead_code)]
2319 fn render_section(
2320 &self,
2321 output: &mut dyn std::io::Write,
2322 graph: &CuGraph,
2323 label: Option<&str>,
2324 ) -> CuResult<()> {
2325 use std::fmt::Write as FmtWrite;
2326
2327 let mut topology = build_render_topology(graph, &self.bridges);
2328 topology.nodes.sort_by(|a, b| a.id.cmp(&b.id));
2329 topology.sort_connections();
2330
2331 let cluster_id = label.map(|lbl| format!("cluster_{}", sanitize_identifier(lbl)));
2332 if let Some(ref cluster_id) = cluster_id {
2333 writeln!(output, " subgraph \"{cluster_id}\" {{")
2334 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2335 writeln!(
2336 output,
2337 " label=<<B>Mission: {}</B>>;",
2338 encode_text(label.unwrap())
2339 )
2340 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2341 writeln!(
2342 output,
2343 " labelloc=t; labeljust=l; color=\"#bbbbbb\"; style=\"rounded\"; margin=20;"
2344 )
2345 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2346 }
2347 let indent = if cluster_id.is_some() {
2348 " "
2349 } else {
2350 " "
2351 };
2352 let node_prefix = label
2353 .map(|lbl| format!("{}__", sanitize_identifier(lbl)))
2354 .unwrap_or_default();
2355
2356 let mut port_lookup: HashMap<String, PortLookup> = HashMap::new();
2357 let mut id_lookup: HashMap<String, String> = HashMap::new();
2358
2359 for node in &topology.nodes {
2360 let node_idx = graph
2361 .get_node_id_by_name(node.id.as_str())
2362 .ok_or_else(|| CuError::from(format!("Node '{}' missing from graph", node.id)))?;
2363 let node_weight = graph
2364 .get_node(node_idx)
2365 .ok_or_else(|| CuError::from(format!("Node '{}' missing weight", node.id)))?;
2366
2367 let is_src = graph.get_dst_edges(node_idx).unwrap_or_default().is_empty();
2368 let is_sink = graph.get_src_edges(node_idx).unwrap_or_default().is_empty();
2369
2370 let fillcolor = match node.flavor {
2371 Flavor::Bridge => "#faedcd",
2372 Flavor::Task if is_src => "#ddefc7",
2373 Flavor::Task if is_sink => "#cce0ff",
2374 _ => "#f2f2f2",
2375 };
2376
2377 let port_base = format!("{}{}", node_prefix, sanitize_identifier(&node.id));
2378 let (inputs_table, input_map, default_input) =
2379 build_port_table("Inputs", &node.inputs, &port_base, "in");
2380 let (outputs_table, output_map, default_output) =
2381 build_port_table("Outputs", &node.outputs, &port_base, "out");
2382 let config_html = node_weight.config.as_ref().and_then(build_config_table);
2383
2384 let mut label_html = String::new();
2385 write!(
2386 label_html,
2387 "<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"6\" COLOR=\"gray\" BGCOLOR=\"white\">"
2388 )
2389 .unwrap();
2390 write!(
2391 label_html,
2392 "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\" BGCOLOR=\"{fillcolor}\"><FONT POINT-SIZE=\"12\"><B>{}</B></FONT><BR/><FONT COLOR=\"dimgray\">[{}]</FONT></TD></TR>",
2393 encode_text(&node.id),
2394 encode_text(&node.type_name)
2395 )
2396 .unwrap();
2397 write!(
2398 label_html,
2399 "<TR><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{inputs_table}</TD><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{outputs_table}</TD></TR>"
2400 )
2401 .unwrap();
2402
2403 if let Some(config_html) = config_html {
2404 write!(
2405 label_html,
2406 "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\">{config_html}</TD></TR>"
2407 )
2408 .unwrap();
2409 }
2410
2411 label_html.push_str("</TABLE>");
2412
2413 let identifier_raw = if node_prefix.is_empty() {
2414 node.id.clone()
2415 } else {
2416 format!("{node_prefix}{}", node.id)
2417 };
2418 let identifier = escape_dot_id(&identifier_raw);
2419 writeln!(output, "{indent}\"{identifier}\" [label=<{label_html}>];")
2420 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2421
2422 id_lookup.insert(node.id.clone(), identifier);
2423 port_lookup.insert(
2424 node.id.clone(),
2425 PortLookup {
2426 inputs: input_map,
2427 outputs: output_map,
2428 default_input,
2429 default_output,
2430 },
2431 );
2432 }
2433
2434 for cnx in &topology.connections {
2435 let src_id = id_lookup
2436 .get(&cnx.src)
2437 .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.src)))?;
2438 let dst_id = id_lookup
2439 .get(&cnx.dst)
2440 .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.dst)))?;
2441 let src_suffix = port_lookup
2442 .get(&cnx.src)
2443 .and_then(|lookup| lookup.resolve_output(cnx.src_port.as_deref()))
2444 .map(|port| format!(":\"{port}\":e"))
2445 .unwrap_or_default();
2446 let dst_suffix = port_lookup
2447 .get(&cnx.dst)
2448 .and_then(|lookup| lookup.resolve_input(cnx.dst_port.as_deref()))
2449 .map(|port| format!(":\"{port}\":w"))
2450 .unwrap_or_default();
2451 let msg = encode_text(&cnx.msg);
2452 writeln!(
2453 output,
2454 "{indent}\"{src_id}\"{src_suffix} -> \"{dst_id}\"{dst_suffix} [label=< <B><FONT COLOR=\"gray\">{msg}</FONT></B> >];"
2455 )
2456 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2457 }
2458
2459 if cluster_id.is_some() {
2460 writeln!(output, " }}")
2461 .map_err(|e| CuError::new_with_cause("Failed to write render output", e))?;
2462 }
2463
2464 Ok(())
2465 }
2466}
2467
2468#[cfg(feature = "std")]
2469pub(crate) fn build_render_topology(graph: &CuGraph, bridges: &[BridgeConfig]) -> RenderTopology {
2470 let mut bridge_lookup = HashMap::new();
2471 for bridge in bridges {
2472 bridge_lookup.insert(bridge.id.as_str(), bridge);
2473 }
2474
2475 let mut nodes: Vec<RenderNode> = Vec::new();
2476 let mut node_lookup: HashMap<String, usize> = HashMap::new();
2477 for (_, node) in graph.get_all_nodes() {
2478 let node_id = node.get_id();
2479 let mut inputs = Vec::new();
2480 let mut outputs = Vec::new();
2481 if node.get_flavor() == Flavor::Bridge
2482 && let Some(bridge) = bridge_lookup.get(node_id.as_str())
2483 {
2484 for channel in &bridge.channels {
2485 match channel {
2486 BridgeChannelConfigRepresentation::Rx { id, .. } => outputs.push(id.clone()),
2488 BridgeChannelConfigRepresentation::Tx { id, .. } => inputs.push(id.clone()),
2490 }
2491 }
2492 }
2493
2494 node_lookup.insert(node_id.clone(), nodes.len());
2495 nodes.push(RenderNode {
2496 id: node_id,
2497 type_name: node.get_type().to_string(),
2498 flavor: node.get_flavor(),
2499 inputs,
2500 outputs,
2501 });
2502 }
2503
2504 let mut output_port_lookup: Vec<HashMap<String, String>> = vec![HashMap::new(); nodes.len()];
2505 let mut output_edges: Vec<_> = graph.0.edge_references().collect();
2506 output_edges.sort_by_key(|edge| edge.id().index());
2507 for edge in output_edges {
2508 let cnx = edge.weight();
2509 if let Some(&idx) = node_lookup.get(&cnx.src)
2510 && nodes[idx].flavor == Flavor::Task
2511 && cnx.src_channel.is_none()
2512 {
2513 let port_map = &mut output_port_lookup[idx];
2514 if !port_map.contains_key(&cnx.msg) {
2515 let label = format!("out{}: {}", port_map.len(), cnx.msg);
2516 port_map.insert(cnx.msg.clone(), label.clone());
2517 nodes[idx].outputs.push(label);
2518 }
2519 }
2520 }
2521
2522 let mut auto_input_counts = vec![0usize; nodes.len()];
2523 for edge in graph.0.edge_references() {
2524 let cnx = edge.weight();
2525 if let Some(&idx) = node_lookup.get(&cnx.dst)
2526 && nodes[idx].flavor == Flavor::Task
2527 && cnx.dst_channel.is_none()
2528 {
2529 auto_input_counts[idx] += 1;
2530 }
2531 }
2532
2533 let mut next_auto_input = vec![0usize; nodes.len()];
2534 let mut connections = Vec::new();
2535 for edge in graph.0.edge_references() {
2536 let cnx = edge.weight();
2537 let mut src_port = cnx.src_channel.clone();
2538 let mut dst_port = cnx.dst_channel.clone();
2539
2540 if let Some(&idx) = node_lookup.get(&cnx.src) {
2541 let node = &mut nodes[idx];
2542 if node.flavor == Flavor::Task && src_port.is_none() {
2543 src_port = output_port_lookup[idx].get(&cnx.msg).cloned();
2544 }
2545 }
2546 if let Some(&idx) = node_lookup.get(&cnx.dst) {
2547 let node = &mut nodes[idx];
2548 if node.flavor == Flavor::Task && dst_port.is_none() {
2549 let count = auto_input_counts[idx];
2550 let next = if count <= 1 {
2551 "in".to_string()
2552 } else {
2553 let next = format!("in.{}", next_auto_input[idx]);
2554 next_auto_input[idx] += 1;
2555 next
2556 };
2557 node.inputs.push(next.clone());
2558 dst_port = Some(next);
2559 }
2560 }
2561
2562 connections.push(RenderConnection {
2563 src: cnx.src.clone(),
2564 src_port,
2565 src_channel: cnx.src_channel.clone(),
2566 dst: cnx.dst.clone(),
2567 dst_port,
2568 dst_channel: cnx.dst_channel.clone(),
2569 msg: cnx.msg.clone(),
2570 });
2571 }
2572
2573 RenderTopology { nodes, connections }
2574}
2575
2576#[cfg(feature = "std")]
2577impl PortLookup {
2578 pub fn resolve_input(&self, name: Option<&str>) -> Option<&str> {
2579 if let Some(name) = name
2580 && let Some(port) = self.inputs.get(name)
2581 {
2582 return Some(port.as_str());
2583 }
2584 self.default_input.as_deref()
2585 }
2586
2587 pub fn resolve_output(&self, name: Option<&str>) -> Option<&str> {
2588 if let Some(name) = name
2589 && let Some(port) = self.outputs.get(name)
2590 {
2591 return Some(port.as_str());
2592 }
2593 self.default_output.as_deref()
2594 }
2595}
2596
2597#[cfg(feature = "std")]
2598#[allow(dead_code)]
2599fn build_port_table(
2600 title: &str,
2601 names: &[String],
2602 base_id: &str,
2603 prefix: &str,
2604) -> (String, HashMap<String, String>, Option<String>) {
2605 use std::fmt::Write as FmtWrite;
2606
2607 let mut html = String::new();
2608 write!(
2609 html,
2610 "<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">"
2611 )
2612 .unwrap();
2613 write!(
2614 html,
2615 "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT></TD></TR>",
2616 encode_text(title)
2617 )
2618 .unwrap();
2619
2620 let mut lookup = HashMap::new();
2621 let mut default_port = None;
2622
2623 if names.is_empty() {
2624 html.push_str("<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"lightgray\">—</FONT></TD></TR>");
2625 } else {
2626 for (idx, name) in names.iter().enumerate() {
2627 let port_id = format!("{base_id}_{prefix}_{idx}");
2628 write!(
2629 html,
2630 "<TR><TD PORT=\"{port_id}\" ALIGN=\"LEFT\">{}</TD></TR>",
2631 encode_text(name)
2632 )
2633 .unwrap();
2634 lookup.insert(name.clone(), port_id.clone());
2635 if idx == 0 {
2636 default_port = Some(port_id);
2637 }
2638 }
2639 }
2640
2641 html.push_str("</TABLE>");
2642 (html, lookup, default_port)
2643}
2644
2645#[cfg(feature = "std")]
2646#[allow(dead_code)]
2647fn build_config_table(config: &ComponentConfig) -> Option<String> {
2648 use std::fmt::Write as FmtWrite;
2649
2650 if config.0.is_empty() {
2651 return None;
2652 }
2653
2654 let mut entries: Vec<_> = config.0.iter().collect();
2655 entries.sort_by(|a, b| a.0.cmp(b.0));
2656
2657 let mut html = String::new();
2658 html.push_str("<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">");
2659 for (key, value) in entries {
2660 let value_txt = format!("{value}");
2661 write!(
2662 html,
2663 "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT> = {}</TD></TR>",
2664 encode_text(key),
2665 encode_text(&value_txt)
2666 )
2667 .unwrap();
2668 }
2669 html.push_str("</TABLE>");
2670 Some(html)
2671}
2672
2673#[cfg(feature = "std")]
2674#[allow(dead_code)]
2675fn sanitize_identifier(value: &str) -> String {
2676 value
2677 .chars()
2678 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
2679 .collect()
2680}
2681
2682#[cfg(feature = "std")]
2683#[allow(dead_code)]
2684fn escape_dot_id(value: &str) -> String {
2685 let mut escaped = String::with_capacity(value.len());
2686 for ch in value.chars() {
2687 match ch {
2688 '"' => escaped.push_str("\\\""),
2689 '\\' => escaped.push_str("\\\\"),
2690 _ => escaped.push(ch),
2691 }
2692 }
2693 escaped
2694}
2695
2696impl LoggingConfig {
2697 pub fn validate(&self) -> CuResult<()> {
2699 if let Some(copperlist_count) = self.copperlist_count
2700 && copperlist_count == 0
2701 {
2702 return Err(CuError::from(
2703 "CopperList count cannot be zero. Set logging.copperlist_count to at least 1.",
2704 ));
2705 }
2706
2707 if let Some(section_size_mib) = self.section_size_mib
2708 && let Some(slab_size_mib) = self.slab_size_mib
2709 && section_size_mib > slab_size_mib
2710 {
2711 return Err(CuError::from(format!(
2712 "Section size ({section_size_mib} MiB) cannot be larger than slab size ({slab_size_mib} MiB). Adjust the parameters accordingly."
2713 )));
2714 }
2715
2716 Ok(())
2717 }
2718}
2719
2720impl RuntimeConfig {
2721 pub fn validate(&self) -> CuResult<()> {
2723 if let Some(rate_target_hz) = self.rate_target_hz {
2724 if rate_target_hz == 0 {
2725 return Err(CuError::from(
2726 "Runtime rate target cannot be zero. Set runtime.rate_target_hz to at least 1.",
2727 ));
2728 }
2729
2730 if rate_target_hz > MAX_RATE_TARGET_HZ {
2731 return Err(CuError::from(format!(
2732 "Runtime rate target ({rate_target_hz} Hz) exceeds the supported maximum of {MAX_RATE_TARGET_HZ} Hz."
2733 )));
2734 }
2735 }
2736
2737 Ok(())
2738 }
2739}
2740
2741#[allow(dead_code)] fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
2743 let mut result = content.to_string();
2744
2745 for (key, value) in params {
2746 let pattern = format!("{{{{{key}}}}}");
2747 result = result.replace(&pattern, &value.to_string());
2748 }
2749
2750 result
2751}
2752
2753#[cfg(feature = "std")]
2755fn process_includes(
2756 file_path: &str,
2757 base_representation: CuConfigRepresentation,
2758 processed_files: &mut Vec<String>,
2759) -> CuResult<CuConfigRepresentation> {
2760 processed_files.push(file_path.to_string());
2762
2763 let mut result = base_representation;
2764
2765 if let Some(includes) = result.includes.take() {
2766 for include in includes {
2767 let include_path = if include.path.starts_with('/') {
2768 include.path.clone()
2769 } else {
2770 let current_dir = std::path::Path::new(file_path).parent();
2771
2772 match current_dir.map(|path| path.to_string_lossy().to_string()) {
2773 Some(current_dir) if !current_dir.is_empty() => {
2774 format!("{}/{}", current_dir, include.path)
2775 }
2776 _ => include.path,
2777 }
2778 };
2779
2780 let include_content = read_to_string(&include_path).map_err(|e| {
2781 CuError::from(format!("Failed to read include file: {include_path}"))
2782 .add_cause(e.to_string().as_str())
2783 })?;
2784
2785 let processed_content = substitute_parameters(&include_content, &include.params);
2786
2787 let mut included_representation: CuConfigRepresentation = match Options::default()
2788 .with_default_extension(Extensions::IMPLICIT_SOME)
2789 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2790 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2791 .from_str(&processed_content)
2792 {
2793 Ok(rep) => rep,
2794 Err(e) => {
2795 return Err(CuError::from(format!(
2796 "Failed to parse include file: {} - Error: {} at position {}",
2797 include_path, e.code, e.span
2798 )));
2799 }
2800 };
2801
2802 included_representation =
2803 process_includes(&include_path, included_representation, processed_files)?;
2804
2805 if let Some(included_tasks) = included_representation.tasks {
2806 if result.tasks.is_none() {
2807 result.tasks = Some(included_tasks);
2808 } else {
2809 let mut tasks = result.tasks.take().unwrap();
2810 for included_task in included_tasks {
2811 if !tasks.iter().any(|t| t.id == included_task.id) {
2812 tasks.push(included_task);
2813 }
2814 }
2815 result.tasks = Some(tasks);
2816 }
2817 }
2818
2819 if let Some(included_bridges) = included_representation.bridges {
2820 if result.bridges.is_none() {
2821 result.bridges = Some(included_bridges);
2822 } else {
2823 let mut bridges = result.bridges.take().unwrap();
2824 for included_bridge in included_bridges {
2825 if !bridges.iter().any(|b| b.id == included_bridge.id) {
2826 bridges.push(included_bridge);
2827 }
2828 }
2829 result.bridges = Some(bridges);
2830 }
2831 }
2832
2833 if let Some(included_resources) = included_representation.resources {
2834 if result.resources.is_none() {
2835 result.resources = Some(included_resources);
2836 } else {
2837 let mut resources = result.resources.take().unwrap();
2838 for included_resource in included_resources {
2839 if !resources.iter().any(|r| r.id == included_resource.id) {
2840 resources.push(included_resource);
2841 }
2842 }
2843 result.resources = Some(resources);
2844 }
2845 }
2846
2847 if let Some(included_cnx) = included_representation.cnx {
2848 if result.cnx.is_none() {
2849 result.cnx = Some(included_cnx);
2850 } else {
2851 let mut cnx = result.cnx.take().unwrap();
2852 for included_c in included_cnx {
2853 if !cnx
2854 .iter()
2855 .any(|c| c.src == included_c.src && c.dst == included_c.dst)
2856 {
2857 cnx.push(included_c);
2858 }
2859 }
2860 result.cnx = Some(cnx);
2861 }
2862 }
2863
2864 if let Some(included_monitors) = included_representation.monitors {
2865 if result.monitors.is_none() {
2866 result.monitors = Some(included_monitors);
2867 } else {
2868 let mut monitors = result.monitors.take().unwrap();
2869 for included_monitor in included_monitors {
2870 if !monitors.iter().any(|m| m.type_ == included_monitor.type_) {
2871 monitors.push(included_monitor);
2872 }
2873 }
2874 result.monitors = Some(monitors);
2875 }
2876 }
2877
2878 if result.logging.is_none() {
2879 result.logging = included_representation.logging;
2880 }
2881
2882 if result.runtime.is_none() {
2883 result.runtime = included_representation.runtime;
2884 }
2885
2886 if let Some(included_missions) = included_representation.missions {
2887 if result.missions.is_none() {
2888 result.missions = Some(included_missions);
2889 } else {
2890 let mut missions = result.missions.take().unwrap();
2891 for included_mission in included_missions {
2892 if !missions.iter().any(|m| m.id == included_mission.id) {
2893 missions.push(included_mission);
2894 }
2895 }
2896 result.missions = Some(missions);
2897 }
2898 }
2899 }
2900 }
2901
2902 Ok(result)
2903}
2904
2905#[cfg(feature = "std")]
2906fn parse_instance_config_overrides_string(
2907 content: &str,
2908) -> CuResult<InstanceConfigOverridesRepresentation> {
2909 Options::default()
2910 .with_default_extension(Extensions::IMPLICIT_SOME)
2911 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2912 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2913 .from_str(content)
2914 .map_err(|e| {
2915 CuError::from(format!(
2916 "Failed to parse instance override file: Error: {} at position {}",
2917 e.code, e.span
2918 ))
2919 })
2920}
2921
2922#[cfg(feature = "std")]
2923fn merge_component_config(target: &mut Option<ComponentConfig>, value: &ComponentConfig) {
2924 if let Some(existing) = target {
2925 existing.merge_from(value);
2926 } else {
2927 *target = Some(value.clone());
2928 }
2929}
2930
2931#[cfg(feature = "std")]
2932fn apply_task_config_override_to_graph(
2933 graph: &mut CuGraph,
2934 task_id: &str,
2935 value: &ComponentConfig,
2936) -> usize {
2937 let mut matches = 0usize;
2938 let node_indices: Vec<_> = graph.0.node_indices().collect();
2939 for node_index in node_indices {
2940 let node = &mut graph.0[node_index];
2941 if node.get_flavor() == Flavor::Task && node.id == task_id {
2942 merge_component_config(&mut node.config, value);
2943 matches += 1;
2944 }
2945 }
2946 matches
2947}
2948
2949#[cfg(feature = "std")]
2950fn apply_bridge_node_config_override_to_graph(
2951 graph: &mut CuGraph,
2952 bridge_id: &str,
2953 value: &ComponentConfig,
2954) {
2955 let node_indices: Vec<_> = graph.0.node_indices().collect();
2956 for node_index in node_indices {
2957 let node = &mut graph.0[node_index];
2958 if node.get_flavor() == Flavor::Bridge && node.id == bridge_id {
2959 merge_component_config(&mut node.config, value);
2960 }
2961 }
2962}
2963
2964#[cfg(feature = "std")]
2965fn parse_instance_override_target(path: &str) -> CuResult<(InstanceConfigTargetKind, String)> {
2966 let mut parts = path.split('/');
2967 let scope = parts.next().unwrap_or_default();
2968 let id = parts.next().unwrap_or_default();
2969 let leaf = parts.next().unwrap_or_default();
2970
2971 if scope.is_empty() || id.is_empty() || leaf.is_empty() || parts.next().is_some() {
2972 return Err(CuError::from(format!(
2973 "Invalid instance override path '{}'. Expected 'tasks/<id>/config', 'resources/<id>/config', or 'bridges/<id>/config'.",
2974 path
2975 )));
2976 }
2977
2978 if leaf != "config" {
2979 return Err(CuError::from(format!(
2980 "Invalid instance override path '{}'. Only the '/config' leaf is supported.",
2981 path
2982 )));
2983 }
2984
2985 let kind = match scope {
2986 "tasks" => InstanceConfigTargetKind::Task,
2987 "resources" => InstanceConfigTargetKind::Resource,
2988 "bridges" => InstanceConfigTargetKind::Bridge,
2989 _ => {
2990 return Err(CuError::from(format!(
2991 "Invalid instance override path '{}'. Supported roots are 'tasks', 'resources', and 'bridges'.",
2992 path
2993 )));
2994 }
2995 };
2996
2997 Ok((kind, id.to_string()))
2998}
2999
3000#[cfg(feature = "std")]
3001fn apply_instance_config_set_operation(
3002 config: &mut CuConfig,
3003 operation: &InstanceConfigSetOperation,
3004) -> CuResult<()> {
3005 let (target_kind, target_id) = parse_instance_override_target(&operation.path)?;
3006
3007 match target_kind {
3008 InstanceConfigTargetKind::Task => {
3009 let matches = match &mut config.graphs {
3010 ConfigGraphs::Simple(graph) => {
3011 apply_task_config_override_to_graph(graph, &target_id, &operation.value)
3012 }
3013 ConfigGraphs::Missions(graphs) => graphs
3014 .values_mut()
3015 .map(|graph| {
3016 apply_task_config_override_to_graph(graph, &target_id, &operation.value)
3017 })
3018 .sum(),
3019 };
3020
3021 if matches == 0 {
3022 return Err(CuError::from(format!(
3023 "Instance override path '{}' targets unknown task '{}'.",
3024 operation.path, target_id
3025 )));
3026 }
3027 }
3028 InstanceConfigTargetKind::Resource => {
3029 let mut matches = 0usize;
3030 for resource in &mut config.resources {
3031 if resource.id == target_id {
3032 merge_component_config(&mut resource.config, &operation.value);
3033 matches += 1;
3034 }
3035 }
3036 if matches == 0 {
3037 return Err(CuError::from(format!(
3038 "Instance override path '{}' targets unknown resource '{}'.",
3039 operation.path, target_id
3040 )));
3041 }
3042 }
3043 InstanceConfigTargetKind::Bridge => {
3044 let mut matches = 0usize;
3045 for bridge in &mut config.bridges {
3046 if bridge.id == target_id {
3047 merge_component_config(&mut bridge.config, &operation.value);
3048 matches += 1;
3049 }
3050 }
3051 if matches == 0 {
3052 return Err(CuError::from(format!(
3053 "Instance override path '{}' targets unknown bridge '{}'.",
3054 operation.path, target_id
3055 )));
3056 }
3057
3058 match &mut config.graphs {
3059 ConfigGraphs::Simple(graph) => {
3060 apply_bridge_node_config_override_to_graph(graph, &target_id, &operation.value);
3061 }
3062 ConfigGraphs::Missions(graphs) => {
3063 for graph in graphs.values_mut() {
3064 apply_bridge_node_config_override_to_graph(
3065 graph,
3066 &target_id,
3067 &operation.value,
3068 );
3069 }
3070 }
3071 }
3072 }
3073 }
3074
3075 Ok(())
3076}
3077
3078#[cfg(feature = "std")]
3079fn apply_instance_overrides(
3080 config: &mut CuConfig,
3081 overrides: &InstanceConfigOverridesRepresentation,
3082) -> CuResult<()> {
3083 for operation in &overrides.set {
3084 apply_instance_config_set_operation(config, operation)?;
3085 }
3086 Ok(())
3087}
3088
3089#[cfg(feature = "std")]
3090fn apply_instance_overrides_from_file(
3091 config: &mut CuConfig,
3092 override_path: &std::path::Path,
3093) -> CuResult<()> {
3094 let override_content = read_to_string(override_path).map_err(|e| {
3095 CuError::from(format!(
3096 "Failed to read instance override file '{}'",
3097 override_path.display()
3098 ))
3099 .add_cause(e.to_string().as_str())
3100 })?;
3101 let overrides = parse_instance_config_overrides_string(&override_content).map_err(|e| {
3102 CuError::from(format!(
3103 "Failed to parse instance override file '{}': {e}",
3104 override_path.display()
3105 ))
3106 })?;
3107 apply_instance_overrides(config, &overrides)
3108}
3109
3110#[cfg(feature = "std")]
3111#[allow(dead_code)]
3112fn parse_multi_config_string(content: &str) -> CuResult<MultiCopperConfigRepresentation> {
3113 Options::default()
3114 .with_default_extension(Extensions::IMPLICIT_SOME)
3115 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
3116 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
3117 .from_str(content)
3118 .map_err(|e| {
3119 CuError::from(format!(
3120 "Failed to parse multi-Copper configuration: Error: {} at position {}",
3121 e.code, e.span
3122 ))
3123 })
3124}
3125
3126#[cfg(feature = "std")]
3127#[allow(dead_code)]
3128fn resolve_relative_config_path(base_path: Option<&str>, referenced_path: &str) -> String {
3129 if referenced_path.starts_with('/') || base_path.is_none() {
3130 return referenced_path.to_string();
3131 }
3132
3133 let current_dir = std::path::Path::new(base_path.expect("checked above"))
3134 .parent()
3135 .unwrap_or_else(|| std::path::Path::new(""))
3136 .to_path_buf();
3137 current_dir
3138 .join(referenced_path)
3139 .to_string_lossy()
3140 .to_string()
3141}
3142
3143#[cfg(feature = "std")]
3144#[allow(dead_code)]
3145fn parse_multi_endpoint(endpoint: &str) -> CuResult<MultiCopperEndpoint> {
3146 let mut parts = endpoint.split('/');
3147 let subsystem_id = parts.next().unwrap_or_default();
3148 let bridge_id = parts.next().unwrap_or_default();
3149 let channel_id = parts.next().unwrap_or_default();
3150
3151 if subsystem_id.is_empty()
3152 || bridge_id.is_empty()
3153 || channel_id.is_empty()
3154 || parts.next().is_some()
3155 {
3156 return Err(CuError::from(format!(
3157 "Invalid multi-Copper endpoint '{endpoint}'. Expected 'subsystem/bridge/channel'."
3158 )));
3159 }
3160
3161 Ok(MultiCopperEndpoint {
3162 subsystem_id: subsystem_id.to_string(),
3163 bridge_id: bridge_id.to_string(),
3164 channel_id: channel_id.to_string(),
3165 })
3166}
3167
3168#[cfg(feature = "std")]
3169#[allow(dead_code)]
3170fn multi_channel_key(bridge_id: &str, channel_id: &str) -> String {
3171 format!("{bridge_id}/{channel_id}")
3172}
3173
3174#[cfg(feature = "std")]
3175#[allow(dead_code)]
3176fn register_multi_channel_msg(
3177 contracts: &mut HashMap<String, MultiCopperChannelContract>,
3178 bridge_id: &str,
3179 channel_id: &str,
3180 expected_direction: MultiCopperChannelDirection,
3181 msg: &str,
3182) -> CuResult<()> {
3183 let key = multi_channel_key(bridge_id, channel_id);
3184 let contract = contracts.get_mut(&key).ok_or_else(|| {
3185 CuError::from(format!(
3186 "Bridge channel '{bridge_id}/{channel_id}' is referenced by the graph but not declared in the bridge config."
3187 ))
3188 })?;
3189
3190 if contract.direction != expected_direction {
3191 let expected = match expected_direction {
3192 MultiCopperChannelDirection::Rx => "Rx",
3193 MultiCopperChannelDirection::Tx => "Tx",
3194 };
3195 return Err(CuError::from(format!(
3196 "Bridge channel '{bridge_id}/{channel_id}' is used as {expected} in the graph but declared with the opposite direction."
3197 )));
3198 }
3199
3200 match &contract.msg {
3201 Some(existing) if existing != msg => Err(CuError::from(format!(
3202 "Bridge channel '{bridge_id}/{channel_id}' carries inconsistent message types '{existing}' and '{msg}'."
3203 ))),
3204 Some(_) => Ok(()),
3205 None => {
3206 contract.msg = Some(msg.to_string());
3207 Ok(())
3208 }
3209 }
3210}
3211
3212#[cfg(feature = "std")]
3213#[allow(dead_code)]
3214fn build_multi_bridge_channel_contracts(
3215 config: &CuConfig,
3216) -> CuResult<HashMap<String, MultiCopperChannelContract>> {
3217 let graph = config.graphs.get_default_mission_graph().map_err(|e| {
3218 CuError::from(format!(
3219 "Multi-Copper subsystem configs currently require exactly one local graph: {e}"
3220 ))
3221 })?;
3222
3223 let mut contracts = HashMap::new();
3224 for bridge in &config.bridges {
3225 for channel in &bridge.channels {
3226 let (channel_id, direction) = match channel {
3227 BridgeChannelConfigRepresentation::Rx { id, .. } => {
3228 (id.as_str(), MultiCopperChannelDirection::Rx)
3229 }
3230 BridgeChannelConfigRepresentation::Tx { id, .. } => {
3231 (id.as_str(), MultiCopperChannelDirection::Tx)
3232 }
3233 };
3234
3235 let key = multi_channel_key(&bridge.id, channel_id);
3236 if contracts.contains_key(&key) {
3237 return Err(CuError::from(format!(
3238 "Duplicate bridge channel declaration for '{key}'."
3239 )));
3240 }
3241
3242 contracts.insert(
3243 key,
3244 MultiCopperChannelContract {
3245 bridge_type: bridge.type_.clone(),
3246 direction,
3247 msg: None,
3248 },
3249 );
3250 }
3251 }
3252
3253 for edge in graph.edges() {
3254 if let Some(channel_id) = &edge.src_channel {
3255 register_multi_channel_msg(
3256 &mut contracts,
3257 &edge.src,
3258 channel_id,
3259 MultiCopperChannelDirection::Rx,
3260 &edge.msg,
3261 )?;
3262 }
3263 if let Some(channel_id) = &edge.dst_channel {
3264 register_multi_channel_msg(
3265 &mut contracts,
3266 &edge.dst,
3267 channel_id,
3268 MultiCopperChannelDirection::Tx,
3269 &edge.msg,
3270 )?;
3271 }
3272 }
3273
3274 Ok(contracts)
3275}
3276
3277#[cfg(feature = "std")]
3278#[allow(dead_code)]
3279fn validate_multi_config_representation(
3280 representation: MultiCopperConfigRepresentation,
3281 file_path: Option<&str>,
3282) -> CuResult<MultiCopperConfig> {
3283 if representation
3284 .instance_overrides_root
3285 .as_ref()
3286 .is_some_and(|root| root.trim().is_empty())
3287 {
3288 return Err(CuError::from(
3289 "Multi-Copper instance_overrides_root must not be empty.",
3290 ));
3291 }
3292
3293 if representation.subsystems.is_empty() {
3294 return Err(CuError::from(
3295 "Multi-Copper config must declare at least one subsystem.",
3296 ));
3297 }
3298 if representation.subsystems.len() > usize::from(u16::MAX) + 1 {
3299 return Err(CuError::from(
3300 "Multi-Copper config supports at most 65536 distinct subsystem ids.",
3301 ));
3302 }
3303
3304 let mut seen_subsystems = std::collections::HashSet::new();
3305 for subsystem in &representation.subsystems {
3306 if subsystem.id.trim().is_empty() {
3307 return Err(CuError::from(
3308 "Multi-Copper subsystem ids must not be empty.",
3309 ));
3310 }
3311 if !seen_subsystems.insert(subsystem.id.clone()) {
3312 return Err(CuError::from(format!(
3313 "Duplicate multi-Copper subsystem id '{}'.",
3314 subsystem.id
3315 )));
3316 }
3317 }
3318
3319 let mut sorted_ids: Vec<_> = representation
3320 .subsystems
3321 .iter()
3322 .map(|subsystem| subsystem.id.clone())
3323 .collect();
3324 sorted_ids.sort();
3325 let subsystem_code_map: HashMap<_, _> = sorted_ids
3326 .into_iter()
3327 .enumerate()
3328 .map(|(idx, id)| {
3329 (
3330 id,
3331 u16::try_from(idx).expect("subsystem count was validated against u16 range"),
3332 )
3333 })
3334 .collect();
3335
3336 let mut subsystem_contracts: HashMap<String, HashMap<String, MultiCopperChannelContract>> =
3337 HashMap::new();
3338 let mut subsystems = Vec::with_capacity(representation.subsystems.len());
3339
3340 for subsystem in representation.subsystems {
3341 let resolved_config_path = resolve_relative_config_path(file_path, &subsystem.config);
3342 let config = read_configuration(&resolved_config_path).map_err(|e| {
3343 CuError::from(format!(
3344 "Failed to read subsystem '{}' from '{}': {e}",
3345 subsystem.id, resolved_config_path
3346 ))
3347 })?;
3348 let contracts = build_multi_bridge_channel_contracts(&config).map_err(|e| {
3349 CuError::from(format!(
3350 "Invalid subsystem '{}' for multi-Copper validation: {e}",
3351 subsystem.id
3352 ))
3353 })?;
3354 subsystem_contracts.insert(subsystem.id.clone(), contracts);
3355 subsystems.push(MultiCopperSubsystem {
3356 subsystem_code: *subsystem_code_map
3357 .get(&subsystem.id)
3358 .expect("subsystem code map must contain every subsystem"),
3359 id: subsystem.id,
3360 config_path: resolved_config_path,
3361 config,
3362 });
3363 }
3364
3365 let mut interconnects = Vec::with_capacity(representation.interconnects.len());
3366 for interconnect in representation.interconnects {
3367 let from = parse_multi_endpoint(&interconnect.from).map_err(|e| {
3368 CuError::from(format!(
3369 "Invalid multi-Copper interconnect source '{}': {e}",
3370 interconnect.from
3371 ))
3372 })?;
3373 let to = parse_multi_endpoint(&interconnect.to).map_err(|e| {
3374 CuError::from(format!(
3375 "Invalid multi-Copper interconnect destination '{}': {e}",
3376 interconnect.to
3377 ))
3378 })?;
3379
3380 let from_contracts = subsystem_contracts.get(&from.subsystem_id).ok_or_else(|| {
3381 CuError::from(format!(
3382 "Interconnect source '{}' references unknown subsystem '{}'.",
3383 from, from.subsystem_id
3384 ))
3385 })?;
3386 let to_contracts = subsystem_contracts.get(&to.subsystem_id).ok_or_else(|| {
3387 CuError::from(format!(
3388 "Interconnect destination '{}' references unknown subsystem '{}'.",
3389 to, to.subsystem_id
3390 ))
3391 })?;
3392
3393 let from_contract = from_contracts
3394 .get(&multi_channel_key(&from.bridge_id, &from.channel_id))
3395 .ok_or_else(|| {
3396 CuError::from(format!(
3397 "Interconnect source '{}' references unknown bridge channel.",
3398 from
3399 ))
3400 })?;
3401 let to_contract = to_contracts
3402 .get(&multi_channel_key(&to.bridge_id, &to.channel_id))
3403 .ok_or_else(|| {
3404 CuError::from(format!(
3405 "Interconnect destination '{}' references unknown bridge channel.",
3406 to
3407 ))
3408 })?;
3409
3410 if from_contract.direction != MultiCopperChannelDirection::Tx {
3411 return Err(CuError::from(format!(
3412 "Interconnect source '{}' must reference a Tx bridge channel.",
3413 from
3414 )));
3415 }
3416 if to_contract.direction != MultiCopperChannelDirection::Rx {
3417 return Err(CuError::from(format!(
3418 "Interconnect destination '{}' must reference an Rx bridge channel.",
3419 to
3420 )));
3421 }
3422
3423 if from_contract.bridge_type != to_contract.bridge_type {
3424 return Err(CuError::from(format!(
3425 "Interconnect '{}' -> '{}' mixes incompatible bridge types '{}' and '{}'.",
3426 from, to, from_contract.bridge_type, to_contract.bridge_type
3427 )));
3428 }
3429
3430 let from_msg = from_contract.msg.as_ref().ok_or_else(|| {
3431 CuError::from(format!(
3432 "Interconnect source '{}' is not wired inside subsystem '{}', so its message type cannot be inferred.",
3433 from, from.subsystem_id
3434 ))
3435 })?;
3436 let to_msg = to_contract.msg.as_ref().ok_or_else(|| {
3437 CuError::from(format!(
3438 "Interconnect destination '{}' is not wired inside subsystem '{}', so its message type cannot be inferred.",
3439 to, to.subsystem_id
3440 ))
3441 })?;
3442
3443 if from_msg != to_msg {
3444 return Err(CuError::from(format!(
3445 "Interconnect '{}' -> '{}' connects incompatible message types '{}' and '{}'.",
3446 from, to, from_msg, to_msg
3447 )));
3448 }
3449 if interconnect.msg != *from_msg {
3450 return Err(CuError::from(format!(
3451 "Interconnect '{}' -> '{}' declares message type '{}' but subsystem graphs require '{}'.",
3452 from, to, interconnect.msg, from_msg
3453 )));
3454 }
3455
3456 interconnects.push(MultiCopperInterconnect {
3457 from,
3458 to,
3459 msg: interconnect.msg,
3460 bridge_type: from_contract.bridge_type.clone(),
3461 });
3462 }
3463
3464 let instance_overrides_root = representation
3465 .instance_overrides_root
3466 .as_ref()
3467 .map(|root| resolve_relative_config_path(file_path, root));
3468
3469 Ok(MultiCopperConfig {
3470 subsystems,
3471 interconnects,
3472 instance_overrides_root,
3473 })
3474}
3475
3476#[cfg(feature = "std")]
3478pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
3479 let config_content = read_to_string(config_filename).map_err(|e| {
3480 CuError::from(format!(
3481 "Failed to read configuration file: {:?}",
3482 &config_filename
3483 ))
3484 .add_cause(e.to_string().as_str())
3485 })?;
3486 read_configuration_str(config_content, Some(config_filename))
3487}
3488
3489fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
3493 Options::default()
3494 .with_default_extension(Extensions::IMPLICIT_SOME)
3495 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
3496 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
3497 .from_str(content)
3498 .map_err(|e| {
3499 CuError::from(format!(
3500 "Failed to parse configuration: Error: {} at position {}",
3501 e.code, e.span
3502 ))
3503 })
3504}
3505
3506fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
3509 #[allow(unused_mut)]
3510 let mut cuconfig = CuConfig::deserialize_impl(representation)
3511 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
3512
3513 #[cfg(feature = "std")]
3514 cuconfig.ensure_threadpool_bundle();
3515
3516 cuconfig.validate_logging_config()?;
3517 cuconfig.validate_runtime_config()?;
3518
3519 Ok(cuconfig)
3520}
3521
3522#[allow(unused_variables)]
3523pub fn read_configuration_str(
3524 config_content: String,
3525 file_path: Option<&str>,
3526) -> CuResult<CuConfig> {
3527 let representation = parse_config_string(&config_content)?;
3529
3530 #[cfg(feature = "std")]
3533 let representation = if let Some(path) = file_path {
3534 process_includes(path, representation, &mut Vec::new())?
3535 } else {
3536 representation
3537 };
3538
3539 config_representation_to_config(representation)
3541}
3542
3543#[cfg(feature = "std")]
3545#[allow(dead_code)]
3546pub fn read_multi_configuration(config_filename: &str) -> CuResult<MultiCopperConfig> {
3547 let config_content = read_to_string(config_filename).map_err(|e| {
3548 CuError::from(format!(
3549 "Failed to read multi-Copper configuration file: {:?}",
3550 &config_filename
3551 ))
3552 .add_cause(e.to_string().as_str())
3553 })?;
3554 read_multi_configuration_str(config_content, Some(config_filename))
3555}
3556
3557#[cfg(feature = "std")]
3559#[allow(dead_code)]
3560pub fn read_multi_configuration_str(
3561 config_content: String,
3562 file_path: Option<&str>,
3563) -> CuResult<MultiCopperConfig> {
3564 let representation = parse_multi_config_string(&config_content)?;
3565 validate_multi_config_representation(representation, file_path)
3566}
3567
3568#[cfg(test)]
3570mod tests {
3571 use super::*;
3572 #[cfg(not(feature = "std"))]
3573 use alloc::vec;
3574 use serde::Deserialize;
3575 #[cfg(feature = "std")]
3576 use std::path::{Path, PathBuf};
3577
3578 #[test]
3579 fn test_plain_serialize() {
3580 let mut config = CuConfig::default();
3581 let graph = config.get_graph_mut(None).unwrap();
3582 let n1 = graph
3583 .add_node(Node::new("test1", "package::Plugin1"))
3584 .unwrap();
3585 let n2 = graph
3586 .add_node(Node::new("test2", "package::Plugin2"))
3587 .unwrap();
3588 graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
3589 let serialized = config.serialize_ron().unwrap();
3590 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3591 let graph = config.graphs.get_graph(None).unwrap();
3592 let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
3593 assert_eq!(graph.node_count(), deserialized_graph.node_count());
3594 assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
3595 }
3596
3597 #[test]
3598 fn test_serialize_with_params() {
3599 let mut config = CuConfig::default();
3600 let graph = config.get_graph_mut(None).unwrap();
3601 let mut camera = Node::new("copper-camera", "camerapkg::Camera");
3602 camera.set_param::<Value>("resolution-height", 1080.into());
3603 graph.add_node(camera).unwrap();
3604 let serialized = config.serialize_ron().unwrap();
3605 let config = CuConfig::deserialize_ron(&serialized).unwrap();
3606 let deserialized = config.get_graph(None).unwrap();
3607 let resolution = deserialized
3608 .get_node(0)
3609 .unwrap()
3610 .get_param::<i32>("resolution-height")
3611 .expect("resolution-height lookup failed");
3612 assert_eq!(resolution, Some(1080));
3613 }
3614
3615 #[derive(Debug, Deserialize, PartialEq)]
3616 struct InnerSettings {
3617 threshold: u32,
3618 flags: Option<bool>,
3619 }
3620
3621 #[derive(Debug, Deserialize, PartialEq)]
3622 struct SettingsConfig {
3623 gain: f32,
3624 matrix: [[f32; 3]; 3],
3625 inner: InnerSettings,
3626 tags: Vec<String>,
3627 }
3628
3629 #[test]
3630 fn test_component_config_get_value_structured() {
3631 let txt = r#"
3632 (
3633 tasks: [
3634 (
3635 id: "task",
3636 type: "pkg::Task",
3637 config: {
3638 "settings": {
3639 "gain": 1.5,
3640 "matrix": [
3641 [1.0, 0.0, 0.0],
3642 [0.0, 1.0, 0.0],
3643 [0.0, 0.0, 1.0],
3644 ],
3645 "inner": { "threshold": 42, "flags": Some(true) },
3646 "tags": ["alpha", "beta"],
3647 },
3648 },
3649 ),
3650 ],
3651 cnx: [],
3652 )
3653 "#;
3654 let config = CuConfig::deserialize_ron(txt).unwrap();
3655 let graph = config.graphs.get_graph(None).unwrap();
3656 let node = graph.get_node(0).unwrap();
3657 let component = node.get_instance_config().expect("missing config");
3658 let settings = component
3659 .get_value::<SettingsConfig>("settings")
3660 .expect("settings lookup failed")
3661 .expect("missing settings");
3662 let expected = SettingsConfig {
3663 gain: 1.5,
3664 matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
3665 inner: InnerSettings {
3666 threshold: 42,
3667 flags: Some(true),
3668 },
3669 tags: vec!["alpha".to_string(), "beta".to_string()],
3670 };
3671 assert_eq!(settings, expected);
3672 }
3673
3674 #[test]
3675 fn test_component_config_get_value_scalar_compatibility() {
3676 let txt = r#"
3677 (
3678 tasks: [
3679 (id: "task", type: "pkg::Task", config: { "scalar": 7 }),
3680 ],
3681 cnx: [],
3682 )
3683 "#;
3684 let config = CuConfig::deserialize_ron(txt).unwrap();
3685 let graph = config.graphs.get_graph(None).unwrap();
3686 let node = graph.get_node(0).unwrap();
3687 let component = node.get_instance_config().expect("missing config");
3688 let scalar = component
3689 .get::<u32>("scalar")
3690 .expect("scalar lookup failed");
3691 assert_eq!(scalar, Some(7));
3692 }
3693
3694 #[test]
3695 fn test_component_config_get_value_mixed_usage() {
3696 let txt = r#"
3697 (
3698 tasks: [
3699 (
3700 id: "task",
3701 type: "pkg::Task",
3702 config: {
3703 "scalar": 12,
3704 "settings": {
3705 "gain": 2.5,
3706 "matrix": [
3707 [1.0, 2.0, 3.0],
3708 [4.0, 5.0, 6.0],
3709 [7.0, 8.0, 9.0],
3710 ],
3711 "inner": { "threshold": 7, "flags": None },
3712 "tags": ["gamma"],
3713 },
3714 },
3715 ),
3716 ],
3717 cnx: [],
3718 )
3719 "#;
3720 let config = CuConfig::deserialize_ron(txt).unwrap();
3721 let graph = config.graphs.get_graph(None).unwrap();
3722 let node = graph.get_node(0).unwrap();
3723 let component = node.get_instance_config().expect("missing config");
3724 let scalar = component
3725 .get::<u32>("scalar")
3726 .expect("scalar lookup failed");
3727 let settings = component
3728 .get_value::<SettingsConfig>("settings")
3729 .expect("settings lookup failed");
3730 assert_eq!(scalar, Some(12));
3731 assert!(settings.is_some());
3732 }
3733
3734 #[test]
3735 fn test_component_config_get_value_error_includes_key() {
3736 let txt = r#"
3737 (
3738 tasks: [
3739 (
3740 id: "task",
3741 type: "pkg::Task",
3742 config: { "settings": { "gain": 1.0 } },
3743 ),
3744 ],
3745 cnx: [],
3746 )
3747 "#;
3748 let config = CuConfig::deserialize_ron(txt).unwrap();
3749 let graph = config.graphs.get_graph(None).unwrap();
3750 let node = graph.get_node(0).unwrap();
3751 let component = node.get_instance_config().expect("missing config");
3752 let err = component
3753 .get_value::<u32>("settings")
3754 .expect_err("expected type mismatch");
3755 assert!(err.to_string().contains("settings"));
3756 }
3757
3758 #[test]
3759 fn test_deserialization_error() {
3760 let txt = r#"( tasks: (), cnx: [], monitors: [(type: "ExampleMonitor", )] ) "#;
3762 let err = CuConfig::deserialize_ron(txt).expect_err("expected deserialization error");
3763 assert!(
3764 err.to_string()
3765 .contains("Syntax Error in config: Expected opening `[` at position 1:9-1:10")
3766 );
3767 }
3768 #[test]
3769 fn test_missions() {
3770 let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
3771 let config = CuConfig::deserialize_ron(txt).unwrap();
3772 let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
3773 assert!(graph.node_count() == 0);
3774 let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
3775 assert!(graph.node_count() == 0);
3776 }
3777
3778 #[test]
3779 fn test_monitor_plural_syntax() {
3780 let txt = r#"( tasks: [], cnx: [], monitors: [(type: "ExampleMonitor", )] ) "#;
3781 let config = CuConfig::deserialize_ron(txt).unwrap();
3782 assert_eq!(config.get_monitor_config().unwrap().type_, "ExampleMonitor");
3783
3784 let txt = r#"( tasks: [], cnx: [], monitors: [(type: "ExampleMonitor", config: { "toto": 4, } )] ) "#;
3785 let config = CuConfig::deserialize_ron(txt).unwrap();
3786 assert_eq!(
3787 config
3788 .get_monitor_config()
3789 .unwrap()
3790 .config
3791 .as_ref()
3792 .unwrap()
3793 .0["toto"]
3794 .0,
3795 4u8.into()
3796 );
3797 }
3798
3799 #[test]
3800 fn test_monitor_singular_syntax() {
3801 let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } ) ) "#;
3802 let config = CuConfig::deserialize_ron(txt).unwrap();
3803 assert_eq!(config.get_monitor_configs().len(), 1);
3804 assert_eq!(config.get_monitor_config().unwrap().type_, "ExampleMonitor");
3805 assert_eq!(
3806 config
3807 .get_monitor_config()
3808 .unwrap()
3809 .config
3810 .as_ref()
3811 .unwrap()
3812 .0["toto"]
3813 .0,
3814 4u8.into()
3815 );
3816 }
3817
3818 #[test]
3819 #[cfg(feature = "std")]
3820 fn test_render_topology_multi_input_ports() {
3821 let mut config = CuConfig::default();
3822 let graph = config.get_graph_mut(None).unwrap();
3823 let src1 = graph.add_node(Node::new("src1", "tasks::Source1")).unwrap();
3824 let src2 = graph.add_node(Node::new("src2", "tasks::Source2")).unwrap();
3825 let dst = graph.add_node(Node::new("dst", "tasks::Dst")).unwrap();
3826 graph.connect(src1, dst, "msg::A").unwrap();
3827 graph.connect(src2, dst, "msg::B").unwrap();
3828
3829 let topology = build_render_topology(graph, &[]);
3830 let dst_node = topology
3831 .nodes
3832 .iter()
3833 .find(|node| node.id == "dst")
3834 .expect("missing dst node");
3835 assert_eq!(dst_node.inputs.len(), 2);
3836
3837 let mut dst_ports: Vec<_> = topology
3838 .connections
3839 .iter()
3840 .filter(|cnx| cnx.dst == "dst")
3841 .map(|cnx| cnx.dst_port.as_deref().expect("missing dst port"))
3842 .collect();
3843 dst_ports.sort();
3844 assert_eq!(dst_ports, vec!["in.0", "in.1"]);
3845 }
3846
3847 #[test]
3848 fn test_logging_parameters() {
3849 let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
3851
3852 let config = CuConfig::deserialize_ron(txt).unwrap();
3853 assert!(config.logging.is_some());
3854 let logging_config = config.logging.unwrap();
3855 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
3856 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
3857 assert!(!logging_config.enable_task_logging);
3858
3859 let txt =
3861 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
3862 let config = CuConfig::deserialize_ron(txt).unwrap();
3863 assert!(config.logging.is_some());
3864 let logging_config = config.logging.unwrap();
3865 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
3866 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
3867 assert!(logging_config.enable_task_logging);
3868 }
3869
3870 #[test]
3871 fn test_bridge_parsing() {
3872 let txt = r#"
3873 (
3874 tasks: [
3875 (id: "dst", type: "tasks::Destination"),
3876 (id: "src", type: "tasks::Source"),
3877 ],
3878 bridges: [
3879 (
3880 id: "radio",
3881 type: "tasks::SerialBridge",
3882 config: { "path": "/dev/ttyACM0", "baud": 921600 },
3883 channels: [
3884 Rx ( id: "status", route: "sys/status" ),
3885 Tx ( id: "motor", route: "motor/cmd" ),
3886 ],
3887 ),
3888 ],
3889 cnx: [
3890 (src: "radio/status", dst: "dst", msg: "mymsgs::Status"),
3891 (src: "src", dst: "radio/motor", msg: "mymsgs::MotorCmd"),
3892 ],
3893 )
3894 "#;
3895
3896 let config = CuConfig::deserialize_ron(txt).unwrap();
3897 assert_eq!(config.bridges.len(), 1);
3898 let bridge = &config.bridges[0];
3899 assert_eq!(bridge.id, "radio");
3900 assert_eq!(bridge.channels.len(), 2);
3901 match &bridge.channels[0] {
3902 BridgeChannelConfigRepresentation::Rx { id, route, .. } => {
3903 assert_eq!(id, "status");
3904 assert_eq!(route.as_deref(), Some("sys/status"));
3905 }
3906 _ => panic!("expected Rx channel"),
3907 }
3908 match &bridge.channels[1] {
3909 BridgeChannelConfigRepresentation::Tx { id, route, .. } => {
3910 assert_eq!(id, "motor");
3911 assert_eq!(route.as_deref(), Some("motor/cmd"));
3912 }
3913 _ => panic!("expected Tx channel"),
3914 }
3915 let graph = config.graphs.get_graph(None).unwrap();
3916 let bridge_id = graph
3917 .get_node_id_by_name("radio")
3918 .expect("bridge node missing");
3919 let bridge_node = graph.get_node(bridge_id).unwrap();
3920 assert_eq!(bridge_node.get_flavor(), Flavor::Bridge);
3921
3922 let mut edges = Vec::new();
3924 for edge_idx in graph.0.edge_indices() {
3925 edges.push(graph.0[edge_idx].clone());
3926 }
3927 assert_eq!(edges.len(), 2);
3928 let status_edge = edges
3929 .iter()
3930 .find(|e| e.dst == "dst")
3931 .expect("status edge missing");
3932 assert_eq!(status_edge.src_channel.as_deref(), Some("status"));
3933 assert!(status_edge.dst_channel.is_none());
3934 let motor_edge = edges
3935 .iter()
3936 .find(|e| e.dst_channel.is_some())
3937 .expect("motor edge missing");
3938 assert_eq!(motor_edge.dst_channel.as_deref(), Some("motor"));
3939 }
3940
3941 #[test]
3942 fn test_bridge_roundtrip() {
3943 let mut config = CuConfig::default();
3944 let mut bridge_config = ComponentConfig::default();
3945 bridge_config.set("port", "/dev/ttyACM0".to_string());
3946 config.bridges.push(BridgeConfig {
3947 id: "radio".to_string(),
3948 type_: "tasks::SerialBridge".to_string(),
3949 config: Some(bridge_config),
3950 resources: None,
3951 missions: None,
3952 run_in_sim: None,
3953 channels: vec![
3954 BridgeChannelConfigRepresentation::Rx {
3955 id: "status".to_string(),
3956 route: Some("sys/status".to_string()),
3957 config: None,
3958 },
3959 BridgeChannelConfigRepresentation::Tx {
3960 id: "motor".to_string(),
3961 route: Some("motor/cmd".to_string()),
3962 config: None,
3963 },
3964 ],
3965 });
3966
3967 let serialized = config.serialize_ron().unwrap();
3968 assert!(
3969 serialized.contains("bridges"),
3970 "bridges section missing from serialized config"
3971 );
3972 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
3973 assert_eq!(deserialized.bridges.len(), 1);
3974 let bridge = &deserialized.bridges[0];
3975 assert!(bridge.is_run_in_sim());
3976 assert_eq!(bridge.channels.len(), 2);
3977 assert!(matches!(
3978 bridge.channels[0],
3979 BridgeChannelConfigRepresentation::Rx { .. }
3980 ));
3981 assert!(matches!(
3982 bridge.channels[1],
3983 BridgeChannelConfigRepresentation::Tx { .. }
3984 ));
3985 }
3986
3987 #[test]
3988 fn test_resource_parsing() {
3989 let txt = r#"
3990 (
3991 resources: [
3992 (
3993 id: "fc",
3994 provider: "copper_board_px4::Px4Bundle",
3995 config: { "baud": 921600 },
3996 missions: ["m1"],
3997 ),
3998 (
3999 id: "misc",
4000 provider: "cu29_runtime::StdClockBundle",
4001 ),
4002 ],
4003 )
4004 "#;
4005
4006 let config = CuConfig::deserialize_ron(txt).unwrap();
4007 assert_eq!(config.resources.len(), 2);
4008 let fc = &config.resources[0];
4009 assert_eq!(fc.id, "fc");
4010 assert_eq!(fc.provider, "copper_board_px4::Px4Bundle");
4011 assert_eq!(fc.missions.as_deref(), Some(&["m1".to_string()][..]));
4012 let baud: u32 = fc
4013 .config
4014 .as_ref()
4015 .expect("missing config")
4016 .get::<u32>("baud")
4017 .expect("baud lookup failed")
4018 .expect("missing baud");
4019 assert_eq!(baud, 921_600);
4020 let misc = &config.resources[1];
4021 assert_eq!(misc.id, "misc");
4022 assert_eq!(misc.provider, "cu29_runtime::StdClockBundle");
4023 assert!(misc.config.is_none());
4024 }
4025
4026 #[test]
4027 fn test_resource_roundtrip() {
4028 let mut config = CuConfig::default();
4029 let mut bundle_cfg = ComponentConfig::default();
4030 bundle_cfg.set("path", "/dev/ttyACM0".to_string());
4031 config.resources.push(ResourceBundleConfig {
4032 id: "fc".to_string(),
4033 provider: "copper_board_px4::Px4Bundle".to_string(),
4034 config: Some(bundle_cfg),
4035 missions: Some(vec!["m1".to_string()]),
4036 });
4037
4038 let serialized = config.serialize_ron().unwrap();
4039 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
4040 assert_eq!(deserialized.resources.len(), 1);
4041 let res = &deserialized.resources[0];
4042 assert_eq!(res.id, "fc");
4043 assert_eq!(res.provider, "copper_board_px4::Px4Bundle");
4044 assert_eq!(res.missions.as_deref(), Some(&["m1".to_string()][..]));
4045 let path: String = res
4046 .config
4047 .as_ref()
4048 .expect("missing config")
4049 .get::<String>("path")
4050 .expect("path lookup failed")
4051 .expect("missing path");
4052 assert_eq!(path, "/dev/ttyACM0");
4053 }
4054
4055 #[test]
4056 fn test_bridge_channel_config() {
4057 let txt = r#"
4058 (
4059 tasks: [],
4060 bridges: [
4061 (
4062 id: "radio",
4063 type: "tasks::SerialBridge",
4064 channels: [
4065 Rx ( id: "status", route: "sys/status", config: { "filter": "fast" } ),
4066 Tx ( id: "imu", route: "telemetry/imu", config: { "rate": 100 } ),
4067 ],
4068 ),
4069 ],
4070 cnx: [],
4071 )
4072 "#;
4073
4074 let config = CuConfig::deserialize_ron(txt).unwrap();
4075 let bridge = &config.bridges[0];
4076 match &bridge.channels[0] {
4077 BridgeChannelConfigRepresentation::Rx {
4078 config: Some(cfg), ..
4079 } => {
4080 let val = cfg
4081 .get::<String>("filter")
4082 .expect("filter lookup failed")
4083 .expect("filter missing");
4084 assert_eq!(val, "fast");
4085 }
4086 _ => panic!("expected Rx channel with config"),
4087 }
4088 match &bridge.channels[1] {
4089 BridgeChannelConfigRepresentation::Tx {
4090 config: Some(cfg), ..
4091 } => {
4092 let rate = cfg
4093 .get::<i32>("rate")
4094 .expect("rate lookup failed")
4095 .expect("rate missing");
4096 assert_eq!(rate, 100);
4097 }
4098 _ => panic!("expected Tx channel with config"),
4099 }
4100 }
4101
4102 #[test]
4103 fn test_task_resources_roundtrip() {
4104 let txt = r#"
4105 (
4106 tasks: [
4107 (
4108 id: "imu",
4109 type: "tasks::ImuDriver",
4110 resources: { "bus": "fc.spi_1", "irq": "fc.gpio_imu" },
4111 ),
4112 ],
4113 cnx: [],
4114 )
4115 "#;
4116
4117 let config = CuConfig::deserialize_ron(txt).unwrap();
4118 let graph = config.graphs.get_graph(None).unwrap();
4119 let node = graph.get_node(0).expect("missing task node");
4120 let resources = node.get_resources().expect("missing resources map");
4121 assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
4122 assert_eq!(
4123 resources.get("irq").map(String::as_str),
4124 Some("fc.gpio_imu")
4125 );
4126
4127 let serialized = config.serialize_ron().unwrap();
4128 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
4129 let graph = deserialized.graphs.get_graph(None).unwrap();
4130 let node = graph.get_node(0).expect("missing task node");
4131 let resources = node
4132 .get_resources()
4133 .expect("missing resources map after roundtrip");
4134 assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
4135 assert_eq!(
4136 resources.get("irq").map(String::as_str),
4137 Some("fc.gpio_imu")
4138 );
4139 }
4140
4141 #[test]
4142 fn test_bridge_resources_preserved() {
4143 let mut config = CuConfig::default();
4144 config.resources.push(ResourceBundleConfig {
4145 id: "fc".to_string(),
4146 provider: "board::Bundle".to_string(),
4147 config: None,
4148 missions: None,
4149 });
4150 let bridge_resources = HashMap::from([("serial".to_string(), "fc.serial0".to_string())]);
4151 config.bridges.push(BridgeConfig {
4152 id: "radio".to_string(),
4153 type_: "tasks::SerialBridge".to_string(),
4154 config: None,
4155 resources: Some(bridge_resources),
4156 missions: None,
4157 run_in_sim: None,
4158 channels: vec![BridgeChannelConfigRepresentation::Tx {
4159 id: "uplink".to_string(),
4160 route: None,
4161 config: None,
4162 }],
4163 });
4164
4165 let serialized = config.serialize_ron().unwrap();
4166 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
4167 let graph = deserialized.graphs.get_graph(None).expect("missing graph");
4168 let bridge_id = graph
4169 .get_node_id_by_name("radio")
4170 .expect("bridge node missing");
4171 let node = graph.get_node(bridge_id).expect("missing bridge node");
4172 let resources = node
4173 .get_resources()
4174 .expect("bridge resources were not preserved");
4175 assert_eq!(
4176 resources.get("serial").map(String::as_str),
4177 Some("fc.serial0")
4178 );
4179 }
4180
4181 #[test]
4182 fn test_demo_config_parses() {
4183 let txt = r#"(
4184 resources: [
4185 (
4186 id: "fc",
4187 provider: "crate::resources::RadioBundle",
4188 ),
4189 ],
4190 tasks: [
4191 (id: "thr", type: "tasks::ThrottleControl"),
4192 (id: "tele0", type: "tasks::TelemetrySink0"),
4193 (id: "tele1", type: "tasks::TelemetrySink1"),
4194 (id: "tele2", type: "tasks::TelemetrySink2"),
4195 (id: "tele3", type: "tasks::TelemetrySink3"),
4196 ],
4197 bridges: [
4198 ( id: "crsf",
4199 type: "cu_crsf::CrsfBridge<SerialResource, SerialPortError>",
4200 resources: { "serial": "fc.serial" },
4201 channels: [
4202 Rx ( id: "rc_rx" ), // receiving RC Channels
4203 Tx ( id: "lq_tx" ), // Sending LineQuality back
4204 ],
4205 ),
4206 (
4207 id: "bdshot",
4208 type: "cu_bdshot::RpBdshotBridge",
4209 channels: [
4210 Tx ( id: "esc0_tx" ),
4211 Tx ( id: "esc1_tx" ),
4212 Tx ( id: "esc2_tx" ),
4213 Tx ( id: "esc3_tx" ),
4214 Rx ( id: "esc0_rx" ),
4215 Rx ( id: "esc1_rx" ),
4216 Rx ( id: "esc2_rx" ),
4217 Rx ( id: "esc3_rx" ),
4218 ],
4219 ),
4220 ],
4221 cnx: [
4222 (src: "crsf/rc_rx", dst: "thr", msg: "cu_crsf::messages::RcChannelsPayload"),
4223 (src: "thr", dst: "bdshot/esc0_tx", msg: "cu_bdshot::EscCommand"),
4224 (src: "thr", dst: "bdshot/esc1_tx", msg: "cu_bdshot::EscCommand"),
4225 (src: "thr", dst: "bdshot/esc2_tx", msg: "cu_bdshot::EscCommand"),
4226 (src: "thr", dst: "bdshot/esc3_tx", msg: "cu_bdshot::EscCommand"),
4227 (src: "bdshot/esc0_rx", dst: "tele0", msg: "cu_bdshot::EscTelemetry"),
4228 (src: "bdshot/esc1_rx", dst: "tele1", msg: "cu_bdshot::EscTelemetry"),
4229 (src: "bdshot/esc2_rx", dst: "tele2", msg: "cu_bdshot::EscTelemetry"),
4230 (src: "bdshot/esc3_rx", dst: "tele3", msg: "cu_bdshot::EscTelemetry"),
4231 ],
4232)"#;
4233 let config = CuConfig::deserialize_ron(txt).unwrap();
4234 assert_eq!(config.resources.len(), 1);
4235 assert_eq!(config.bridges.len(), 2);
4236 }
4237
4238 #[test]
4239 fn test_bridge_tx_cannot_be_source() {
4240 let txt = r#"
4241 (
4242 tasks: [
4243 (id: "dst", type: "tasks::Destination"),
4244 ],
4245 bridges: [
4246 (
4247 id: "radio",
4248 type: "tasks::SerialBridge",
4249 channels: [
4250 Tx ( id: "motor", route: "motor/cmd" ),
4251 ],
4252 ),
4253 ],
4254 cnx: [
4255 (src: "radio/motor", dst: "dst", msg: "mymsgs::MotorCmd"),
4256 ],
4257 )
4258 "#;
4259
4260 let err = CuConfig::deserialize_ron(txt).expect_err("expected bridge source error");
4261 assert!(
4262 err.to_string()
4263 .contains("channel 'motor' is Tx and cannot act as a source")
4264 );
4265 }
4266
4267 #[test]
4268 fn test_bridge_rx_cannot_be_destination() {
4269 let txt = r#"
4270 (
4271 tasks: [
4272 (id: "src", type: "tasks::Source"),
4273 ],
4274 bridges: [
4275 (
4276 id: "radio",
4277 type: "tasks::SerialBridge",
4278 channels: [
4279 Rx ( id: "status", route: "sys/status" ),
4280 ],
4281 ),
4282 ],
4283 cnx: [
4284 (src: "src", dst: "radio/status", msg: "mymsgs::Status"),
4285 ],
4286 )
4287 "#;
4288
4289 let err = CuConfig::deserialize_ron(txt).expect_err("expected bridge destination error");
4290 assert!(
4291 err.to_string()
4292 .contains("channel 'status' is Rx and cannot act as a destination")
4293 );
4294 }
4295
4296 #[test]
4297 fn test_validate_logging_config() {
4298 let txt =
4300 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
4301 let config = CuConfig::deserialize_ron(txt).unwrap();
4302 assert!(config.validate_logging_config().is_ok());
4303
4304 let txt =
4306 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
4307 let config = CuConfig::deserialize_ron(txt).unwrap();
4308 assert!(config.validate_logging_config().is_err());
4309 }
4310
4311 #[test]
4313 fn test_deserialization_edge_id_assignment() {
4314 let txt = r#"(
4317 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
4318 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
4319 )"#;
4320 let config = CuConfig::deserialize_ron(txt).unwrap();
4321 let graph = config.graphs.get_graph(None).unwrap();
4322 assert!(config.validate_logging_config().is_ok());
4323
4324 let src1_id = 0;
4326 assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
4327 let src2_id = 1;
4328 assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
4329
4330 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
4333 assert_eq!(src1_edge_id, 1);
4334 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
4335 assert_eq!(src2_edge_id, 0);
4336 }
4337
4338 #[test]
4339 fn test_simple_missions() {
4340 let txt = r#"(
4342 missions: [ (id: "m1"),
4343 (id: "m2"),
4344 ],
4345 tasks: [(id: "src1", type: "a", missions: ["m1"]),
4346 (id: "src2", type: "b", missions: ["m2"]),
4347 (id: "sink", type: "c")],
4348
4349 cnx: [
4350 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
4351 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
4352 ],
4353 )
4354 "#;
4355
4356 let config = CuConfig::deserialize_ron(txt).unwrap();
4357 let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
4358 assert_eq!(m1_graph.edge_count(), 1);
4359 assert_eq!(m1_graph.node_count(), 2);
4360 let index = 0;
4361 let cnx = m1_graph.get_edge_weight(index).unwrap();
4362
4363 assert_eq!(cnx.src, "src1");
4364 assert_eq!(cnx.dst, "sink");
4365 assert_eq!(cnx.msg, "u32");
4366 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
4367
4368 let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
4369 assert_eq!(m2_graph.edge_count(), 1);
4370 assert_eq!(m2_graph.node_count(), 2);
4371 let index = 0;
4372 let cnx = m2_graph.get_edge_weight(index).unwrap();
4373 assert_eq!(cnx.src, "src2");
4374 assert_eq!(cnx.dst, "sink");
4375 assert_eq!(cnx.msg, "u32");
4376 assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
4377 }
4378 #[test]
4379 fn test_mission_serde() {
4380 let txt = r#"(
4382 missions: [ (id: "m1"),
4383 (id: "m2"),
4384 ],
4385 tasks: [(id: "src1", type: "a", missions: ["m1"]),
4386 (id: "src2", type: "b", missions: ["m2"]),
4387 (id: "sink", type: "c")],
4388
4389 cnx: [
4390 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
4391 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
4392 ],
4393 )
4394 "#;
4395
4396 let config = CuConfig::deserialize_ron(txt).unwrap();
4397 let serialized = config.serialize_ron().unwrap();
4398 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
4399 let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
4400 assert_eq!(m1_graph.edge_count(), 1);
4401 assert_eq!(m1_graph.node_count(), 2);
4402 let index = 0;
4403 let cnx = m1_graph.get_edge_weight(index).unwrap();
4404 assert_eq!(cnx.src, "src1");
4405 assert_eq!(cnx.dst, "sink");
4406 assert_eq!(cnx.msg, "u32");
4407 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
4408 }
4409
4410 #[test]
4411 fn test_mission_scoped_nc_connection_survives_serialize_roundtrip() {
4412 let txt = r#"(
4413 missions: [(id: "m1"), (id: "m2")],
4414 tasks: [
4415 (id: "src_m1", type: "a", missions: ["m1"]),
4416 (id: "src_m2", type: "b", missions: ["m2"]),
4417 ],
4418 cnx: [
4419 (src: "src_m1", dst: "__nc__", msg: "msg::A", missions: ["m1"]),
4420 (src: "src_m2", dst: "__nc__", msg: "msg::B", missions: ["m2"]),
4421 ]
4422 )"#;
4423
4424 let config = CuConfig::deserialize_ron(txt).unwrap();
4425 let serialized = config.serialize_ron().unwrap();
4426 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
4427
4428 let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
4429 let src_m1_id = m1_graph.get_node_id_by_name("src_m1").unwrap();
4430 let src_m1 = m1_graph.get_node(src_m1_id).unwrap();
4431 assert_eq!(src_m1.nc_outputs(), &["msg::A".to_string()]);
4432
4433 let m2_graph = deserialized.graphs.get_graph(Some("m2")).unwrap();
4434 let src_m2_id = m2_graph.get_node_id_by_name("src_m2").unwrap();
4435 let src_m2 = m2_graph.get_node(src_m2_id).unwrap();
4436 assert_eq!(src_m2.nc_outputs(), &["msg::B".to_string()]);
4437 }
4438
4439 #[test]
4440 fn test_keyframe_interval() {
4441 let txt = r#"(
4444 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
4445 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
4446 logging: ( keyframe_interval: 314 )
4447 )"#;
4448 let config = CuConfig::deserialize_ron(txt).unwrap();
4449 let logging_config = config.logging.unwrap();
4450 assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
4451 }
4452
4453 #[test]
4454 fn test_default_keyframe_interval() {
4455 let txt = r#"(
4458 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
4459 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
4460 logging: ( slab_size_mib: 200, section_size_mib: 1024, )
4461 )"#;
4462 let config = CuConfig::deserialize_ron(txt).unwrap();
4463 let logging_config = config.logging.unwrap();
4464 assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
4465 }
4466
4467 #[test]
4468 fn test_runtime_rate_target_rejects_zero() {
4469 let txt = r#"(
4470 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4471 cnx: [(src: "src", dst: "sink", msg: "msg::A")],
4472 runtime: (rate_target_hz: 0)
4473 )"#;
4474
4475 let err =
4476 read_configuration_str(txt.to_string(), None).expect_err("runtime config should fail");
4477 assert!(
4478 err.to_string()
4479 .contains("Runtime rate target cannot be zero"),
4480 "unexpected error: {err}"
4481 );
4482 }
4483
4484 #[test]
4485 fn test_runtime_rate_target_rejects_above_nanosecond_resolution() {
4486 let txt = format!(
4487 r#"(
4488 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4489 cnx: [(src: "src", dst: "sink", msg: "msg::A")],
4490 runtime: (rate_target_hz: {})
4491 )"#,
4492 MAX_RATE_TARGET_HZ + 1
4493 );
4494
4495 let err = read_configuration_str(txt, None).expect_err("runtime config should fail");
4496 assert!(
4497 err.to_string().contains("exceeds the supported maximum"),
4498 "unexpected error: {err}"
4499 );
4500 }
4501
4502 #[test]
4503 fn test_nc_connection_marks_source_output_without_creating_edge() {
4504 let txt = r#"(
4505 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4506 cnx: [
4507 (src: "src", dst: "sink", msg: "msg::A"),
4508 (src: "src", dst: "__nc__", msg: "msg::B"),
4509 ]
4510 )"#;
4511 let config = CuConfig::deserialize_ron(txt).unwrap();
4512 let graph = config.get_graph(None).unwrap();
4513 let src_id = graph.get_node_id_by_name("src").unwrap();
4514 let src_node = graph.get_node(src_id).unwrap();
4515
4516 assert_eq!(graph.edge_count(), 1);
4517 assert_eq!(src_node.nc_outputs(), &["msg::B".to_string()]);
4518 }
4519
4520 #[test]
4521 fn test_nc_connection_survives_serialize_roundtrip() {
4522 let txt = r#"(
4523 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4524 cnx: [
4525 (src: "src", dst: "sink", msg: "msg::A"),
4526 (src: "src", dst: "__nc__", msg: "msg::B"),
4527 ]
4528 )"#;
4529 let config = CuConfig::deserialize_ron(txt).unwrap();
4530 let serialized = config.serialize_ron().unwrap();
4531 let deserialized = CuConfig::deserialize_ron(&serialized).unwrap();
4532 let graph = deserialized.get_graph(None).unwrap();
4533 let src_id = graph.get_node_id_by_name("src").unwrap();
4534 let src_node = graph.get_node(src_id).unwrap();
4535
4536 assert_eq!(graph.edge_count(), 1);
4537 assert_eq!(src_node.nc_outputs(), &["msg::B".to_string()]);
4538 }
4539
4540 #[test]
4541 fn test_nc_connection_preserves_original_connection_order() {
4542 let txt = r#"(
4543 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4544 cnx: [
4545 (src: "src", dst: "__nc__", msg: "msg::A"),
4546 (src: "src", dst: "sink", msg: "msg::B"),
4547 ]
4548 )"#;
4549 let config = CuConfig::deserialize_ron(txt).unwrap();
4550 let graph = config.get_graph(None).unwrap();
4551 let src_id = graph.get_node_id_by_name("src").unwrap();
4552 let src_node = graph.get_node(src_id).unwrap();
4553 let edge_id = graph.get_src_edges(src_id).unwrap()[0];
4554 let edge = graph.edge(edge_id).unwrap();
4555
4556 assert_eq!(edge.msg, "msg::B");
4557 assert_eq!(edge.order, 1);
4558 assert_eq!(
4559 src_node
4560 .nc_outputs_with_order()
4561 .map(|(msg, order)| (msg.as_str(), order))
4562 .collect::<Vec<_>>(),
4563 vec![("msg::A", 0)]
4564 );
4565 }
4566
4567 #[cfg(feature = "std")]
4568 fn multi_config_test_dir(name: &str) -> PathBuf {
4569 let unique = std::time::SystemTime::now()
4570 .duration_since(std::time::UNIX_EPOCH)
4571 .expect("system time before unix epoch")
4572 .as_nanos();
4573 let dir = std::env::temp_dir().join(format!("cu29_multi_config_{name}_{unique}"));
4574 std::fs::create_dir_all(&dir).expect("create temp test dir");
4575 dir
4576 }
4577
4578 #[cfg(feature = "std")]
4579 fn write_multi_config_file(dir: &Path, name: &str, contents: &str) -> PathBuf {
4580 let path = dir.join(name);
4581 std::fs::write(&path, contents).expect("write temp config file");
4582 path
4583 }
4584
4585 #[cfg(feature = "std")]
4586 fn alpha_subsystem_config() -> &'static str {
4587 r#"(
4588 tasks: [
4589 (id: "src", type: "demo::Src"),
4590 (id: "sink", type: "demo::Sink"),
4591 ],
4592 bridges: [
4593 (
4594 id: "zenoh",
4595 type: "demo::ZenohBridge",
4596 channels: [
4597 Tx(id: "ping"),
4598 Rx(id: "pong"),
4599 ],
4600 ),
4601 ],
4602 cnx: [
4603 (src: "src", dst: "zenoh/ping", msg: "demo::Ping"),
4604 (src: "zenoh/pong", dst: "sink", msg: "demo::Pong"),
4605 ],
4606 )"#
4607 }
4608
4609 #[cfg(feature = "std")]
4610 fn beta_subsystem_config() -> &'static str {
4611 r#"(
4612 tasks: [
4613 (id: "responder", type: "demo::Responder"),
4614 ],
4615 bridges: [
4616 (
4617 id: "zenoh",
4618 type: "demo::ZenohBridge",
4619 channels: [
4620 Rx(id: "ping"),
4621 Tx(id: "pong"),
4622 ],
4623 ),
4624 ],
4625 cnx: [
4626 (src: "zenoh/ping", dst: "responder", msg: "demo::Ping"),
4627 (src: "responder", dst: "zenoh/pong", msg: "demo::Pong"),
4628 ],
4629 )"#
4630 }
4631
4632 #[cfg(feature = "std")]
4633 fn instance_override_subsystem_config() -> &'static str {
4634 r#"(
4635 tasks: [
4636 (
4637 id: "imu",
4638 type: "demo::ImuTask",
4639 config: {
4640 "sample_hz": 200,
4641 },
4642 ),
4643 ],
4644 resources: [
4645 (
4646 id: "board",
4647 provider: "demo::BoardBundle",
4648 config: {
4649 "bus": "i2c-1",
4650 },
4651 ),
4652 ],
4653 bridges: [
4654 (
4655 id: "radio",
4656 type: "demo::RadioBridge",
4657 config: {
4658 "mtu": 32,
4659 },
4660 channels: [
4661 Tx(id: "tx"),
4662 Rx(id: "rx"),
4663 ],
4664 ),
4665 ],
4666 cnx: [
4667 (src: "imu", dst: "radio/tx", msg: "demo::Packet"),
4668 (src: "radio/rx", dst: "imu", msg: "demo::Packet"),
4669 ],
4670 )"#
4671 }
4672
4673 #[cfg(feature = "std")]
4674 #[test]
4675 fn test_read_multi_configuration_assigns_stable_subsystem_codes() {
4676 let dir = multi_config_test_dir("stable_ids");
4677 write_multi_config_file(&dir, "alpha.ron", alpha_subsystem_config());
4678 write_multi_config_file(&dir, "beta.ron", beta_subsystem_config());
4679 let network_path = write_multi_config_file(
4680 &dir,
4681 "network.ron",
4682 r#"(
4683 subsystems: [
4684 (id: "beta", config: "beta.ron"),
4685 (id: "alpha", config: "alpha.ron"),
4686 ],
4687 interconnects: [
4688 (from: "alpha/zenoh/ping", to: "beta/zenoh/ping", msg: "demo::Ping"),
4689 (from: "beta/zenoh/pong", to: "alpha/zenoh/pong", msg: "demo::Pong"),
4690 ],
4691 )"#,
4692 );
4693
4694 let config =
4695 read_multi_configuration(network_path.to_str().expect("network path utf8")).unwrap();
4696
4697 let alpha = config.subsystem("alpha").expect("alpha subsystem missing");
4698 let beta = config.subsystem("beta").expect("beta subsystem missing");
4699 assert_eq!(alpha.subsystem_code, 0);
4700 assert_eq!(beta.subsystem_code, 1);
4701 assert_eq!(config.interconnects.len(), 2);
4702 assert_eq!(config.interconnects[0].bridge_type, "demo::ZenohBridge");
4703 }
4704
4705 #[cfg(feature = "std")]
4706 #[test]
4707 fn test_read_multi_configuration_rejects_wrong_direction() {
4708 let dir = multi_config_test_dir("wrong_direction");
4709 write_multi_config_file(&dir, "alpha.ron", alpha_subsystem_config());
4710 write_multi_config_file(&dir, "beta.ron", beta_subsystem_config());
4711 let network_path = write_multi_config_file(
4712 &dir,
4713 "network.ron",
4714 r#"(
4715 subsystems: [
4716 (id: "alpha", config: "alpha.ron"),
4717 (id: "beta", config: "beta.ron"),
4718 ],
4719 interconnects: [
4720 (from: "alpha/zenoh/pong", to: "beta/zenoh/ping", msg: "demo::Pong"),
4721 ],
4722 )"#,
4723 );
4724
4725 let err = read_multi_configuration(network_path.to_str().expect("network path utf8"))
4726 .expect_err("direction mismatch should fail");
4727
4728 assert!(
4729 err.to_string()
4730 .contains("must reference a Tx bridge channel"),
4731 "unexpected error: {err}"
4732 );
4733 }
4734
4735 #[cfg(feature = "std")]
4736 #[test]
4737 fn test_read_multi_configuration_rejects_declared_message_mismatch() {
4738 let dir = multi_config_test_dir("msg_mismatch");
4739 write_multi_config_file(&dir, "alpha.ron", alpha_subsystem_config());
4740 write_multi_config_file(&dir, "beta.ron", beta_subsystem_config());
4741 let network_path = write_multi_config_file(
4742 &dir,
4743 "network.ron",
4744 r#"(
4745 subsystems: [
4746 (id: "alpha", config: "alpha.ron"),
4747 (id: "beta", config: "beta.ron"),
4748 ],
4749 interconnects: [
4750 (from: "alpha/zenoh/ping", to: "beta/zenoh/ping", msg: "demo::Wrong"),
4751 ],
4752 )"#,
4753 );
4754
4755 let err = read_multi_configuration(network_path.to_str().expect("network path utf8"))
4756 .expect_err("message mismatch should fail");
4757
4758 assert!(
4759 err.to_string()
4760 .contains("declares message type 'demo::Wrong'"),
4761 "unexpected error: {err}"
4762 );
4763 }
4764
4765 #[cfg(feature = "std")]
4766 #[test]
4767 fn test_read_multi_configuration_resolves_instance_override_root() {
4768 let dir = multi_config_test_dir("instance_root");
4769 write_multi_config_file(&dir, "robot.ron", instance_override_subsystem_config());
4770 let network_path = write_multi_config_file(
4771 &dir,
4772 "multi_copper.ron",
4773 r#"(
4774 subsystems: [
4775 (id: "robot", config: "robot.ron"),
4776 ],
4777 interconnects: [],
4778 instance_overrides_root: "instances",
4779 )"#,
4780 );
4781
4782 let config =
4783 read_multi_configuration(network_path.to_str().expect("network path utf8")).unwrap();
4784
4785 assert_eq!(
4786 config.instance_overrides_root.as_deref().map(Path::new),
4787 Some(dir.join("instances").as_path())
4788 );
4789 }
4790
4791 #[cfg(feature = "std")]
4792 #[test]
4793 fn test_resolve_subsystem_config_for_instance_applies_overrides() {
4794 let dir = multi_config_test_dir("instance_apply");
4795 write_multi_config_file(&dir, "robot.ron", instance_override_subsystem_config());
4796 let instances_dir = dir.join("instances").join("17");
4797 std::fs::create_dir_all(&instances_dir).expect("create instance dir");
4798 write_multi_config_file(
4799 &instances_dir,
4800 "robot.ron",
4801 r#"(
4802 set: [
4803 (
4804 path: "tasks/imu/config",
4805 value: {
4806 "gyro_bias": [0.1, -0.2, 0.3],
4807 },
4808 ),
4809 (
4810 path: "resources/board/config",
4811 value: {
4812 "bus": "robot17-imu",
4813 },
4814 ),
4815 (
4816 path: "bridges/radio/config",
4817 value: {
4818 "mtu": 64,
4819 },
4820 ),
4821 ],
4822 )"#,
4823 );
4824 let network_path = write_multi_config_file(
4825 &dir,
4826 "multi_copper.ron",
4827 r#"(
4828 subsystems: [
4829 (id: "robot", config: "robot.ron"),
4830 ],
4831 interconnects: [],
4832 instance_overrides_root: "instances",
4833 )"#,
4834 );
4835
4836 let multi =
4837 read_multi_configuration(network_path.to_str().expect("network path utf8")).unwrap();
4838 let effective = multi
4839 .resolve_subsystem_config_for_instance("robot", 17)
4840 .expect("effective config");
4841
4842 let graph = effective.get_graph(None).expect("graph");
4843 let imu_id = graph.get_node_id_by_name("imu").expect("imu node");
4844 let imu = graph.get_node(imu_id).expect("imu weight");
4845 let imu_cfg = imu.get_instance_config().expect("imu config");
4846 assert_eq!(imu_cfg.get::<u64>("sample_hz").unwrap(), Some(200));
4847 let gyro_bias: Vec<f64> = imu_cfg
4848 .get_value("gyro_bias")
4849 .expect("gyro_bias deserialize")
4850 .expect("gyro_bias value");
4851 assert_eq!(gyro_bias, vec![0.1, -0.2, 0.3]);
4852
4853 let board = effective
4854 .resources
4855 .iter()
4856 .find(|resource| resource.id == "board")
4857 .expect("board resource");
4858 assert_eq!(
4859 board.config.as_ref().unwrap().get::<String>("bus").unwrap(),
4860 Some("robot17-imu".to_string())
4861 );
4862
4863 let radio = effective
4864 .bridges
4865 .iter()
4866 .find(|bridge| bridge.id == "radio")
4867 .expect("radio bridge");
4868 assert_eq!(
4869 radio.config.as_ref().unwrap().get::<u64>("mtu").unwrap(),
4870 Some(64)
4871 );
4872
4873 let radio_id = graph.get_node_id_by_name("radio").expect("radio node");
4874 let radio_node = graph.get_node(radio_id).expect("radio weight");
4875 assert_eq!(
4876 radio_node
4877 .get_instance_config()
4878 .unwrap()
4879 .get::<u64>("mtu")
4880 .unwrap(),
4881 Some(64)
4882 );
4883 }
4884
4885 #[cfg(feature = "std")]
4886 #[test]
4887 fn test_resolve_subsystem_config_for_instance_rejects_unknown_path() {
4888 let dir = multi_config_test_dir("instance_unknown");
4889 write_multi_config_file(&dir, "robot.ron", instance_override_subsystem_config());
4890 let instances_dir = dir.join("instances").join("17");
4891 std::fs::create_dir_all(&instances_dir).expect("create instance dir");
4892 write_multi_config_file(
4893 &instances_dir,
4894 "robot.ron",
4895 r#"(
4896 set: [
4897 (
4898 path: "tasks/missing/config",
4899 value: {
4900 "gyro_bias": [1.0, 2.0, 3.0],
4901 },
4902 ),
4903 ],
4904 )"#,
4905 );
4906 let network_path = write_multi_config_file(
4907 &dir,
4908 "multi_copper.ron",
4909 r#"(
4910 subsystems: [
4911 (id: "robot", config: "robot.ron"),
4912 ],
4913 interconnects: [],
4914 instance_overrides_root: "instances",
4915 )"#,
4916 );
4917
4918 let multi =
4919 read_multi_configuration(network_path.to_str().expect("network path utf8")).unwrap();
4920 let err = multi
4921 .resolve_subsystem_config_for_instance("robot", 17)
4922 .expect_err("unknown task override should fail");
4923
4924 assert!(
4925 err.to_string().contains("targets unknown task 'missing'"),
4926 "unexpected error: {err}"
4927 );
4928 }
4929}