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}; };