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