☰
ROS2无人机飞控迁移:节点生命周期与QoS配置实战指南
2026/10/14 7:54:56 网站建设 项目流程

简介:这是一份面向ROS开发者与无人机系统工程师的进阶学习资料,聚焦从ROS1平滑迁移至ROS2的核心编程实践,特别适用于参与无人机导航、感知与控制模块开发的中高级工程师及高校机器人方向研究生。资源包含847个文件,总大小16.33MB,涵盖C/C++源码(273个.c/.cpp/.h/.hpp文件,含AprilTag视觉识别相关算法如tag36h10.c、analyze_image等)、ROS专用配置(17个.launch、14个.msg、2个.srv、2个.action、10个.yaml)、构建与部署脚本(11个CMakeLists、6个.sh、3个Makefile)以及HTML文档、Markdown说明和RVIZ可视化配置等,结构完整,便于按功能模块快速定位。已有1126人学习下载,内容覆盖ROS2节点通信、自定义接口定义、跨版本移植要点、硬件抽象层适配及典型无人机任务流程实现,是少有的兼顾理论迁移逻辑与工程落地细节的实战型代码+文档一体化资源包。

1. 这不是升级手册,是无人机飞控逻辑重写指南:ROS1到ROS2迁移中90%开发者卡死在节点生命周期与QoS配置上

你手头那套跑得稳稳当当的ROS1四旋翼避障demo,在ROS2里编译通过、节点也启动了,但激光雷达数据就是不进控制环?TF树显示正常,ros2 topic echo /scan有数据,可自定义的路径规划节点却收不到——不是订阅失败,是“静默丢包”。这不是环境没配好,是ROS2把“消息能发出去”和“消息真能被收到”拆成了两个独立可控的维度。这份《从ROS1到ROS2无人机编程实战指南》不是概念翻译器,它是一份带血丝的排错日志合集:用真实飞控场景倒推ROS2核心机制——为什么rclcpp::Node必须显式管理生命周期?为什么sensor_msgs::msg::LaserScan在ROS1里能直连,在ROS2里必须配Reliability和Durability?它专为正在把PX4+ROS1视觉导航栈迁移到ROS2 Humble/Foxy的嵌入式开发者、飞控算法工程师和高校机器人实验室的实操者准备。如果你的诉求是“让无人机在ROS2里真正动起来,而不是只看到节点在running”,这份资源就是你调试到凌晨三点后,唯一愿意再打开的PDF+源码包。


2. ROS2节点生命周期不是语法糖,是飞控安全的硬性闸门:从rclcpp::Node构造到on_deactivate的完整控制链

ROS1里ros::NodeHandle像空气——你创建它,它就在;你不管它,它也活得好好的。ROS2彻底翻脸:节点不是“存在即合理”,而是“状态即责任”。尤其对无人机,电机使能、IMU校准、GPS定位初始化这些操作,必须绑定到明确的状态跃迁上。否则,你可能在configure阶段就尝试读取未校准的陀螺仪零偏,或者在cleanup时强行关闭正在执行紧急降落的控制器。

2.1 为什么无人机节点必须继承rclcpp_lifecycle::LifecycleNode

ROS1的ros::NodeHandle没有状态机,所有初始化逻辑全塞进构造函数。ROS2要求你显式声明节点的生命周期阶段:unconfigured→inactive→active→finalized。这对飞控意味着什么?

  • on_configure():加载参数、初始化传感器驱动句柄(如/dev/ttyUSB0串口)、预分配内存池(避免飞行中malloc);
  • on_activate():使能电机驱动芯片、启动IMU数据流、发布初始/tf静态变换;
  • on_deactivate():停电机PWM输出、关闭IMU中断、清空实时队列;
  • on_cleanup():释放串口资源、删除动态tf broadcaster。

不走这套流程?你的节点可能在ros2 lifecycle set /drone_control activate命令下发前,就已经在构造函数里打开了串口——而此时系统还没确认硬件就绪,导致串口冲突或驱动崩溃。

