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