diff --git a/rmcs_ws/src/hikcamera b/rmcs_ws/src/hikcamera
new file mode 160000
index 00000000..f0077f03
--- /dev/null
+++ b/rmcs_ws/src/hikcamera
@@ -0,0 +1 @@
+Subproject commit f0077f034800bcd0dde4fffeff270b733772a57e
diff --git a/rmcs_ws/src/odin_ros_driver b/rmcs_ws/src/odin_ros_driver
new file mode 160000
index 00000000..8cbaf718
--- /dev/null
+++ b/rmcs_ws/src/odin_ros_driver
@@ -0,0 +1 @@
+Subproject commit 8cbaf718ddc383afe98f63e63585e79117d25901
diff --git a/rmcs_ws/src/rmcs_bringup/config/flight.yaml b/rmcs_ws/src/rmcs_bringup/config/flight.yaml
index d0082799..33b3a146 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,64 @@ 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.1
+ offset_yaw: +1.5 #越大越左
+ offset_pitch: +3.8 #越大越下
+ attack_window: 120.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 +138,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/CMakeLists.txt b/rmcs_ws/src/rmcs_core/CMakeLists.txt
index 0ae6350a..234c0eeb 100644
--- a/rmcs_ws/src/rmcs_core/CMakeLists.txt
+++ b/rmcs_ws/src/rmcs_core/CMakeLists.txt
@@ -13,6 +13,7 @@ endif()
find_package(ament_cmake_auto REQUIRED)
ament_auto_find_build_dependencies()
+find_package(mavlink REQUIRED)
include(FetchContent)
set(BUILD_STATIC_LIBRMCS ON CACHE BOOL "Build static librmcs SDK" FORCE)
@@ -33,6 +34,7 @@ ament_auto_add_library(
${PROJECT_NAME} SHARED
${PROJECT_SOURCE}
)
+target_include_directories(${PROJECT_NAME} PRIVATE ${mavlink_INCLUDE_DIRS})
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(${PROJECT_SOURCE_DIR}/src)
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 1803a1d4..7f94f061 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..8b7e58d2
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/controller/flight/px4_vision_bridge.cpp
@@ -0,0 +1,315 @@
+#include "nav_msgs/msg/odometry.hpp"
+#include
+
+#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::thread{[this]{odin_manager_loop();}};
+ }
+
+ ~Px4VisionBridge() override {
+ {
+ const std::scoped_lock lock{odin_stop_mutex_};
+ odin_stop_ =true;
+ }
+ odin_stop_cv_.notify_all();
+ if(odin_manager_thread_.joinable())
+ odin_manager_thread_.join();
+ }
+
+ 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_)(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::string script =
+ 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 offline_reported =false;
+ bool launch_failure_reported =false;
+ bool online =false;
+
+ std::unique_lock lock{odin_stop_mutex_};
+ while(!odin_stop_cv_.wait_for(
+ lock,std::chrono::seconds{1},[this]{return odin_stop_;})){
+ lock.unlock();
+
+ const auto now_ns =steady_now_ns();
+ const bool data_fresh =
+ (now_ns-last_odom_arrival_ns_.load(std::memory_order_relaxed))cooldown_ns){
+ last_launch_ns =now_ns;
+ // system() 最坏阻塞十余秒(脚本内部的清杀/启动轮询),只能在本线程做
+ if(std::system(("\""+script+"\"").c_str())!=0
+ &&!launch_failure_reported){
+ launch_failure_reported =true;
+ RCLCPP_ERROR(logger_,"Odin1 launch script failed");
+ }
+ }
+ }
+
+ lock.lock();
+ }
+ }
+
+ 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};
+ std::thread odin_manager_thread_;
+ std::mutex odin_stop_mutex_;
+ std::condition_variable odin_stop_cv_;
+ bool odin_stop_ =false;
+};
+
+}// namespace rmcs_core::hardware
+
+#include
+PLUGINLIB_EXPORT_CLASS(rmcs_core::hardware::Px4VisionBridge, rmcs_executor::Component)
\ No newline at end of file
diff --git a/rmcs_ws/src/rmcs_core/src/hardware/flight.cpp b/rmcs_ws/src/rmcs_core/src/hardware/flight.cpp
index d1c5b214..65135426 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,20 @@ 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};
};