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