// drone_control_node.cpp 核心节选 #include "rclcpp_lifecycle/lifecycle_node.hpp" #include "rclcpp_lifecycle/lifecycle_publisher.hpp" class DroneControlNode : public rclcpp_lifecycle::LifecycleNode { public: explicit DroneControlNode(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()) : LifecycleNode("drone_control", options) {} protected: // 阶段1:配置——只做参数加载和资源预检 rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_configure(const rclcpp_lifecycle::State &) override { this->declare_parameter("motor_pwm_pin", 18); this->declare_parameter("imu_device_path", "/dev/imu0"); // 关键:此处不做硬件初始化!只验证路径是否存在 std::string imu_path; this->get_parameter("imu_device_path", imu_path); if (!std::filesystem::exists(imu_path)) { RCLCPP_ERROR(this->get_logger(), "IMU device %s not found!", imu_path.c_str()); return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::FAILURE; } return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS; } // 阶段2:激活——硬件使能、数据流启动 rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_activate(const rclcpp_lifecycle::State &) override { // 此时才真正打开串口、配置IMU寄存器 imu_driver_ = std::make_unique<IMUDriver>(this->get_parameter("imu_device_path").as_string()); if (!imu_driver_->init()) { RCLCPP_ERROR(this->get_logger(), "IMU init failed!"); return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::ERROR; } // 启动IMU数据采集线程(非阻塞) imu_thread_ = std::thread(&IMUDriver::start_streaming, imu_driver_.get()); // 发布初始静态tf:base_link -> imu_link static_tf_broadcaster_ = std::make_shared<tf2_ros::StaticTransformBroadcaster>(this); geometry_msgs::msg::TransformStamped static_tf; static_tf.header.stamp = this->now(); static_tf.header.frame_id = "base_link"; static_tf.child_frame_id = "imu_link"; static_tf.transform.rotation.w = 1.0; static_tf_broadcaster_->sendTransform(static_tf); return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS; } // 阶段3:停用——安全切断硬件 rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_deactivate(const rclcpp_lifecycle::State &) override { if (imu_driver_) { imu_driver_->stop_streaming(); // 停IMU中断 imu_driver_.reset(); // 释放驱动对象 } if (imu_thread_.joinable()) { imu_thread_.join(); } return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS; } private: std::unique_ptr<IMUDriver> imu_driver_; std::thread imu_thread_; std::shared_ptr<tf2_ros::StaticTransformBroadcaster> static_tf_broadcaster_; };

提示:代码中on_configure()只做路径检查,on_activate()才调用imu_driver_->init(),这是ROS2飞控开发的铁律。ROS1里你在构造函数里open("/dev/imu0")没问题,ROS2里这会导致节点在unconfigured状态就持有硬件资源,违反状态机契约,ros2 lifecycle set命令会直接拒绝切换状态。

2.2 生命周期命令行操作:从手动触发到自动状态机联动

ROS2提供ros2 lifecycle子命令管理节点状态,但无人机场景需要自动化——比如GPS信号锁定后自动激活导航节点。你不能靠人工敲命令。

# 手动触发(仅调试用) ros2 lifecycle set /drone_control configure ros2 lifecycle set /drone_control activate ros2 lifecycle set /drone_control deactivate ros2 lifecycle set /drone_control cleanup # 自动化:用launch文件定义状态跃迁依赖 # launch/drone_launch.py from launch import LaunchDescription from launch_ros.actions import LifecycleNode from launch.actions import RegisterEventHandler, EmitEvent from launch_ros.event_handlers import OnStateTransition def generate_launch_description(): drone_node = LifecycleNode( package='drone_control', executable='drone_control_node', name='drone_control', output='screen', # 初始状态设为 inactive,等待外部触发 lifecycle_hooks=[OnStateTransition( target_state='configuring', on_transition=lambda event: print("Drone node entering configuring..."), )] ) # 当GPS节点进入active状态时,自动触发drone_control的configure gps_node = LifecycleNode( package='gps_driver', executable='gps_node', name='gps', output='screen' ) # 关键:监听gps节点状态,触发drone节点状态变更 gps_active_handler = RegisterEventHandler( event_handler=OnStateTransition( target_state='active', entities=[ EmitEvent(event=launch.events.lifecycle.ChangeState( lifecycle_node_matcher=launch.events.matches_action(gps_node), transition_id=lifecycle_msgs.msg.Transition.TRANSITION_CONFIGURE )) ] ) ) return LaunchDescription([ gps_node, drone_node, gps_active_handler ])

