用Rust + dora-rs + Webots打造自动驾驶仿真系统:Mac M1完整实战

0 阅读17分钟

提到自动驾驶开发,C++ 曾经是绝对的王者。但近年来,凭借着内存安全、零成本抽象和极高的并发性能,Rust 正迅速成为机器人和自动驾驶领域的新宠。

今天,我们要搞点硬核的。我将手把手带大家在 Mac M1 环境下,使用 Rust 配合当下最火的机器人数据流框架 dora-rs,以及 Webots 仿真器,搭建一个自动驾驶的模拟仿真 Demo。

重要声明:本项目基于 Rust、dora-rs 和 Webots 开发,仅用于自动驾驶的模拟仿真和演示目的。系统为 Demo 版本,不涉及真实的自动驾驶系统的开发与应用,请勿将仿真结果直接应用于实际驾驶场景。

01 为什么选择这套技术栈?

1. Rust:安全与性能的完美平衡

自动驾驶系统容不得半点内存泄漏或空指针引用。Rust 的所有权机制在编译阶段就扼杀了这些 bug,同时在 M1 芯片上运行效率极高,完美契合对实时性要求极高的仿真场景。

2. dora-rs:下一代机器人中间件

传统的 ROS 在处理大数据流(如图像、点云)时往往有延迟。dora-rs 是一个基于 Rust 编写的低延迟数据流框架,利用共享内存技术,让节点间通信快如闪电,架构清晰且极易扩展。

3. Webots:轻量级仿真利器

Webots 是一个开源、跨平台、功能强大的机器人仿真器,支持物理引擎、传感器模型、复杂环境等,并且对 macOS M1 支持极好(原生运行)且安装简单。在本项目中,我们将利用 Webots 模拟真实的驾驶环境,包括车辆、交通流量、环境等。

️02 环境搭建:Mac M1 避坑指南

在 M1 上配置环境主要要注意架构兼容性。以下是我的实测步骤:

1. 基础环境安装

首先确保你安装了 Rust 工具链:

# 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

接着安装 dora-rs 的 CLI 工具:

# 安装 Dora 命令行工具
cargo install dora-cli

2. Webots 与 SUMO 交通流

Webots: 下载链接 下载对应 macOS (Universal/Arm64) 的版本并安装。

SUMO: 用于生成复杂的交通路网。下载链接 安装后必须配置环境变量。 由于 SUMO 最新的版本采用的GUI的方式,不推荐使用 homebrew 安装,直接从官网下载安装包。

修改环境变量


# 编辑 ~/.zshrc 文件
vim ~/.zshrc
# 增加下面2行内容
# 针对 1.25.0 PKG 安装版的正确路径
export SUMO_HOME="/Library/Frameworks/EclipseSUMO.framework/Versions/Current/EclipseSUMO/share/sumo"

# 将 bin 目录加入 PATH 以后,你就可以直接在终端输入 sumo, netedit 等命令
export PATH="$SUMO_HOME/bin:$PATH"

Webots 配置: 打开 Webots,在车辆模型上添加传感器设备:Lidar (激光雷达)Camera (摄像头) 等。 本项目使用的 city_traffic.wbt 已默认配置了 Camera 传感器Lidar 传感器 需要手动添加。 选中车辆后,在左侧找到 Slot 节点,点击添加 Lidar 传感器, 如下图:

device.png 车辆的 controller 项必须配置为:extern, 这样才能在 Rust 中调用 Webots 的 C 接口, 实现对车辆的控制。

controler.png

Webots 配置

重点提示:直接点击图标启动 Webots 不会读取系统环境变量,需要 从终端启动 Webots , 如下所示:

open /Applications/Webots.app

03 核心代码实战

我们的目标是构建一个基于 dora-rsDataflow 系统,整个项目全部使用 Rust 编写!

先看一下运行效果:

ScreenShot_2025-12-30_225139_032.png

1 搞定 Webots 绑定 (webots-sys)

创建 webots-sys 绑定库项目:

cargo new --lib webots-sys

配置 build.rs Webots 提供的是 C/C++ 接口,需要用 Rust 的 FFI 进行绑定。在 build.rs 中告诉 Cargo 去哪里找库:

// build.rs
use std::env;
use std::path::PathBuf;
use std::process::Command;

