7 unstable releases (3 breaking)
new 0.5.1 | Dec 5, 2024 |
---|---|
0.5.0 | Dec 2, 2024 |
0.4.1 | Nov 15, 2024 |
0.4.0 | Oct 29, 2024 |
0.2.3 | Sep 11, 2024 |
#103 in Robotics
404 downloads per month
51KB
919 lines
Copper
Copper is a user-friendly runtime engine for creating fast and reliable robots. Copper is to robots what a game engine is to games.
-
Easy: Copper offers a high-level configuration system and a natural Rust-first API.
-
Fast: Copper uses Rust's zero-cost abstractions and a data-oriented approach to achieve sub-microsecond latency on commodity hardware, avoiding heap allocation during execution.
-
Reliable: Copper leverages Rust's ownership, type system, and concurrency model to minimize bugs and ensure thread safety.
-
Product Oriented: Copper aims to avoid late-stage infra integration issues by generating a very predictable runtime.
Copper has been tested on: Linux (x86_64, armv7, aarch64 & riskv64) and MacOS (arm64). Testers would be welcomed on Windows and other platforms.
Technical Overview
Copper is a data-oriented Robot SDK with these key components:
-
Task Graph: Described in RON, this configures the system's topology, specifying which tasks communicate and setting types for nodes and messages.
-
Runtime Generator: This component decides on an execution plan based on the graph's metadata. It preallocates a " Copper List" to maximize sequential memory access during execution.
-
Zero-Copy Data Logging: Records all messages between tasks without copying data, ensuring efficient logging.
-
Fast Structured Logging: Interns and indexes logging strings at compile time, avoiding runtime string construction and ensuring high-speed textual logging.
You don't have a real robot yet? Try it in our minimalistic sim environment!
Here is robot developed with Copper in action driving its digital twin in a simulation environment with Bevy (Game Engine in Rust) and Avian3d (Physics Engine in Rust)
You have a mac or a linux machine (x86-64 or Arm) just run ...
$ cargo install cu-rp-balancebot
$ balancebot-sim
... to try it locally.
The source code for this demo is available in the examples/cu_rp_balancebot directory.
Implemented Features So Far
- Task interface and Lifecycle: Those are stable traits you can use to implement new algorithms, sensors, and actuators.
- Runtime generation: The current implementation works for up to middle size robots (~a couple dozen of tasks).
- Log reader: You can reread the logs generated by Copper from your robot, sim or resim.
- Structured log reader: debug logs are indexed and string interned at compile time for maximum efficiency.
- Components: We have a growing number of drivers, algorithms and standard interfaces, if you have implemented a new component, ping us and we will add it to the list!
- log replay / resim: You can deterministically replay/resim a log. If all you tasks are deterministic, you will get the exact same result as a real log on the robot or from the sim.
- Simulation: We have a simple simulation environment to test your robot without a real robot.
Components
Category | Type | Description | Crate Name | |
---|---|---|---|---|
Sensors | Lidar | Velodyne/Ouster VLP16 | cu-vlp16 | |
Lidar | Hesai/XT32 | cu-hesai | ||
IMU | WitMotion WT901 | cu-wt901 | ||
ADC/Position | ADS 7883 3MPSPS SPI ADC | cu-ads7883 | ||
Encoder | Generic Directional Wheel encoder | cu-rp-encoder | ||
Actuators | GPIO | Raspberry Pi | cu-rp-gpio | |
Servo | Lewansoul Servo Bus (LX-16A, etc.) | cu-lewansoul | ||
DC Motor Driver | Half-H Driver for CD Motors | cu-rp-sn754410 | ||
Monitors | TUI Monitor | Console based monitor | cu-consolemon | |
Algorithms | PID Controller | PID Controller | cu-pid | |
Middleware | Shared Mem IPC | Iceoryx2 source Iceoryx2 sink |
cu-iceoryx2-src cu-iceoryx2-sink |
Kickstarting a copper project for the impatients
You can generate a project from a template present in the repo. It will ask you the name you want to pick interactively.
cargo install cargo-generate
git clone https://github.com/copper-project/copper-rs
cd copper-rs/templates
cargo cunew [path_where_you_want_your_project_created]
🤷 Project Name:
Check out copper-templates for more info.
How does a Copper application look like?
Here is a simple example of a task graph in RON:
(
tasks: [
(
id: "src", // this is a friendly name
type: "FlippingSource", // This is a Rust struct name for this task see main below
),
(
id: "gpio", // another task, another name
type: "cu_rp_gpio::RPGpio", // This is the Rust struct name from another crate
config: { // You can attach config elements to your task
"pin": 4,
},
),
],
cnx: [
// Here we simply connect the tasks telling to the framework what type of messages we want to use.
(src: "src", dst: "gpio", msg: "cu_rp_gpio::RPGpioMsg"),
],
Then, on your main.rs:
// Your application will be a struct that will hold the runtime, loggers etc.
// This proc macro is where all the runtime generation happens. You can see the code generated by the macro at
// compile time.
#[copper_runtime(config = "copperconfig.ron")] // this is the ron config we just created.
struct MyApplication {}
// Here we define our own Copper Task
// It will be a source flipping a boolean
pub struct FlippingSource {
state: bool,
}
// We implement the CuSrcTask trait for our task as it is a source / driver (with no internal input from Copper itself).
impl<'cl> CuSrcTask<'cl> for FlippingSource {
type Output = output_msg!('cl, RPGpioPayload);
// You need to provide at least "new" out of the lifecycle methods.
// But you have other hooks in to the Lifecycle you can leverage to maximize your opportunity
// to not use resources outside of the critical execution path: for example start, stop,
// pre_process, post_process etc...
fn new(config: Option<&copper::config::ComponentConfig>) -> CuResult<Self>
where
Self: Sized,
{
// the config is passed from the RON config file as a Map.
Ok(Self { state: true })
}
// Process is called by the runtime at each cycle. It will give:
// 1. the reference to a monotonic clock
// 2. a mutable reference to the output message (so no need to allocate of copy anything)
// 3. a CuResult to handle errors
fn process(&mut self, clock: &RobotClock, output: Self::Output) -> CuResult<()> {
self.state = !self.state; // Flip our internal state and send the message in our output.
output.set_payload(RPGpioPayload {
on: self.state,
creation: Some(clock.now()).into(),
actuation: Some(clock.now()).into(),
});
Ok(())
}
}
fn main() {
// Copper uses a special log format called "unified logger" that is optimized for writing. It stores the messages between tasks
// but also the structured logs and telemetry.
// A log reader can be generated at the same time as the application to convert this format for post processing.
let logger_path = "/tmp/mylogfile.copper";
// This basic setup is a shortcut to get you running. If needed you can check out the content of it and customize it.
let copper_ctx =
basic_copper_setup(&PathBuf::from(logger_path), true).expect("Failed to setup logger.");
// This is the struct logging implementation tailored for Copper.
// It will store the string away from the application in an index format at compile time.
// and will store the parameter as an actual field.
// You can even name those: debug!("This string will not be constructed at runtime at all: my_parameter: {} <- but this will be logged as 1 byte.", my_parameter = 42);
debug!("Logger created at {}.", logger_path);
// A high precision monotonic clock is provided. It can be mocked for testing.
// Cloning the clock is cheap and gives you the exact same clock.
let clock = copper_ctx.clock;
debug!("Creating application... ");
let mut application =
MyApplication::new(clock.clone(), copper_ctx.unified_logger.clone())
.expect("Failed to create runtime.");
debug!("Running... starting clock: {}.", clock.now()); // The clock will be displayed with units etc.
application.run().expect("Failed to run application.");
debug!("End of program: {}.", clock.now());
}
But this is a very minimal example for a task, please see lifecycle for a more complete explanation of a task lifecycle.
Deployment of the application
Check out the deployment page for more information.
How is it better or different from ROS?
Performance
In the example directory, we have 2 equivalent applications. One written in C++ for ROS and a port in Rust with Copper.
examples/cu_caterpillar
examples/ros_caterpillar
You can try them out by either just logging on a desktop, or with GPIOs on a RPi. You should see a couple order of magnitude difference in performance.
Copper has been designed for performance first. Not unlike a game engine, we use a data oriented approach to minimize latency and maximize throughput.
Safety
As Copper is written in Rust, it is memory safe and thread safe by design. It is also designed to be easy to use and avoid common pitfalls.
As we progress on this project we plan on implementing more and more early warnings to help you avoid "the death by a thousand cuts" that can happen in a complex system.
Release Notes
You can find the release notes here.
Roadmap
[!NOTE] We are looking for contributors to help us build the best robotics framework possible. If you are interested, please join us on Discord or open an issue.
Here are some of the features we plan to implement next (in ~order of priority), if you are interested in contributing on any of those, please let us know!:
- Parallel Copper Lists: Today Copper is single-threaded; this should enable concurrent Copper Lists to be executed at the same time with no contention.
- ROS/DDS interfacing: Build a pair of sink and source to connect to existing ROS systems, helping users migrate their infra bit by bit.
- Extensible scheduling: Enables a way to give hints to copper to schedule the workload.
- Modular Configuration: As robots built with Copper gain complexity, users will need to build "variations" of their robots without duplicating their entire RON file.
- Distributed Copper: Currently, we can only create one process. We need proper RPC filtering copper lists per subsystem.
Dependencies
~5–15MB
~195K SLoC