这段launch脚本实现了“GPS就绪→触发飞控配置”的硬性耦合。ROS1里你靠ros::topic::waitForMessage()轮询GPS话题,ROS2里必须用状态机事件驱动——因为waitForMessage在unconfigured状态下根本无法订阅任何话题(订阅器尚未创建)。

2.3 状态回调中的实时性陷阱:为什么on_activate()里不能做耗时计算

无人机对延迟极度敏感。on_activate()必须在毫秒级完成,否则整个状态机卡住,下游节点无法启动。常见错误是在这里做SLAM建图或神经网络权重加载。

// ❌ 错误示范:在on_activate里加载ONNX模型(耗时200ms+) on_activate(...) override { // ... 其他硬件初始化 model_ = Ort::Session(env_, L"model.onnx", session_options_); // 卡住! } // ✅ 正确做法:异步加载 + 状态标记 on_activate(...) override { // 启动后台线程加载模型 model_load_thread_ = std::thread([this]() { model_ = Ort::Session(env_, L"model.onnx", session_options_); // 加载完成后,发布一个自定义事件通知主循环 auto event_msg = std::make_unique<std_msgs::msg::String>(); event_msg->data = "model_loaded"; model_load_event_pub_->publish(std::move(event_msg)); }); }

注意:ROS2的生命周期回调函数运行在单线程中(默认rclcpp::executors::SingleThreadedExecutor),任何阻塞操作都会冻结整个节点的状态机。无人机飞控中,on_activate()应只做原子级硬件使能,所有计算密集型任务必须剥离到独立线程或使用rclcpp::CallbackGroup隔离。


3. QoS策略不是可选项,是无人机数据链路的“交通规则”:Reliability、Durability、History三参数如何决定激光雷达能否喂饱控制器

ROS1的rostopic pub和rostopic sub背后是TCPROS协议,可靠性由传输层兜底。ROS2把“消息是否送达”交给了应用层——QoS(Quality of Service)策略。对无人机,这直接决定:

  • 激光雷达每秒40帧数据,控制器是否总能拿到最新一帧?
  • IMU高频数据(1kHz)在CPU负载高时会不会批量丢失?
  • 紧急降落指令(/cmd/land)发出后,飞控节点是否100%收到?

不配QoS?你的节点可能在ROS1里跑得好好的,到了ROS2里变成“间歇性失明”。

3.1 三参数黄金组合:为什么无人机必须用Reliability::RELIABLE+Durability::TRANSIENT_LOCAL+History::KEEP_LAST(10)

QoS参数可选值无人机场景含义推荐值原因
ReliabilityBEST_EFFORT/RELIABLE是否重传丢包RELIABLE激光雷达数据丢一帧可能导致避障失效;BEST_EFFORT适合监控类话题(如电池温度),但绝不适用于控制链路
DurabilityVOLATILE/TRANSIENT_LOCAL历史消息是否缓存TRANSIENT_LOCAL新启动的路径规划节点需要立即获取最新的地图(/map)和位姿(/tf),VOLATILE下它只能等下一帧发布,造成数秒空白期
HistoryKEEP_LAST(n)/KEEP_ALL缓存多少条历史消息KEEP_LAST(10)KEEP_ALL内存爆炸(IMU每秒1000条,1分钟就是6万条);KEEP_LAST(10)保证控制器总有最近10帧可用,兼顾实时性与容错
// 在节点构造函数中创建带QoS的订阅器 DroneControlNode::DroneControlNode(...) : LifecycleNode("drone_control", options) { // 为关键控制话题设置RELIABLE + TRANSIENT_LOCAL auto qos_control = rclcpp::QoS(rclcpp::KeepLast(10)) .reliability(RMW_QOS_POLICY_RELIABILITY_RELIABLE) .durability(RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL); // 订阅/cmd/vel(速度指令),必须100%可靠 vel_sub_ = this->create_subscription<geometry_msgs::msg::Twist>( "/cmd/vel", qos_control, std::bind(&DroneControlNode::vel_callback, this, _1) ); // 为传感器话题设置RELIABLE + VOLATILE(高频数据不需历史) auto qos_sensor = rclcpp::QoS(rclcpp::KeepLast(1)) .reliability(RMW_QOS_POLICY_RELIABILITY_RELIABLE) .durability(RMW_QOS_POLICY_DURABILITY_VOLATILE); // 订阅/imu/data_raw(原始IMU),只关心最新一帧 imu_sub_ = this->create_subscription<sensor_msgs::msg::Imu>( "/imu/data_raw", qos_sensor, std::bind(&DroneControlNode::imu_callback, this, _1) ); }

