diff --git a/Dockerfile b/Dockerfile
index cf973436..f53de75e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -38,7 +38,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libceres-dev \
ros-$ROS_DISTRO-rviz2 ros-$ROS_DISTRO-foxglove-bridge \
ros-$ROS_DISTRO-pcl-ros ros-$ROS_DISTRO-pcl-conversions ros-$ROS_DISTRO-pcl-msgs \
- ros-$ROS_DISTRO-navigation2 ros-$ROS_DISTRO-nav2-msgs \
+ ros-$ROS_DISTRO-navigation2 ros-$ROS_DISTRO-nav2-msgs ros-$ROS_DISTRO-mavlink \
lua5.4 liblua5.4-0 liblua5.4-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/*
diff --git a/rmcs_ws/src/rmcs_bringup/config/flight.yaml b/rmcs_ws/src/rmcs_bringup/config/flight.yaml
index d0082799..8dba1b22 100644
--- a/rmcs_ws/src/rmcs_bringup/config/flight.yaml
+++ b/rmcs_ws/src/rmcs_bringup/config/flight.yaml
@@ -3,6 +3,7 @@ rmcs_executor:
update_rate: 1000.0
components:
- rmcs_core::hardware::Flight -> flight_hardware
+ - rmcs_core::hardware::Px4VisionBridge -> px4_vision_bridge
- rmcs_core::controller::gimbal::SimpleGimbalController -> gimbal_controller
- rmcs_core::controller::pid::ErrorPidController -> yaw_angle_pid_controller
@@ -26,15 +27,65 @@ rmcs_executor:
# - rmcs_core::broadcaster::ValueBroadcaster -> value_broadcaster
# - rmcs_core::broadcaster::TfBroadcaster -> tf_broadcaster
- - rmcs::AutoAimComponent
-
-odin_ros_driver:
- ros__parameters:
- enabled: true
- config_file: "/rmcs_install/share/odin_ros_driver/config/control_command.yaml"
- node_name: host_sdk_sample
- respawn: true
- respawn_delay: 1.0
+ - rmcs::AutoAimCapturerComponent -> auto_aim_capturer
+ # - rmcs::AutoAimPlayerComponent -> auto_aim_player
+ - rmcs::AutoAimRecorderComponent -> auto_aim_recorder
+ - rmcs::AutoAimComponent -> auto_aim_component
+
+auto_aim_capturer:
+ ros__parameters:
+ camera_name: ""
+ exposure_us: 3000.0
+ gain: 8.0
+ framerate: 120.0
+ invert_image: false
+ rls_tau_sec: 10.0
+ use_hardware_sync: false
+ delay_ms: 6.5
+
+auto_aim_recorder:
+ ros__parameters:
+ output_path: "/tmp/autoaim/records"
+ queue_depth: 16
+ flush_every_n_frames: 64
+ max_duration_seconds: 0
+ max_videos_size_gb: 0.0
+
+auto_aim_component:
+ ros__parameters:
+ # WARN: 危险!可选 red | blue,生效后裁判系统 ID 缺席时
+ # 将强行绑定为对应阵营哨兵,仅供调试,严禁比赛启用。
+ # 留空或填 unknow 表示禁用。
+ dangerous_fallback: ""
+ manual_shoot: true
+ camera_translation: [0.10238, 0.0, 0.05286]
+ fire_control:
+ bullet_speed: 22.5
+ shoot_delay: 0.05
+ offset_yaw: +1.3 #越大越左
+ offset_pitch: +1.7 #越大越下
+ attack_window: 80.0
+ degraded_angle_speed: 12.0
+ window_hysteresis: 0.2
+ is_lazy_gimbal: false
+ attack_preaim: false
+ require_stable_command: false
+ yaw_tolerance: 0.07
+ pitch_tolerance: 0.04
+
+px4_vision_bridge:
+ ros__parameters:
+ source_topic: /odin1/odometry_highfreq
+ system_id: 1 # 与 PX4 MAV_SYS_ID 一致
+ component_id: 197 # MAV_COMP_ID_VISUAL_INERTIAL_ODOMETRY
+ max_send_rate_hz: 50.0
+ # 挂载角(ZYX, rad): 传感器系->机体系, 初值 roll=π pitch=π/2 yaw=0
+ mount_rpy: [3.14159265358979, 1.5707963267949, 0.0]
+ # Odin1 自启看门狗: 里程计断流超过 timeout 秒且距上次拉起超过 cooldown 秒
+ # 才执行 tmux-launch.sh; 数据正常时不会重启 Odin1 (保住 SLAM 预热)
+ odin_autostart: true
+ odin_watchdog_timeout: 10.0
+ odin_restart_cooldown: 60.0 # 需覆盖 SLAM 预热 30~60s
value_broadcaster:
ros__parameters:
@@ -88,7 +139,7 @@ yaw_velocity_pid_controller:
measurement: /gimbal/yaw/velocity_imu
setpoint: /gimbal/yaw/control_velocity
control: /gimbal/yaw/control_torque
- kp: 8.0
+ kp: 6.0
ki: 0.0
kd: 0.0
diff --git a/rmcs_ws/src/rmcs_core/package.xml b/rmcs_ws/src/rmcs_core/package.xml
index 4312d334..b3d6d14a 100644
--- a/rmcs_ws/src/rmcs_core/package.xml
+++ b/rmcs_ws/src/rmcs_core/package.xml
@@ -20,6 +20,9 @@
rmcs_msgs
rmcs_executor
rmcs_description
+ mavlink
+ nav_msgs
+ ament_index_cpp
ament_lint_auto
ament_lint_common
diff --git a/rmcs_ws/src/rmcs_core/plugins.xml b/rmcs_ws/src/rmcs_core/plugins.xml
index 95777e34..a9845583 100644
--- a/rmcs_ws/src/rmcs_core/plugins.xml
+++ b/rmcs_ws/src/rmcs_core/plugins.xml
@@ -5,6 +5,8 @@
+
+
diff --git a/rmcs_ws/src/rmcs_core/src/controller/flight/px4_vision_bridge.cpp b/rmcs_ws/src/rmcs_core/src/controller/flight/px4_vision_bridge.cpp
new file mode 100644
index 00000000..40c64049
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/controller/flight/px4_vision_bridge.cpp
@@ -0,0 +1,266 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace rmcs_core::hardware {
+
+class Px4VisionBridge
+ : public rmcs_executor::Component
+ , public rclcpp::Node {
+public:
+ Px4VisionBridge()
+ : Node{
+ get_component_name(),
+ rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)}
+ , logger_(get_logger()) {
+
+ register_input("/px4/serial", px4_serial_);
+
+ source_topic_ = get_parameter("source_topic").as_string();
+ system_id_ = get_parameter("system_id").as_int();
+ component_id_ = get_parameter("component_id").as_int();
+ max_send_rate_hz_ = get_parameter("max_send_rate_hz").as_double();
+
+ const auto m = get_parameter("mount_rpy").as_double_array();
+
+ q_mount_ = quaternion_from_rpy_zyx(m.at(0), m.at(1), m.at(2));
+
+ min_send_interval_ = rclcpp::Duration::from_seconds(1.0 / max_send_rate_hz_);
+ last_send_time_ = now();
+ last_heartbeat_send_ = now();
+
+ odom_subscription_ = create_subscription(
+ source_topic_, rclcpp::SensorDataQoS{},
+ [this](nav_msgs::msg::Odometry::UniquePtr msg) { odometry_callback(std::move(msg)); });
+
+ odin_autostart_ = get_parameter("odin_autostart").as_bool();
+ odin_watchdog_timeout_ = get_parameter("odin_watchdog_timeout").as_double();
+ odin_restart_cooldown_ = get_parameter("odin_restart_cooldown").as_double();
+
+ last_odom_arrival_ns_.store(steady_now_ns(), std::memory_order_relaxed);
+ if (odin_autostart_)
+ odin_manager_thread_ =
+ std::jthread{[this](const std::stop_token& st) { odin_manager_loop(st); }};
+ }
+
+ Px4VisionBridge(const Px4VisionBridge&) = delete;
+ Px4VisionBridge& operator=(const Px4VisionBridge&) = delete;
+
+ void update() override {
+ send_heartbeat_if_due(); // 1hz HEARTBEAT
+ send_vision_if_pending(); // 消费回调缓存的最新位姿,变换并发送
+ }
+
+private:
+ struct OdomSample {
+ rclcpp::Time stamp{0, 0, RCL_ROS_TIME};
+ Eigen::Vector3d position{Eigen::Vector3d::Zero()};
+ Eigen::Quaterniond orientation{Eigen::Quaterniond::Identity()};
+ };
+
+ void odometry_callback(const nav_msgs::msg::Odometry::UniquePtr msg) {
+ last_odom_arrival_ns_.store(steady_now_ns(), std::memory_order_relaxed);
+
+ const auto& p = msg->pose.pose.position;
+ const auto& o = msg->pose.pose.orientation;
+
+ const std::scoped_lock lock{odom_mutex_};
+ pending_odom_.stamp = rclcpp::Time{msg->header.stamp};
+ pending_odom_.position = Eigen::Vector3d{p.x, p.y, p.z};
+ pending_odom_.orientation = Eigen::Quaterniond{o.w, o.x, o.y, o.z};
+ odom_pending_ = true;
+ }
+
+ void send_vision_if_pending() {
+ if ((now() - last_send_time_) < min_send_interval_)
+ return;
+
+ OdomSample sample;
+ {
+ const std::scoped_lock lock{odom_mutex_};
+ if (!odom_pending_)
+ return;
+ odom_pending_ = false;
+ sample = pending_odom_;
+ }
+ last_send_time_ = now();
+
+ const auto x = static_cast(sample.position.y());
+ const auto y = static_cast(sample.position.x());
+ const auto z = static_cast(-sample.position.z());
+
+ const Eigen::Quaterniond q_enu_flu = sample.orientation * q_mount_;
+
+ Eigen::Quaterniond q_ned_frd = kNedEnuQ * (q_enu_flu * kAircraftBaselinkQ);
+
+ q_ned_frd.normalize();
+ double roll, pitch, yaw;
+ quaternion_to_rpy_zyx(q_ned_frd, roll, pitch, yaw);
+
+ const auto usec = static_cast(sample.stamp.nanoseconds()) / 1000ull;
+
+ float covariance[21];
+ std::fill(std::begin(covariance), std::end(covariance), NAN);
+
+ mavlink_message_t msg_mavlink;
+ mavlink_msg_vision_position_estimate_pack(
+ system_id_, component_id_, &msg_mavlink, usec, x, y, z, static_cast(roll),
+ static_cast(pitch), static_cast(yaw), covariance,
+ /*reset_counter=*/0);
+
+ send_message(msg_mavlink);
+ }
+
+ void send_heartbeat_if_due() {
+ if ((now() - last_heartbeat_send_).seconds() < 1.0)
+ return;
+ last_heartbeat_send_ = now();
+ mavlink_message_t msg_mavlink;
+ mavlink_msg_heartbeat_pack(
+ system_id_, component_id_, &msg_mavlink, MAV_TYPE_ONBOARD_CONTROLLER,
+ MAV_AUTOPILOT_INVALID,
+ /*base_mode=*/0,
+ /*custom_mode=*/0, MAV_STATE_ACTIVE);
+ send_message(msg_mavlink);
+ }
+
+ void send_message(const mavlink_message_t& msg) {
+ if (!px4_serial_.active()) [[unlikely]]
+ return;
+ uint8_t buffer[MAVLINK_MAX_PACKET_LEN];
+ const uint16_t len = mavlink_msg_to_send_buffer(buffer, &msg);
+ px4_serial_->write(reinterpret_cast(buffer), len);
+ }
+
+ static int64_t steady_now_ns() {
+ return std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch())
+ .count();
+ }
+
+ // 独立线程看门狗:断流超时且过了冷却期就拉起 tmux-launch.sh;
+ void odin_manager_loop(const std::stop_token& st) {
+ const std::string cmd = "\"" + ament_index_cpp::get_package_prefix("odin_ros_driver")
+ + "/lib/odin_ros_driver/tmux-launch.sh\"";
+
+ const auto timeout_ns = static_cast(odin_watchdog_timeout_ * 1e9);
+ const auto cooldown_ns = static_cast(odin_restart_cooldown_ * 1e9);
+ int64_t last_launch_ns = steady_now_ns() - cooldown_ns; // 首次断流即可拉起
+ bool online = true; // 启动宽限:先假定在线,超时才告警
+
+ // 锁仅为 wait_for 语义存在 request_stop 会直接唤醒等待
+ std::mutex sleep_mutex;
+ std::condition_variable_any sleep_cv;
+ std::unique_lock lock{sleep_mutex};
+ while (!sleep_cv.wait_for(
+ lock, st, std::chrono::seconds{1}, [&] { return st.stop_requested(); })) {
+
+ const auto now_ns = steady_now_ns();
+ const bool fresh =
+ (now_ns - last_odom_arrival_ns_.load(std::memory_order_relaxed)) < timeout_ns;
+
+ if (fresh != online) {
+ online = fresh;
+ if (online)
+ RCLCPP_INFO(logger_, "Odin1 online, odometry flowing");
+ else
+ RCLCPP_WARN(
+ logger_,
+ "Odin1 offline (no odometry for %.0fs), relaunching every %.0fs until "
+ "online",
+ odin_watchdog_timeout_, odin_restart_cooldown_);
+ }
+
+ if (!online && (now_ns - last_launch_ns) > cooldown_ns) {
+ last_launch_ns = now_ns;
+ // (脚本内部的清杀/启动轮询)存在阻塞,单独在本线程做
+ if (std::system(cmd.c_str()) != 0)
+ RCLCPP_ERROR(logger_, "Odin1 launch script failed");
+ }
+ }
+ }
+
+ static Eigen::Quaterniond quaternion_from_rpy_zyx(double roll, double pitch, double yaw) {
+ return Eigen::Quaterniond{
+ Eigen::AngleAxisd{yaw, Eigen::Vector3d::UnitZ()}
+ * Eigen::AngleAxisd{pitch, Eigen::Vector3d::UnitY()}
+ * Eigen::AngleAxisd{roll, Eigen::Vector3d::UnitX()}};
+ }
+
+ static void quaternion_to_rpy_zyx(
+ const Eigen::Quaterniond& q, double& roll, double& pitch, double& yaw) {
+ const double w = q.w(), x = q.x(), y = q.y(), z = q.z();
+ const double m20 = 2.0 * (x * z - w * y); // -sin(pitch)
+ const double m21 = 2.0 * (y * z + w * x);
+ const double m22 = 1.0 - 2.0 * (x * x + y * y);
+ const double sin_pitch = std::clamp(-m20, -1.0, 1.0);
+ const double cos_pitch = std::sqrt(m21 * m21 + m22 * m22);
+
+ if (cos_pitch > 1e-9) {
+ pitch = std::atan2(sin_pitch, cos_pitch);
+ roll = std::atan2(m21, m22);
+ yaw = std::atan2(2.0 * (x * y + w * z), 1.0 - 2.0 * (y * y + z * z));
+ return;
+ }
+ // 万向节锁 (pitch=±π/2):roll/yaw 只剩组合自由度可观测,
+ // 约定 roll=0、组合角全部归 yaw,保证三元组重建仍是原旋转
+ const double m11 = 1.0 - 2.0 * (x * x + z * z);
+ const double m12 = 2.0 * (y * z - w * x);
+ roll = 0.0;
+ pitch = (sin_pitch > 0.0) ? M_PI_2 : -M_PI_2;
+ yaw = (sin_pitch > 0.0) ? std::atan2(m12, m11) : std::atan2(-m12, m11);
+ }
+
+ // ENU->NED
+ static inline const Eigen::Quaterniond kNedEnuQ{0.0, 0.70710678118655, 0.70710678118655, 0.0};
+ // FLU->FRD
+ static inline const Eigen::Quaterniond kAircraftBaselinkQ{0.0, 1.0, 0.0, 0.0};
+
+ rclcpp::Logger logger_;
+ InputInterface px4_serial_;
+ rclcpp::Subscription::SharedPtr odom_subscription_;
+
+ // 回调线程(rclcpp spin)与 update() 线程(executor)间的最新位姿交接
+ std::mutex odom_mutex_;
+ OdomSample pending_odom_;
+ bool odom_pending_ = false;
+
+ std::string source_topic_;
+ uint8_t system_id_;
+ uint8_t component_id_;
+ double max_send_rate_hz_;
+ Eigen::Quaterniond q_mount_;
+
+ rclcpp::Duration min_send_interval_ = rclcpp::Duration::from_seconds(0.02);
+ rclcpp::Time last_send_time_ = rclcpp::Time(0, 0, RCL_ROS_TIME);
+ rclcpp::Time last_heartbeat_send_{0, 0, RCL_ROS_TIME};
+
+ // Odin1 拉起与看门狗
+ bool odin_autostart_;
+ double odin_watchdog_timeout_;
+ double odin_restart_cooldown_;
+ std::atomic last_odom_arrival_ns_{0};
+ // 必须最后声明:其隐式析构 request_stop+join,须早于线程所用成员的销毁
+ std::jthread odin_manager_thread_;
+};
+
+} // namespace rmcs_core::hardware
+
+#include
+PLUGINLIB_EXPORT_CLASS(rmcs_core::hardware::Px4VisionBridge, rmcs_executor::Component)
diff --git a/rmcs_ws/src/rmcs_core/src/hardware/flight.cpp b/rmcs_ws/src/rmcs_core/src/hardware/flight.cpp
index d1c5b214..75480707 100644
--- a/rmcs_ws/src/rmcs_core/src/hardware/flight.cpp
+++ b/rmcs_ws/src/rmcs_core/src/hardware/flight.cpp
@@ -2,7 +2,6 @@
#include
#include
#include
-#include
#include
#include
@@ -10,11 +9,14 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
-#include "hardware/device/bmi088.hpp"
+#include "hardware/device/bmi088_ekf.hpp"
+#include "hardware/device/board_clock_lifter.hpp"
#include "hardware/device/can_packet.hpp"
#include "hardware/device/dji_motor.hpp"
#include "hardware/device/dr16.hpp"
@@ -51,9 +53,6 @@ class Flight
gimbal_bullet_feeder_.configure(
device::DjiMotor::Config{device::DjiMotor::Type::kM2006, 1}.enable_multi_turn_angle());
- bmi088_.set_coordinate_mapping(
- [](double x, double y, double z) { return std::tuple{y, z, x}; });
-
using namespace rmcs_description;
constexpr auto kCameraPostionX = 0.10238;
@@ -61,12 +60,11 @@ class Flight
tf_->set_transform(
Eigen::Translation3d{kCameraPostionX, 0.0, kCameraPostionZ});
- register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_);
- register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_);
+ register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_, 0.0);
+ register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_, 0.0);
register_output("/tf", tf_);
- register_output("/auto_aim/camera_transform", camera_transform_);
- register_output("/auto_aim/barrel_direction", barrel_direction_);
+ register_output("/gimbal/auto_aim/imu_snapshot", imu_snapshot_output_);
register_output("/referee/serial", referee_serial_);
referee_serial_->read = [this](std::byte* buffer, size_t size) {
@@ -79,6 +77,14 @@ class Flight
return size;
};
+ register_output("/px4/serial", px4_serial_);
+ px4_serial_->read = [](std::byte*, size_t) { return size_t{0}; };
+ px4_serial_->write = [this](const std::byte* buffer, size_t size) {
+ board_->start_transmit().uart_transmit(
+ Spec::kUarts.kUart0, {.uart_data = std::span{buffer, size}});
+ return size;
+ };
+
remote_control_ = std::make_unique(*this);
remote_control_->register_dr16(&dr16_);
@@ -99,11 +105,6 @@ class Flight
update_imu();
dr16_.update_status();
remote_control_->update();
-
- using namespace rmcs_description;
- *camera_transform_ = fast_tf::lookup_transform(*tf_);
- *barrel_direction_ =
- *fast_tf::cast(PitchLink::DirectionVector{Eigen::Vector3d::UnitX()}, *tf_);
}
void command_update() {
@@ -162,13 +163,12 @@ class Flight
void update_imu() {
using namespace rmcs_description;
- bmi088_.update_status();
- const auto gimbal_imu_pose =
- Eigen::Quaterniond{bmi088_.q0(), bmi088_.q1(), bmi088_.q2(), bmi088_.q3()};
- tf_->set_transform(gimbal_imu_pose.conjugate());
+ if (const auto snapshot = bmi088_.snapshot()) {
+ tf_->set_transform(snapshot->orientation.conjugate());
- *gimbal_yaw_velocity_imu_ = bmi088_.gz();
- *gimbal_pitch_velocity_imu_ = bmi088_.gy();
+ *gimbal_yaw_velocity_imu_ = snapshot->gyro_body.z();
+ *gimbal_pitch_velocity_imu_ = snapshot->gyro_body.y();
+ }
}
void
@@ -225,11 +225,21 @@ class Flight
}
void accelerometer_receive_callback(const View::ImuAccelerometer& data) override {
- bmi088_.store_accelerometer_status(data.x, data.y, data.z);
+ const auto timestamp = board_clock_lifter_.advance_timebase(data.timestamp_quarter_us);
+ bmi088_.push_accelerometer_sample(data.x, data.y, data.z, timestamp);
}
void gyroscope_receive_callback(const View::ImuGyroscope& data) override {
- bmi088_.store_gyroscope_status(data.x, data.y, data.z);
+ const auto timestamp = board_clock_lifter_.lift_timestamp(data.timestamp_quarter_us);
+ if (!timestamp.has_value())
+ return;
+
+ const auto snapshot =
+ bmi088_.try_update_with_gyroscope_sample(data.x, data.y, data.z, *timestamp);
+ if (!snapshot)
+ return;
+
+ imu_snapshot_output_.emit(*snapshot);
}
private:
@@ -258,15 +268,19 @@ class Flight
device::Dr16 dr16_;
std::unique_ptr remote_control_;
- device::Bmi088 bmi088_{1000.0, 0.2, 0.00};
+ // 等价于旧 Bmi088 的坐标映射 (x, y, z) -> (y, z, x):body = body_to_sensor^T * sensor
+ device::Bmi088Ekf bmi088_{device::Bmi088Ekf::Config{
+ .body_to_sensor = (Eigen::Matrix3d{} << 0, 0, 1, 1, 0, 0, 0, 1, 0).finished(),
+ }};
+ device::BoardClockLifter board_clock_lifter_;
OutputInterface gimbal_yaw_velocity_imu_;
OutputInterface gimbal_pitch_velocity_imu_;
OutputInterface tf_;
OutputInterface referee_serial_;
+ OutputInterface px4_serial_;
- OutputInterface camera_transform_;
- OutputInterface barrel_direction_;
+ EventOutputInterface imu_snapshot_output_;
rmcs_utility::RingBuffer referee_ring_buffer_receive_{256};
};