Skip to main content

cu29/
lib.rs

1//! # Copper Runtime & SDK
2//!
3//! Think of Copper as a robotics game engine: define a task graph, compile once,
4//! and get deterministic execution, unified logging, and sub-microsecond
5//! latency from Linux workstations all the way down to bare-metal MPU builds.
6//!
7//! ## Quick start
8//!
9//! ```bash
10//! cargo install cargo-cunew
11//! cargo cunew /path/to/my_robot
12//! cd /path/to/my_robot
13//! cargo run
14//! ```
15//!
16//! It will generate a minimal Copper robot project at `/path/to/my_robot` using the latest
17//! stable Copper crates from crates.io by default.
18//!
19//! ## Feature flags
20//!
21//! - `default` = `["std", "signal-handler", "textlogs", "units"]`
22//! - `units`: exposes `cu29::units` (re-export of `cu29-units`)
23//! - `std`: host/runtime support that is also safe to compile for browser targets
24//! - `signal-handler`: desktop Ctrl-C integration for generated `run()` loops
25//! - `reflect`: reflection support for runtime and units types
26//! - `textlogs`: text logging derive support
27//! - `remote-debug`: remote debug transport support
28//! - `sysclock-perf`: use a host/system clock for runtime perf timing while keeping robot time for `tov` and `rate_target_hz`
29//! - `high-precision-limiter`: std-only hybrid sleep/spin loop limiter for tighter `rate_target_hz` cadence
30//! - `async-cl-io`: offload CopperList serialization/logging to a dedicated std thread
31//! - `parallel-rt`: prepare the runtime for a future multi-threaded deterministic executor
32//! - `safety-ids`: std-only safety-case metadata collection and JSON export helpers
33//!
34//! ## Concepts behind Copper
35//!
36//! Check out the [Copper Wiki](https://github.com/copper-project/copper-rs/wiki) to understand the
37//! deployments concepts, task lifecycle, available components, etc ...
38//!
39//! ## More examples to get you started
40//!
41//! - `examples/cu_caterpillar`: a minimal running example passing around booleans.
42//! - `examples/cu_rp_balancebot`: a more complete example try Copper without hardware via
43//!   `cargo install cu-rp-balancebot` + `balancebot-sim` (Bevy + Avian3d).
44//!
45//! ## Key traits and structs to check out
46//!
47//! - `cu29_runtime::app::CuApp`: the main trait the copper runtime will expose to run your application. (when run() etc .. is coming from)
48//! - `cu29_runtime::config::CuConfig`: the configuration of your runtime
49//! - `cu29_runtime::cutask::CuTask`: the core trait and helpers to implement your own tasks.
50//! - `cu29_runtime::cubridge::CuBridge`: the trait to implement bridges to hardware or other software.
51//! - `cu29_runtime::curuntime::CuRuntime`: the runtime that manages task execution.
52//! - `cu29_runtime::simulation`: This will explain how to hook up your tasks to a simulation environment.
53//!
54//! ## V1 API status
55//!
56//! The V1 public contract is defined in `doc/v1-api-surface.md`. The prelude is the
57//! canonical application import surface; lower-level modules remain addressable by
58//! module path when needed, but are not implicitly part of the prelude contract.
59//!
60//! Need help or want to show what you're building? Join
61//! [Discord](https://discord.gg/VkCG7Sb9Kw) and hop into the #general channel.
62//!
63
64#![cfg_attr(not(feature = "std"), no_std)]
65#[cfg(all(feature = "parallel-rt", not(feature = "std")))]
66compile_error!("feature `parallel-rt` requires `std`");
67#[cfg(not(feature = "std"))]
68extern crate alloc;
69extern crate self as cu29;
70
71pub use cu29_derive::{bundle_resources, resources, safety_case};
72pub use cu29_runtime::app;
73pub use cu29_runtime::config;
74pub use cu29_runtime::context;
75pub use cu29_runtime::copperlist;
76#[cfg(feature = "std")]
77pub use cu29_runtime::cuasynctask;
78pub use cu29_runtime::cubridge;
79pub use cu29_runtime::curuntime;
80pub use cu29_runtime::cutask;
81#[cfg(feature = "std")]
82pub use cu29_runtime::debug;
83#[cfg(feature = "std")]
84pub use cu29_runtime::distributed_replay;
85pub use cu29_runtime::input_msg;
86pub use cu29_runtime::logcodec;
87pub use cu29_runtime::monitoring;
88pub use cu29_runtime::output_msg;
89#[cfg(all(feature = "std", feature = "parallel-rt"))]
90pub use cu29_runtime::parallel_queue;
91#[cfg(all(feature = "std", feature = "parallel-rt"))]
92pub use cu29_runtime::parallel_rt;
93pub use cu29_runtime::payload;
94pub use cu29_runtime::reflect;
95pub use cu29_runtime::reflect as bevy_reflect;
96#[cfg(feature = "remote-debug")]
97pub use cu29_runtime::remote_debug;
98#[cfg(feature = "std")]
99pub use cu29_runtime::replay;
100pub use cu29_runtime::resource;
101pub use cu29_runtime::rx_channels;
102#[cfg(feature = "std")]
103pub use cu29_runtime::simulation;
104pub use cu29_runtime::tx_channels;
105#[cfg(feature = "safety-ids")]
106pub mod safety;
107#[cfg(all(feature = "std", any(test, feature = "safety-ids")))]
108mod safety_runtime_cases;
109
110#[cfg(feature = "safety-ids")]
111#[doc(hidden)]
112pub fn link_safety_ids() {
113    safety_runtime_cases::link_safety_ids();
114}
115
116#[cfg(feature = "rtsan")]
117pub mod rtsan {
118    pub use rtsan_standalone::*;
119}
120
121#[cfg(not(feature = "rtsan"))]
122pub mod rtsan {
123    use core::ffi::CStr;
124
125    #[derive(Default)]
126    pub struct ScopedSanitizeRealtime;
127
128    #[derive(Default)]
129    pub struct ScopedDisabler;
130
131    #[inline]
132    pub fn realtime_enter() {}
133
134    #[inline]
135    pub fn realtime_exit() {}
136
137    #[inline]
138    pub fn disable() {}
139
140    #[inline]
141    pub fn enable() {}
142
143    #[inline]
144    pub fn ensure_initialized() {}
145
146    #[allow(unused_variables)]
147    pub fn notify_blocking_call(_function_name: &'static CStr) {}
148}
149
150pub use bincode;
151pub use cu29_clock as clock;
152#[cfg(feature = "units")]
153pub use cu29_units as units;
154#[doc(hidden)]
155pub use serde;
156#[cfg(feature = "defmt")]
157pub mod defmt {
158    pub use defmt::{debug, error, info, warn};
159}
160#[cfg(feature = "std")]
161pub use cu29_runtime::config::read_configuration;
162#[cfg(feature = "std")]
163pub use cu29_runtime::config::read_multi_configuration;
164pub use cu29_traits::*;
165
166#[cfg(feature = "std")]
167pub use rayon;
168
169#[doc(hidden)]
170pub mod __private {
171    #[doc(hidden)]
172    pub mod sync {
173        #[cfg(not(feature = "std"))]
174        pub use alloc::sync::Arc;
175        #[cfg(not(feature = "std"))]
176        pub use spin::Mutex;
177        #[cfg(feature = "std")]
178        pub use std::sync::{Arc, Mutex};
179    }
180}
181
182// defmt shims re-exported for proc-macro call sites
183#[cfg(all(feature = "defmt", not(feature = "std")))]
184#[macro_export]
185macro_rules! defmt_debug {
186    ($fmt:literal $(, $arg:expr)* $(,)?) => {
187        $crate::defmt::debug!($fmt $(, $arg)*);
188    }
189}
190#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
191#[macro_export]
192macro_rules! defmt_debug {
193    ($($tt:tt)*) => {{}};
194}
195
196#[cfg(all(feature = "defmt", not(feature = "std")))]
197#[macro_export]
198macro_rules! defmt_info {
199    ($fmt:literal $(, $arg:expr)* $(,)?) => {
200        $crate::defmt::info!($fmt $(, $arg)*);
201    }
202}
203#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
204#[macro_export]
205macro_rules! defmt_info {
206    ($($tt:tt)*) => {{}};
207}
208
209#[cfg(all(feature = "defmt", not(feature = "std")))]
210#[macro_export]
211macro_rules! defmt_warn {
212    ($fmt:literal $(, $arg:expr)* $(,)?) => {
213        $crate::defmt::warn!($fmt $(, $arg)*);
214    }
215}
216#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
217#[macro_export]
218macro_rules! defmt_warn {
219    ($($tt:tt)*) => {{}};
220}
221
222#[cfg(all(feature = "defmt", not(feature = "std")))]
223#[macro_export]
224macro_rules! defmt_error {
225    ($fmt:literal $(, $arg:expr)* $(,)?) => {
226        $crate::defmt::error!($fmt $(, $arg)*);
227    }
228}
229#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
230#[macro_export]
231macro_rules! defmt_error {
232    ($($tt:tt)*) => {{}};
233}
234
235#[macro_export]
236macro_rules! safety_check {
237    ($check_id:literal, $requirement_id:literal, $condition:expr $(,)?) => {
238        assert!(
239            $condition,
240            "safety check {} for requirement {} failed",
241            $check_id, $requirement_id
242        );
243    };
244}
245
246#[macro_export]
247macro_rules! safety_check_eq {
248    ($check_id:literal, $requirement_id:literal, $left:expr, $right:expr $(,)?) => {
249        assert_eq!(
250            $left, $right,
251            "safety check {} for requirement {} failed",
252            $check_id, $requirement_id
253        );
254    };
255}
256
257/// Canonical imports for Copper applications.
258///
259/// This module intentionally re-exports each stable application-facing group once.
260/// Runtime internals, remote-debug plumbing, and experimental executor APIs should
261/// be imported from their explicit module paths instead of from the prelude.
262pub mod prelude {
263    pub use crate::bevy_reflect;
264    #[cfg(feature = "units")]
265    pub use crate::units;
266    pub use crate::{defmt_debug, defmt_error, defmt_info, defmt_warn};
267    pub use crate::{safety_case, safety_check, safety_check_eq};
268    #[cfg(feature = "reflect")]
269    pub use bevy_reflect_derive::Reflect;
270    #[cfg(feature = "signal-handler")]
271    pub use ctrlc;
272    pub use cu29_clock::*;
273    pub use cu29_derive::*;
274    pub use cu29_log::*;
275    pub use cu29_log_derive::*;
276    pub use cu29_log_runtime::*;
277    #[cfg(not(feature = "reflect"))]
278    pub use cu29_reflect_derive::Reflect;
279    pub use cu29_runtime::app;
280    pub use cu29_runtime::app::*;
281    pub use cu29_runtime::config::*;
282    pub use cu29_runtime::context::*;
283    pub use cu29_runtime::copperlist::*;
284    pub use cu29_runtime::cubridge::*;
285    pub use cu29_runtime::curuntime::{
286        CuRuntime, KeyFrame, RuntimeLifecycleConfigSource, RuntimeLifecycleEvent,
287        RuntimeLifecycleRecord, RuntimeLifecycleStackInfo,
288    };
289    pub use cu29_runtime::cutask::*;
290    #[cfg(feature = "std")]
291    pub use cu29_runtime::debug::*;
292    pub use cu29_runtime::input_msg;
293    pub use cu29_runtime::monitoring::*;
294    pub use cu29_runtime::output_msg;
295    pub use cu29_runtime::payload::*;
296    #[cfg(feature = "std")]
297    pub use cu29_runtime::pool::*;
298    #[cfg(feature = "reflect")]
299    pub use cu29_runtime::reflect::serde as reflect_serde;
300    #[cfg(feature = "reflect")]
301    pub use cu29_runtime::reflect::serde::{
302        ReflectSerializer, SerializationData, TypedReflectSerializer,
303    };
304    pub use cu29_runtime::reflect::{
305        GetTypeRegistration, ReflectTaskIntrospection, ReflectTypePath, TypeInfo, TypePath,
306        TypeRegistry, dump_type_registry_schema,
307    };
308    pub use cu29_runtime::resource::*;
309    pub use cu29_runtime::rx_channels;
310    #[cfg(feature = "std")]
311    pub use cu29_runtime::simulation::*;
312    pub use cu29_runtime::tx_channels;
313    pub use cu29_traits::{
314        COMPACT_STRING_CAPACITY, CopperListTuple, CuCompactString, CuError, CuMsgMetadataTrait,
315        CuMsgOrigin, CuPayloadRawBytes, CuResult, DebugFieldDescriptor, DebugFieldKind,
316        DebugFieldSemantics, DebugScalarRegistration, DebugScalarType, ErasedCuStampedData,
317        ErasedCuStampedDataSet, MatchingTasks, Metadata, ObservedWriter, PayloadSchemas,
318        TaskOutputSpec, UnifiedLogType, WriteStream, abort_observed_encode, begin_observed_encode,
319        finish_observed_encode, observed_encode_bytes, record_observed_encode_bytes, with_cause,
320    };
321    #[cfg(feature = "std")]
322    pub use cu29_unifiedlog::memmap;
323    pub use cu29_unifiedlog::*;
324    pub use cu29_value::Value;
325    pub use cu29_value::to_value;
326    pub use serde_derive::{Deserialize, Serialize};
327}
328
329#[cfg(all(test, feature = "std"))]
330mod tests {
331    use super::prelude::*;
332    use std::sync::{Arc, Mutex, OnceLock};
333
334    #[derive(Debug)]
335    struct CaptureStream;
336
337    impl WriteStream<CuLogEntry> for CaptureStream {
338        fn log(&mut self, _obj: &CuLogEntry) -> CuResult<()> {
339            Ok(())
340        }
341    }
342
343    fn logger_test_lock() -> std::sync::MutexGuard<'static, ()> {
344        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
345        LOCK.get_or_init(|| Mutex::new(()))
346            .lock()
347            .unwrap_or_else(|poison| poison.into_inner())
348    }
349
350    fn capture_one_log<F>(emit: F) -> CuLogEntry
351    where
352        F: FnOnce(),
353    {
354        let _guard = logger_test_lock();
355        let runtime = LoggerRuntime::init(RobotClock::default(), CaptureStream, None::<NullLog>);
356        let captured = Arc::new(Mutex::new(Vec::new()));
357        let sink = captured.clone();
358        register_live_log_listener(move |entry, _, _| {
359            sink.lock()
360                .unwrap_or_else(|poison| poison.into_inner())
361                .push(entry.clone());
362        });
363
364        emit();
365
366        unregister_live_log_listener();
367        drop(runtime);
368
369        let entries = captured.lock().unwrap_or_else(|poison| poison.into_inner());
370        assert_eq!(entries.len(), 1, "expected exactly one captured log entry");
371        entries[0].clone()
372    }
373
374    #[test]
375    fn explicit_context_logs_capture_task_origin() {
376        let mut ctx = CuContext::builder(RobotClock::default())
377            .cl_id(77)
378            .task_ids(&["task-0"])
379            .build();
380        ctx.set_current_task(0);
381
382        let entry = capture_one_log(|| {
383            debug!(ctx, "task log {}", 7);
384        });
385
386        assert_eq!(entry.origin.culistid, Some(77));
387        assert_eq!(entry.origin.component_id, Some(0));
388        assert_eq!(entry.origin.task_index, Some(0));
389    }
390
391    #[test]
392    fn explicit_context_logs_capture_bridge_component_origin() {
393        let mut ctx = CuContext::builder(RobotClock::default()).cl_id(88).build();
394        ctx.set_current_component(5);
395        ctx.clear_current_task();
396
397        let entry = capture_one_log(|| {
398            info!(ctx, "bridge log {}", 3);
399        });
400
401        assert_eq!(entry.origin.culistid, Some(88));
402        assert_eq!(entry.origin.component_id, Some(5));
403        assert_eq!(entry.origin.task_index, None);
404    }
405
406    #[test]
407    fn context_free_logs_leave_origin_empty() {
408        let entry = capture_one_log(|| {
409            warning!("context free {}", 1);
410        });
411
412        assert_eq!(entry.origin, CuLogOrigin::default());
413    }
414}