逻辑说明:qos_control用于/cmd/vel这类低频但关键的指令话题,TRANSIENT_LOCAL确保即使指令在节点启动前已发布,节点也能“回溯”获取;qos_sensor用于/imu/data_raw这类高频话题,VOLATILE避免缓存积压,KeepLast(1)强制只保留最新一帧——控制器永远处理最及时的数据,不会被旧数据拖慢。

3.2 QoS不匹配的静默故障:为什么ros2 topic info比ros2 topic echo更重要

ROS2中,发布端和订阅端QoS不匹配时,不会报错,也不会警告,只是静默不通信。这是ROS1开发者迁移时最痛的坑。

# 查看话题的QoS策略(发布端和订阅端必须一致才能通信) ros2 topic info /scan --verbose # 输出示例: Type: sensor_msgs/msg/LaserScan Publisher count: 1 Subscription count: 1 Publisher: Node name: laser_driver Node namespace: / Topic type: sensor_msgs/msg/LaserScan Endpoint type: PUBLISHER GID: 01.0f.9a.22.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00 QoS profile: Reliability: RELIABLE Durability: TRANSIENT_LOCAL # ← 发布端用的是TRANSIENT_LOCAL History: KEEP_LAST Depth: 10 Subscription: Node name: path_planner Node namespace: / Topic type: sensor_msgs/msg/LaserScan Endpoint type: SUBSCRIPTION GID: 01.0f.9a.22.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.01 QoS profile: Reliability: RELIABLE Durability: VOLATILE # ← 订阅端用VOLATILE → 不匹配!静默断连 History: KEEP_LAST Depth: 10

参数说明:ros2 topic info --verbose输出中,Publisher和Subscription的Durability字段必须完全一致(TRANSIENT_LOCAL↔TRANSIENT_LOCAL)。只要有一项不同(如RELIABLEvsBEST_EFFORT),通信即中断。ROS1里你从不关心这个,ROS2里这是第一排查项。

3.3 动态QoS适配:当你的飞控要同时对接ROS1桥接器和原生ROS2节点

实际项目中,你可能要用ros1_bridge接入ROS1的视觉SLAM节点(如ORB-SLAM2),同时与ROS2原生的路径规划节点通信。桥接器对QoS有特殊要求:

  • ROS1桥接器只支持Reliability::RELIABLE和Durability::VOLATILE;
  • 它不支持TRANSIENT_LOCAL,因为ROS1没有对应概念;
  • 因此,与桥接器通信的话题,必须降级QoS。
// 与ROS1桥接器通信的QoS(必须妥协) auto qos_bridge = rclcpp::QoS(rclcpp::KeepLast(5)) .reliability(RMW_QOS_POLICY_RELIABILITY_RELIABLE) .durability(RMW_QOS_POLICY_DURABILITY_VOLATILE); // 强制VOLATILE! // 创建bridge专用订阅器 bridge_sub_ = this->create_subscription<nav_msgs::msg::Odometry>( "/slam/odom", // 来自ros1_bridge的里程计 qos_bridge, std::bind(&DroneControlNode::bridge_odom_callback, this, _1) ); // 与ROS2原生节点通信的QoS(保持高标准) auto qos_native = rclcpp::QoS(rclcpp::KeepLast(10)) .reliability(RMW_QOS_POLICY_RELIABILITY_RELIABLE) .durability(RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL); native_sub_ = this->create_subscription<nav_msgs::msg::Path>( "/planning/path", // 来自ROS2路径规划节点 qos_native, std::bind(&DroneControlNode::path_callback, this, _1) );