fn main() {
    // 1. 定义路径
    let webots_home = "/Applications/Webots.app/Contents";
    let lib_path = format!("{}/lib/controller", webots_home);
    let include_path = format!("{}/include/controller/c/", webots_home);

    let webots_app_root = "/Applications/Webots.app";

    // 2. 告诉 Cargo 如何链接库文件
    println!("cargo:rustc-link-search=native={}", lib_path);
    println!("cargo:rustc-link-lib=dylib=Controller");
    println!("cargo:rustc-link-lib=dylib=driver"); // 必须添加这一行

    println!("cargo:rustc-link-arg=-Wl,-rpath,{}", webots_app_root);

    println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_path);

    //  获取 macOS SDK 路径 
    let sdk_path = String::from_utf8(
        Command::new("xcrun")
            .args(&["--show-sdk-path"])
            .output()
            .expect("failed to get sdk path")
            .stdout,
    )
    .unwrap()
    .trim()
    .to_string();

    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .clang_arg(format!("-I{}", include_path))
        .clang_arg(format!("-isysroot{}", sdk_path))
        .clang_arg("-target")
        .clang_arg("arm64-apple-macos")
        .allowlist_function("wb_.*")
        .allowlist_function("wbu_.*"// 关键:允许 driver 函数
        .allowlist_type("Wb.*")
        .allowlist_var("WB_.*")
        .prepend_enum_name(false)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

lib.rs: 导入 Webots 机器人的 API 接口:

// lib.rs
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

// 引入 build.rs 生成的代码
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

mod robot;

pub use robot::WebotsRobot;

实现 WebotsRobot 结构体: 对 Webots 机器人的接口进行封装,为机器人的传感器和驱动电机提供安全的 Rust 接口。

// robot.rs
pub struct WebotsRobot {
    pub time_step: i32,
    pub camera: WbDeviceTag,
    pub lidar: WbDeviceTag,
    pub gps: WbDeviceTag,
    // 改为存储所有驱动轮,确保动力充足
    pub drive_motors: Vec<WbDeviceTag>,
    pub steering_wheel: WbDeviceTag,
    pub brakes: Vec<WbDeviceTag>,
    pub imu: WbDeviceTag,
    pub opendrive_data: String,
}

impl WebotsRobot {
    pub fn get_device_list() {
        unsafe {
            let count = wb_robot_get_number_of_devices();
            println!("Total devices found: {}", count);
            for i in 0..count {
                let tag = wb_robot_get_device_by_index(i);

                // 尝试这个最原始的 C 函数名
                let name_ptr = wb_device_get_name(tag);

                if !name_ptr.is_null() {
                    let name = std::ffi::CStr::from_ptr(name_ptr).to_string_lossy();
                    println!("Device Index {}: {}", i, name);
                }
            }
        }
    }
    pub fn new() -> Self {
        unsafe {
            wb_robot_init();
            wbu_driver_init();
            let time_step = wb_robot_get_basic_time_step() as i32;

            // WebotsRobot::get_device_list();

            // 1. 基础传感器 (保持不变)
            let camera = Self::get_device("camera");
            if camera != 0 {
                wb_camera_enable(camera, time_step);
            }

            let gps = Self::get_device("gps");
            if gps != 0 {
                wb_gps_enable(gps, time_step);
            }

            // 2. 初始化驱动电机
            let motor_names = ["left_front_wheel""right_front_wheel"];
            let mut drive_motors = Vec::new();
            for name in motor_names {
                let motor = Self::get_device(name);
                if motor != 0 {
                    wb_motor_set_position(motor, f64::INFINITY);
                    wb_motor_set_velocity(motor, 100.0);
                    drive_motors.push(motor);
                }
            }

            // 3. 初始化转向电机
            let steering_wheel = Self::get_device("steering_wheel_motor");
            if steering_wheel != 0 {
                wb_motor_set_position(steering_wheel, 0.0);
            }

            // 4. 彻底释放刹车 (Brakes)
            let brake_names = [
                "left_front_brake",
                "right_front_brake",
                "left_rear_brake",
                "right_rear_brake",
            ];
            let mut brakes = Vec::new();
            for name in brake_names {
                let tag = Self::get_device(name);
                if tag != 0 {
                    // 关键:将阻尼设为 0,防止物理阻力
                    wb_brake_set_damping_constant(tag, 0.0);
                    brakes.push(tag);
                }
            }

            //  Lidar 初始化 
            let lidar = Self::get_device("lidar"); // 确保 Webots 里设备名叫 "lidar"
            if lidar != 0 {
                wb_lidar_enable(lidar, time_step);
                // 【关键步骤】必须显式开启点云生成,否则 get_point_cloud 会返回 null
                wb_lidar_enable_point_cloud(lidar);
            }

            let imu = Self::get_device("inertial unit"); // 注意:请检查你名单里的实际名称,有时叫 "imu"
            if imu != 0 {
                unsafe {
                    wb_inertial_unit_enable(imu, time_step);
                }
            }

            // 尝试加载 OpenDrive 文件(确保该文件已放在项目根目录)
            let current_dir = env::current_dir().expect("Failed to get current working directory");
            let relative_path = "webots-sys/data/sumo_map.xodr";
            let path = current_dir.join(relative_path);
            let opendrive_data = std::fs::read_to_string(path).unwrap_or_else(|_| {
                println!("Warning: world_map.xodr not found. Using empty string.");
                "".to_string()
            });

            Self {
                time_step,
                camera,
                lidar,
                gps,
                drive_motors,
                steering_wheel,
                brakes,
                imu,
                opendrive_data,
            }
        }
    }

    ......
}

impl Drop for WebotsRobot {
    fn drop(&mut self) {
        unsafe { wb_robot_cleanup() };
    }
}

2 Webots-Bridge 节点

这是连接仿真世界和算法世界的桥梁,此节点负责双向通信。

此节点中实现了以下功能:

  1. 初始化 Webots 机器人和传感器。
  2. 接收来自控制节点的控制指令。
  3. 读取摄像头图像、Lidar 点云和 GPS 位置、IMU 数据及 OpenDrive 地图。
  4. 将数据发送到下游节点(如路径规划器)。
// main.rs
use dora_node_api::{
    arrow::array::{Float32Array, StringArray, UInt8Array},
    dora_core::config::DataId,
    DoraNode, Event, Parameter,
};
use std::error::Error;
use webots_sys::WebotsRobot;

fn main() -> Result<(), Box<dyn Error>> {
    let (mut node, mut events) = DoraNode::init_from_env()?;

    // 强制设置 Webots 控制器地址
    std::env::set_var("WEBOTS_CONTROLLER_URL""tcp://127.0.0.1:1234");

    let robot = WebotsRobot::new();
    // 预先解析 Waypoints
    let road_ids = (230..=401)
        .map(|id| id.to_string())
        .collect::<Vec<String>>();
    let mut all_waypoints = Vec::new();

    for id in road_ids {
        let road_waypoints = robot.get_waypoints_from_opendrive(&id);
        if !road_waypoints.is_empty() {
            // 提取 [x, y, z] 并推入全局列表
            for pt in road_waypoints {
                all_waypoints.push(pt);
            }
        }
    }

    let global_waypoints = all_waypoints;

    // 将 [ [x,y], [x,y] ] 展平为 [x1, y1, x2, y2, ...] 方便发送
    let waypoints_flatVec<f32> = global_waypoints.into_iter().flatten().collect();

    if waypoints_flat.is_empty() {
        println!("Warning: No waypoints extracted from Road ");
    } else {
        println!(
            "Successfully loaded {} waypoints from OpenDRIVE",
            waypoints_flat.len() / 2
        );
    }

    while let Some(event) = events.recv() {
        match event {
            Event::Input { id, metadata, data } => match id.as_str() {
                "tick" => {
                    if !robot.step() {
                        break;
                    }

                    // 1. Camera Image
                    let image = robot.get_camera_image();
                    let w = robot.get_camera_width();
                    let h = robot.get_camera_height();
                    let mut params = metadata.parameters.clone();
                    params.insert("width".into(), Parameter::Integer(w as i64));
                    params.insert("height".into(), Parameter::Integer(h as i64));

                    node.send_output(
                        DataId::from("image".to_owned()),
                        params,
                        UInt8Array::from(image.to_vec()),
                    )?

                    // 2. Lidar Point Cloud
                    let pc = robot.get_lidar_points();
                    node.send_output(
                        DataId::from("lidar_pc".to_owned()),
                        metadata.parameters.clone(),
                        Float32Array::from(pc),
                    )?;

                    // 3. Position & Attitude (6DOF: x, y, z, r, p, y)
                    let pos = robot.get_gps_position();
                    let att = robot.get_attitude();
                    let full_pose = vec![pos[0], pos[1], pos[2], att[0], att[1], att[2]];
                    node.send_output(
                        DataId::from("position".to_owned()),
                        metadata.parameters.clone(),
                        Float32Array::from(full_pose),
                    )?;

                    // 4. Speed
                    let speed = vec![robot.get_speed()];
                    node.send_output(
                        DataId::from("speed".to_owned()),
                        metadata.parameters.clone(),
                        Float32Array::from(speed),
                    )?;

                    // 5. OpenDrive Raw Data
                    let opendrive = robot.get_opendrive();
                    node.send_output(
                        DataId::from("opendrive".to_owned()),
                        metadata.parameters.clone(),
                        StringArray::from(vec![opendrive]),
                    )?;

                    // 发送解析后的 Objective Waypoints
                    node.send_output(
                        DataId::from("objective_waypoints".to_owned()),
                        metadata.parameters.clone(),
                        Float32Array::from(waypoints_flat.clone()),
                    )?;
                }
                // 在 webots_bridge 的 main 循环中添加对 control_command 的处理
                "control_command" => {
                    let array = data.as_any().downcast_ref::<Float32Array>().unwrap();
                    let cmd = array.values(); 
                    let steering = cmd[0as f64;
                    let speed = cmd[1as f64;
                    // 应用到 Webots 电机
                    robot.set_steering(steering);
                    robot.set_drive_speed(speed);
                }
                other => eprintln!("Received unknown input: {:?}", other),
            },
            _ => {}
        }
    }

    Ok(())
}

waypoint生成:Webots 项目使用 sumo.net.xml 描述路网,通过 netconvert 工具解析并转换为 ODX 格式:

sumo.net.xml 文件一般位于与 wbt 文件同级同名的目录下,比如 city_traffic.wbt 对应的 sumo.net.xml 文件路径为 city_traffic/sumo.net.xml 找到 sumo.net.xml 文件后,运行以下代码将其转换为 ODX 格式的 waypoint 文件:

netconvert --sumo-net-file sumo.net.xml --opendrive-output sumo_map.xodr

3 YOLO 目标检测节点

与上一篇的智能目标检测节点类似,只是数据处理部分不同。

// object-detection/main.rs
while let Some(event) = events.recv() {
        match event {
            Event::Input { id, metadata, data } => match id.as_str() {
                "frame" => {
                    // 从 metadata 中提取 width 和 height
                    // 尝试获取 width
                    let cols = match metadata.parameters.get("width") {
                        Some(Parameter::Integer(v)) => *v as i32,
                        _ => 640// 默认值
                    };

                    // 尝试获取 height
                    let rows = match metadata.parameters.get("height") {
                        Some(Parameter::Integer(v)) => *v as i32,
                        _ => 480// 默认值
                    };
                    // 将接收到的字节数据转换为 OpenCV Vector
                    // 1. 将 Arrow trait 对象强转为具体的 UInt8Array
                    let uint8_array = data
                        .as_any()
                        .downcast_ref::<UInt8Array>()
                        .context("Arrow data is not UInt8Array (expected byte array)")?;

                    // 2. 提取 UInt8Array 的字节切片
                    let byte_slice = uint8_array.values(); // 返回 &[u8]
                    if byte_slice.len() != (rows * cols * 4as usize {
                        eprintln!(
                            "Data size mismatch! Expected {}, got {}",
                            rows * cols * 4,
                            byte_slice.len()
                        );
                        continue;
                    }

                    // 关键点:将 &[u8] 转换为 &[Vec4b]
                    // Vec4b 代表一个由 4 个 u8 组成的像素点
                    let (head, vec4_slice, tail) = unsafe { byte_slice.align_to::<Vec4b>() };

                    if !head.is_empty() || !tail.is_empty() {
                        // 如果数据不是 4 的倍数,可能会进到这里
                        eprintln!("Warning: Byte slice alignment issue");
                    }

                    // 因为 vec4_slice 的长度刚好是 byte_slice.len() / 4
                    let frame_raw = Mat::new_rows_cols_with_data(
                        rows,       // 128 (行)
                        cols,       // 64 (列)
                        vec4_slice, // 32768字节的 u8 切片现在变成了 8192长度的 Vec4b 切片
                    )?;

                    let mut frame = Mat::default();
                    // 注意:Webots 的颜色顺序可能是 BGRA,如果颜色不对,请尝试 COLOR_BGRA2RGB
                    imgproc::cvt_color(
                        &frame_raw,
                        &mut frame,
                        imgproc::COLOR_BGRA2BGR,
                        0,
                        AlgorithmHint::ALGO_HINT_DEFAULT,
                    )?;

                    if frame.empty() {
                        eprintln!("Warning: Decoded frame is empty. Skipping this iteration.");
                        continue// 跳过当前循环,不进入 preprocess_image
                    }
                    //  步骤 A: 图像预处理 (OpenCV -> Candle Tensor) 
                    let (processed_tensor, ratio, pad_w, pad_h) =
                        preprocess_image(&frame, &device)?;

                    //  步骤 B: 模型推理 
                    let predictions = model.forward(&processed_tensor)?;

                    //  步骤 C: 后处理 (NMS) 
                    // predictions 维度通常是 (1, 84, 8400) -> (Batch, Classes+Coords, Anchors)
                    let preds = predictions.squeeze(0)?;
                    let (bboxes, keypoints) = report_detect(&preds, &frame, ratio, pad_w, pad_h)?;

                    let arrow_array = bboxes_to_arrow(bboxes)?;

                    node.send_output(output.clone(), metadata.parameters, arrow_array)?;
                }
                other => eprintln!("Received input `{other}`"),
            },
            _ => {}
        }
    }

4 障碍物定位节点

接收激光雷达点云和相机检测结果、当前车辆位置,计算障碍物的3D位置。

// obstacle_location/main.rs
while let Some(event) = events.recv() {
        match event {
            Event::Input { id, data, metadata } => match id.as_str() {
                "lidar_pc" => {
                    let array = data
                        .as_any()
                        .downcast_ref::<Float32Array>()
                        .context("Lidar data is not Float32Array")?;

                    let raw_points = array.values();
                    current_pc = raw_points
                        .chunks_exact(3)
                        .map(|c| {
                            let p = nalgebra::Vector3::new(c[0], c[1], c[2]);
                            let transformed = velodyne_to_camera * p;
                            [transformed[0], transformed[1], transformed[2]]
                        })
                        // 优化过滤:排除车体自身点(1.0m内)以及高于路面的点(p[1]是高度)
                        .filter(|p| p[2] > 1.0 && p[2] < 60.0 && p[1] < 1.2)
                        .collect();

                    camera_pc = utils::project_to_camera(&current_pc, &intrinsic);
                }

                "position" => {
                    let array = data
                        .as_any()
                        .downcast_ref::<Float32Array>()
                        .context("Position data is not Float32Array")?;
                    extrinsic_matrix = utils::get_projection_matrix(array.values());
                }

                "obstacles_bbox" => {
                    let mut obstacles_3d = Vec::new();

                    let struct_array = data
                        .as_any()
                        .downcast_ref::<StructArray>()
                        .context("Input is not a StructArray")?;

                    let received_bboxes = utils::arrow_to_bboxes(struct_array)?;

                    for (name, rect, conf) in received_bboxes {
                        let min_x = rect.x as f32;
                        let max_x = (rect.x + rect.w) as f32;
                        let min_y = rect.y as f32;
                        let max_y = (rect.y + rect.h) as f32;

                        let mut pts_in_bbox: Vec<usize> = Vec::new();
                        for (i, cam_p) in camera_pc.iter().enumerate() {
                            // 增加深度约束,防止 BBox 误匹配背景噪点
                            if cam_p[0] > min_x
                                && cam_p[0] < max_x
                                && cam_p[1] > min_y
                                && cam_p[1] < max_y
                            {
                                pts_in_bbox.push(i);
                            }
                        }

                        if !pts_in_bbox.is_empty() {
                            // 取 1/4 分位数点,这是车辆后表面的稳健估计
                            pts_in_bbox.sort_by(|&a, &b| {
                                camera_pc[a][2].partial_cmp(&camera_pc[b][2]).unwrap()
                            });
                            let idx = pts_in_bbox[pts_in_bbox.len() / 4];
                            let local_pos = current_pc[idx];

                            let world_pos = extrinsic_matrix
                                * Vector4::new(local_pos[0], local_pos[1], local_pos[2], 1.0);

                            obstacles_3d.push(world_pos[0]);
                            obstacles_3d.push(world_pos[1]);
                            obstacles_3d.push(world_pos[2]);
                            obstacles_3d.push(conf);
                            obstacles_3d.push(label_to_id(&name)); // 存入对应的物体 ID
                        }
                    }

                    node.send_output(
                        DataId::from("obstacles".to_owned()),
                        metadata.parameters,
                        Float32Array::from(obstacles_3d),
                    )?;
                }
                _ => {}
            },
            _ => {}
        }
    }

5 路径规划节点

基于当前位置、目标点和障碍物信息,实时生成目标路径。

// planning_op/main.rs
while let Some(event) = events.recv() {
        match event {
            Event::Input { id, data, metadata } => match id.as_str() {
                "position" => {
                    let array = data.as_any().downcast_ref::<Float32Array>().unwrap();
                    let val = array.values();
                    current_pose = Vector3::new(val[0], val[1], val[5]);
                }
                "objective_waypoints" => {
                    let array = data.as_any().downcast_ref::<Float32Array>().unwrap();
                    global_waypoints = array
                        .values()
                        .chunks_exact(2)
                        .map(|c| Vector2::new(c[0], c[1]))
                        .collect();
                }
                "tick" => {
                    if global_waypoints.is_empty() {
                        continue;
                    }

                    let mut path = Vec::new();
                    let car_pos = Vector2::new(current_pose.x, current_pose.y);

                    // 1. 找最近点
                    let (closest_idx, min_dist) = global_waypoints
                        .iter()
                        .enumerate()
                        .map(|(i, wp)| (i, (wp - car_pos).norm()))
                        .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
                        .unwrap();

                    // 2. 速度决策
                    let is_far = min_dist > 5.0;
                    let capture_speed = 12.0f32; // 追路速度
                    let cruise_speed = 22.0f32; // 巡航速度 (~80km/h)

                    // 3. 生成局部路径片段
                    let lookahead_dist = 50.0;
                    let mut accumulated_dist = 0.0;

                    for i in closest_idx..global_waypoints.len() {
                        let wp = global_waypoints[i];
                        let remaining = global_waypoints.len() - i;

                        // 动态速度规划
                        let v = if is_far {
                            capture_speed
                        } else if remaining < 15 {
                            5.0 // 终点前速度从3.0提高到5.0
                        } else if remaining < 40 {
                            15.0 // 接近终点速度从10.0提高到15.0
                        } else {
                            cruise_speed
                        };

                        path.push(TrajectoryPoint {
                            x: wp.x,
                            y: wp.y,
                            v,
                        });

                        if i > closest_idx {
                            accumulated_dist +=
                                (global_waypoints[i] - global_waypoints[i - 1]).norm();
                        }
                        if accumulated_dist > lookahead_dist || path.len() >= 30 {
                            break;
                        }
                    }

                    // 发送数据
                    let mut output = Vec::new();
                    for p in path {
                        output.push(p.x);
                        output.push(p.y);
                        output.push(p.v);
                    }
                    node.send_output(
                        DataId::from("waypoints".to_owned()),
                        metadata.parameters,
                        Float32Array::from(output),
                    )?;
                }
                _ => {}
            },
            _ => {}
        }
    }

6 控制节点

接收路径指令,结合当前车辆位置,控制车辆运动。

// control_op/main.rs
while let Some(event) = events.recv() {
        match event {
            Event::Input { id, data, metadata } => match id.as_str() {
                "position" => {
                    let array = data.as_any().downcast_ref::<Float32Array>().unwrap();
                    current_pose.copy_from_slice(array.values());
                }
                "waypoints" => {
                    let array = data.as_any().downcast_ref::<Float32Array>().unwrap();
                    planned_path = array
                        .values()
                        .chunks_exact(3)
                        .map(|c| [c[0], c[1], c[2]])
                        .collect();
                }
                "tick" => {
                    if planned_path.is_empty() {
                        continue;
                    }

                    let x = current_pose[0];
                    let y = current_pose[1];
                    let yaw = current_pose[5];

                    // 1. 全局搜索最近点(去掉局部搜索,防止掉头时索引卡死)
                    let (closest_idx, min_dist) = planned_path
                        .iter()
                        .enumerate()
                        .map(|(i, p)| (i, ((p[0] - x).powi(2) + (p[1] - y).powi(2)).sqrt()))
                        .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
                        .unwrap();

                    // 2. 动态预瞄距离
                    let ld = if min_dist > 20.0 { 3.0f32 } else { 6.0f32 };

                    // 3. 计算 Alpha
                    let target_pt = planned_path[closest_idx]; // 掉头时直接瞄准最近点,确保转弯半径最小
                    let target_angle = (target_pt[1] - y).atan2(target_pt[0] - x);
                    let mut alpha = target_angle - yaw;
                    ......

                    let target_speed: f32;
                    let target_steer: f32;

                    // 4. 核心状态机逻辑优化
                    if alpha.abs() > 1.2 {
                        // 【掉头模式】
                        if uturn_lock == 0.0 {
                            uturn_lock = if alpha > 0.0 { 1.0 } else { -1.0 };
                        }
                        target_steer = uturn_lock * MAX_STEER_ANGLE;
                        // 关键:大幅降低掉头速度,防止转圈过猛
                        target_speed = 1.2;
                    } else if uturn_lock != 0.0 {
                        // 【掉头保持模式】
                        if alpha.abs() < 0.3 {
                            uturn_lock = 0.0; // 角度很小时才解除锁定
                            target_steer = 0.0;
                        } else {
                            target_steer = uturn_lock * MAX_STEER_ANGLE * 0.8;
                        }
                        target_speed = 2.0;
                    } else {
                        // 【正常循迹模式】
                        let curvature = 2.0 * alpha.sin() / ld;
                        target_steer = (curvature * WHEEL_BASE).atan();
                        target_speed = 4.0;
                    }

                    // 5. 转向处理(解决右转过快问题)
                    let mut final_steer = target_steer.clamp(-MAX_STEER_ANGLE, MAX_STEER_ANGLE);

                    ......

                    last_steering = final_steer;

                    node.send_output(
                        DataId::from("control_command".to_owned()),
                        metadata.parameters.clone(),
                        Float32Array::from(vec![final_steer, final_speed]),
                    )?;
                }
                _ => {}
            },
            _ => {}
        }
    }

7 可视化节点 (Viewer)

使用 opencv 实时显示所有数据流,包括相机图像、检测框、障碍物、路径等。

// viewer/main.rs
while let Some(event) = events.recv() {
        match event {
            Event::Input { id, metadata, data } => match id.as_str() {
                //  A. 接收 YOLO 检测框 
                "detections" => {
                    if let Some(struct_array) = data.as_any().downcast_ref::<StructArray>() {
                        if let Ok(received_bboxes) = arrow_to_bboxes(struct_array) {
                            bboxes = received_bboxes;
                        }
                    }
                }

                //  B. 接收 3D 障碍物位置 
                "obstacles" => {
                    if let Some(array) = data.as_any().downcast_ref::<Float32Array>() {
                        // 格式: [x, y, z, conf, label, ...]
                        obstacles_3d = array
                            .values()
                            .chunks_exact(5)
                            .map(|c| [c[0], c[2]]) // 提取 X 和 Z
                            .collect();
                    }
                }

                //  C. 接收自车位姿 
                "position" => {
                    if let Some(array) = data.as_any().downcast_ref::<Float32Array>() {
                        if array.len() >= 6 {
                            current_position.copy_from_slice(array.values());
                        }
                    }
                }

                //  D. 接收规划路径 
                "waypoints" => {
                    if let Some(array) = data.as_any().downcast_ref::<Float32Array>() {
                        // 格式: [x, y, v, ...]
                        println!("Received {} waypoints", array.len() / 3);
                        planned_path = array
                            .values()
                            .chunks_exact(3)
                            .map(|c| [c[0], c[1]])
                            .collect();
                    }
                }

                //  F. 核心渲染逻辑:处理图像输入 
                "frame" => {
                    let cols = match metadata.parameters.get("width") {
                        Some(Parameter::Integer(v)) => *v as i32,
                        _ => 640,
                    };
                    let rows = match metadata.parameters.get("height") {
                        Some(Parameter::Integer(v)) => *v as i32,
                        _ => 480,
                    };

                    let uint8_array = data
                        .as_any()
                        .downcast_ref::<UInt8Array>()
                        .context("Expected UInt8Array for image")?;
                    let byte_slice = uint8_array.values();

                    if byte_slice.len() != (rows * cols * 4as usize {
                        continue;
                    }

                    // 1. BGRA 转 BGR
                    let (_head, vec4_slice, _tail) = unsafe { byte_slice.align_to::<Vec4b>() };
                    let frame_raw = Mat::new_rows_cols_with_data(rows, cols, vec4_slice)?;
                    let mut display_frame = Mat::default();
                    imgproc::cvt_color(
                        &frame_raw,
                        &mut display_frame,
                        imgproc::COLOR_BGRA2BGR,
                        0,
                        AlgorithmHint::ALGO_HINT_DEFAULT,
                    )?;

                    // 2. 绘制 YOLO 2D 检测框
                    for (classname, bbox, conf) in &bboxes {
                        imgproc::rectangle(
                            &mut display_frame,
                            *bbox,
                            Scalar::new(0.0255.00.00.0),
                            2,
                            8,
                            0,
                        )?;
                        let label = format!("{}: {:.2}", classname, conf);
                        imgproc::put_text(
                            &mut display_frame,
                            &label,
                            Point::new(bbox.x, bbox.y - 5),
                            imgproc::FONT_HERSHEY_SIMPLEX,
                            0.5,
                            Scalar::new(0.0255.00.00.0),
                            1,
                            8,
                            false,
                        )?;
                    }

                    // 3. 绘制 HUD 仪表盘文本
                    let hud_color = Scalar::new(255.0255.0255.00.0);
                    let pos_info = format!(
                        "GPS: X:{:.1} Y:{:.1} Yaw:{:.2}",
                        current_position[0], current_position[1], current_position[5]
                    );
                    imgproc::put_text(
                        &mut display_frame,
                        &pos_info,
                        Point::new(2030),
                        imgproc::FONT_HERSHEY_SIMPLEX,
                        0.6,
                        hud_color,
                        2,
                        8,
                        false,
                    )?;

                    ......

                    //  4.2 绘制规划路径 
                    let step = 2;
                    for wp in planned_path.iter().step_by(step) {
                        // 计算相对坐标
                        let dx = (wp[0] - current_position[0]) * scale;
                        let dy = (wp[1] - current_position[1]) * scale;

                        let p = Point::new(center.x + dx as i32, center.y - dy as i32);

                        if map_rect.contains(p) {
                            imgproc::circle(
                                &mut display_frame,
                                p,
                                2,
                                Scalar::new(0.0255.0255.00.0),
                                -1,
                                8,
                                0,
                            )?;
                        }
                    }

                    ......

                    // 5. 显示并刷新
                    highgui::imshow(win_name, &display_frame)?;
                    if highgui::wait_key(1)? == 27 {
                        break;
                    } // ESC 退出
                }
                _ => {}
            },
            _ => {}
        }
    }

04 DataFlow 配置:一个文件串联所有节点

dora-rs 最酷的地方就是不需要写复杂的网络连接代码,只需要一个 .yml 文件就能把所有 Rust 节点串联起来。

数据流图:

graph.001.jpeg yml配置:

nodes:
  - id: webots_bridge
    build: cargo build -p webots-bridge
    path: target/debug/webots-bridge
    inputs:
      tick: dora/timer/millis/100
      control_command: control_op/control_command
    outputs:
      - image
      - lidar_pc
      - position
      - speed
      - opendrive
      - objective_waypoints

  - id: object_detection
    build: cargo build -p object-detection
    path: target/debug/object-detection
    inputs:
      frame: webots_bridge/image
    outputs:
      - detections

  - id: viewer
    build: cargo build -p viewer
    path: target/debug/viewer
    inputs:
      detections: object_detection/detections
      frame: webots_bridge/image
      obstacles: obstacle_location/obstacles
      position: webots_bridge/position
      waypoints: planning_op/waypoints
      control: control_op/control_command

  - id: obstacle_location
    build: cargo build -p obstacle-location
    path: target/debug/obstacle-location
    inputs:
      lidar_pc: webots_bridge/lidar_pc
      position: webots_bridge/position
      obstacles_bbox: object_detection/detections
    outputs:
      - obstacles

  - id: planning_op
    build: cargo build -p planning-op
    path: target/debug/planning-op
    inputs:
      position: webots_bridge/position
      objective_waypoints: webots_bridge/objective_waypoints
      obstacles: obstacle_location/obstacles
      tick: dora/timer/millis/100
    outputs:
      - waypoints

  - id: control_op
    build: cargo build -p control-op
    path: target/debug/control-op
    inputs:
      position: webots_bridge/position
      speed: webots_bridge/speed
      waypoints: planning_op/waypoints
      tick: dora/timer/millis/100
    outputs:
      - control_command

04 构建与运行

构建运行

# 1. 启动 Webots 仿真
open /Applications/Webots.app
# 2. 构建所有 Rust 节点
dora build dataflow.yml
# 3. 启动数据流
dora run dataflow.yml

Webots 输出

INFO: sumo_supervisor: Starting controller: /usr/bin/python3 -u sumo_supervisor.py --no-gui --no-netconvert --max-vehicles=100 --port=8873 --seed=1 --step=200 --radius=-1 --maximum-lateral-speed=2.5 --maximum-angular-speed=3 --lane-change-delay=3
INFO: 'vehicle' extern controller: Waiting for local or remote connection on port 1234 targeting robot named 'vehicle'.
INFO: generic_traffic_light: Starting controller: /Users/gustaf/webots/wbt/controllers/generic_traffic_light/generic_traffic_light 10 10 r
INFO: crossroads_traffic_lights: Starting controller: /Users/gustaf/webots/wbt/controllers/crossroads_traffic_lights/crossroads_traffic_lights

dora-rs 输出

planning_op: INFO   daemon    node is ready
viewer: INFO   daemon    node is ready
webots_bridge: INFO   daemon    node is ready
obstacle_location: INFO   daemon    node is ready
control_op: INFO   daemon    node is ready
object_detection: INFO   daemon    node is ready
INFO   daemon    all nodes are ready, starting dataflow
object_detection: stdout    Loading YOLOv8 model...
object_detection: stdout    🚀 Using Metal device.
object_detection: stdout    Model loaded successfully.
webots_bridge: stdout    Initialized: 2 motors, 4 brakes, 16 lidar, 18 imu,  opendrive.
webots_bridge: stdout    Webots Bridge 优化版已启动...
webots_bridge: stdout    Extracted 37 waypoints from Road 230
viewer: stdout    Plot operator initialized. Waiting for data...
......

总结

通过这个项目,我们验证了 Rust + dora-rs 在机器人开发中的强大潜力:

  1. 开发体验爽Cargo 包管理让依赖处理变得异常简单。
  2. 运行效率高:在 Mac M1 上跑这套仿真,CPU 占用率极低,通信几乎无延迟。
  3. 架构清晰:Dataflow 的设计让每个节点各司其职,解耦得非常彻底。

如果你也是自动驾驶或机器人爱好者,强烈建议尝试一下 Rust 生态!

源码传送门

本项目完整代码已开源,欢迎 Star ⭐ 和 Fork: 👉 github: rust-zhixingshe-examples