#middleware #real-time #copper #decoupling #robot

cu29-traits

为解耦机器人系统组件而设计的常见系统和机器人特性。这些特性可以从Copper项目中独立使用。

2 个版本

0.2.2 2024年8月12日
0.2.1 2024年7月29日

144机器人 中排名

Download history 127/week @ 2024-07-28 5/week @ 2024-08-04 153/week @ 2024-08-11

285 每月下载次数
13 个crate中(直接使用7个) 使用

Apache-2.0

5KB
64

logo

Copper

copper GitHub last commit License Gitter

Copper 是一个用户友好的机器人框架,旨在创建快速且可靠的机器人。Copper 对机器人来说,就像游戏引擎对游戏一样。

  • 简单:Copper 提供了一个高级配置系统和自然的 Rust 首选 API。

  • 快速:Copper 使用 Rust 的零成本抽象和面向数据的方法,在通用硬件上实现亚微秒级延迟,避免执行过程中的堆分配。

  • 可靠:Copper 利用 Rust 的所有权、类型系统和并发模型来最小化错误并确保线程安全。

  • 面向产品:Copper 致力于通过生成非常可预测的运行时来避免晚期基础设施集成问题。

[!NOTE] Copper 目前仍处于 早期开发/alpha 阶段,API 可能会更改。我们正在寻找贡献者帮助我们构建最好的机器人框架。如果您有兴趣,请加入我们 Gitter 或提出一个问题。

技术概述

Copper 是一个面向数据运行的时序,具有以下关键组件

  • 任务图graphRON 所述,该配置系统拓扑,指定哪些任务进行通信并设置节点和消息的类型。

  • 运行时生成器:该组件根据图的元数据决定执行计划。它预先分配一个“Copper List”,以最大化执行过程中的顺序内存访问。

  • 零拷贝数据记录:记录任务之间的所有消息而无需复制数据,确保高效记录。

  • 快速结构化记录:在编译时索引和存储记录字符串,避免运行时字符串构造并确保高速文本记录。

已实现哪些功能?

  1. 基本任务生命周期接口:对于您开始贡献新的算法、传感器和执行器来说,应该是相对稳定的。
  2. 运行时生成:虽然可以工作,但非常简单;这仅仅是一种BFS类型的执行。
  3. 日志读取器与结构化日志读取器:可以导出数据,目前以Rust调试格式。
  4. 简单的最小化驱动程序:这些更多的是向您展示如何实现自己的。
类别 类型 描述 库名
传感器 激光雷达 vlp16 Velodyne/Ouster VLP16 cu_vlp16
惯性测量单元(IMU) wt901 WitMotion WT901 cu_wt901
执行器 通用输入输出(GPIO) rp 树莓派 cu_rp_gpio
伺服电机 lewansoul 乐旺斯尔伺服总线(LX-16A等) cu_lewansoul

缺少哪些功能?我们计划接下来实现什么?

很多!如果您对其中任何一项感兴趣并希望贡献,请随时联系我们!

以下是我们计划接下来实现的一些功能(按优先级排序)

  • 确定性日志回放:由于运行时以确定性方式生成,我们需要添加钩子以将消息注入现有运行时。
  • 并行铜列表:今天铜是单线程的;这应该可以允许多个铜列表同时无冲突地执行。
  • 成组:由于各种传感器的输出频率差异很大,我们需要能够使用丢弃/算法策略(平均最大值等)将消息成组。今天这可以在用户端实现,但很痛苦。
  • 监控:我们需要一个并行系统,可以监听监控消息并相应地行动。
  • 分布式铜:目前,我们只能创建一个进程。我们需要为每个子系统正确地进行RPC过滤铜列表。
  • ROS接口:构建一对接收器和发送器以连接到现有的ROS系统,帮助用户逐步迁移其基础设施。
  • 模块化配置:随着用铜构建的机器人的复杂性增加,用户将需要构建其机器人的“变体”,而无需复制整个RON文件。
  • “PGO”-类似调度:将以前的日志运行传递给运行时生成器,以允许它做出更好的调度决策。

所以我们才刚刚开始,但有很多酷炫的东西即将到来!

为急于启动铜项目的用户

您可以从仓库中现有的模板生成一个项目。它会交互式地询问您想要选择的名称。

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:

有关更多信息,请参阅copper-templates

铜应用程序看起来是什么样子?

以下是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"),
    ],    

然后在您的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,
}

// You need to provide at least "new". 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...
impl CuTaskLifecycle for FlippingSource {
    fn new(_config: Option<&copper::config::NodeInstanceConfig>) -> CuResult<Self>
    where
        Self: Sized,
    {
        Ok(Self { state: true })
    }
}

// We implement the CuSrcTask trait for our task as it is a source / driver (with no internal input from Copper itself).
impl CuSrcTask for FlippingSource {
    type Output = RPGpioMsg;

    // 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: &mut CuMsg<Self::Output>) -> CuResult<()> {
        self.state = !self.state;   // Flip our internal state and send the message in our output.
        output.payload = RPGpioMsg {
            on: self.state,
            creation: 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());
}

但这是一个非常简单的任务示例,请参阅生命周期以获取对任务生命周期的更完整解释。

应用程序部署

有关更多信息,请参阅部署页面。

它与ROS相比有什么更好或不同之处?

性能

在示例目录中,我们有2个等效的应用程序。一个是用C++为ROS编写的,另一个是用Rust和铜移植的。

examples/cu_caterpillar
examples/ros_caterpillar

您可以在桌面或带有GPIO的RPi上运行它们,您应该会看到几个数量级的性能差异。

铜被设计为首先考虑性能。与游戏引擎不同,我们使用面向数据的方法来最小化延迟并最大化吞吐量。

安全性

由于铜是用Rust编写的,它设计为内存安全和线程安全。它还设计得易于使用,以避免常见的陷阱。

随着我们在这个项目上的进展,我们计划实施更多早期预警措施,以帮助您避免在复杂系统中可能出现的“千刀割死”现象。

依赖项

~0.7–1.2MB
~29K SLoC