血泪经验:某次调试中,我们把/slam/odom的QoS设为TRANSIENT_LOCAL,结果ros1_bridge进程直接崩溃退出,日志只有一行Failed to create publisher for topic /slam/odom。翻遍文档才发现桥接器的QoS限制——这是ROS2生态兼容性的真实代价,必须主动适配,不能强求统一。


4. 避坑:ROS2无人机迁移中五个必踩的“静默杀手”,现象、原因与解法全记录

ROS2的错误往往不报错,只沉默。以下五条是我们在三个真实无人机项目中反复验证的“静默杀手”,每一条都曾让团队加班到凌晨。

4.1 现象:ros2 topic list能看到/tf,但ros2 run tf2_tools view_frames生成的PDF里base_link和camera_link之间无连线

原因:tf2_ros::TransformBroadcaster在ROS2中必须在active状态才能发布,而tf2_ros::StaticTransformBroadcaster可在unconfigured状态发布。但很多开发者误用TransformBroadcaster发布静态tf,导致节点未激活时tf树断裂。
解决:静态tf(如base_link→camera_link)必须用StaticTransformBroadcaster,且在on_configure()中创建;动态tf(如odom→base_link)才用TransformBroadcaster,并在on_activate()中启动。

4.2 现象:ros2 topic hz /scan显示频率正常(40Hz),但rqt_plot画出的激光点云数据跳变、缺失整圈

原因:激光雷达驱动节点发布的QoS是RELIABLE,但你的控制器节点订阅时用了BEST_EFFORT(默认值),导致网络抖动时数据被静默丢弃。
解决:所有订阅传感器数据的节点,必须显式声明RELIABLEQoS。不要依赖默认值,ROS2默认是BEST_EFFORT。

4.3 现象:ros2 lifecycle get /drone_control返回inactive,但ros2 node info /drone_control显示所有订阅器都已创建

原因:节点在on_configure()中返回SUCCESS,但在on_activate()中因硬件初始化失败(如串口被占用)返回ERROR,节点卡在inactive,但订阅器已在configure阶段创建(ROS2允许)。此时节点看似“活着”,实则无法处理数据。
解决:在on_activate()中增加硬件就绪检查(如ioctl(fd, TIOCMGET, &status)读串口DTR状态),失败时返回ERROR并打印具体设备错误码,绝不在on_activate()中吞掉异常。

4.4 现象:ros2 param dump /drone_control导出的YAML中,motor_pwm_pin参数值是18,但节点实际读取为0

原因:参数在on_configure()中通过this->get_parameter()读取,但该调用发生在declare_parameter()之后。如果launch文件中未显式设置参数,get_parameter()返回默认值(int为0),而非声明时的默认值(18)。
解决:必须在on_configure()中先declare_parameter()再get_parameter(),且launch文件中显式传参:

<!-- launch file --> <param name="motor_pwm_pin" value="18"/>

4.5 现象:ros2 action list看不到/navigate_to_pose,但ros2 node info /nav2_controller显示action server已注册

原因:Nav2的action server在ROS2中默认使用TRANSIENT_LOCALQoS,但你的自定义客户端(如地面站)若用VOLATILE订阅action topic,则无法发现server。
解决:客户端action client必须用TRANSIENT_LOCALQoS初始化:

