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;
94#[cfg(feature = "std")]
95pub use cu29_runtime::pool;
96pub use cu29_runtime::reflect;
97pub use cu29_runtime::reflect as bevy_reflect;
98#[cfg(feature = "remote-debug")]
99pub use cu29_runtime::remote_debug;
100#[cfg(feature = "std")]
101pub use cu29_runtime::replay;
102pub use cu29_runtime::resource;
103pub use cu29_runtime::rx_channels;
104#[cfg(feature = "std")]
105pub use cu29_runtime::simulation;
106#[cfg(feature = "std")]
107pub use cu29_runtime::thread_pool;
108pub use cu29_runtime::tx_channels;
109#[cfg(feature = "safety-ids")]
110pub mod safety;
111#[cfg(all(feature = "std", any(test, feature = "safety-ids")))]
112mod safety_runtime_cases;
113
114#[cfg(feature = "safety-ids")]
115#[doc(hidden)]
116pub fn link_safety_ids() {
117    safety_runtime_cases::link_safety_ids();
118}
119
120#[cfg(feature = "rtsan")]
121pub mod rtsan {
122    pub use rtsan_standalone::*;
123}
124
125#[cfg(not(feature = "rtsan"))]
126pub mod rtsan {
127    use core::ffi::CStr;
128
129    #[derive(Default)]
130    pub struct ScopedSanitizeRealtime;
131
132    #[derive(Default)]
133    pub struct ScopedDisabler;
134
135    #[inline]
136    pub fn realtime_enter() {}
137
138    #[inline]
139    pub fn realtime_exit() {}
140
141    #[inline]
142    pub fn disable() {}
143
144    #[inline]
145    pub fn enable() {}
146
147    #[inline]
148    pub fn ensure_initialized() {}
149
150    #[allow(unused_variables)]
151    pub fn notify_blocking_call(_function_name: &'static CStr) {}
152}
153
154pub use bincode;
155pub use cu29_clock as clock;
156#[cfg(feature = "units")]
157pub use cu29_units as units;
158#[doc(hidden)]
159pub use serde;
160#[cfg(feature = "defmt")]
161pub mod defmt {
162    pub use defmt::{debug, error, info, warn};
163}
164#[cfg(feature = "std")]
165pub use cu29_runtime::config::read_configuration;
166#[cfg(feature = "std")]
167pub use cu29_runtime::config::read_multi_configuration;
168pub use cu29_traits::*;
169
170#[cfg(feature = "std")]
171pub use rayon;
172
173#[doc(hidden)]
174pub mod __private {
175    #[doc(hidden)]
176    pub mod sync {
177        #[cfg(not(feature = "std"))]
178        pub use alloc::sync::Arc;
179        #[cfg(not(feature = "std"))]
180        pub use spin::Mutex;
181        #[cfg(feature = "std")]
182        pub use std::sync::{Arc, Mutex};
183    }
184}
185
186// defmt shims re-exported for proc-macro call sites
187#[cfg(all(feature = "defmt", not(feature = "std")))]
188#[macro_export]
189macro_rules! defmt_debug {
190    ($fmt:literal $(, $arg:expr)* $(,)?) => {
191        $crate::defmt::debug!($fmt $(, $arg)*);
192    }
193}
194#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
195#[macro_export]
196macro_rules! defmt_debug {
197    ($($tt:tt)*) => {{}};
198}
199
200#[cfg(all(feature = "defmt", not(feature = "std")))]
201#[macro_export]
202macro_rules! defmt_info {
203    ($fmt:literal $(, $arg:expr)* $(,)?) => {
204        $crate::defmt::info!($fmt $(, $arg)*);
205    }
206}
207#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
208#[macro_export]
209macro_rules! defmt_info {
210    ($($tt:tt)*) => {{}};
211}
212
213#[cfg(all(feature = "defmt", not(feature = "std")))]
214#[macro_export]
215macro_rules! defmt_warn {
216    ($fmt:literal $(, $arg:expr)* $(,)?) => {
217        $crate::defmt::warn!($fmt $(, $arg)*);
218    }
219}
220#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
221#[macro_export]
222macro_rules! defmt_warn {
223    ($($tt:tt)*) => {{}};
224}
225
226#[cfg(all(feature = "defmt", not(feature = "std")))]
227#[macro_export]
228macro_rules! defmt_error {
229    ($fmt:literal $(, $arg:expr)* $(,)?) => {
230        $crate::defmt::error!($fmt $(, $arg)*);
231    }
232}
233#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
234#[macro_export]
235macro_rules! defmt_error {
236    ($($tt:tt)*) => {{}};
237}
238
239#[macro_export]
240macro_rules! safety_check {
241    ($check_id:literal, $requirement_id:literal, $condition:expr $(,)?) => {
242        assert!(
243            $condition,
244            "safety check {} for requirement {} failed",
245            $check_id, $requirement_id
246        );
247    };
248}
249
250#[macro_export]
251macro_rules! safety_check_eq {
252    ($check_id:literal, $requirement_id:literal, $left:expr, $right:expr $(,)?) => {
253        assert_eq!(
254            $left, $right,
255            "safety check {} for requirement {} failed",
256            $check_id, $requirement_id
257        );
258    };
259}
260
261/// Canonical imports for Copper applications.
262///
263/// This module intentionally re-exports each stable application-facing group once.
264/// Runtime internals, remote-debug plumbing, and experimental executor APIs should
265/// be imported from their explicit module paths instead of from the prelude.
266pub mod prelude {
267    pub use crate::bevy_reflect;
268    #[cfg(feature = "units")]
269    pub use crate::units;
270    pub use crate::{defmt_debug, defmt_error, defmt_info, defmt_warn};
271    pub use crate::{safety_case, safety_check, safety_check_eq};
272    #[cfg(feature = "reflect")]
273    pub use bevy_reflect_derive::Reflect;
274    #[cfg(feature = "signal-handler")]
275    pub use ctrlc;
276    pub use cu29_clock::*;
277    pub use cu29_derive::*;
278    pub use cu29_log::*;
279    pub use cu29_log_derive::*;
280    pub use cu29_log_runtime::*;
281    #[cfg(not(feature = "reflect"))]
282    pub use cu29_reflect_derive::Reflect;
283    pub use cu29_runtime::app;
284    pub use cu29_runtime::app::*;
285    pub use cu29_runtime::config::*;
286    pub use cu29_runtime::context::*;
287    pub use cu29_runtime::copperlist::*;
288    pub use cu29_runtime::cubridge::*;
289    pub use cu29_runtime::curuntime::{
290        CuRuntime, KeyFrame, RuntimeLifecycleConfigSource, RuntimeLifecycleEvent,
291        RuntimeLifecycleRecord, RuntimeLifecycleStackInfo,
292    };
293    pub use cu29_runtime::cutask::*;
294    #[cfg(feature = "std")]
295    pub use cu29_runtime::debug::*;
296    pub use cu29_runtime::input_msg;
297    pub use cu29_runtime::monitoring::*;
298    pub use cu29_runtime::output_msg;
299    pub use cu29_runtime::payload::*;
300    #[cfg(feature = "std")]
301    pub use cu29_runtime::pool::*;
302    #[cfg(feature = "reflect")]
303    pub use cu29_runtime::reflect::serde as reflect_serde;
304    #[cfg(feature = "reflect")]
305    pub use cu29_runtime::reflect::serde::{
306        ReflectSerializer, SerializationData, TypedReflectSerializer,
307    };
308    pub use cu29_runtime::reflect::{
309        GetTypeRegistration, ReflectTaskIntrospection, ReflectTypePath, TypeInfo, TypePath,
310        TypeRegistry, dump_type_registry_schema,
311    };
312    pub use cu29_runtime::resource::*;
313    pub use cu29_runtime::rx_channels;
314    #[cfg(feature = "std")]
315    pub use cu29_runtime::simulation::*;
316    pub use cu29_runtime::tx_channels;
317    pub use cu29_traits::{
318        COMPACT_STRING_CAPACITY, CopperListTuple, CuCompactString, CuError, CuMsgMetadataTrait,
319        CuMsgOrigin, CuPayloadRawBytes, CuResult, DebugFieldDescriptor, DebugFieldKind,
320        DebugFieldSemantics, DebugScalarKind, DebugScalarRegistration, DebugScalarType,
321        ErasedCuStampedData, ErasedCuStampedDataSet, MatchingTasks, Metadata, ObservedWriter,
322        PayloadSchemas, TaskOutputSpec, UnifiedLogType, WriteStream, abort_observed_encode,
323        begin_observed_encode, finish_observed_encode, observed_encode_bytes,
324        record_observed_encode_bytes, with_cause,
325    };
326    #[cfg(feature = "std")]
327    pub use cu29_unifiedlog::memmap;
328    pub use cu29_unifiedlog::*;
329    pub use cu29_value::Value;
330    pub use cu29_value::to_value;
331    pub use serde_derive::{Deserialize, Serialize};
332}
333
334#[cfg(all(test, feature = "std"))]
335mod tests {
336    use super::prelude::*;
337    use std::sync::{Arc, Mutex, OnceLock};
338
339    #[derive(Debug)]
340    struct CaptureStream;
341
342    impl WriteStream<CuLogEntry> for CaptureStream {
343        fn log(&mut self, _obj: &CuLogEntry) -> CuResult<()> {
344            Ok(())
345        }
346    }
347
348    fn logger_test_lock() -> std::sync::MutexGuard<'static, ()> {
349        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
350        LOCK.get_or_init(|| Mutex::new(()))
351            .lock()
352            .unwrap_or_else(|poison| poison.into_inner())
353    }
354
355    fn capture_one_log<F>(emit: F) -> CuLogEntry
356    where
357        F: FnOnce(),
358    {
359        let _guard = logger_test_lock();
360        let runtime = LoggerRuntime::init(RobotClock::default(), CaptureStream, None::<NullLog>);
361        let captured = Arc::new(Mutex::new(Vec::new()));
362        let sink = captured.clone();
363        let _listener = scoped_live_log_listener(move |entry, _, _| {
364            sink.lock()
365                .unwrap_or_else(|poison| poison.into_inner())
366                .push(entry.clone());
367        });
368
369        emit();
370
371        drop(runtime);
372
373        let entries = captured.lock().unwrap_or_else(|poison| poison.into_inner());
374        assert_eq!(entries.len(), 1, "expected exactly one captured log entry");
375        entries[0].clone()
376    }
377
378    #[test]
379    fn explicit_context_logs_capture_task_origin() {
380        let mut ctx = CuContext::builder(RobotClock::default())
381            .cl_id(77)
382            .task_ids(&["task-0"])
383            .build();
384        ctx.set_current_task(0);
385
386        let entry = capture_one_log(|| {
387            debug!(ctx, "task log {}", 7);
388        });
389
390        assert_eq!(entry.origin.culistid, Some(77));
391        assert_eq!(entry.origin.component_id, Some(0));
392        assert_eq!(entry.origin.task_index, Some(0));
393    }
394
395    #[test]
396    fn explicit_context_logs_capture_bridge_component_origin() {
397        let mut ctx = CuContext::builder(RobotClock::default()).cl_id(88).build();
398        ctx.set_current_component(5);
399        ctx.clear_current_task();
400
401        let entry = capture_one_log(|| {
402            info!(ctx, "bridge log {}", 3);
403        });
404
405        assert_eq!(entry.origin.culistid, Some(88));
406        assert_eq!(entry.origin.component_id, Some(5));
407        assert_eq!(entry.origin.task_index, None);
408    }
409
410    #[test]
411    fn context_free_logs_leave_origin_empty() {
412        let entry = capture_one_log(|| {
413            warning!("context free {}", 1);
414        });
415
416        assert_eq!(entry.origin, CuLogOrigin::default());
417    }
418}