cu29_traits/
lib.rs

1//! Common copper traits and types for robotics systems.
2//!
3//! This crate is no_std compatible by default. Enable the "std" feature for additional
4//! functionality like implementing `std::error::Error` for `CuError` and the
5//! `new_with_cause` method that accepts types implementing `std::error::Error`.
6//!
7//! # Features
8//!
9//! - `std` (default): Enables standard library support
10//!   - Implements `std::error::Error` for `CuError`
11//!   - Adds `CuError::new_with_cause()` method for interop with std error types
12//!
13//! # no_std Usage
14//!
15//! To use without the standard library:
16//!
17//! ```toml
18//! [dependencies]
19//! cu29-traits = { version = "0.9", default-features = false }
20//! ```
21
22#![cfg_attr(not(feature = "std"), no_std)]
23extern crate alloc;
24
25use bincode::de::{BorrowDecoder, Decoder};
26use bincode::enc::Encoder;
27use bincode::error::{DecodeError, EncodeError};
28use bincode::{BorrowDecode, Decode as dDecode, Decode, Encode, Encode as dEncode};
29use compact_str::CompactString;
30use cu29_clock::{PartialCuTimeRange, Tov};
31use serde::{Deserialize, Serialize};
32
33use alloc::string::{String, ToString};
34use alloc::vec::Vec;
35#[cfg(not(feature = "std"))]
36use core::error::Error as CoreError;
37use core::fmt::{Debug, Display, Formatter};
38#[cfg(feature = "std")]
39use std::error::Error;
40
41/// Common copper Error type.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct CuError {
44    message: String,
45    cause: Option<String>,
46}
47
48impl Display for CuError {
49    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
50        let context_str = match &self.cause {
51            Some(c) => c.to_string(),
52            None => "None".to_string(),
53        };
54        write!(f, "{}\n   context:{}", self.message, context_str)?;
55        Ok(())
56    }
57}
58
59#[cfg(not(feature = "std"))]
60impl CoreError for CuError {}
61
62#[cfg(feature = "std")]
63impl Error for CuError {}
64
65impl From<&str> for CuError {
66    fn from(s: &str) -> CuError {
67        CuError {
68            message: s.to_string(),
69            cause: None,
70        }
71    }
72}
73
74impl From<String> for CuError {
75    fn from(s: String) -> CuError {
76        CuError {
77            message: s,
78            cause: None,
79        }
80    }
81}
82
83impl CuError {
84    pub fn new_with_cause(message: &str, cause: impl Display) -> CuError {
85        CuError {
86            message: message.to_string(),
87            cause: Some(cause.to_string()),
88        }
89    }
90
91    pub fn add_cause(mut self, context: &str) -> CuError {
92        self.cause = Some(context.into());
93        self
94    }
95}
96
97// Generic Result type for copper.
98pub type CuResult<T> = Result<T, CuError>;
99
100/// Defines a basic write, append only stream trait to be able to log or send serializable objects.
101pub trait WriteStream<E: Encode>: Debug + Send + Sync {
102    fn log(&mut self, obj: &E) -> CuResult<()>;
103    fn flush(&mut self) -> CuResult<()> {
104        Ok(())
105    }
106}
107
108/// Defines the types of what can be logged in the unified logger.
109#[derive(dEncode, dDecode, Copy, Clone, Debug, PartialEq)]
110pub enum UnifiedLogType {
111    Empty,             // Dummy default used as a debug marker
112    StructuredLogLine, // This is for the structured logs (ie. debug! etc..)
113    CopperList,        // This is the actual data log storing activities between tasks.
114    FrozenTasks,       // Log of all frozen state of the tasks.
115    LastEntry,         // This is a special entry that is used to signal the end of the log.
116}
117/// Represent the minimum set of traits to be usable as Metadata in Copper.
118pub trait Metadata: Default + Debug + Clone + Encode + Decode<()> + Serialize {}
119
120impl Metadata for () {}
121
122/// Key metadata piece attached to every message in Copper.
123pub trait CuMsgMetadataTrait {
124    /// The time range used for the processing of this message
125    fn process_time(&self) -> PartialCuTimeRange;
126
127    /// Small status text for user UI to get the realtime state of task (max 24 chrs)
128    fn status_txt(&self) -> &CuCompactString;
129}
130
131/// A generic trait to expose the generated CuStampedDataSet from the task graph.
132pub trait ErasedCuStampedData {
133    fn payload(&self) -> Option<&dyn erased_serde::Serialize>;
134    fn tov(&self) -> Tov;
135    fn metadata(&self) -> &dyn CuMsgMetadataTrait;
136}
137
138/// Trait to get a vector of type-erased CuStampedDataSet
139/// This is used for generic serialization of the copperlists
140pub trait ErasedCuStampedDataSet {
141    fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData>;
142}
143
144/// Trait to trace back from the CopperList the origin of the messages
145pub trait MatchingTasks {
146    fn get_all_task_ids() -> &'static [&'static str];
147}
148
149/// A CopperListTuple needs to be encodable, decodable and fixed size in memory.
150pub trait CopperListTuple:
151    bincode::Encode
152    + bincode::Decode<()>
153    + Debug
154    + Serialize
155    + ErasedCuStampedDataSet
156    + MatchingTasks
157    + Default
158{
159} // Decode forces Sized already
160
161// Also anything that follows this contract can be a payload (blanket implementation)
162impl<T> CopperListTuple for T where
163    T: bincode::Encode
164        + bincode::Decode<()>
165        + Debug
166        + Serialize
167        + ErasedCuStampedDataSet
168        + MatchingTasks
169        + Default
170{
171}
172
173// We use this type to convey very small status messages.
174// MAX_SIZE from their repr module is not accessible so we need to copy paste their definition for 24
175// which is the maximum size for inline allocation (no heap)
176pub const COMPACT_STRING_CAPACITY: usize = size_of::<String>();
177
178#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
179pub struct CuCompactString(pub CompactString);
180
181impl Encode for CuCompactString {
182    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
183        let CuCompactString(compact_string) = self;
184        let bytes = &compact_string.as_bytes();
185        bytes.encode(encoder)
186    }
187}
188
189impl Debug for CuCompactString {
190    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
191        if self.0.is_empty() {
192            return write!(f, "CuCompactString(Empty)");
193        }
194        write!(f, "CuCompactString({})", self.0)
195    }
196}
197
198impl<Context> Decode<Context> for CuCompactString {
199    fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
200        let bytes = <Vec<u8> as Decode<D::Context>>::decode(decoder)?; // Decode into a byte buffer
201        let compact_string =
202            CompactString::from_utf8(bytes).map_err(|e| DecodeError::Utf8 { inner: e })?;
203        Ok(CuCompactString(compact_string))
204    }
205}
206
207impl<'de, Context> BorrowDecode<'de, Context> for CuCompactString {
208    fn borrow_decode<D: BorrowDecoder<'de>>(decoder: &mut D) -> Result<Self, DecodeError> {
209        CuCompactString::decode(decoder)
210    }
211}
212
213#[cfg(feature = "defmt")]
214impl defmt::Format for CuError {
215    fn format(&self, f: defmt::Formatter) {
216        match &self.cause {
217            Some(c) => defmt::write!(
218                f,
219                "CuError {{ message: {}, cause: {} }}",
220                defmt::Display2Format(&self.message),
221                defmt::Display2Format(c),
222            ),
223            None => defmt::write!(
224                f,
225                "CuError {{ message: {}, cause: None }}",
226                defmt::Display2Format(&self.message),
227            ),
228        }
229    }
230}
231
232#[cfg(feature = "defmt")]
233impl defmt::Format for CuCompactString {
234    fn format(&self, f: defmt::Formatter) {
235        if self.0.is_empty() {
236            defmt::write!(f, "CuCompactString(Empty)");
237        } else {
238            defmt::write!(f, "CuCompactString({})", defmt::Display2Format(&self.0));
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use crate::CuCompactString;
246    use bincode::{config, decode_from_slice, encode_to_vec};
247    use compact_str::CompactString;
248
249    #[test]
250    fn test_cucompactstr_encode_decode_empty() {
251        let cstr = CuCompactString(CompactString::from(""));
252        let config = config::standard();
253        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
254        assert_eq!(encoded.len(), 1); // This encodes the usize 0 in variable encoding so 1 byte which is 0.
255        let (decoded, _): (CuCompactString, usize) =
256            decode_from_slice(&encoded, config).expect("Decoding failed");
257        assert_eq!(cstr.0, decoded.0);
258    }
259
260    #[test]
261    fn test_cucompactstr_encode_decode_small() {
262        let cstr = CuCompactString(CompactString::from("test"));
263        let config = config::standard();
264        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
265        assert_eq!(encoded.len(), 5); // This encodes a 4-byte string "test" plus 1 byte for the length prefix.
266        let (decoded, _): (CuCompactString, usize) =
267            decode_from_slice(&encoded, config).expect("Decoding failed");
268        assert_eq!(cstr.0, decoded.0);
269    }
270}