auto action_client = rclcpp_action::create_client<NavigateToPose>( this->get_node_base_interface(), this->get_node_graph_interface(), this->get_node_logging_interface(), this->get_node_waitables_interface(), "navigate_to_pose", rcl_action_client_get_default_options() // ← 默认已是TRANSIENT_LOCAL );

5. 参数化飞控:用YAML+Launch实现“改个数字就换机型”,从PID调参到电机映射的全链路解耦

无人机项目最痛苦的不是写代码,是调参——同一套飞控逻辑,换一架新机型,PID要重调、电机PWM极性要重测、IMU坐标系要重标定。ROS2的参数系统+Launch组合,能把这些硬编码全部抽离成YAML文件,实现“改配置,不改代码”。

5.1 分层参数设计:platform.yaml(机型)、tuning.yaml(性能)、hardware.yaml(硬件)

将参数按变更频率分层,避免“一改全动”:

文件名内容变更频率示例
platform.yaml机型物理参数极低(换机才改)mass_kg: 1.2,rotor_count: 4,arm_length_m: 0.25
tuning.yaml控制器参数中(每次试飞微调)pid_roll_p: 2.1,pid_roll_i: 0.05,yaw_rate_max_deg_s: 120
hardware.yaml硬件接口参数低(硬件升级才改)imu_device_path: "/dev/imu0",motor_pwm_pins: [18,19,20,21],motor_reverse: [false,true,false,true]
# config/platform.yaml platform: mass_kg: 1.2 rotor_count: 4 arm_length_m: 0.25 max_thrust_N: 12.0 # config/tuning.yaml controller: pid_roll: p: 2.1 i: 0.05 d: 0.1 pid_pitch: p: 2.1 i: 0.05 d: 0.1 yaw_rate_max_deg_s: 120 # config/hardware.yaml hardware: imu: device_path: "/dev/imu0" frame_id: "imu_link" motor: pwm_pins: [18, 19, 20, 21] reverse: [false, true, false, true] pwm_range_us: [1000, 2000] # min/max PWM pulse width

5.2 Launch文件注入参数:用IncludeLaunchDescription实现配置复用

一个launch文件可组合多个YAML,不同机型只需换一个include:

# launch/quadcopter_launch.py from launch import LaunchDescription from launch_ros.actions import LifecycleNode from launch.actions import IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource from ament_index_python.packages import get_package_share_directory import os def generate_launch_description(): # 通用飞控节点 drone_node = LifecycleNode( package='drone_control', executable='drone_control_node', name='drone_control', output='screen', # 注入三层参数 parameters=[ os.path.join(get_package_share_directory('drone_control'), 'config', 'platform.yaml'), os.path.join(get_package_share_directory('drone_control'), 'config', 'tuning.yaml'), os.path.join(get_package_share_directory('drone_control'), 'config', 'hardware.yaml'), ] ) # 机型专用launch(如mavic_pro.launch.py只include platform_mavic.yaml) mavic_launch = IncludeLaunchDescription( PythonLaunchDescriptionSource([ os.path.join(get_package_share_directory('drone_control'), 'launch', 'mavic_pro.launch.py') ]), launch_arguments={'use_sim_time': 'false'}.items() ) return LaunchDescription([ drone_node, mavic_launch ])

5.3 运行时热重载参数:不用重启节点,PID实时生效

ROS2支持运行时参数更新,这对飞控调参是救命功能:

# 1. 查看当前参数 ros2 param list /drone_control # 2. 修改PID参数(立即生效) ros2 param set /drone_control controller.pid_roll.p 2.3 # 3. 保存当前参数到文件(下次启动自动加载) ros2 param dump /drone_control > config/tuning_current.yaml

在节点中,用add_on_set_parameters_callback监听参数变更:

// 在on_configure()中注册回调 this->add_on_set_parameters_callback( std::bind(&DroneControlNode::parameter_callback, this, _1) ); rcl_interfaces::msg::SetParametersResult DroneControlNode::parameter_callback(const std::vector<rclcpp::Parameter> & parameters) { rcl_interfaces::msg::SetParametersResult result; result.successful = true; for (const auto & param : parameters) { if (param.get_name() == "controller.pid_roll.p") { pid_roll_.p_ = param.as_double(); RCLCPP_INFO(this->get_logger(), "Updated roll P to %.2f", pid_roll_.p_); } else if (param.get_name() == "controller.yaw_rate_max_deg_s") { yaw_rate_max_rad_s_ = param.as_double() * M_PI / 180.0; } } return result; }

从那以后我每次调PID,都强制走一遍ros2 param set+ros2 param dump流程,绝不改代码重新编译。一次试飞下来,能攒出3个tuning_flight1.yaml、tuning_flight2.yaml,最后merge成最终版。参数即代码,这才是ROS2该有的样子。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询