From 7e16876f0187addc2762d4352da73668d0fdaa24 Mon Sep 17 00:00:00 2001 From: Fin_Resect Date: Sun, 15 Mar 2026 21:57:40 +0800 Subject: [PATCH 01/12] Pick steering hero resources from branch steering-hero Co-authored-by: zhzy-star <2807406212@qq.com> Co-authored-by: floatpigeon Co-authored-by: fan --- .../rmcs_bringup/config/steering-hero.yaml | 257 +++++++ rmcs_ws/src/rmcs_core/plugins.xml | 155 +++- .../chassis/chassis_climber_controller.cpp | 185 +++++ .../controller/gimbal/dual_yaw_controller.cpp | 3 - .../bullet_feeder_controller_42mm.cpp | 61 +- .../controller/shooting/putter_controller.cpp | 387 ++++++++++ .../controller/shooting/shooting_recorder.cpp | 215 +++++- .../rmcs_core/src/hardware/steering-hero.cpp | 711 ++++++++++-------- 8 files changed, 1584 insertions(+), 390 deletions(-) create mode 100644 rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml create mode 100644 rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp create mode 100644 rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp diff --git a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml new file mode 100644 index 000000000..7e7c551e7 --- /dev/null +++ b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml @@ -0,0 +1,257 @@ +rmcs_executor: + ros__parameters: + update_rate: 1000.0 + components: + - rmcs_core::hardware::SteeringHero -> hero_hardware + + - rmcs_core::referee::Status -> referee_status + - rmcs_core::referee::command::Interaction -> referee_interaction + # - rmcs_core::referee::command::interaction::Ui -> referee_ui + # - rmcs_core::referee::app::ui::Hero -> referee_ui_hero + - rmcs_core::referee::Command -> referee_command + + - rmcs_core::controller::gimbal::HeroGimbalController -> gimbal_controller + - rmcs_core::controller::gimbal::DualYawController -> dual_yaw_controller + - rmcs_core::controller::pid::ErrorPidController -> pitch_angle_pid_controller + + # - rmcs_core::controller::gimbal::PlayerViewer -> gimbal_player_viewer_controller + # - rmcs_core::controller::pid::ErrorPidController -> viewer_angle_pid_controller + + - rmcs_core::controller::shooting::FrictionWheelController -> friction_wheel_controller + - rmcs_core::controller::shooting::HeatController -> heat_controller + - rmcs_core::controller::shooting::PutterController -> bullet_feeder_controller + - rmcs_core::controller::pid::PidController -> first_left_friction_velocity_pid_controller + - rmcs_core::controller::pid::PidController -> first_right_friction_velocity_pid_controller + - rmcs_core::controller::pid::PidController -> second_left_friction_velocity_pid_controller + - rmcs_core::controller::pid::PidController -> second_right_friction_velocity_pid_controller + + - rmcs_core::controller::chassis::ChassisController -> chassis_controller + - rmcs_core::controller::chassis::ChassisPowerController -> chassis_power_controller + - rmcs_core::controller::chassis::SteeringWheelController -> steering_wheel_controller + - rmcs_core::controller::chassis::ChassisClimberController -> climber_controller + + # - rmcs_auto_aim::AutoAimInitializer -> auto_aim_initializer + # - rmcs_auto_aim::AutoAimController -> auto_aim_controller + + - rmcs_core::broadcaster::ValueBroadcaster -> value_broadcaster + +hero_hardware: + ros__parameters: + usb_pid_top_board: 0xce3b + usb_pid_bottom_board_one: 0x902e + usb_pid_bottom_board_two: 0x317e + bottom_yaw_motor_zero_point: 50656 + pitch_motor_zero_point: 63917 + top_yaw_motor_zero_point: 59095 + viewer_motor_zero_point: 3030 + external_imu_port: /dev/ttyUSB0 + left_front_zero_point: 4438 + right_front_zero_point: 7167 + left_back_zero_point: 370 + right_back_zero_point: 5156 + q0: 0.0 + q1: 0.0 + q2: 0.0 + q3: 1.0 + # left_front_zero_point: 3473 + # left_back_zero_point: 6124 + # right_back_zero_point: 6157 + # right_front_zero_point: 2723 + +value_broadcaster: + ros__parameters: + forward_list: + - /chassis/left_front_steering/angle + - /chassis/left_back_steering/angle + - /chassis/right_back_steering/angle + - /chassis/right_front_steering/angle + - /chassis/left_front_steering/control_torque + - /chassis/left_back_steering/control_torque + - /chassis/right_back_steering/control_torque + - /chassis/right_front_steering/control_torque + - /gimbal/bottom_yaw/angle + +climber_controller: + ros__parameters: + front_climber_velocity: 50.0 + back_climber_velocity: 10.0 + front_kp: 2.0 + front_ki: 0.0 + front_kd: 0.5 + back_kp: 2.0 + back_ki: 0.0 + back_kd: 0.0 + +gimbal_controller: + ros__parameters: + upper_limit: -0.6266 + lower_limit: 0.4629 + +dual_yaw_controller: + ros__parameters: + top_yaw_angle_kp: 10.0 + top_yaw_angle_ki: 0.0 + top_yaw_angle_kd: 0.0 + top_yaw_velocity_kp: 50.0 + top_yaw_velocity_ki: 0.004 + top_yaw_velocity_kd: 0.0 + bottom_yaw_angle_kp: 5.0 + bottom_yaw_angle_ki: 0.0 + bottom_yaw_angle_kd: 0.0 + bottom_yaw_velocity_kp: 10.0 + bottom_yaw_velocity_ki: 0.0 + bottom_yaw_velocity_kd: 50.0 + +# dual_yaw_controller: +# ros__parameters: +# top_yaw_angle_kp: 24.5 +# top_yaw_angle_ki: 0.0 +# top_yaw_angle_kd: 0.0 +# top_yaw_velocity_kp: 77.4 +# top_yaw_velocity_ki: 0.004 +# top_yaw_velocity_kd: 1.0 +# bottom_yaw_angle_kp: 8.6 +# bottom_yaw_angle_ki: 0.0 +# bottom_yaw_angle_kd: 0.0 +# bottom_yaw_velocity_kp: 25.85 +# bottom_yaw_velocity_ki: 0.0 +# bottom_yaw_velocity_kd: 50.0 + +pitch_angle_pid_controller: + ros__parameters: + measurement: /gimbal/pitch/control_angle_error + control: /gimbal/pitch/control_velocity + kp: 28.00 + ki: 0.0 + kd: 0.6 + +pitch_velocity_pid_controller: + ros__parameters: + measurement: /gimbal/pitch/velocity_imu + setpoint: /gimbal/pitch/control_velocity + control: /gimbal/pitch/control_torque + kp: 45.00 + ki: 0.00 + kd: 1.00 + +gimbal_player_viewer_controller: + ros__parameters: + upper_limit: 0.0101 + lower_limit: 0.6191 + +viewer_angle_pid_controller: + ros__parameters: + measurement: /gimbal/player_viewer/control_angle_error + control: /gimbal/player_viewer/control_velocity + kp: 17.00 + ki: 0.00 + kd: 2.00 + +friction_wheel_controller: + ros__parameters: + friction_wheels: + - /gimbal/first_left_friction + - /gimbal/first_right_friction + - /gimbal/second_left_friction + - /gimbal/second_right_friction + friction_velocities: + - 450.00 + - 450.00 + - -535.00 + - 535.00 + friction_soft_start_stop_time: 1.0 + +heat_controller: + ros__parameters: + heat_per_shot: 100000 + reserved_heat: 0 + +shooting_recorder: + ros__parameters: + friction_wheel_count: 2 + log_mode: 2 # 1: trigger, 2: timing + +first_left_friction_velocity_pid_controller: + ros__parameters: + measurement: /gimbal/first_left_friction/velocity + setpoint: /gimbal/first_left_friction/control_velocity + control: /gimbal/first_left_friction/control_torque + kp: 0.003436926 + ki: 0.00 + kd: 0.009373434 + +first_right_friction_velocity_pid_controller: + ros__parameters: + measurement: /gimbal/first_right_friction/velocity + setpoint: /gimbal/first_right_friction/control_velocity + control: /gimbal/first_right_friction/control_torque + kp: 0.003436926 + ki: 0.00 + kd: 0.009373434 + +second_left_friction_velocity_pid_controller: + ros__parameters: + measurement: /gimbal/second_left_friction/velocity + setpoint: /gimbal/second_left_friction/control_velocity + control: /gimbal/second_left_friction/control_torque + kp: 0.003436926 + ki: 0.00 + kd: 0.009373434 + +second_right_friction_velocity_pid_controller: + ros__parameters: + measurement: /gimbal/second_right_friction/velocity + setpoint: /gimbal/second_right_friction/control_velocity + control: /gimbal/second_right_friction/control_torque + kp: 0.003436926 + ki: 0.00 + kd: 0.009373434 + +steering_wheel_controller: + ros__parameters: + mess: 22.0 + moment_of_inertia: 1.08 + vehicle_radius: 0.28284271247462 + wheel_radius: 0.055 + friction_coefficient: 0.6 + k1: 2.958580e+00 + k2: 3.082190e-03 + no_load_power: 11.37 + +auto_aim_controller: + ros__parameters: + # capture + use_video: false # If true, use video stream instead of camera. + video_path: "/workspaces/RMCS/rmcs_ws/resources/1.avi" + exposure_time: 1 + invert_image: false + # identifier + armor_model_path: "/models/mlp.onnx" + # pnp + fx: 1.722231837421459e+03 + fy: 1.724876404292754e+03 + cx: 7.013056440882832e+02 + cy: 5.645821718351237e+02 + k1: -0.064232403853946 + k2: -0.087667493884102 + k3: 0.792381808294582 + # tracker + armor_predict_duration: 500 + # controller + gimbal_predict_duration: 100 + yaw_error: 0. + pitch_error: 0. + shoot_velocity: 28.0 + predict_sec: 0.095 + # etc + buff_predict_duration: 200 + buff_model_path: "/models/buff_nocolor_v6.onnx" + omni_exposure: 1000.0 + record_fps: 120 + debug: false # Setup in actual using.Debug mode is used when referee is not ready + debug_color: 0 # 0 For blue while 1 for red. mine + debug_robot_id: 4 + debug_buff_mode: false + record: false + raw_img_pub: false # Set false in actual use + image_viewer_type: 2 diff --git a/rmcs_ws/src/rmcs_core/plugins.xml b/rmcs_ws/src/rmcs_core/plugins.xml index f7847151c..9df7668b7 100644 --- a/rmcs_ws/src/rmcs_core/plugins.xml +++ b/rmcs_ws/src/rmcs_core/plugins.xml @@ -1,29 +1,128 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Steering wheel controller. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Gimbal player viewer + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + the recorder of Hero + + + Test plugin. + + + Test plugin. + + + Feedforward pid controller. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + + Test plugin. + + \ No newline at end of file diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp new file mode 100644 index 000000000..3a5241879 --- /dev/null +++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp @@ -0,0 +1,185 @@ +#include "controller/pid/matrix_pid_calculator.hpp" +#include "rmcs_msgs/switch.hpp" +#include +#include +#include +#include +#include +#include +#include + +/* + 所有的电机运动方向均以令底盘升高为正方向 + 暂时使用手动控制,遥控器键位不够用了,屏蔽自瞄和小陀螺模式 +*/ + +namespace rmcs_core::controller::chassis { +class ChassisClimberController + : public rmcs_executor::Component + , public rclcpp::Node { +public: + ChassisClimberController() + : Node( + get_component_name(), + rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)) + , logger_(get_logger()) + , front_velocity_pid_calculator_( + get_parameter("front_kp").as_double(), get_parameter("front_ki").as_double(), + get_parameter("front_kd").as_double()) + , back_velocity_pid_calculator_( + get_parameter("back_kp").as_double(), get_parameter("back_ki").as_double(), + get_parameter("back_kd").as_double()) { + + track_velocity_max_ = get_parameter("front_climber_velocity").as_double(); + climber_back_control_velocity_abs_ = get_parameter("back_climber_velocity").as_double(); + + register_output( + "/chassis/climber/left_front_motor/control_torque", climber_front_left_control_torque_, + nan_); + register_output( + "/chassis/climber/right_front_motor/control_torque", + climber_front_right_control_torque_, nan_); + register_output( + "/chassis/climber/left_back_motor/control_torque", climber_back_left_control_torque_, + nan_); + register_output( + "/chassis/climber/right_back_motor/control_torque", climber_back_right_control_torque_, + nan_); + + register_input("/chassis/climber/left_front_motor/velocity", climber_front_left_velocity_); + register_input( + "/chassis/climber/right_front_motor/velocity", climber_front_right_velocity_); + register_input("/chassis/climber/left_back_motor/velocity", climber_back_left_velocity_); + register_input("/chassis/climber/right_back_motor/velocity", climber_back_right_velocity_); + + register_input("/chassis/climber/left_back_motor/torque", climber_back_left_torque_); + register_input("/chassis/climber/right_back_motor/torque", climber_back_right_torque_); + + register_input("/remote/switch/right", switch_right_); + register_input("/remote/switch/left", switch_left_); + register_input("/remote/joystick/right", joystick_right_); + } + + void update() override { + using namespace rmcs_msgs; + + auto switch_right = *switch_right_; + auto switch_left = *switch_left_; + + if ((switch_left == Switch::UNKNOWN || switch_right == Switch::UNKNOWN) + || (switch_left == Switch::DOWN && switch_right == Switch::DOWN)) { + reset_all_controls(); + } else if (switch_left != Switch::DOWN) { + + if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::UP) { + front_climber_enable_ = !front_climber_enable_; + + } else if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::DOWN) { + back_climber_dir_ = -1 * back_climber_dir_; + back_climber_block_count_ = 0; + } + + double track_control_velocity = + front_climber_enable_ ? joystick_right_->x() * track_velocity_max_ : nan_; + double back_climber_control_velocity; + + if ((std::abs(*climber_back_left_torque_) > 0.1 + && std::abs(*climber_back_right_velocity_) < 0.1) + || (std::abs(*climber_back_left_velocity_) < 0.1 + && std::abs(*climber_back_right_torque_) > 0.1)) { + back_climber_block_count_++; + } + + dual_motor_sync_control( + track_control_velocity, *climber_front_left_velocity_, + *climber_front_right_velocity_, front_velocity_pid_calculator_, + *climber_front_left_control_torque_, *climber_front_right_control_torque_); + + if (back_climber_block_count_ >= 500) { + back_climber_control_velocity = 0; + // *climber_back_left_control_torque_ = 0; + // *climber_back_right_control_torque_ = 0; + // last_switch_left_ = switch_left; + // last_switch_right_ = switch_right; + // return; + } else { + back_climber_control_velocity = + climber_back_control_velocity_abs_ * back_climber_dir_; + } + + // dual_motor_sync_control( + // back_climber_control_velocity, *climber_back_left_velocity_, + // *climber_back_right_velocity_, back_velocity_pid_calculator_, + // *climber_back_left_control_torque_, *climber_back_right_control_torque_); + } + last_switch_left_ = switch_left; + last_switch_right_ = switch_right; + } + +private: + void reset_all_controls() { + *climber_front_left_control_torque_ = 0; + *climber_front_right_control_torque_ = 0; + *climber_back_left_control_torque_ = 0; + *climber_back_right_control_torque_ = 0; + front_climber_enable_ = false; + } + + void dual_motor_sync_control( + double setpoint, double left_velocity, double right_velocity, + pid::MatrixPidCalculator<2>& pid_calculator, double& left_torque_out, + double& right_torque_out) const { + + Eigen::Vector2d setpoint_error{setpoint - left_velocity, setpoint - right_velocity}; + + Eigen::Vector2d relative_velocity{ + left_velocity - right_velocity, right_velocity - left_velocity}; + + Eigen::Vector2d control_error = setpoint_error - sync_coefficient_ * relative_velocity; + + auto control_torques = pid_calculator.update(control_error); + + left_torque_out = control_torques[0]; + right_torque_out = control_torques[1]; + } + + int back_climber_block_count_; + + rclcpp::Logger logger_; + static constexpr double nan_ = std::numeric_limits::quiet_NaN(); + double sync_coefficient_; + + bool front_climber_enable_ = false; + double back_climber_dir_ = -1; + + double track_velocity_max_; + double climber_back_control_velocity_abs_; + + OutputInterface climber_front_left_control_torque_; + OutputInterface climber_front_right_control_torque_; + OutputInterface climber_back_left_control_torque_; + OutputInterface climber_back_right_control_torque_; + + InputInterface climber_front_left_velocity_; + InputInterface climber_front_right_velocity_; + InputInterface climber_back_left_velocity_; + InputInterface climber_back_right_velocity_; + + InputInterface climber_back_left_torque_; + InputInterface climber_back_right_torque_; + + InputInterface switch_right_; + InputInterface switch_left_; + InputInterface joystick_right_; + + rmcs_msgs::Switch last_switch_right_ = rmcs_msgs::Switch::UNKNOWN; + rmcs_msgs::Switch last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; + + pid::MatrixPidCalculator<2> front_velocity_pid_calculator_, back_velocity_pid_calculator_; +}; +} // namespace rmcs_core::controller::chassis + +#include + +PLUGINLIB_EXPORT_CLASS( + rmcs_core::controller::chassis::ChassisClimberController, rmcs_executor::Component) \ No newline at end of file diff --git a/rmcs_ws/src/rmcs_core/src/controller/gimbal/dual_yaw_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/gimbal/dual_yaw_controller.cpp index 80d7a72e3..20c092363 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/gimbal/dual_yaw_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/gimbal/dual_yaw_controller.cpp @@ -45,9 +45,6 @@ class DualYawController register_output("/gimbal/top_yaw/control_torque", top_yaw_control_torque_, 0.0); register_output("/gimbal/bottom_yaw/control_torque", bottom_yaw_control_torque_, 0.0); - register_output("/gimbal/top_yaw/control_angle", top_yaw_control_torque_, 0.0); - register_output("/gimbal/bottom_yaw/control_angle_shift", bottom_yaw_control_torque_, 0.0); - status_component_ = create_partner_component(get_component_name() + "_status"); } diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/bullet_feeder_controller_42mm.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/bullet_feeder_controller_42mm.cpp index bf3076e30..95d75cb93 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/bullet_feeder_controller_42mm.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/bullet_feeder_controller_42mm.cpp @@ -31,14 +31,15 @@ class BulletFeederController42mm register_input( "/gimbal/control_bullet_allowance/limited_by_heat", control_bullet_allowance_limited_by_heat_); + register_input("/gimbal/bullet_fired", bullet_fired_); - bullet_feeder_velocity_pid_.kp = 50.0; - bullet_feeder_velocity_pid_.ki = 10.0; - bullet_feeder_velocity_pid_.kd = 0.0; + bullet_feeder_velocity_pid_.kp = 50.0; + bullet_feeder_velocity_pid_.ki = 10.0; + bullet_feeder_velocity_pid_.kd = 0.0; bullet_feeder_velocity_pid_.integral_max = 60.0; bullet_feeder_velocity_pid_.integral_min = 0.0; - bullet_feeder_angle_pid_.kp = 50.0; + bullet_feeder_angle_pid_.kp = 60.0; bullet_feeder_angle_pid_.ki = 0.0; bullet_feeder_angle_pid_.kd = 2.0; @@ -51,9 +52,9 @@ class BulletFeederController42mm void update() override { const auto switch_right = *switch_right_; - const auto switch_left = *switch_left_; - const auto mouse = *mouse_; - const auto keyboard = *keyboard_; + const auto switch_left = *switch_left_; + const auto mouse = *mouse_; + const auto keyboard = *keyboard_; using namespace rmcs_msgs; if ((switch_left == Switch::UNKNOWN || switch_right == Switch::UNKNOWN) @@ -61,7 +62,6 @@ class BulletFeederController42mm reset_all_controls(); return; } - overdrive_mode_ = keyboard.f; if (keyboard.ctrl && !last_keyboard_.r && keyboard.r) low_latency_mode_ = !low_latency_mode_; @@ -84,8 +84,8 @@ class BulletFeederController42mm } else { if (!*friction_ready_ || std::isnan(bullet_feeder_control_angle_)) { bullet_feeder_control_angle_ = *bullet_feeder_angle_; - shoot_stage_ = ShootStage::PRELOADED; - bullet_fed_count_ = static_cast( + shoot_stage_ = ShootStage::PRELOADED; + bullet_fed_count_ = static_cast( (*bullet_feeder_angle_ - bullet_feeder_compressed_zero_point_ - 0.1) / bullet_feeder_angle_per_bullet_); } @@ -135,24 +135,24 @@ class BulletFeederController42mm } last_switch_right_ = switch_right; - last_switch_left_ = switch_left; - last_mouse_ = mouse; - last_keyboard_ = keyboard; + last_switch_left_ = switch_left; + last_mouse_ = mouse; + last_keyboard_ = keyboard; } private: void reset_all_controls() { last_switch_right_ = rmcs_msgs::Switch::UNKNOWN; - last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; - last_mouse_ = rmcs_msgs::Mouse::zero(); - last_keyboard_ = rmcs_msgs::Keyboard::zero(); + last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; + last_mouse_ = rmcs_msgs::Mouse::zero(); + last_keyboard_ = rmcs_msgs::Keyboard::zero(); overdrive_mode_ = low_latency_mode_ = false; - shoot_stage_ = ShootStage::PRELOADED; + shoot_stage_ = ShootStage::PRELOADED; bullet_fed_count_ = std::numeric_limits::min(); - bullet_feeder_control_angle_ = nan_; + bullet_feeder_control_angle_ = nan_; bullet_feeder_angle_pid_.output_max = inf_; bullet_feeder_velocity_pid_.reset(); @@ -160,13 +160,13 @@ class BulletFeederController42mm *bullet_feeder_control_torque_ = nan_; bullet_feeder_faulty_count_ = 0; - bullet_feeder_cool_down_ = 0; + bullet_feeder_cool_down_ = 0; } void set_preloading() { RCLCPP_INFO(get_logger(), "PRELOADING"); bullet_fed_count_++; - shoot_stage_ = ShootStage::PRELOADING; + shoot_stage_ = ShootStage::PRELOADING; bullet_feeder_control_angle_ = bullet_feeder_compressed_zero_point_ + (bullet_fed_count_ + 0.5) * bullet_feeder_angle_per_bullet_; bullet_feeder_angle_pid_.output_max = 1.0; @@ -179,7 +179,7 @@ class BulletFeederController42mm void set_compressing() { RCLCPP_INFO(get_logger(), "COMPRESSING"); - shoot_stage_ = ShootStage::COMPRESSING; + shoot_stage_ = ShootStage::COMPRESSING; bullet_feeder_control_angle_ = bullet_feeder_compressed_zero_point_ + (bullet_fed_count_ + 1) * bullet_feeder_angle_per_bullet_; bullet_feeder_angle_pid_.output_max = 0.8; @@ -192,7 +192,7 @@ class BulletFeederController42mm void set_shooting() { RCLCPP_INFO(get_logger(), "SHOOTING"); - shoot_stage_ = ShootStage::SHOOTING; + shoot_stage_ = ShootStage::SHOOTING; bullet_feeder_control_angle_ = bullet_feeder_compressed_zero_point_ + (bullet_fed_count_ + 1.2) * bullet_feeder_angle_per_bullet_; bullet_feeder_angle_pid_.output_max = 1.0; @@ -215,7 +215,7 @@ class BulletFeederController42mm void enter_jam_protection() { bullet_feeder_control_angle_ = nan_; - bullet_feeder_cool_down_ = 1000; + bullet_feeder_cool_down_ = 1000; bullet_feeder_angle_pid_.reset(); bullet_feeder_velocity_pid_.reset(); RCLCPP_INFO(get_logger(), "Jammed!"); @@ -225,9 +225,10 @@ class BulletFeederController42mm static constexpr double inf_ = std::numeric_limits::infinity(); static constexpr double bullet_feeder_compressed_zero_point_ = 0.58; - static constexpr double bullet_feeder_angle_per_bullet_ = 2 * std::numbers::pi / 6; + static constexpr double bullet_feeder_angle_per_bullet_ = 2 * std::numbers::pi / 6; InputInterface friction_ready_; + InputInterface bullet_fired_; InputInterface switch_right_; InputInterface switch_left_; @@ -235,9 +236,9 @@ class BulletFeederController42mm InputInterface keyboard_; rmcs_msgs::Switch last_switch_right_ = rmcs_msgs::Switch::UNKNOWN; - rmcs_msgs::Switch last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; - rmcs_msgs::Mouse last_mouse_ = rmcs_msgs::Mouse::zero(); - rmcs_msgs::Keyboard last_keyboard_ = rmcs_msgs::Keyboard::zero(); + rmcs_msgs::Switch last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; + rmcs_msgs::Mouse last_mouse_ = rmcs_msgs::Mouse::zero(); + rmcs_msgs::Keyboard last_keyboard_ = rmcs_msgs::Keyboard::zero(); bool overdrive_mode_ = false, low_latency_mode_ = false; @@ -247,8 +248,8 @@ class BulletFeederController42mm InputInterface control_bullet_allowance_limited_by_heat_; enum class ShootStage { PRELOADING, PRELOADED, COMPRESSING, COMPRESSED, SHOOTING }; - ShootStage shoot_stage_ = ShootStage::PRELOADED; - int bullet_fed_count_ = std::numeric_limits::min(); + ShootStage shoot_stage_ = ShootStage::PRELOADED; + int bullet_fed_count_ = std::numeric_limits::min(); double bullet_feeder_control_angle_ = nan_; pid::PidCalculator bullet_feeder_velocity_pid_; @@ -256,7 +257,7 @@ class BulletFeederController42mm OutputInterface bullet_feeder_control_torque_; int bullet_feeder_faulty_count_ = 0; - int bullet_feeder_cool_down_ = 0; + int bullet_feeder_cool_down_ = 0; OutputInterface shoot_mode_; }; diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp new file mode 100644 index 000000000..4efdbf520 --- /dev/null +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp @@ -0,0 +1,387 @@ +#include +#include +#include +#include +#include +#include + +#include "controller/pid/pid_calculator.hpp" + +namespace rmcs_core::controller::shooting { + +/** + * @class PutterController + * @brief 推弹机构控制器 + * + * 发射机制说明: + * 由于光电门放置于弹舱口,经测试,双中先触发推杆向后复位,然后堵转检测复位完毕, + * 默认情况下会给一点点的力保证推杆不会滑下去,再然后上弹采用速度环,在光电门被触发时 + * 记录角度并转为角度环,然后推杆检测发弹根据两部分来检测,一是摩擦轮,二是推杆的行程 + * 整套方案以于暑假前完成压力测试 + */ +class PutterController + : public rmcs_executor::Component + , public rclcpp::Node { +public: + PutterController() + : Node( + get_component_name(), + rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)) { + + register_input("/remote/switch/right", switch_right_); + register_input("/remote/switch/left", switch_left_); + register_input("/remote/mouse", mouse_); + register_input("/remote/keyboard", keyboard_); + + register_input("/gimbal/friction_ready", friction_ready_); + + register_input("/gimbal/bullet_feeder/angle", bullet_feeder_angle_); + register_input("/gimbal/bullet_feeder/velocity", bullet_feeder_velocity_); + + register_input( + "/gimbal/control_bullet_allowance/limited_by_heat", + control_bullet_allowance_limited_by_heat_); + + register_input("/gimbal/photoelectric_sensor", photoelectric_sensor_status_); + register_input("/gimbal/bullet_fired", bullet_fired_); + + register_input("/gimbal/putter/angle", putter_angle_); + register_input("/gimbal/putter/velocity", putter_velocity_); + + last_preload_flag_ = false; + + bullet_feeder_velocity_pid_.kp = 50.0; + bullet_feeder_velocity_pid_.ki = 10.0; + bullet_feeder_velocity_pid_.kd = 0.0; + bullet_feeder_velocity_pid_.integral_max = 60.0; + bullet_feeder_velocity_pid_.integral_min = 0.0; + + bullet_feeder_angle_pid_.kp = 8.0; + bullet_feeder_angle_pid_.ki = 0.0; + bullet_feeder_angle_pid_.kd = 2.0; + + puttter_return_velocity_pid_.kp = 0.0015; + puttter_return_velocity_pid_.ki = 0.00005; + puttter_return_velocity_pid_.kd = 0.; + puttter_return_velocity_pid_.integral_max = 0.; + puttter_return_velocity_pid_.integral_min = -0.03; + + putter_velocity_pid_.kp = 0.004; + putter_velocity_pid_.ki = 0.0001; + putter_velocity_pid_.kd = 0.001; + puttter_return_velocity_pid_.integral_max = 0.03; + puttter_return_velocity_pid_.integral_min = 0.; + + putter_return_angle_pid.kp = 0.0001; + // putter_return_angle_pid.ki = 0.000001; + putter_return_angle_pid.kd = 0.; + + register_output( + "/gimbal/bullet_feeder/control_torque", bullet_feeder_control_torque_, nan_); + register_output("/gimbal/putter/control_torque", putter_control_torque_, nan_); + + register_output("/gimbal/shooter/mode", shoot_mode_, rmcs_msgs::ShootMode::SINGLE); + } + + void update() override { + const auto switch_right = *switch_right_; + const auto switch_left = *switch_left_; + const auto mouse = *mouse_; + const auto keyboard = *keyboard_; + + using namespace rmcs_msgs; + + if ((switch_left == Switch::UNKNOWN || switch_right == Switch::UNKNOWN) + || (switch_left == Switch::DOWN && switch_right == Switch::DOWN)) { + reset_all_controls(); + return; + } + + // 推杆已初始化后的正常控制流程 + if (putter_initialized) { + // 供弹轮卡弹保护冷却期间的处理 + if (bullet_feeder_cool_down_ > 0) { + bullet_feeder_cool_down_--; + + // 冷却期前期:反转供弹轮以解除卡弹 + if (bullet_feeder_cool_down_ > 500) + *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update( + -bullet_feeder_angle_per_bullet_ - *bullet_feeder_velocity_); + else { + // 冷却期后期:停止控制 + bullet_feeder_velocity_pid_.reset(); + *bullet_feeder_control_torque_ = 0.0; + } + + bullet_feeder_angle_pid_.reset(); + + if (!bullet_feeder_cool_down_) + RCLCPP_INFO(get_logger(), "Jamming Solved!"); + } else { + // 正常运行模式:摩擦轮就绪时才允许发射 + if (*friction_ready_) { + // 发射触发检测 + // RCLCPP_INFO(get_logger(), "%.2f", *bullet_feeder_angle_); + // RCLCPP_INFO(get_logger(), "%.2f", *bullet_feeder_velocity_); + if (switch_right != Switch::DOWN) { + if ((!last_mouse_.left && mouse.left) + || (last_switch_left_ == rmcs_msgs::Switch::MIDDLE + && switch_left == rmcs_msgs::Switch::DOWN)) { + if ( + // *control_bullet_allowance_limited_by_heat_ > 0 && + shoot_stage_ == ShootStage::PRELOADED) + set_shooting(); + } + } + + if (shoot_stage_ == ShootStage::PRELOADING) { + if (last_preload_flag_) { + // 角度环控制模式:光电门触发后精确定位 + const auto angle_err = + bullet_feeder_control_angle_ - *bullet_feeder_angle_; + if (angle_err < 0.1) { + set_preloaded(); + } + double velocity_err = + bullet_feeder_angle_pid_.update( + bullet_feeder_control_angle_ - *bullet_feeder_angle_) + - *bullet_feeder_velocity_; + *bullet_feeder_control_torque_ = + bullet_feeder_velocity_pid_.update(velocity_err); + } else { + // 速度环控制模式:等待光电门触发 + if (*photoelectric_sensor_status_) { + RCLCPP_INFO(get_logger(), "Photoelectric Sensor Triggered"); + last_preload_flag_ = true; + bullet_feeder_control_angle_ = + *bullet_feeder_angle_ + bullet_feeder_angle_per_bullet_ * 1.; + } else + *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update( + low_latency_velocity_ - *bullet_feeder_velocity_); + } + update_jam_detection(); + } else { + // 其他状态:角度环保持角度不变防止弹链退弹 + double velocity_err = + bullet_feeder_angle_pid_.update( + bullet_feeder_control_angle_ - *bullet_feeder_angle_) + - *bullet_feeder_velocity_; + *bullet_feeder_control_torque_ = + bullet_feeder_velocity_pid_.update(velocity_err); + } + + if (shoot_stage_ == ShootStage::SHOOTING) { + // 发射状态:检测子弹是否发出 + if (*bullet_fired_ + || *putter_angle_ - putter_startpoint >= putter_stroke_) { + shooted = true; + } + + update_putter_jam_detection(); + + if (shooted) { + // 子弹已发出:推杆复位 + const auto angle_err = putter_startpoint - *putter_angle_; + if (angle_err > -0.05) { + *putter_control_torque_ = 0.; + set_preloading(); + shooted = false; + } + + *putter_control_torque_ = + puttter_return_velocity_pid_.update(-80. - *putter_velocity_); + } else { + // 子弹未发出:继续推进 + *putter_control_torque_ = + puttter_return_velocity_pid_.update(60. - *putter_velocity_); + } + } + } else { + // 摩擦轮未就绪:停止供弹轮 + *bullet_feeder_control_torque_ = 0.; + } + + // 非发射状态:推杆给少许力保持位置 + if (shoot_stage_ != ShootStage::SHOOTING) + *putter_control_torque_ = -0.02; + } + } else { + // 推杆未初始化:执行复位操作 + *putter_control_torque_ = puttter_return_velocity_pid_.update(-80. - *putter_velocity_); + update_putter_jam_detection(); + } + + // 保存当前状态用于下次比较 + last_switch_right_ = switch_right; + last_switch_left_ = switch_left; + last_mouse_ = mouse; + last_keyboard_ = keyboard; + } + +private: + void reset_all_controls() { + last_switch_right_ = rmcs_msgs::Switch::UNKNOWN; + last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; + last_mouse_ = rmcs_msgs::Mouse::zero(); + last_keyboard_ = rmcs_msgs::Keyboard::zero(); + + overdrive_mode_ = false; + + bullet_feeder_control_angle_ = nan_; + bullet_feeder_angle_pid_.output_max = inf_; + bullet_feeder_velocity_pid_.reset(); + bullet_feeder_angle_pid_.reset(); + *bullet_feeder_control_torque_ = nan_; + + // shoot_stage_ = ShootStage::PRELOADING; + + putter_initialized = false; + putter_startpoint = nan_; + puttter_return_velocity_pid_.reset(); + putter_velocity_pid_.reset(); + putter_return_angle_pid.reset(); + *putter_control_torque_ = nan_; + + last_preload_flag_ = false; + last_photoelectric_sensor_status_ = false; + + bullet_feeder_faulty_count_ = 0; + bullet_feeder_cool_down_ = 0; + } + + void set_preloading() { + RCLCPP_INFO(get_logger(), "PRELOADING"); + shoot_stage_ = ShootStage::PRELOADING; + } + + void set_preloaded() { + RCLCPP_INFO(get_logger(), "PRELOADED"); + shoot_stage_ = ShootStage::PRELOADED; + last_preload_flag_ = false; + bullet_feeder_control_angle_ = *bullet_feeder_angle_ - 0.3; + } + + void set_shooting() { + RCLCPP_INFO(get_logger(), "SHOOTING"); + shoot_stage_ = ShootStage::SHOOTING; + } + + void update_jam_detection() { + // RCLCPP_INFO(get_logger(), "%.2f --", *bullet_feeder_control_torque_); + if (*bullet_feeder_control_torque_ < 300.0 || std::isnan(*bullet_feeder_control_torque_)) { + bullet_feeder_faulty_count_ = 0; + return; + } + + // 扭矩持续过大时累计故障计数 + if (bullet_feeder_faulty_count_ < 1000) + bullet_feeder_faulty_count_++; + else { + bullet_feeder_faulty_count_ = 0; + if (last_preload_flag_) + set_preloaded(); + else + enter_jam_protection(); + } + } + + void update_putter_jam_detection() { + if ((*putter_control_torque_ > -0.03 && shoot_stage_ == ShootStage::PRELOADING) + || (*putter_control_torque_ < 0.05 && shoot_stage_ == ShootStage::SHOOTING) + || std::isnan(*putter_control_torque_)) { + putter_faulty_count_ = 0; + return; + } + + // 扭矩异常时累计故障计数 + if (putter_faulty_count_ < 500) + ++putter_faulty_count_; + else { + putter_faulty_count_ = 0; + if (shoot_stage_ != ShootStage::SHOOTING) { + // 非发射状态下检测到堵转:推杆已到位,设置为已初始化 + putter_initialized = true; + putter_startpoint = *putter_angle_; + } else { + // 发射状态下检测到堵转:认为子弹已发出 + shooted = true; + } + } + } + + void enter_jam_protection() { + bullet_feeder_control_angle_ = nan_; + bullet_feeder_cool_down_ = 1000; + bullet_feeder_angle_pid_.reset(); + bullet_feeder_velocity_pid_.reset(); + RCLCPP_INFO(get_logger(), "Jammed!"); + } + + static constexpr double nan_ = std::numeric_limits::quiet_NaN(); ///< 非数值常量 + static constexpr double inf_ = std::numeric_limits::infinity(); ///< 无穷大常量 + + static constexpr double low_latency_velocity_ = 5.0; ///< + // 低延迟预装弹速度 + + static constexpr double putter_stroke_ = 11.5; ///< 推杆行程长度 + + static constexpr double max_bullet_feeder_control_torque_ = 0.1; + static constexpr double bullet_feeder_angle_per_bullet_ = 2 * std::numbers::pi / 6; + + InputInterface photoelectric_sensor_status_; + bool last_photoelectric_sensor_status_; + InputInterface bullet_fired_; + bool shooted{false}; + + InputInterface friction_ready_; + + InputInterface switch_right_; + InputInterface switch_left_; + InputInterface mouse_; + InputInterface keyboard_; + + rmcs_msgs::Switch last_switch_right_ = rmcs_msgs::Switch::UNKNOWN; + rmcs_msgs::Switch last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; + rmcs_msgs::Mouse last_mouse_ = rmcs_msgs::Mouse::zero(); + rmcs_msgs::Keyboard last_keyboard_ = rmcs_msgs::Keyboard::zero(); + + bool overdrive_mode_ = false; + + InputInterface bullet_feeder_angle_; + InputInterface bullet_feeder_velocity_; + + InputInterface control_bullet_allowance_limited_by_heat_; + + bool last_preload_flag_ = false; + + bool putter_initialized = false; + int putter_faulty_count_ = 0; + double putter_startpoint = nan_; + pid::PidCalculator puttter_return_velocity_pid_; + InputInterface putter_velocity_; + + pid::PidCalculator putter_velocity_pid_; + + enum class ShootStage { PRELOADING, PRELOADED, SHOOTING }; + ShootStage shoot_stage_ = ShootStage::PRELOADING; + double bullet_feeder_control_angle_ = nan_; + + pid::PidCalculator bullet_feeder_velocity_pid_; + pid::PidCalculator bullet_feeder_angle_pid_; + OutputInterface bullet_feeder_control_torque_; + + InputInterface putter_angle_; + pid::PidCalculator putter_return_angle_pid; + OutputInterface putter_control_torque_; + + int bullet_feeder_faulty_count_ = 0; + int bullet_feeder_cool_down_ = 0; + + OutputInterface shoot_mode_; +}; + +} // namespace rmcs_core::controller::shooting + +#include + +PLUGINLIB_EXPORT_CLASS(rmcs_core::controller::shooting::PutterController, rmcs_executor::Component) \ No newline at end of file diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp index 5500b15c1..c0ef5af23 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp @@ -1,7 +1,10 @@ +#include +#include #include #include #include #include +#include namespace rmcs_core::controller::shooting { @@ -18,44 +21,71 @@ class ShootingRecorder log_mode_ = static_cast(get_parameter("log_mode").as_int()); + aim_velocity = get_parameter("aim_velocity").as_double(); + register_input("/referee/shooter/initial_speed", initial_speed_); register_input("/referee/shooter/shoot_timestamp", shoot_timestamp_); - register_input("/friction_wheels/temperature", fractional_temperature_); if (friction_wheel_count_ == 2) { - const auto topic = std::array{ - "/gimbal/left_friction/control_velocity", - "/gimbal/right_friction/control_velocity", - }; - for (int i = 0; i < 2; i++) - register_input(topic[i], friction_wheels_velocity_[i]); - } else if (friction_wheel_count_ == 4) { const auto topic = std::array{ "/gimbal/first_left_friction/control_velocity", "/gimbal/first_right_friction/control_velocity", - "/gimbal/second_left_friction/control_velocity", - "/gimbal/second_right_friction/control_velocity", }; - for (int i = 0; i < 4; i++) + for (int i = 0; i < 2; i++) register_input(topic[i], friction_wheels_velocity_[i]); + register_input("/gimbal/first_left_friction/velocity", friction_velocities_[0]); + register_input("/gimbal/first_right_friction/velocity", friction_velocities_[1]); } using namespace std::chrono; auto now = high_resolution_clock::now(); - auto ms = duration_cast(now.time_since_epoch()).count(); + auto ms = duration_cast(now.time_since_epoch()).count(); auto file = fmt::format("/robot_shoot/{}.log", ms); log_stream_.open(file); + + std::ofstream outFile("shoot_recorder"); + RCLCPP_INFO(get_logger(), "ShootingRecorder initialized, log file: %s", file.c_str()); } ~ShootingRecorder() { log_stream_.close(); } void update() override { + // if (*friction_velocities_[0] <= 366.0 && *friction_velocities_[1] <= 366.0) + // return; + // if (start_time_ == 0.0) { + // start_time_ = GetTime(); + // } + + // int flag = 0; + // if (GetTime() - start_time_ > 10.0) { + // if (flag == 0) { + // RCLCPP_INFO(get_logger(), "vv = %f", vv); + // flag = 1; + // } + // return; + // } + + // vv = std::max(vv, abs(*friction_velocities_[0] + *friction_velocities_[1])); + + // auto log_text = std::string{}; + // log_text = fmt::format( + // "{:.3f}, {:.3f}, {:.3f}", *friction_velocities_[0], *friction_velocities_[1], + // (*friction_velocities_[0] + *friction_velocities_[1])); + + // log_stream_ << log_text << std::endl; + // RCLCPP_INFO(get_logger(), "%s", log_text.c_str()); + + // std::ofstream outFile("shoot_recorder", std::ios::app); + // if (outFile.is_open()) { + // outFile << log_text << std::endl; + // outFile.close(); + // } switch (log_mode_) { case LogMode::TRIGGER: // It will be triggered by shooting action - if (*shoot_timestamp_ == last_shoot_timestamp_) + if (*shoot_timestamp_ == last_shoot_timestamp_ || v == *shoot_timestamp_) return; break; case LogMode::TIMING: @@ -64,38 +94,45 @@ class ShootingRecorder return; break; } + v = *shoot_timestamp_; - auto log_text = std::string{}; + velocities.push_back(*initial_speed_); + + analysis3(); + + auto log_text = std::string{}; auto timestamp = timestamp_to_string(*shoot_timestamp_); if (friction_wheel_count_ == 2) { log_text = fmt::format( - "{},{},{},{},{}", timestamp, *initial_speed_, // - *friction_wheels_velocity_[0], *friction_wheels_velocity_[1], - *fractional_temperature_); - } else if (friction_wheel_count_ == 4) { - log_text = fmt::format( - "{},{},{},{},{},{},{}", timestamp, *initial_speed_, // - *friction_wheels_velocity_[0], *friction_wheels_velocity_[1], - *friction_wheels_velocity_[2], *friction_wheels_velocity_[3], - *fractional_temperature_); + "{},{},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f}", *initial_speed_, + (int)velocities.size(), // + velocity_, excellence_rate_, pass_rate_, range_, range2_, velocity_max, + velocity_min); } log_stream_ << log_text << std::endl; RCLCPP_INFO(get_logger(), "%s", log_text.c_str()); + log_velocity = fmt::format("{:.3f}", *initial_speed_); + std::ofstream outFile("shoot_recorder", std::ios::app); + if (outFile.is_open()) { + outFile << log_velocity << std::endl; + outFile.close(); + } + last_shoot_timestamp_ = *shoot_timestamp_; } private: /// @brief Component interface + std::array, 2> friction_velocities_; + InputInterface initial_speed_; InputInterface shoot_timestamp_; - InputInterface fractional_temperature_; - std::size_t friction_wheel_count_ = 2; - std::array, 4> friction_wheels_velocity_; + std::array, 2> friction_wheels_velocity_; /// @brief For log enum class LogMode { TRIGGER = 1, TIMING = 2 }; @@ -103,25 +140,147 @@ class ShootingRecorder double last_shoot_timestamp_ = 0; std::ofstream log_stream_; + std::string log_velocity; std::size_t log_count_ = 0; + std::vector velocities; + + double velocity_; + + double excellence_rate_; + double pass_rate_; + + double range_; + double range2_; + + double velocity_min; + double velocity_max; + + double v; + double aim_velocity; + + double start_time_ = 0.0; + double vv = 0.0; + private: static std::string timestamp_to_string(double timestamp) { - auto time = static_cast(timestamp); + auto time = static_cast(timestamp); auto local_time = std::localtime(&time); char buffer[100]; std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", local_time); double fractional_seconds = timestamp - std::floor(timestamp); - int milliseconds = static_cast(fractional_seconds * 1000); + int milliseconds = static_cast(fractional_seconds * 1000); char result[150]; std::snprintf(result, sizeof(result), "%s.%03d", buffer, milliseconds); return result; } + void analysis1() { + double sum = 0.0; + for (const auto& v : velocities) { + sum += v; + } + velocity_ = sum / double(velocities.size()); + + sort(velocities.begin(), velocities.end()); + + range_ = velocities.back() - velocities.front(); + range2_ = velocities[int(velocities.size() - 2)] - velocities[1]; + + velocity_max = velocities.back(); + velocity_min = velocities.front(); + + int excellence_count = 0; + int pass_count = 0; + for (int i = 0; i < int(velocities.size()); i++) { + if (velocities[i] >= velocity_ - 0.1 && velocities[i] <= velocity_ + 0.1) { + pass_count += 1; + } + if (velocities[i] >= velocity_ - 0.05 && velocities[i] <= velocity_ + 0.05) { + excellence_count += 1; + } + } + excellence_rate_ = double(excellence_count) / double(velocities.size()); + pass_rate_ = double(pass_count) / double(velocities.size()); + } + + void analysis2() { + double sum = 0.0; + for (const auto& v : velocities) { + sum += v; + } + + sort(velocities.begin(), velocities.end()); + + velocity_max = velocities.back(); + velocity_min = velocities.front(); + + int n_adjust = std::max(1, int((int)velocities.size() * 0.05)); + + for (int i = 0; i < n_adjust; i++) { + sum -= velocities[i]; + sum -= velocities[velocities.size() - 1 - i]; + } + + velocity_ = sum / double(velocities.size() - 2 * n_adjust); + + range_ = velocities.back() - velocities.front(); + range2_ = velocities[int(velocities.size() - 2)] - velocities[1]; + + int excellence_count = 0; + int pass_count = 0; + for (int i = 0; i < int(velocities.size()); i++) { + if (velocities[i] >= velocity_ - 0.1 && velocities[i] <= velocity_ + 0.1) { + pass_count += 1; + } + if (velocities[i] >= velocity_ - 0.05 && velocities[i] <= velocity_ + 0.05) { + excellence_count += 1; + } + } + excellence_rate_ = double(excellence_count) / double(velocities.size()); + pass_rate_ = double(pass_count) / double(velocities.size()); + } + + void analysis3() { + double sum = 0.0; + for (const auto& v : velocities) { + sum += v; + } + velocity_ = sum / double(velocities.size()); + + int excellence_count = 0; + int pass_count = 0; + + for (const auto& v : velocities) { + if (v >= aim_velocity - 0.05 && v <= aim_velocity + 0.05) { + excellence_count += 1; + } + if (v >= aim_velocity - 0.1 && v <= aim_velocity + 0.1) { + pass_count += 1; + } + } + excellence_rate_ = double(excellence_count) / double(velocities.size()); + pass_rate_ = double(pass_count) / double(velocities.size()); + + sort(velocities.begin(), velocities.end()); + velocity_max = velocities.back(); + velocity_min = velocities.front(); + + range_ = velocities.back() - velocities.front(); + range2_ = velocities[int(velocities.size() - 2)] - velocities[1]; + } + + static double GetTime() { + using namespace std::chrono; + static auto start = high_resolution_clock::now(); + auto now = high_resolution_clock::now(); + duration elapsed = now - start; + return elapsed.count(); + } }; } // namespace rmcs_core::controller::shooting diff --git a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp index d30c1ce06..8f89cd2a8 100644 --- a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp +++ b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp @@ -1,29 +1,22 @@ +#include +#include #include -#include +#include #include -#include -#include -#include -#include - -#include -#include -#include +#include +#include #include +#include + +#include #include -#include -#include -#include #include #include #include -#include #include #include -#include "hardware/device/benewake.hpp" #include "hardware/device/bmi088.hpp" -#include "hardware/device/can_packet.hpp" #include "hardware/device/dji_motor.hpp" #include "hardware/device/dr16.hpp" #include "hardware/device/lk_motor.hpp" @@ -36,16 +29,14 @@ class SteeringHero , public rclcpp::Node { public: SteeringHero() - : Node{ - get_component_name(), - rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)} + : Node{get_component_name(), rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)} , command_component_( create_partner_component( get_component_name() + "_command", *this)) { + using namespace rmcs_description; register_output("/tf", tf_); - tf_->set_transform( - Eigen::Translation3d{0.16, 0.0, 0.15}); + tf_->set_transform(Eigen::Translation3d{0.16, 0.0, 0.15}); gimbal_calibrate_subscription_ = create_subscription( "/gimbal/calibrate", rclcpp::QoS{0}, [this](std_msgs::msg::Int32::UniquePtr&& msg) { @@ -53,67 +44,66 @@ class SteeringHero }); top_board_ = std::make_unique( - *this, *command_component_, get_parameter("board_serial_top_board").as_string()); - bottom_board_ = std::make_unique( - *this, *command_component_, get_parameter("board_serial_bottom_board").as_string()); - - temperature_logging_timer_.reset(1000); + *this, *command_component_, + static_cast(get_parameter("usb_pid_top_board").as_int())); + bottom_board_one_ = std::make_unique( + *this, *command_component_, + static_cast(get_parameter("usb_pid_bottom_board_one").as_int())); + bottom_board_two_ = std::make_unique( + *this, *command_component_, + static_cast(get_parameter("usb_pid_bottom_board_two").as_int())); + + // temperature_logging_timer_.reset(1000); } - SteeringHero(const SteeringHero&) = delete; - SteeringHero& operator=(const SteeringHero&) = delete; - SteeringHero(SteeringHero&&) = delete; - SteeringHero& operator=(SteeringHero&&) = delete; - ~SteeringHero() override = default; void update() override { top_board_->update(); - bottom_board_->update(); - - if (temperature_logging_timer_.tick()) { - temperature_logging_timer_.reset(1000); - RCLCPP_INFO( - get_logger(), - "Temperature: pitch: %.1f, top_yaw: %.1f, bottom_yaw: %.1f, feeder: %.1f", - top_board_->gimbal_pitch_motor_.temperature(), - top_board_->gimbal_top_yaw_motor_.temperature(), - bottom_board_->gimbal_bottom_yaw_motor_.temperature(), - top_board_->gimbal_bullet_feeder_.temperature()); - } + bottom_board_one_->update(); + bottom_board_two_->update(); + + // if (temperature_logging_timer_.tick()) { + // temperature_logging_timer_.reset(1000); + // RCLCPP_INFO( + // get_logger(), + // "Temperature: pitch: %.1f, top_yaw: %.1f, bottom_yaw: %.1f, feeder: %.1f", + // top_board_->gimbal_pitch_motor_.temperature(), + // top_board_->gimbal_top_yaw_motor_.temperature(), + // bottom_board_->gimbal_bottom_yaw_motor_.temperature(), + // top_board_->gimbal_bullet_feeder_.temperature()); + // } } void command_update() { top_board_->command_update(); - bottom_board_->command_update(); + bottom_board_one_->command_update(); + bottom_board_two_->command_update(); } private: void gimbal_calibrate_subscription_callback(std_msgs::msg::Int32::UniquePtr) { RCLCPP_INFO( get_logger(), "[gimbal calibration] New bottom yaw offset: %ld", - bottom_board_->gimbal_bottom_yaw_motor_.calibrate_zero_point()); + bottom_board_two_->gimbal_bottom_yaw_motor_.calibrate_zero_point()); RCLCPP_INFO( get_logger(), "[gimbal calibration] New pitch offset: %ld", top_board_->gimbal_pitch_motor_.calibrate_zero_point()); RCLCPP_INFO( get_logger(), "[gimbal calibration] New top yaw offset: %ld", top_board_->gimbal_top_yaw_motor_.calibrate_zero_point()); - RCLCPP_INFO( - get_logger(), "[gimbal calibration] New viewer offset: %ld", - top_board_->gimbal_player_viewer_motor_.calibrate_zero_point()); RCLCPP_INFO( get_logger(), "[chassis calibration] left front steering offset: %d", - bottom_board_->chassis_steering_motors_[0].calibrate_zero_point()); + bottom_board_one_->chassis_steering_motors_[0].calibrate_zero_point()); + RCLCPP_INFO( + get_logger(), "[chassis calibration] right front steering offset: %d", + bottom_board_one_->chassis_steering_motors_[1].calibrate_zero_point()); RCLCPP_INFO( get_logger(), "[chassis calibration] left back steering offset: %d", - bottom_board_->chassis_steering_motors_[1].calibrate_zero_point()); + bottom_board_two_->chassis_steering_motors2_[0].calibrate_zero_point()); RCLCPP_INFO( get_logger(), "[chassis calibration] right back steering offset: %d", - bottom_board_->chassis_steering_motors_[2].calibrate_zero_point()); - RCLCPP_INFO( - get_logger(), "[chassis calibration] right front steering offset: %d", - bottom_board_->chassis_steering_motors_[3].calibrate_zero_point()); + bottom_board_two_->chassis_steering_motors2_[1].calibrate_zero_point()); } class SteeringHeroCommand : public rmcs_executor::Component { @@ -123,61 +113,54 @@ class SteeringHero void update() override { hero_.command_update(); } - private: SteeringHero& hero_; }; std::shared_ptr command_component_; - class TopBoard final : private librmcs::agent::CBoard { + class TopBoard final : private librmcs::client::CBoard { public: friend class SteeringHero; - explicit TopBoard( - SteeringHero& hero, SteeringHeroCommand& hero_command, - std::string_view board_serial = {}) - : librmcs::agent::CBoard(board_serial) + explicit TopBoard(SteeringHero& hero, SteeringHeroCommand& hero_command, int usb_pid = + -1) + : librmcs::client::CBoard(usb_pid) + , logger_(hero.get_logger()) , tf_(hero.tf_) , imu_(1000, 0.2, 0.0) - , benewake_(hero, "/gimbal/auto_aim/laser_distance") , gimbal_top_yaw_motor_( hero, hero_command, "/gimbal/top_yaw", - device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10} + device::LkMotor::Config{device::LkMotor::Type::MG5010E_I10} .set_encoder_zero_point( static_cast( hero.get_parameter("top_yaw_motor_zero_point").as_int()))) , gimbal_pitch_motor_( hero, hero_command, "/gimbal/pitch", - device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10} + device::LkMotor::Config{device::LkMotor::Type::MG5010E_I10} .set_encoder_zero_point( static_cast(hero.get_parameter("pitch_motor_zero_point").as_int()))) , gimbal_friction_wheels_( - {hero, hero_command, "/gimbal/second_left_friction", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio( - 1.)}, {hero, hero_command, "/gimbal/first_left_friction", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio( - 1.)}, + device::DjiMotor::Config{device::DjiMotor::Type::M3508}.set_reduction_ratio(1.)}, {hero, hero_command, "/gimbal/first_right_friction", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + device::DjiMotor::Config{device::DjiMotor::Type::M3508} .set_reduction_ratio(1.) .set_reversed()}, + {hero, hero_command, "/gimbal/second_left_friction", + device::DjiMotor::Config{device::DjiMotor::Type::M3508} + .set_reduction_ratio(1.) + .set_reversed()}, {hero, hero_command, "/gimbal/second_right_friction", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + device::DjiMotor::Config{device::DjiMotor::Type::M3508} .set_reduction_ratio(1.) .set_reversed()}) , gimbal_bullet_feeder_( hero, hero_command, "/gimbal/bullet_feeder", - device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10} + device::LkMotor::Config{device::LkMotor::Type::MG5010E_I10} .set_reversed() .enable_multi_turn_angle()) - , gimbal_scope_motor_( - hero, hero_command, "/gimbal/scope", - device::DjiMotor::Config{device::DjiMotor::Type::kM2006}) - , gimbal_player_viewer_motor_( - hero, hero_command, "/gimbal/player_viewer", - device::LkMotor::Config{device::LkMotor::Type::kMG4005Ei10} - .set_encoder_zero_point( - static_cast(hero.get_parameter("viewer_motor_zero_point").as_int())) - .set_reversed()) { + , putter_motor_{hero, hero_command, "/gimbal/putter", + device::DjiMotor::Config{device::DjiMotor::Type::M3508}.set_reduction_ratio(1.).enable_multi_turn_angle()} + , transmit_buffer_(*this, 32) + , event_thread_([this]() { handle_events(); }) { imu_.set_coordinate_mapping([](double x, double y, double z) { // Get the mapping with the following code. @@ -194,24 +177,26 @@ class SteeringHero hero.register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_); hero.register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_); - } - TopBoard(const TopBoard&) = delete; - TopBoard& operator=(const TopBoard&) = delete; - TopBoard(TopBoard&&) = delete; - TopBoard& operator=(TopBoard&&) = delete; + hero.register_output( + "/gimbal/photoelectric_sensor", photoelectric_sensor_status_, false); + hero.register_output( + "/auto_aim/image_capturer/timestamp", camera_capturer_trigger_timestamp_, 0); + hero.register_output("/auto_aim/image_capturer/trigger", camera_capturer_trigger_, 0); + } - ~TopBoard() final = default; + ~TopBoard() final { + stop_handling_events(); + event_thread_.join(); + } void update() { imu_.update_status(); - const Eigen::Quaterniond gimbal_imu_pose{imu_.q0(), imu_.q1(), imu_.q2(), imu_.q3()}; + Eigen::Quaterniond gimbal_imu_pose{imu_.q0(), imu_.q1(), imu_.q2(), imu_.q3()}; tf_->set_transform( gimbal_imu_pose.conjugate()); - benewake_.update_status(); - *gimbal_yaw_velocity_imu_ = imu_.gz(); *gimbal_pitch_velocity_imu_ = imu_.gy(); @@ -221,123 +206,97 @@ class SteeringHero tf_->set_state( gimbal_pitch_motor_.angle()); - gimbal_player_viewer_motor_.update_status(); - tf_->set_state( - gimbal_player_viewer_motor_.angle()); - - gimbal_scope_motor_.update_status(); - for (auto& motor : gimbal_friction_wheels_) motor.update_status(); gimbal_bullet_feeder_.update_status(); + putter_motor_.update_status(); + + if (last_camera_capturer_trigger_timestamp_ != *camera_capturer_trigger_timestamp_) + *camera_capturer_trigger_ = true; + last_camera_capturer_trigger_timestamp_ = *camera_capturer_trigger_timestamp_; } void command_update() { - auto builder = start_transmit(); - - builder.can1_transmit({ - .can_id = 0x200, - .can_data = - device::CanPacket8{ - gimbal_friction_wheels_[0].generate_command(), - gimbal_friction_wheels_[1].generate_command(), - gimbal_friction_wheels_[2].generate_command(), - gimbal_friction_wheels_[3].generate_command(), - } - .as_bytes(), - }); + uint16_t batch_commands[4]{}; + + batch_commands[0] = gimbal_friction_wheels_[3].generate_command(); + batch_commands[1] = gimbal_friction_wheels_[1].generate_command(); + batch_commands[2] = gimbal_friction_wheels_[2].generate_command(); + batch_commands[3] = gimbal_friction_wheels_[0].generate_command(); + transmit_buffer_.add_can1_transmission(0x200, std::bit_cast(batch_commands)); + + frequency_control_flag_ = !frequency_control_flag_; + if (frequency_control_flag_) { + batch_commands[0] = putter_motor_.generate_command(); + transmit_buffer_.add_can1_transmission( + 0x1ff, std::bit_cast(batch_commands)); + } - builder.can1_transmit({ - .can_id = 0x141, - .can_data = gimbal_bullet_feeder_ - .generate_torque_command(gimbal_bullet_feeder_.control_torque()) - .as_bytes(), - }); + transmit_buffer_.add_can1_transmission( + 0x141, gimbal_bullet_feeder_.generate_torque_command( + gimbal_bullet_feeder_.control_torque())); - builder.can2_transmit({ - .can_id = 0x200, - .can_data = - device::CanPacket8{ - gimbal_scope_motor_.generate_command(), - device::CanPacket8::PaddingQuarter{}, - device::CanPacket8::PaddingQuarter{}, - device::CanPacket8::PaddingQuarter{}, - } - .as_bytes(), - }); + transmit_buffer_.add_can2_transmission(0x141, gimbal_top_yaw_motor_.generate_command()); - builder.can2_transmit({ - .can_id = 0x143, - .can_data = - gimbal_player_viewer_motor_ - .generate_velocity_command(gimbal_player_viewer_motor_.control_velocity()) - .as_bytes(), - }); - - builder.can2_transmit({ - .can_id = 0x141, - .can_data = gimbal_top_yaw_motor_.generate_command().as_bytes(), - }); + transmit_buffer_.add_can2_transmission(0x142, gimbal_pitch_motor_.generate_command()); - builder.can2_transmit({ - .can_id = 0x142, - .can_data = gimbal_pitch_motor_.generate_command().as_bytes(), - }); + transmit_buffer_.trigger_transmission(); } private: - void can1_receive_callback(const librmcs::data::CanDataView& data) override { - if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] + void can1_receive_callback( + uint32_t can_id, uint64_t can_data, bool is_extended_can_id, + bool is_remote_transmission, uint8_t can_data_length) override { + if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] return; - auto can_id = data.can_id; - if (can_id == 0x201) { - gimbal_friction_wheels_[0].store_status(data.can_data); + if (can_id == 0x204) { + gimbal_friction_wheels_[0].store_status(can_data); } else if (can_id == 0x202) { - gimbal_friction_wheels_[1].store_status(data.can_data); + gimbal_friction_wheels_[1].store_status(can_data); } else if (can_id == 0x203) { - gimbal_friction_wheels_[2].store_status(data.can_data); - } else if (can_id == 0x204) { - gimbal_friction_wheels_[3].store_status(data.can_data); + gimbal_friction_wheels_[2].store_status(can_data); + } else if (can_id == 0x201) { + gimbal_friction_wheels_[3].store_status(can_data); + } else if (can_id == 0x205) { + putter_motor_.store_status(can_data); } else if (can_id == 0x141) { - gimbal_bullet_feeder_.store_status(data.can_data); + gimbal_bullet_feeder_.store_status(can_data); } } - void can2_receive_callback(const librmcs::data::CanDataView& data) override { - if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] + void can2_receive_callback( + uint32_t can_id, uint64_t can_data, bool is_extended_can_id, + bool is_remote_transmission, uint8_t can_data_length) override { + if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] return; - auto can_id = data.can_id; if (can_id == 0x141) { - gimbal_top_yaw_motor_.store_status(data.can_data); + gimbal_top_yaw_motor_.store_status(can_data); } else if (can_id == 0x142) { - gimbal_pitch_motor_.store_status(data.can_data); - } else if (can_id == 0x143) { - gimbal_player_viewer_motor_.store_status(data.can_data); - } else if (can_id == 0x201) { - gimbal_scope_motor_.store_status(data.can_data); + gimbal_pitch_motor_.store_status(can_data); } } - void uart2_receive_callback(const librmcs::data::UartDataView& data) override { - benewake_.store_status(data.uart_data.data(), data.uart_data.size()); + void accelerometer_receive_callback(int16_t x, int16_t y, int16_t z) override { + imu_.store_accelerometer_status(x, y, z); } - void accelerometer_receive_callback( - const librmcs::data::AccelerometerDataView& data) override { - imu_.store_accelerometer_status(data.x, data.y, data.z); + void gyroscope_receive_callback(int16_t x, int16_t y, int16_t z) override { + imu_.store_gyroscope_status(x, y, z); } - void gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data) override { - imu_.store_gyroscope_status(data.x, data.y, data.z); - } + void putter_receive_callback(bool status) { *photoelectric_sensor_status_ = status; } + rclcpp::Logger logger_; OutputInterface& tf_; + OutputInterface photoelectric_sensor_status_; + OutputInterface camera_capturer_trigger_; + OutputInterface camera_capturer_trigger_timestamp_; + std::time_t last_camera_capturer_trigger_timestamp_{0}; device::Bmi088 imu_; - device::Benewake benewake_; OutputInterface gimbal_yaw_velocity_imu_; OutputInterface gimbal_pitch_velocity_imu_; @@ -348,230 +307,379 @@ class SteeringHero device::DjiMotor gimbal_friction_wheels_[4]; device::LkMotor gimbal_bullet_feeder_; - device::DjiMotor gimbal_scope_motor_; - device::LkMotor gimbal_player_viewer_motor_; + bool frequency_control_flag_{false}; + device::DjiMotor putter_motor_; + + librmcs::client::CBoard::TransmitBuffer transmit_buffer_; + std::thread event_thread_; }; - class BottomBoard final : private librmcs::agent::CBoard { + class BottomBoard_one final : private librmcs::client::CBoard { public: friend class SteeringHero; - explicit BottomBoard( - SteeringHero& hero, SteeringHeroCommand& hero_command, - std::string_view board_serial = {}) - : librmcs::agent::CBoard(board_serial) + explicit BottomBoard_one( + SteeringHero& hero, SteeringHeroCommand& hero_command, int usb_pid = -1) + : librmcs::client::CBoard(usb_pid) + , logger_(hero.get_logger()) , imu_(1000, 0.2, 0.0) - , tf_(hero.tf_) - , dr16_(hero) + , chassis_front_climber_motor_( + {hero, hero_command, "/chassis/climber/left_front_motor", + device::DjiMotor::Config{device::DjiMotor::Type::M3508} + .set_reversed() + .set_reduction_ratio(19.)}, + {hero, hero_command, "/chassis/climber/right_front_motor", + device::DjiMotor::Config{device::DjiMotor::Type::M3508}.set_reduction_ratio( + 19.)}) + , chassis_back_climber_motor_( + {hero, hero_command, "/chassis/climber/left_back_motor", + device::DjiMotor::Config{device::DjiMotor::Type::M3508}.set_reduction_ratio( + 19.)}, + {hero, hero_command, "/chassis/climber/right_back_motor", + device::DjiMotor::Config{device::DjiMotor::Type::M3508} + .set_reversed() + .set_reduction_ratio(19.)}) , chassis_steering_motors_( {hero, hero_command, "/chassis/left_front_steering", - device::DjiMotor::Config{device::DjiMotor::Type::kGM6020} + device::DjiMotor::Config{device::DjiMotor::Type::GM6020} .set_encoder_zero_point( static_cast(hero.get_parameter("left_front_zero_point").as_int())) .set_reversed()}, - {hero, hero_command, "/chassis/left_back_steering", - device::DjiMotor::Config{device::DjiMotor::Type::kGM6020} - .set_encoder_zero_point( - static_cast(hero.get_parameter("left_back_zero_point").as_int())) - .set_reversed()}, - {hero, hero_command, "/chassis/right_back_steering", - device::DjiMotor::Config{device::DjiMotor::Type::kGM6020} - .set_encoder_zero_point( - static_cast(hero.get_parameter("right_back_zero_point").as_int())) - .set_reversed()}, {hero, hero_command, "/chassis/right_front_steering", - device::DjiMotor::Config{device::DjiMotor::Type::kGM6020} + device::DjiMotor::Config{device::DjiMotor::Type::GM6020} .set_encoder_zero_point( static_cast(hero.get_parameter("right_front_zero_point").as_int())) .set_reversed()}) , chassis_wheel_motors_( {hero, hero_command, "/chassis/left_front_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + device::DjiMotor::Config{device::DjiMotor::Type::M3508} .set_reversed() .set_reduction_ratio(2232. / 169.)}, + {hero, hero_command, "/chassis/right_front_wheel", + device::DjiMotor::Config{device::DjiMotor::Type::M3508} + .set_reversed() + .set_reduction_ratio(2232. / 169.)}) + + , transmit_buffer_(*this, 32) + , event_thread_([this]() { handle_events(); }) { + + // hero.register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_); + // hero.register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_); + + hero.register_output("/chassis/yaw/velocity_imu", chassis_yaw_velocity_imu_, 0); + } + + ~BottomBoard_one() final { + stop_handling_events(); + event_thread_.join(); + } + + void update() { + imu_.update_status(); + + *chassis_yaw_velocity_imu_ = imu_.gz(); + + chassis_front_climber_motor_[0].update_status(); + chassis_front_climber_motor_[1].update_status(); + chassis_back_climber_motor_[0].update_status(); + chassis_back_climber_motor_[1].update_status(); + + for (auto& motor : chassis_wheel_motors_) + motor.update_status(); + for (auto& motor : chassis_steering_motors_) + motor.update_status(); + } + + void command_update() { + uint16_t batch_commands[4]{}; + + batch_commands[0] = chassis_wheel_motors_[1].generate_command(); + batch_commands[1] = chassis_wheel_motors_[0].generate_command(); + batch_commands[2] = 0; + batch_commands[3] = 0; + transmit_buffer_.add_can1_transmission(0x200, std::bit_cast(batch_commands)); + + batch_commands[0] = 0; + batch_commands[1] = chassis_steering_motors_[0].generate_command(); + batch_commands[2] = 0; + batch_commands[3] = chassis_steering_motors_[1].generate_command(); + transmit_buffer_.add_can1_transmission(0x1FE, std::bit_cast(batch_commands)); + + batch_commands[0] = chassis_back_climber_motor_[0].generate_command(); + batch_commands[1] = chassis_back_climber_motor_[1].generate_command(); + batch_commands[2] = chassis_front_climber_motor_[0].generate_command(); + batch_commands[3] = chassis_front_climber_motor_[1].generate_command(); + transmit_buffer_.add_can2_transmission(0x200, std::bit_cast(batch_commands)); + + transmit_buffer_.trigger_transmission(); + } + + private: + void can1_receive_callback( + uint32_t can_id, uint64_t can_data, bool is_extended_can_id, + bool is_remote_transmission, uint8_t can_data_length) override { + if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] + return; + + if (can_id == 0x201) { + chassis_wheel_motors_[1].store_status(can_data); + } else if (can_id == 0x202) { + chassis_wheel_motors_[0].store_status(can_data); + } + if (can_id == 0x206) { + chassis_steering_motors_[0].store_status(can_data); + } else if (can_id == 0x208) { + chassis_steering_motors_[1].store_status(can_data); + } + } + + void can2_receive_callback( + uint32_t can_id, uint64_t can_data, bool is_extended_can_id, + bool is_remote_transmission, uint8_t can_data_length) override { + if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] + return; + + if (can_id == 0x203) { + chassis_front_climber_motor_[0].store_status(can_data); + } else if (can_id == 0x204) { + chassis_front_climber_motor_[1].store_status(can_data); + } else if (can_id == 0x201) { + chassis_back_climber_motor_[0].store_status(can_data); + } else if (can_id == 0x202) { + chassis_back_climber_motor_[1].store_status(can_data); + } + } + rclcpp::Logger logger_; + // test + std::chrono::steady_clock::time_point last_time_; + + void calc_can_fps(double can_id) { + auto now = std::chrono::steady_clock::now(); + auto delta = + std::chrono::duration_cast(now - last_time_).count(); + + RCLCPP_INFO(logger_, "can id :%lf,fps:%ld", can_id, 1000000 / delta); + last_time_ = now; + } + + void accelerometer_receive_callback(int16_t x, int16_t y, int16_t z) override { + imu_.store_accelerometer_status(x, y, z); + } + + void gyroscope_receive_callback(int16_t x, int16_t y, int16_t z) override { + imu_.store_gyroscope_status(x, y, z); + } + device::Bmi088 imu_; + + OutputInterface chassis_yaw_velocity_imu_; + + device::DjiMotor chassis_front_climber_motor_[2]; + device::DjiMotor chassis_back_climber_motor_[2]; + + device::DjiMotor chassis_steering_motors_[2]; + device::DjiMotor chassis_wheel_motors_[2]; + + librmcs::utility::RingBuffer referee_ring_buffer_receive_{256}; + OutputInterface referee_serial_; + + librmcs::client::CBoard::TransmitBuffer transmit_buffer_; + std::thread event_thread_; + + OutputInterface gimbal_yaw_velocity_imu_; + OutputInterface gimbal_pitch_velocity_imu_; + }; + + class BottomBoard_two final : private librmcs::client::CBoard { + public: + friend class SteeringHero; + explicit BottomBoard_two( + SteeringHero& hero, SteeringHeroCommand& hero_command, int usb_pid = -1) + : librmcs::client::CBoard(usb_pid) + , logger_(hero.get_logger()) + , imu_(1000, 0.2, 0.0) + , tf_(hero.tf_) + , dr16_(hero) + , supercap_(hero, hero_command) + , chassis_steering_motors2_( + {hero, hero_command, "/chassis/left_back_steering", + device::DjiMotor::Config{device::DjiMotor::Type::GM6020} + .set_encoder_zero_point( + static_cast(hero.get_parameter("left_back_zero_point").as_int())) + .set_reversed()}, + {hero, hero_command, "/chassis/right_back_steering", + device::DjiMotor::Config{device::DjiMotor::Type::GM6020} + .set_encoder_zero_point( + static_cast(hero.get_parameter("right_back_zero_point").as_int())) + .set_reversed()}) + , chassis_wheel_motors2_( {hero, hero_command, "/chassis/left_back_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + device::DjiMotor::Config{device::DjiMotor::Type::M3508} .set_reversed() .set_reduction_ratio(2232. / 169.)}, {hero, hero_command, "/chassis/right_back_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508} - .set_reversed() - .set_reduction_ratio(2232. / 169.)}, - {hero, hero_command, "/chassis/right_front_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + device::DjiMotor::Config{device::DjiMotor::Type::M3508} .set_reversed() .set_reduction_ratio(2232. / 169.)}) - , supercap_(hero, hero_command) , gimbal_bottom_yaw_motor_( hero, hero_command, "/gimbal/bottom_yaw", - device::LkMotor::Config{device::LkMotor::Type::kMG6012Ei8} + device::LkMotor::Config{device::LkMotor::Type::MG6012E_I8} .set_reversed() .set_encoder_zero_point( static_cast( - hero.get_parameter("bottom_yaw_motor_zero_point").as_int()))) { + hero.get_parameter("bottom_yaw_motor_zero_point").as_int()))) + , transmit_buffer_(*this, 32) + , event_thread_([this]() { handle_events(); }) { hero.register_output("/referee/serial", referee_serial_); referee_serial_->read = [this](std::byte* buffer, size_t size) { - return referee_ring_buffer_receive_.pop_front_n( - [&buffer](std::byte byte) noexcept { *buffer++ = byte; }, size); + return referee_ring_buffer_receive_.pop_front_multi( + [&buffer](std::byte byte) { *buffer++ = byte; }, size); }; referee_serial_->write = [this](const std::byte* buffer, size_t size) { - start_transmit().uart1_transmit({ - .uart_data = std::span{buffer, size} - }); + transmit_buffer_.add_uart1_transmission(buffer, size); return size; }; - hero.register_output("/chassis/yaw/velocity_imu", chassis_yaw_velocity_imu_, 0); - hero.register_output( "/chassis/powermeter/control_enable", powermeter_control_enabled_, false); hero.register_output( "/chassis/powermeter/charge_power_limit", powermeter_charge_power_limit_, 0.); - } + hero.register_output("/chassis/climber/angle_imu", chassis_climber_angle_imu_); - BottomBoard(const BottomBoard&) = delete; - BottomBoard& operator=(const BottomBoard&) = delete; - BottomBoard(BottomBoard&&) = delete; - BottomBoard& operator=(BottomBoard&&) = delete; + q0 = hero.get_parameter("q0").as_double(); + q1 = hero.get_parameter("q1").as_double(); + q2 = hero.get_parameter("q2").as_double(); + q3 = hero.get_parameter("q3").as_double(); + } - ~BottomBoard() final = default; + ~BottomBoard_two() final { + stop_handling_events(); + event_thread_.join(); + } void update() { imu_.update_status(); dr16_.update_status(); supercap_.update_status(); - *chassis_yaw_velocity_imu_ = imu_.gz(); + Eigen::Quaterniond q_used{q0, q1, q2, q3}; + Eigen::Quaterniond q_now{imu_.q0(), imu_.q1(), imu_.q2(), imu_.q3()}; + Eigen::Quaterniond chassis_climber_angle_pose = q_used.conjugate() * q_now; - for (auto& motor : chassis_wheel_motors_) + *chassis_climber_angle_imu_ = + chassis_climber_angle_pose.toRotationMatrix().eulerAngles(0, 1, 2).y(); + + for (auto& motor : chassis_wheel_motors2_) motor.update_status(); - for (auto& motor : chassis_steering_motors_) + for (auto& motor : chassis_steering_motors2_) motor.update_status(); + gimbal_bottom_yaw_motor_.update_status(); + tf_->set_state( gimbal_bottom_yaw_motor_.angle()); } void command_update() { - auto builder = start_transmit(); - - builder.can1_transmit({ - .can_id = 0x200, - .can_data = - device::CanPacket8{ - chassis_wheel_motors_[0].generate_command(), - chassis_wheel_motors_[1].generate_command(), - chassis_wheel_motors_[2].generate_command(), - chassis_wheel_motors_[3].generate_command(), - } - .as_bytes(), - }); - - builder.can1_transmit({ - .can_id = 0x1FE, - .can_data = - device::CanPacket8{ - device::CanPacket8::PaddingQuarter{}, - device::CanPacket8::PaddingQuarter{}, - device::CanPacket8::PaddingQuarter{}, - supercap_.generate_command(), - } - .as_bytes(), - }); - - builder.can2_transmit({ - .can_id = 0x1FE, - .can_data = - device::CanPacket8{ - chassis_steering_motors_[0].generate_command(), - chassis_steering_motors_[1].generate_command(), - chassis_steering_motors_[2].generate_command(), - chassis_steering_motors_[3].generate_command(), - } - .as_bytes(), - }); - - builder.can2_transmit({ - .can_id = 0x141, - .can_data = gimbal_bottom_yaw_motor_.generate_command().as_bytes(), - }); + uint16_t batch_commands[4]{}; + + batch_commands[0] = 0; + batch_commands[1] = 0; + batch_commands[2] = chassis_wheel_motors2_[1].generate_command(); + batch_commands[3] = chassis_wheel_motors2_[0].generate_command(); + transmit_buffer_.add_can1_transmission(0x200, std::bit_cast(batch_commands)); + + batch_commands[0] = chassis_steering_motors2_[0].generate_command(); + batch_commands[1] = 0; + batch_commands[2] = chassis_steering_motors2_[1].generate_command(); + batch_commands[3] = supercap_.generate_command(); + transmit_buffer_.add_can1_transmission(0x1FE, std::bit_cast(batch_commands)); + + transmit_buffer_.add_can2_transmission( + 0x141, gimbal_bottom_yaw_motor_.generate_command()); } private: - void can1_receive_callback(const librmcs::data::CanDataView& data) override { - if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] + void can1_receive_callback( + uint32_t can_id, uint64_t can_data, bool is_extended_can_id, + bool is_remote_transmission, uint8_t can_data_length) override { + if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] return; - auto can_id = data.can_id; - if (can_id == 0x201) { - chassis_wheel_motors_[0].store_status(data.can_data); - } else if (can_id == 0x202) { - chassis_wheel_motors_[1].store_status(data.can_data); - } else if (can_id == 0x203) { - chassis_wheel_motors_[2].store_status(data.can_data); + if (can_id == 0x203) { + chassis_wheel_motors2_[1].store_status(can_data); } else if (can_id == 0x204) { - chassis_wheel_motors_[3].store_status(data.can_data); + chassis_wheel_motors2_[0].store_status(can_data); + } + if (can_id == 0x205) { + chassis_steering_motors2_[0].store_status(can_data); + } else if (can_id == 0x207) { + chassis_steering_motors2_[1].store_status(can_data); } else if (can_id == 0x300) { - supercap_.store_status(data.can_data); + supercap_.store_status(can_data); } } - void can2_receive_callback(const librmcs::data::CanDataView& data) override { - if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] + void can2_receive_callback( + uint32_t can_id, uint64_t can_data, bool is_extended_can_id, + bool is_remote_transmission, uint8_t can_data_length) override { + if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] return; - auto can_id = data.can_id; - if (can_id == 0x205) { - chassis_steering_motors_[0].store_status(data.can_data); - } else if (can_id == 0x206) { - chassis_steering_motors_[1].store_status(data.can_data); - } else if (can_id == 0x207) { - chassis_steering_motors_[2].store_status(data.can_data); - } else if (can_id == 0x208) { - chassis_steering_motors_[3].store_status(data.can_data); - } else if (can_id == 0x141) { - gimbal_bottom_yaw_motor_.store_status(data.can_data); + if (can_id == 0x141) { + gimbal_bottom_yaw_motor_.store_status(can_data); } } + rclcpp::Logger logger_; + // test + std::chrono::steady_clock::time_point last_time_; - void uart1_receive_callback(const librmcs::data::UartDataView& data) override { - const auto* uart_data = data.uart_data.data(); - referee_ring_buffer_receive_.emplace_back_n( - [&uart_data](std::byte* storage) noexcept { *storage = *uart_data++; }, - data.uart_data.size()); + void calc_can_fps(double can_id) { + auto now = std::chrono::steady_clock::now(); + auto delta = + std::chrono::duration_cast(now - last_time_).count(); + + RCLCPP_INFO(logger_, "can id :%lf,fps:%ld", can_id, 1000000 / delta); + last_time_ = now; + } + // test + void uart1_receive_callback(const std::byte* uart_data, uint8_t uart_data_length) override { + referee_ring_buffer_receive_.emplace_back_multi( + [&uart_data](std::byte* storage) { *storage = *uart_data++; }, uart_data_length); } - void dbus_receive_callback(const librmcs::data::UartDataView& data) override { - dr16_.store_status(data.uart_data.data(), data.uart_data.size()); + void dbus_receive_callback(const std::byte* uart_data, uint8_t uart_data_length) override { + dr16_.store_status(uart_data, uart_data_length); } - void accelerometer_receive_callback( - const librmcs::data::AccelerometerDataView& data) override { - imu_.store_accelerometer_status(data.x, data.y, data.z); + void accelerometer_receive_callback(int16_t x, int16_t y, int16_t z) override { + imu_.store_accelerometer_status(x, y, z); } - void gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data) override { - imu_.store_gyroscope_status(data.x, data.y, data.z); + void gyroscope_receive_callback(int16_t x, int16_t y, int16_t z) override { + imu_.store_gyroscope_status(x, y, z); } + double q0, q1, q2, q3; + device::Bmi088 imu_; OutputInterface& tf_; - OutputInterface chassis_yaw_velocity_imu_; - OutputInterface powermeter_control_enabled_; OutputInterface powermeter_charge_power_limit_; + OutputInterface chassis_climber_angle_imu_; device::Dr16 dr16_; - - device::DjiMotor chassis_steering_motors_[4]; - device::DjiMotor chassis_wheel_motors_[4]; device::Supercap supercap_; + device::DjiMotor chassis_steering_motors2_[2]; + device::DjiMotor chassis_wheel_motors2_[2]; device::LkMotor gimbal_bottom_yaw_motor_; - rmcs_utility::RingBuffer referee_ring_buffer_receive_{256}; + librmcs::utility::RingBuffer referee_ring_buffer_receive_{256}; OutputInterface referee_serial_; + + librmcs::client::CBoard::TransmitBuffer transmit_buffer_; + std::thread event_thread_; }; OutputInterface tf_; @@ -579,7 +687,8 @@ class SteeringHero rclcpp::Subscription::SharedPtr gimbal_calibrate_subscription_; std::unique_ptr top_board_; - std::unique_ptr bottom_board_; + std::unique_ptr bottom_board_one_; + std::unique_ptr bottom_board_two_; rmcs_utility::TickTimer temperature_logging_timer_; }; @@ -588,4 +697,4 @@ class SteeringHero #include -PLUGINLIB_EXPORT_CLASS(rmcs_core::hardware::SteeringHero, rmcs_executor::Component) +PLUGINLIB_EXPORT_CLASS(rmcs_core::hardware::SteeringHero, rmcs_executor::Component) \ No newline at end of file From a2765250597e3780b939463d46383df08f6ef0c3 Mon Sep 17 00:00:00 2001 From: Fin_Resect Date: Mon, 16 Mar 2026 01:03:18 +0800 Subject: [PATCH 02/12] Update to librmcs v3.1.0b0 --- .../rmcs_bringup/config/steering-hero.yaml | 6 +- rmcs_ws/src/rmcs_core/CMakeLists.txt | 4 +- .../rmcs_core/src/hardware/steering-hero.cpp | 747 +++++++++--------- 3 files changed, 381 insertions(+), 376 deletions(-) diff --git a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml index 7e7c551e7..49957f14a 100644 --- a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml +++ b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml @@ -37,9 +37,9 @@ rmcs_executor: hero_hardware: ros__parameters: - usb_pid_top_board: 0xce3b - usb_pid_bottom_board_one: 0x902e - usb_pid_bottom_board_two: 0x317e + board_serial_top_board: "D4-2BCA-2E47-76CD-23BC-0B78-684B" + board_serial_bottom_board_one: "D4-3674-7174-8768-879E-E44A-3931" + board_serial_bottom_board_two: "D4-7973-19A9-EA40-4A3E-306F-10F9" bottom_yaw_motor_zero_point: 50656 pitch_motor_zero_point: 63917 top_yaw_motor_zero_point: 59095 diff --git a/rmcs_ws/src/rmcs_core/CMakeLists.txt b/rmcs_ws/src/rmcs_core/CMakeLists.txt index dd3c85799..ec80d3ed9 100644 --- a/rmcs_ws/src/rmcs_core/CMakeLists.txt +++ b/rmcs_ws/src/rmcs_core/CMakeLists.txt @@ -17,8 +17,8 @@ include(FetchContent) set(BUILD_STATIC_LIBRMCS ON CACHE BOOL "Build static librmcs SDK" FORCE) FetchContent_Declare( librmcs - URL https://github.com/Alliance-Algorithm/librmcs/releases/download/v3.0.0/librmcs-sdk-src-3.0.0.zip - URL_HASH SHA256=b39f51c21baacdcbf3f0176119b8850137a108b88a67e12395d37d89e5ef53e8 + URL https://github.com/Alliance-Algorithm/librmcs/releases/download/v3.1.0b0/librmcs-sdk-src-3.1.0-beta.0.zip + URL_HASH SHA256=7f3203e8db8cb7954ae0ac2ee226f93e32b367b3956b1f0bb5c7ae203c2ccbef DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) FetchContent_MakeAvailable(librmcs) diff --git a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp index 8f89cd2a8..ae4a6a50a 100644 --- a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp +++ b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp @@ -1,22 +1,28 @@ -#include -#include #include -#include +#include #include -#include +#include +#include +#include +#include + +#include +#include +#include #include #include -#include - -#include #include +#include +#include +#include #include #include #include -#include +#include #include #include "hardware/device/bmi088.hpp" +#include "hardware/device/can_packet.hpp" #include "hardware/device/dji_motor.hpp" #include "hardware/device/dr16.hpp" #include "hardware/device/lk_motor.hpp" @@ -29,50 +35,43 @@ class SteeringHero , public rclcpp::Node { public: SteeringHero() - : Node{get_component_name(), rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)} + : Node( + get_component_name(), + rclcpp::NodeOptions().automatically_declare_parameters_from_overrides(true)) , command_component_( create_partner_component( get_component_name() + "_command", *this)) { - using namespace rmcs_description; - register_output("/tf", tf_); - tf_->set_transform(Eigen::Translation3d{0.16, 0.0, 0.15}); - gimbal_calibrate_subscription_ = create_subscription( - "/gimbal/calibrate", rclcpp::QoS{0}, [this](std_msgs::msg::Int32::UniquePtr&& msg) { - gimbal_calibrate_subscription_callback(std::move(msg)); - }); + // gimbal_calibrate_subscription_ = create_subscription( + // "/gimbal/calibrate", rclcpp::QoS{0}, [this](std_msgs::msg::Int32::UniquePtr&& msg) { + // gimbal_calibrate_subscription_callback(std::move(msg)); + // }); top_board_ = std::make_unique( - *this, *command_component_, - static_cast(get_parameter("usb_pid_top_board").as_int())); + *this, *command_component_, get_parameter("board_serial_top_board").as_string()); + bottom_board_one_ = std::make_unique( - *this, *command_component_, - static_cast(get_parameter("usb_pid_bottom_board_one").as_int())); + *this, *command_component_, get_parameter("board_serial_bottom_board_one").as_string()); + bottom_board_two_ = std::make_unique( - *this, *command_component_, - static_cast(get_parameter("usb_pid_bottom_board_two").as_int())); + *this, *command_component_, get_parameter("board_serial_bottom_board_two").as_string()); - // temperature_logging_timer_.reset(1000); + tf_->set_transform( + Eigen::Translation3d{0.06603, 0.0, 0.082}); } + SteeringHero(const SteeringHero&) = delete; + SteeringHero& operator=(const SteeringHero&) = delete; + SteeringHero(SteeringHero&&) = delete; + SteeringHero& operator=(SteeringHero&&) = delete; + ~SteeringHero() override = default; void update() override { top_board_->update(); bottom_board_one_->update(); bottom_board_two_->update(); - - // if (temperature_logging_timer_.tick()) { - // temperature_logging_timer_.reset(1000); - // RCLCPP_INFO( - // get_logger(), - // "Temperature: pitch: %.1f, top_yaw: %.1f, bottom_yaw: %.1f, feeder: %.1f", - // top_board_->gimbal_pitch_motor_.temperature(), - // top_board_->gimbal_top_yaw_motor_.temperature(), - // bottom_board_->gimbal_bottom_yaw_motor_.temperature(), - // top_board_->gimbal_bullet_feeder_.temperature()); - // } } void command_update() { @@ -117,79 +116,80 @@ class SteeringHero }; std::shared_ptr command_component_; - class TopBoard final : private librmcs::client::CBoard { + class TopBoard final : private librmcs::agent::CBoard { public: friend class SteeringHero; - explicit TopBoard(SteeringHero& hero, SteeringHeroCommand& hero_command, int usb_pid = - -1) - : librmcs::client::CBoard(usb_pid) - , logger_(hero.get_logger()) - , tf_(hero.tf_) + explicit TopBoard( + SteeringHero& steering_hero, SteeringHeroCommand& steering_hero_command, + std::string_view board_serial = {}) + : librmcs::agent::CBoard(board_serial) + , logger_(steering_hero.get_logger()) + , tf_(steering_hero.tf_) , imu_(1000, 0.2, 0.0) - , gimbal_top_yaw_motor_( - hero, hero_command, "/gimbal/top_yaw", - device::LkMotor::Config{device::LkMotor::Type::MG5010E_I10} - .set_encoder_zero_point( - static_cast( - hero.get_parameter("top_yaw_motor_zero_point").as_int()))) - , gimbal_pitch_motor_( - hero, hero_command, "/gimbal/pitch", - device::LkMotor::Config{device::LkMotor::Type::MG5010E_I10} - .set_encoder_zero_point( - static_cast(hero.get_parameter("pitch_motor_zero_point").as_int()))) + , gimbal_top_yaw_motor_(steering_hero, steering_hero_command, "/gimbal/top_yaw") + , gimbal_pitch_motor_(steering_hero, steering_hero_command, "/gimbal/pitch") , gimbal_friction_wheels_( - {hero, hero_command, "/gimbal/first_left_friction", - device::DjiMotor::Config{device::DjiMotor::Type::M3508}.set_reduction_ratio(1.)}, - {hero, hero_command, "/gimbal/first_right_friction", - device::DjiMotor::Config{device::DjiMotor::Type::M3508} - .set_reduction_ratio(1.) - .set_reversed()}, - {hero, hero_command, "/gimbal/second_left_friction", - device::DjiMotor::Config{device::DjiMotor::Type::M3508} - .set_reduction_ratio(1.) - .set_reversed()}, - {hero, hero_command, "/gimbal/second_right_friction", - device::DjiMotor::Config{device::DjiMotor::Type::M3508} - .set_reduction_ratio(1.) - .set_reversed()}) - , gimbal_bullet_feeder_( - hero, hero_command, "/gimbal/bullet_feeder", - device::LkMotor::Config{device::LkMotor::Type::MG5010E_I10} - .set_reversed() - .enable_multi_turn_angle()) - , putter_motor_{hero, hero_command, "/gimbal/putter", - device::DjiMotor::Config{device::DjiMotor::Type::M3508}.set_reduction_ratio(1.).enable_multi_turn_angle()} - , transmit_buffer_(*this, 32) - , event_thread_([this]() { handle_events(); }) { + {steering_hero, steering_hero_command, "/gimbal/first_left_friction"}, + {steering_hero, steering_hero_command, "/gimbal/first_right_friction"}, + {steering_hero, steering_hero_command, "/gimbal/second_left_friction"}, + {steering_hero, steering_hero_command, "/gimbal/second_right_friction"}) + , gimbal_bullet_feeder_(steering_hero, steering_hero_command, "/gimbal/bullet_feeder") + , putter_motor_(steering_hero, steering_hero_command, "/gimbal/putter") { + + gimbal_top_yaw_motor_.configure( + device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10}.set_encoder_zero_point( + static_cast( + steering_hero.get_parameter("top_yaw_motor_zero_point").as_int()))); + gimbal_pitch_motor_.configure( + device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10}.set_encoder_zero_point( + static_cast( + steering_hero.get_parameter("pitch_motor_zero_point").as_int()))); + gimbal_friction_wheels_[0].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio(1.)); + gimbal_friction_wheels_[1].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio(1.)); + gimbal_friction_wheels_[2].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio(1.)); + gimbal_friction_wheels_[3].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio(1.)); + gimbal_bullet_feeder_.configure( + device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10} + .set_reversed() + .enable_multi_turn_angle()); + putter_motor_.configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + .set_reduction_ratio(1.) + .enable_multi_turn_angle()); + + steering_hero.register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_); + steering_hero.register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_); + + steering_hero.register_output( + "/gimbal/photoelectric_sensor", photoelectric_sensor_status_, false); + steering_hero.register_output( + "/auto_aim/image_capturer/timestamp", camera_capturer_trigger_timestamp_, 0); + steering_hero.register_output( + "/auto_aim/image_capturer/trigger", camera_capturer_trigger_, 0); imu_.set_coordinate_mapping([](double x, double y, double z) { // Get the mapping with the following code. - // The rotation angle must be an exact multiple of 90 degrees, otherwise use a - // matrix. + // The rotation angle must be an exact multiple of 90 degrees, otherwise + // use a matrix. - // Eigen::AngleAxisd pitch_link_to_imu_link{ - // std::numbers::pi, Eigen::Vector3d::UnitZ()}; - // Eigen::Vector3d mapping = pitch_link_to_imu_link * Eigen::Vector3d{1, 2, 3}; + // Eigen::AngleAxisd pitch_link_to_bmi088_link{ + // std::numbers::pi / 2, Eigen::Vector3d::UnitZ()}; + // Eigen::Vector3d mapping = pitch_link_to_bmi088_link * Eigen::Vector3d{1, 2, 3}; // std::cout << mapping << std::endl; - - return std::make_tuple(-x, -y, z); + return std::make_tuple(x, y, z); }); - - hero.register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_); - hero.register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_); - - hero.register_output( - "/gimbal/photoelectric_sensor", photoelectric_sensor_status_, false); - hero.register_output( - "/auto_aim/image_capturer/timestamp", camera_capturer_trigger_timestamp_, 0); - hero.register_output("/auto_aim/image_capturer/trigger", camera_capturer_trigger_, 0); } - ~TopBoard() final { - stop_handling_events(); - event_thread_.join(); - } + TopBoard(const TopBoard&) = delete; + TopBoard& operator=(const TopBoard&) = delete; + TopBoard(TopBoard&&) = delete; + TopBoard& operator=(TopBoard&&) = delete; + ~TopBoard() final = default; void update() { imu_.update_status(); Eigen::Quaterniond gimbal_imu_pose{imu_.q0(), imu_.q1(), imu_.q2(), imu_.q3()}; @@ -218,160 +218,163 @@ class SteeringHero } void command_update() { - uint16_t batch_commands[4]{}; - - batch_commands[0] = gimbal_friction_wheels_[3].generate_command(); - batch_commands[1] = gimbal_friction_wheels_[1].generate_command(); - batch_commands[2] = gimbal_friction_wheels_[2].generate_command(); - batch_commands[3] = gimbal_friction_wheels_[0].generate_command(); - transmit_buffer_.add_can1_transmission(0x200, std::bit_cast(batch_commands)); - - frequency_control_flag_ = !frequency_control_flag_; - if (frequency_control_flag_) { - batch_commands[0] = putter_motor_.generate_command(); - transmit_buffer_.add_can1_transmission( - 0x1ff, std::bit_cast(batch_commands)); - } + auto builder = start_transmit(); + + builder.can1_transmit({ + .can_id = 0x200, + .can_data = + device::CanPacket8{ + gimbal_friction_wheels_[3].generate_command(), + gimbal_friction_wheels_[1].generate_command(), + gimbal_friction_wheels_[2].generate_command(), + gimbal_friction_wheels_[0].generate_command(), + } + .as_bytes(), + }); - transmit_buffer_.add_can1_transmission( - 0x141, gimbal_bullet_feeder_.generate_torque_command( - gimbal_bullet_feeder_.control_torque())); + builder.can1_transmit({ + .can_id = 0x1FF, + .can_data = + device::CanPacket8{ + putter_motor_.generate_command(), + device::CanPacket8::PaddingQuarter{}, + device::CanPacket8::PaddingQuarter{}, + device::CanPacket8::PaddingQuarter{}, + } + .as_bytes(), + }); - transmit_buffer_.add_can2_transmission(0x141, gimbal_top_yaw_motor_.generate_command()); + builder.can1_transmit({ + .can_id = 0x141, + .can_data = gimbal_bullet_feeder_.generate_torque_command().as_bytes(), + }); - transmit_buffer_.add_can2_transmission(0x142, gimbal_pitch_motor_.generate_command()); + builder.can2_transmit({ + .can_id = 0x141, + .can_data = gimbal_top_yaw_motor_.generate_torque_command().as_bytes(), + }); - transmit_buffer_.trigger_transmission(); + builder.can2_transmit({ + .can_id = 0x142, + .can_data = gimbal_pitch_motor_.generate_torque_command().as_bytes(), + }); } private: - void can1_receive_callback( - uint32_t can_id, uint64_t can_data, bool is_extended_can_id, - bool is_remote_transmission, uint8_t can_data_length) override { - if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] + void can1_receive_callback(const librmcs::data::CanDataView& data) override { + if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] return; - + auto can_id = data.can_id; if (can_id == 0x204) { - gimbal_friction_wheels_[0].store_status(can_data); + gimbal_friction_wheels_[0].store_status(data.can_data); } else if (can_id == 0x202) { - gimbal_friction_wheels_[1].store_status(can_data); + gimbal_friction_wheels_[1].store_status(data.can_data); } else if (can_id == 0x203) { - gimbal_friction_wheels_[2].store_status(can_data); + gimbal_friction_wheels_[2].store_status(data.can_data); } else if (can_id == 0x201) { - gimbal_friction_wheels_[3].store_status(can_data); + gimbal_friction_wheels_[3].store_status(data.can_data); } else if (can_id == 0x205) { - putter_motor_.store_status(can_data); + putter_motor_.store_status(data.can_data); } else if (can_id == 0x141) { - gimbal_bullet_feeder_.store_status(can_data); + gimbal_bullet_feeder_.store_status(data.can_data); } } - void can2_receive_callback( - uint32_t can_id, uint64_t can_data, bool is_extended_can_id, - bool is_remote_transmission, uint8_t can_data_length) override { - if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] + void can2_receive_callback(const librmcs::data::CanDataView& data) override { + if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] return; + auto can_id = data.can_id; if (can_id == 0x141) { - gimbal_top_yaw_motor_.store_status(can_data); + gimbal_top_yaw_motor_.store_status(data.can_data); } else if (can_id == 0x142) { - gimbal_pitch_motor_.store_status(can_data); + gimbal_pitch_motor_.store_status(data.can_data); } } - void accelerometer_receive_callback(int16_t x, int16_t y, int16_t z) override { - imu_.store_accelerometer_status(x, y, z); + void accelerometer_receive_callback( + const librmcs::data::AccelerometerDataView& data) override { + imu_.store_accelerometer_status(data.x, data.y, data.z); } - void gyroscope_receive_callback(int16_t x, int16_t y, int16_t z) override { - imu_.store_gyroscope_status(x, y, z); + void gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data) override { + imu_.store_gyroscope_status(data.x, data.y, data.z); } void putter_receive_callback(bool status) { *photoelectric_sensor_status_ = status; } rclcpp::Logger logger_; OutputInterface& tf_; - OutputInterface photoelectric_sensor_status_; - OutputInterface camera_capturer_trigger_; - OutputInterface camera_capturer_trigger_timestamp_; + std::time_t last_camera_capturer_trigger_timestamp_{0}; device::Bmi088 imu_; - - OutputInterface gimbal_yaw_velocity_imu_; - OutputInterface gimbal_pitch_velocity_imu_; - device::LkMotor gimbal_top_yaw_motor_; device::LkMotor gimbal_pitch_motor_; - device::DjiMotor gimbal_friction_wheels_[4]; device::LkMotor gimbal_bullet_feeder_; - - bool frequency_control_flag_{false}; device::DjiMotor putter_motor_; - librmcs::client::CBoard::TransmitBuffer transmit_buffer_; - std::thread event_thread_; + OutputInterface gimbal_yaw_velocity_imu_; + OutputInterface gimbal_pitch_velocity_imu_; + OutputInterface photoelectric_sensor_status_; + OutputInterface camera_capturer_trigger_; + OutputInterface camera_capturer_trigger_timestamp_; }; - class BottomBoard_one final : private librmcs::client::CBoard { + class BottomBoard_one final : private librmcs::agent::CBoard { public: friend class SteeringHero; explicit BottomBoard_one( - SteeringHero& hero, SteeringHeroCommand& hero_command, int usb_pid = -1) - : librmcs::client::CBoard(usb_pid) - , logger_(hero.get_logger()) + SteeringHero& steering_hero, SteeringHeroCommand& steering_hero_command, + std::string_view board_serial = {}) + : librmcs::agent::CBoard(board_serial) + , logger_(steering_hero.get_logger()) , imu_(1000, 0.2, 0.0) , chassis_front_climber_motor_( - {hero, hero_command, "/chassis/climber/left_front_motor", - device::DjiMotor::Config{device::DjiMotor::Type::M3508} - .set_reversed() - .set_reduction_ratio(19.)}, - {hero, hero_command, "/chassis/climber/right_front_motor", - device::DjiMotor::Config{device::DjiMotor::Type::M3508}.set_reduction_ratio( - 19.)}) + {steering_hero, steering_hero_command, "/chassis/climber/left_front_motor"}, + {steering_hero, steering_hero_command, "/chassis/climber/right_front_motor"}) , chassis_back_climber_motor_( - {hero, hero_command, "/chassis/climber/left_back_motor", - device::DjiMotor::Config{device::DjiMotor::Type::M3508}.set_reduction_ratio( - 19.)}, - {hero, hero_command, "/chassis/climber/right_back_motor", - device::DjiMotor::Config{device::DjiMotor::Type::M3508} - .set_reversed() - .set_reduction_ratio(19.)}) + {steering_hero, steering_hero_command, "/chassis/climber/left_back_motor"}, + {steering_hero, steering_hero_command, "/chassis/climber/right_back_motor"}) , chassis_steering_motors_( - {hero, hero_command, "/chassis/left_front_steering", - device::DjiMotor::Config{device::DjiMotor::Type::GM6020} - .set_encoder_zero_point( - static_cast(hero.get_parameter("left_front_zero_point").as_int())) - .set_reversed()}, - {hero, hero_command, "/chassis/right_front_steering", - device::DjiMotor::Config{device::DjiMotor::Type::GM6020} - .set_encoder_zero_point( - static_cast(hero.get_parameter("right_front_zero_point").as_int())) - .set_reversed()}) + {steering_hero, steering_hero_command, "/chassis/left_front_steering"}, + {steering_hero, steering_hero_command, "/chassis/right_front_steering"}) , chassis_wheel_motors_( - {hero, hero_command, "/chassis/left_front_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::M3508} - .set_reversed() - .set_reduction_ratio(2232. / 169.)}, - {hero, hero_command, "/chassis/right_front_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::M3508} - .set_reversed() - .set_reduction_ratio(2232. / 169.)}) - - , transmit_buffer_(*this, 32) - , event_thread_([this]() { handle_events(); }) { - - // hero.register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_); - // hero.register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_); - - hero.register_output("/chassis/yaw/velocity_imu", chassis_yaw_velocity_imu_, 0); + {steering_hero, steering_hero_command, "/chassis/left_front_wheel"}, + {steering_hero, steering_hero_command, "/chassis/right_front_wheel"}) { + // + + chassis_steering_motors_[0].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kGM6020} + .set_encoder_zero_point( + static_cast( + steering_hero.get_parameter("left_front_zero_point").as_int())) + .set_reversed()); + chassis_steering_motors_[1].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kGM6020} + .set_encoder_zero_point( + static_cast( + steering_hero.get_parameter("right_front_zero_point").as_int())) + .set_reversed()); + chassis_wheel_motors_[0].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + .set_reversed() + .set_reduction_ratio(2232. / 169.)); + chassis_wheel_motors_[1].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + .set_reversed() + .set_reduction_ratio(2232. / 169.)); + + steering_hero.register_output( + "/chassis/yaw/velocity_imu", chassis_yaw_velocity_imu_, 0); } + BottomBoard_one(const BottomBoard_one&) = delete; + BottomBoard_one& operator=(const BottomBoard_one&) = delete; + BottomBoard_one(BottomBoard_one&&) = delete; + BottomBoard_one& operator=(BottomBoard_one&&) = delete; - ~BottomBoard_one() final { - stop_handling_events(); - event_thread_.join(); - } + ~BottomBoard_one() final = default; void update() { imu_.update_status(); @@ -390,184 +393,176 @@ class SteeringHero } void command_update() { - uint16_t batch_commands[4]{}; - - batch_commands[0] = chassis_wheel_motors_[1].generate_command(); - batch_commands[1] = chassis_wheel_motors_[0].generate_command(); - batch_commands[2] = 0; - batch_commands[3] = 0; - transmit_buffer_.add_can1_transmission(0x200, std::bit_cast(batch_commands)); - - batch_commands[0] = 0; - batch_commands[1] = chassis_steering_motors_[0].generate_command(); - batch_commands[2] = 0; - batch_commands[3] = chassis_steering_motors_[1].generate_command(); - transmit_buffer_.add_can1_transmission(0x1FE, std::bit_cast(batch_commands)); - - batch_commands[0] = chassis_back_climber_motor_[0].generate_command(); - batch_commands[1] = chassis_back_climber_motor_[1].generate_command(); - batch_commands[2] = chassis_front_climber_motor_[0].generate_command(); - batch_commands[3] = chassis_front_climber_motor_[1].generate_command(); - transmit_buffer_.add_can2_transmission(0x200, std::bit_cast(batch_commands)); - - transmit_buffer_.trigger_transmission(); + auto builder = start_transmit(); + + builder.can1_transmit({ + .can_id = 0x200, + .can_data = + device::CanPacket8{ + chassis_wheel_motors_[1].generate_command(), + chassis_wheel_motors_[0].generate_command(), + device::CanPacket8::PaddingQuarter{}, + device::CanPacket8::PaddingQuarter{}, + } + .as_bytes(), + }); + + builder.can1_transmit({ + .can_id = 0x200, + .can_data = + device::CanPacket8{ + device::CanPacket8::PaddingQuarter{}, + chassis_steering_motors_[0].generate_command(), + device::CanPacket8::PaddingQuarter{}, + chassis_steering_motors_[1].generate_command(), + } + .as_bytes(), + }); + + builder.can2_transmit({ + .can_id = 0x200, + .can_data = + device::CanPacket8{ + chassis_back_climber_motor_[0].generate_command(), + chassis_back_climber_motor_[1].generate_command(), + chassis_front_climber_motor_[0].generate_command(), + chassis_front_climber_motor_[1].generate_command(), + } + .as_bytes(), + }); } private: - void can1_receive_callback( - uint32_t can_id, uint64_t can_data, bool is_extended_can_id, - bool is_remote_transmission, uint8_t can_data_length) override { - if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] + void can1_receive_callback(const librmcs::data::CanDataView& data) override { + if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] return; - + auto can_id = data.can_id; if (can_id == 0x201) { - chassis_wheel_motors_[1].store_status(can_data); + chassis_wheel_motors_[1].store_status(data.can_data); } else if (can_id == 0x202) { - chassis_wheel_motors_[0].store_status(can_data); + chassis_wheel_motors_[0].store_status(data.can_data); } if (can_id == 0x206) { - chassis_steering_motors_[0].store_status(can_data); + chassis_steering_motors_[0].store_status(data.can_data); } else if (can_id == 0x208) { - chassis_steering_motors_[1].store_status(can_data); + chassis_steering_motors_[1].store_status(data.can_data); } } - void can2_receive_callback( - uint32_t can_id, uint64_t can_data, bool is_extended_can_id, - bool is_remote_transmission, uint8_t can_data_length) override { - if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] + void can2_receive_callback(const librmcs::data::CanDataView& data) override { + if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] return; + auto can_id = data.can_id; if (can_id == 0x203) { - chassis_front_climber_motor_[0].store_status(can_data); + chassis_front_climber_motor_[0].store_status(data.can_data); } else if (can_id == 0x204) { - chassis_front_climber_motor_[1].store_status(can_data); + chassis_front_climber_motor_[1].store_status(data.can_data); } else if (can_id == 0x201) { - chassis_back_climber_motor_[0].store_status(can_data); + chassis_back_climber_motor_[0].store_status(data.can_data); } else if (can_id == 0x202) { - chassis_back_climber_motor_[1].store_status(can_data); + chassis_back_climber_motor_[1].store_status(data.can_data); } } - rclcpp::Logger logger_; - // test - std::chrono::steady_clock::time_point last_time_; - - void calc_can_fps(double can_id) { - auto now = std::chrono::steady_clock::now(); - auto delta = - std::chrono::duration_cast(now - last_time_).count(); - RCLCPP_INFO(logger_, "can id :%lf,fps:%ld", can_id, 1000000 / delta); - last_time_ = now; + void accelerometer_receive_callback( + const librmcs::data::AccelerometerDataView& data) override { + imu_.store_accelerometer_status(data.x, data.y, data.z); } - void accelerometer_receive_callback(int16_t x, int16_t y, int16_t z) override { - imu_.store_accelerometer_status(x, y, z); + void gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data) override { + imu_.store_gyroscope_status(data.x, data.y, data.z); } - void gyroscope_receive_callback(int16_t x, int16_t y, int16_t z) override { - imu_.store_gyroscope_status(x, y, z); - } - device::Bmi088 imu_; + // // test + // std::chrono::steady_clock::time_point last_time_; + // void calc_can_fps(double can_id) { + // auto now = std::chrono::steady_clock::now(); + // auto delta = + // std::chrono::duration_cast(now - last_time_).count(); - OutputInterface chassis_yaw_velocity_imu_; + // RCLCPP_INFO(logger_, "can id :%lf,fps:%ld", can_id, 1000000 / delta); + // last_time_ = now; + // } + + rclcpp::Logger logger_; + device::Bmi088 imu_; device::DjiMotor chassis_front_climber_motor_[2]; device::DjiMotor chassis_back_climber_motor_[2]; - device::DjiMotor chassis_steering_motors_[2]; device::DjiMotor chassis_wheel_motors_[2]; - librmcs::utility::RingBuffer referee_ring_buffer_receive_{256}; + OutputInterface chassis_yaw_velocity_imu_; OutputInterface referee_serial_; - - librmcs::client::CBoard::TransmitBuffer transmit_buffer_; - std::thread event_thread_; - OutputInterface gimbal_yaw_velocity_imu_; OutputInterface gimbal_pitch_velocity_imu_; }; - class BottomBoard_two final : private librmcs::client::CBoard { + class BottomBoard_two final : private librmcs::agent::CBoard { public: friend class SteeringHero; explicit BottomBoard_two( - SteeringHero& hero, SteeringHeroCommand& hero_command, int usb_pid = -1) - : librmcs::client::CBoard(usb_pid) - , logger_(hero.get_logger()) + SteeringHero& steering_hero, SteeringHeroCommand& steering_hero_command, + std::string_view board_serial = {}) + : librmcs::agent::CBoard(board_serial) + , logger_(steering_hero.get_logger()) , imu_(1000, 0.2, 0.0) - , tf_(hero.tf_) - , dr16_(hero) - , supercap_(hero, hero_command) + , tf_(steering_hero.tf_) + , dr16_(steering_hero) + , supercap_(steering_hero, steering_hero_command) , chassis_steering_motors2_( - {hero, hero_command, "/chassis/left_back_steering", - device::DjiMotor::Config{device::DjiMotor::Type::GM6020} - .set_encoder_zero_point( - static_cast(hero.get_parameter("left_back_zero_point").as_int())) - .set_reversed()}, - {hero, hero_command, "/chassis/right_back_steering", - device::DjiMotor::Config{device::DjiMotor::Type::GM6020} - .set_encoder_zero_point( - static_cast(hero.get_parameter("right_back_zero_point").as_int())) - .set_reversed()}) + {steering_hero, steering_hero_command, "/chassis/left_back_steering"}, + {steering_hero, steering_hero_command, "/chassis/right_back_steering"}) , chassis_wheel_motors2_( - {hero, hero_command, "/chassis/left_back_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::M3508} - .set_reversed() - .set_reduction_ratio(2232. / 169.)}, - {hero, hero_command, "/chassis/right_back_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::M3508} - .set_reversed() - .set_reduction_ratio(2232. / 169.)}) - , gimbal_bottom_yaw_motor_( - hero, hero_command, "/gimbal/bottom_yaw", - device::LkMotor::Config{device::LkMotor::Type::MG6012E_I8} - .set_reversed() - .set_encoder_zero_point( - static_cast( - hero.get_parameter("bottom_yaw_motor_zero_point").as_int()))) - , transmit_buffer_(*this, 32) - , event_thread_([this]() { handle_events(); }) { - - hero.register_output("/referee/serial", referee_serial_); - referee_serial_->read = [this](std::byte* buffer, size_t size) { - return referee_ring_buffer_receive_.pop_front_multi( - [&buffer](std::byte byte) { *buffer++ = byte; }, size); - }; - referee_serial_->write = [this](const std::byte* buffer, size_t size) { - transmit_buffer_.add_uart1_transmission(buffer, size); - return size; - }; - - hero.register_output( + {steering_hero, steering_hero_command, "/chassis/left_back_wheel"}, + {steering_hero, steering_hero_command, "/chassis/right_back_wheel"}) + , gimbal_bottom_yaw_motor_(steering_hero, steering_hero_command, "/gimbal/bottom_yaw") { + chassis_steering_motors2_[0].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kGM6020} + .set_encoder_zero_point( + static_cast( + steering_hero.get_parameter("left_back_zero_point").as_int())) + .set_reversed()); + chassis_steering_motors2_[1].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kGM6020} + .set_encoder_zero_point( + static_cast( + steering_hero.get_parameter("right_back_zero_point").as_int())) + .set_reversed()); + chassis_wheel_motors2_[0].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + .set_reversed() + .set_reduction_ratio(2232. / 169.)); + chassis_wheel_motors2_[1].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + .set_reversed() + .set_reduction_ratio(2232. / 169.)); + gimbal_bottom_yaw_motor_.configure( + device::LkMotor::Config{device::LkMotor::Type::kMG6012Ei8} + .set_reversed() + .set_encoder_zero_point( + static_cast( + steering_hero.get_parameter("bottom_yaw_motor_zero_point").as_int()))); + steering_hero.register_output("/referee/serial", referee_serial_); + steering_hero.register_output( "/chassis/powermeter/control_enable", powermeter_control_enabled_, false); - hero.register_output( + steering_hero.register_output( "/chassis/powermeter/charge_power_limit", powermeter_charge_power_limit_, 0.); - hero.register_output("/chassis/climber/angle_imu", chassis_climber_angle_imu_); - - q0 = hero.get_parameter("q0").as_double(); - q1 = hero.get_parameter("q1").as_double(); - q2 = hero.get_parameter("q2").as_double(); - q3 = hero.get_parameter("q3").as_double(); } - ~BottomBoard_two() final { - stop_handling_events(); - event_thread_.join(); - } + BottomBoard_two(const TopBoard&) = delete; + BottomBoard_two& operator=(const TopBoard&) = delete; + BottomBoard_two(TopBoard&&) = delete; + BottomBoard_two& operator=(TopBoard&&) = delete; + + ~BottomBoard_two() final = default; void update() { imu_.update_status(); dr16_.update_status(); supercap_.update_status(); - Eigen::Quaterniond q_used{q0, q1, q2, q3}; - Eigen::Quaterniond q_now{imu_.q0(), imu_.q1(), imu_.q2(), imu_.q3()}; - Eigen::Quaterniond chassis_climber_angle_pose = q_used.conjugate() * q_now; - - *chassis_climber_angle_imu_ = - chassis_climber_angle_pose.toRotationMatrix().eulerAngles(0, 1, 2).y(); - for (auto& motor : chassis_wheel_motors2_) motor.update_status(); for (auto& motor : chassis_steering_motors2_) @@ -580,55 +575,69 @@ class SteeringHero } void command_update() { - uint16_t batch_commands[4]{}; - - batch_commands[0] = 0; - batch_commands[1] = 0; - batch_commands[2] = chassis_wheel_motors2_[1].generate_command(); - batch_commands[3] = chassis_wheel_motors2_[0].generate_command(); - transmit_buffer_.add_can1_transmission(0x200, std::bit_cast(batch_commands)); - - batch_commands[0] = chassis_steering_motors2_[0].generate_command(); - batch_commands[1] = 0; - batch_commands[2] = chassis_steering_motors2_[1].generate_command(); - batch_commands[3] = supercap_.generate_command(); - transmit_buffer_.add_can1_transmission(0x1FE, std::bit_cast(batch_commands)); - - transmit_buffer_.add_can2_transmission( - 0x141, gimbal_bottom_yaw_motor_.generate_command()); + + auto builder = start_transmit(); + + builder.can1_transmit({ + .can_id = 0x200, + .can_data = + device::CanPacket8{ + device::CanPacket8::PaddingQuarter{}, + device::CanPacket8::PaddingQuarter{}, + chassis_wheel_motors2_[1].generate_command(), + chassis_wheel_motors2_[0].generate_command(), + } + .as_bytes(), + }); + + builder.can1_transmit({ + .can_id = 0x1FE, + .can_data = + device::CanPacket8{ + chassis_steering_motors2_[0].generate_command(), + device::CanPacket8::PaddingQuarter{}, + chassis_steering_motors2_[1].generate_command(), + supercap_.generate_command(), + } + .as_bytes(), + }); + + builder.can2_transmit({ + .can_id = 0x141, + .can_data = gimbal_bottom_yaw_motor_.generate_command().as_bytes(), + }); } private: - void can1_receive_callback( - uint32_t can_id, uint64_t can_data, bool is_extended_can_id, - bool is_remote_transmission, uint8_t can_data_length) override { - if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] + void can1_receive_callback(const librmcs::data::CanDataView& data) override { + if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] return; + auto can_id = data.can_id; if (can_id == 0x203) { - chassis_wheel_motors2_[1].store_status(can_data); + chassis_wheel_motors2_[1].store_status(data.can_data); } else if (can_id == 0x204) { - chassis_wheel_motors2_[0].store_status(can_data); + chassis_wheel_motors2_[0].store_status(data.can_data); } if (can_id == 0x205) { - chassis_steering_motors2_[0].store_status(can_data); + chassis_steering_motors2_[0].store_status(data.can_data); } else if (can_id == 0x207) { - chassis_steering_motors2_[1].store_status(can_data); + chassis_steering_motors2_[1].store_status(data.can_data); } else if (can_id == 0x300) { - supercap_.store_status(can_data); + supercap_.store_status(data.can_data); } } - void can2_receive_callback( - uint32_t can_id, uint64_t can_data, bool is_extended_can_id, - bool is_remote_transmission, uint8_t can_data_length) override { - if (is_extended_can_id || is_remote_transmission || can_data_length < 8) [[unlikely]] + void can2_receive_callback(const librmcs::data::CanDataView& data) override { + if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] return; + auto can_id = data.can_id; if (can_id == 0x141) { - gimbal_bottom_yaw_motor_.store_status(can_data); + gimbal_bottom_yaw_motor_.store_status(data.can_data); } } + rclcpp::Logger logger_; // test std::chrono::steady_clock::time_point last_time_; @@ -642,31 +651,31 @@ class SteeringHero last_time_ = now; } // test - void uart1_receive_callback(const std::byte* uart_data, uint8_t uart_data_length) override { - referee_ring_buffer_receive_.emplace_back_multi( - [&uart_data](std::byte* storage) { *storage = *uart_data++; }, uart_data_length); + void uart1_receive_callback(const librmcs::data::UartDataView& data) override { + const auto* uart_data = data.uart_data.data(); + referee_ring_buffer_receive_.emplace_back_n( + [&uart_data](std::byte* storage) noexcept { *storage = *uart_data++; }, + data.uart_data.size()); } - void dbus_receive_callback(const std::byte* uart_data, uint8_t uart_data_length) override { - dr16_.store_status(uart_data, uart_data_length); + void dbus_receive_callback(const librmcs::data::UartDataView& data) override { + dr16_.store_status(data.uart_data.data(), data.uart_data.size()); } - void accelerometer_receive_callback(int16_t x, int16_t y, int16_t z) override { - imu_.store_accelerometer_status(x, y, z); + void accelerometer_receive_callback( + const librmcs::data::AccelerometerDataView& data) override { + imu_.store_accelerometer_status(data.x, data.y, data.z); } - void gyroscope_receive_callback(int16_t x, int16_t y, int16_t z) override { - imu_.store_gyroscope_status(x, y, z); + void gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data) override { + imu_.store_gyroscope_status(data.x, data.y, data.z); } - double q0, q1, q2, q3; - device::Bmi088 imu_; OutputInterface& tf_; OutputInterface powermeter_control_enabled_; OutputInterface powermeter_charge_power_limit_; - OutputInterface chassis_climber_angle_imu_; device::Dr16 dr16_; device::Supercap supercap_; @@ -675,22 +684,18 @@ class SteeringHero device::DjiMotor chassis_wheel_motors2_[2]; device::LkMotor gimbal_bottom_yaw_motor_; - librmcs::utility::RingBuffer referee_ring_buffer_receive_{256}; + rmcs_utility::RingBuffer referee_ring_buffer_receive_{256}; OutputInterface referee_serial_; - - librmcs::client::CBoard::TransmitBuffer transmit_buffer_; - std::thread event_thread_; }; OutputInterface tf_; rclcpp::Subscription::SharedPtr gimbal_calibrate_subscription_; - std::unique_ptr top_board_; - std::unique_ptr bottom_board_one_; - std::unique_ptr bottom_board_two_; - - rmcs_utility::TickTimer temperature_logging_timer_; + std::shared_ptr command_component; + std::shared_ptr top_board_; + std::shared_ptr bottom_board_one_; + std::shared_ptr bottom_board_two_; }; } // namespace rmcs_core::hardware From 5b219ae5b4210bb4d14554a12aba86e438c6d440 Mon Sep 17 00:00:00 2001 From: Fin_Resect Date: Mon, 16 Mar 2026 02:06:23 +0800 Subject: [PATCH 03/12] Fixed board match, test AC Co-authored-by: zhzy-star <2807406212@qq.com> Co-authored-by: floatpigeon --- .../rmcs_bringup/config/steering-hero.yaml | 4 +- .../rmcs_core/src/hardware/steering-hero.cpp | 63 +++++++++++-------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml index 49957f14a..cdcc10649 100644 --- a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml +++ b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml @@ -38,8 +38,8 @@ rmcs_executor: hero_hardware: ros__parameters: board_serial_top_board: "D4-2BCA-2E47-76CD-23BC-0B78-684B" - board_serial_bottom_board_one: "D4-3674-7174-8768-879E-E44A-3931" - board_serial_bottom_board_two: "D4-7973-19A9-EA40-4A3E-306F-10F9" + board_serial_bottom_board_one: "D4-7973-19A9-EA40-4A3E-306F-10F9" + board_serial_bottom_board_two: "D4-3674-7174-8768-879E-E44A-3931" bottom_yaw_motor_zero_point: 50656 pitch_motor_zero_point: 63917 top_yaw_motor_zero_point: 59095 diff --git a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp index ae4a6a50a..4e74ddb06 100644 --- a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp +++ b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -41,12 +40,13 @@ class SteeringHero , command_component_( create_partner_component( get_component_name() + "_command", *this)) { + register_output("/tf", tf_); - // gimbal_calibrate_subscription_ = create_subscription( - // "/gimbal/calibrate", rclcpp::QoS{0}, [this](std_msgs::msg::Int32::UniquePtr&& msg) { - // gimbal_calibrate_subscription_callback(std::move(msg)); - // }); + gimbal_calibrate_subscription_ = create_subscription( + "/gimbal/calibrate", rclcpp::QoS{0}, [this](std_msgs::msg::Int32::UniquePtr&& msg) { + gimbal_calibrate_subscription_callback(std::move(msg)); + }); top_board_ = std::make_unique( *this, *command_component_, get_parameter("board_serial_top_board").as_string()); @@ -289,7 +289,9 @@ class SteeringHero gimbal_top_yaw_motor_.store_status(data.can_data); } else if (can_id == 0x142) { gimbal_pitch_motor_.store_status(data.can_data); + RCLCPP_INFO(logger_, "0x142 receive"); } + RCLCPP_INFO(logger_, "canid: %d", can_id); } void accelerometer_receive_callback( @@ -408,7 +410,7 @@ class SteeringHero }); builder.can1_transmit({ - .can_id = 0x200, + .can_id = 0x1FE, .can_data = device::CanPacket8{ device::CanPacket8::PaddingQuarter{}, @@ -441,8 +443,7 @@ class SteeringHero chassis_wheel_motors_[1].store_status(data.can_data); } else if (can_id == 0x202) { chassis_wheel_motors_[0].store_status(data.can_data); - } - if (can_id == 0x206) { + } else if (can_id == 0x206) { chassis_steering_motors_[0].store_status(data.can_data); } else if (can_id == 0x208) { chassis_steering_motors_[1].store_status(data.can_data); @@ -474,16 +475,16 @@ class SteeringHero imu_.store_gyroscope_status(data.x, data.y, data.z); } - // // test - // std::chrono::steady_clock::time_point last_time_; - // void calc_can_fps(double can_id) { - // auto now = std::chrono::steady_clock::now(); - // auto delta = - // std::chrono::duration_cast(now - last_time_).count(); + // test + std::chrono::steady_clock::time_point last_time_; + void calc_can_fps(double can_id) { + auto now = std::chrono::steady_clock::now(); + auto delta = + std::chrono::duration_cast(now - last_time_).count(); - // RCLCPP_INFO(logger_, "can id :%lf,fps:%ld", can_id, 1000000 / delta); - // last_time_ = now; - // } + RCLCPP_INFO(logger_, "can id :%lf,fps:%ld", can_id, 1000000 / delta); + last_time_ = now; + } rclcpp::Logger logger_; @@ -494,7 +495,6 @@ class SteeringHero device::DjiMotor chassis_wheel_motors_[2]; OutputInterface chassis_yaw_velocity_imu_; - OutputInterface referee_serial_; OutputInterface gimbal_yaw_velocity_imu_; OutputInterface gimbal_pitch_velocity_imu_; }; @@ -545,16 +545,26 @@ class SteeringHero static_cast( steering_hero.get_parameter("bottom_yaw_motor_zero_point").as_int()))); steering_hero.register_output("/referee/serial", referee_serial_); + referee_serial_->read = [this](std::byte* buffer, size_t size) { + return referee_ring_buffer_receive_.pop_front_n( + [&buffer](std::byte byte) noexcept { *buffer++ = byte; }, size); + }; + referee_serial_->write = [this](const std::byte* buffer, size_t size) { + start_transmit().uart1_transmit({ + .uart_data = std::span{buffer, size} + }); + return size; + }; steering_hero.register_output( "/chassis/powermeter/control_enable", powermeter_control_enabled_, false); steering_hero.register_output( "/chassis/powermeter/charge_power_limit", powermeter_charge_power_limit_, 0.); } - BottomBoard_two(const TopBoard&) = delete; - BottomBoard_two& operator=(const TopBoard&) = delete; - BottomBoard_two(TopBoard&&) = delete; - BottomBoard_two& operator=(TopBoard&&) = delete; + BottomBoard_two(const BottomBoard_two&) = delete; + BottomBoard_two& operator=(const BottomBoard_two&) = delete; + BottomBoard_two(BottomBoard_two&&) = delete; + BottomBoard_two& operator=(BottomBoard_two&&) = delete; ~BottomBoard_two() final = default; @@ -618,8 +628,7 @@ class SteeringHero chassis_wheel_motors2_[1].store_status(data.can_data); } else if (can_id == 0x204) { chassis_wheel_motors2_[0].store_status(data.can_data); - } - if (can_id == 0x205) { + } else if (can_id == 0x205) { chassis_steering_motors2_[0].store_status(data.can_data); } else if (can_id == 0x207) { chassis_steering_motors2_[1].store_status(data.can_data); @@ -639,9 +648,9 @@ class SteeringHero } rclcpp::Logger logger_; + // test std::chrono::steady_clock::time_point last_time_; - void calc_can_fps(double can_id) { auto now = std::chrono::steady_clock::now(); auto delta = @@ -651,6 +660,7 @@ class SteeringHero last_time_ = now; } // test + void uart1_receive_callback(const librmcs::data::UartDataView& data) override { const auto* uart_data = data.uart_data.data(); referee_ring_buffer_receive_.emplace_back_n( @@ -692,7 +702,6 @@ class SteeringHero rclcpp::Subscription::SharedPtr gimbal_calibrate_subscription_; - std::shared_ptr command_component; std::shared_ptr top_board_; std::shared_ptr bottom_board_one_; std::shared_ptr bottom_board_two_; @@ -702,4 +711,4 @@ class SteeringHero #include -PLUGINLIB_EXPORT_CLASS(rmcs_core::hardware::SteeringHero, rmcs_executor::Component) \ No newline at end of file +PLUGINLIB_EXPORT_CLASS(rmcs_core::hardware::SteeringHero, rmcs_executor::Component) From cef01f4111c2463f191ef80cd0583ebac6c249a4 Mon Sep 17 00:00:00 2001 From: floatpigeon Date: Mon, 16 Mar 2026 05:42:52 +0800 Subject: [PATCH 04/12] Fixed gimbal pitch control error and Update climber controller --- .../rmcs_bringup/config/steering-hero.yaml | 36 ++++-- rmcs_ws/src/rmcs_core/CMakeLists.txt | 4 +- .../chassis/chassis_climber_controller.cpp | 107 ++++++++++++------ .../controller/chassis/chassis_controller.cpp | 26 ++--- .../rmcs_core/src/hardware/steering-hero.cpp | 47 ++++---- 5 files changed, 131 insertions(+), 89 deletions(-) diff --git a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml index cdcc10649..aa125924b 100644 --- a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml +++ b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml @@ -13,6 +13,7 @@ rmcs_executor: - rmcs_core::controller::gimbal::HeroGimbalController -> gimbal_controller - rmcs_core::controller::gimbal::DualYawController -> dual_yaw_controller - rmcs_core::controller::pid::ErrorPidController -> pitch_angle_pid_controller + - rmcs_core::controller::pid::PidController -> pitch_velocity_pid_controller # - rmcs_core::controller::gimbal::PlayerViewer -> gimbal_player_viewer_controller # - rmcs_core::controller::pid::ErrorPidController -> viewer_angle_pid_controller @@ -28,7 +29,7 @@ rmcs_executor: - rmcs_core::controller::chassis::ChassisController -> chassis_controller - rmcs_core::controller::chassis::ChassisPowerController -> chassis_power_controller - rmcs_core::controller::chassis::SteeringWheelController -> steering_wheel_controller - - rmcs_core::controller::chassis::ChassisClimberController -> climber_controller + # - rmcs_core::controller::chassis::ChassisClimberController -> climber_controller # - rmcs_auto_aim::AutoAimInitializer -> auto_aim_initializer # - rmcs_auto_aim::AutoAimController -> auto_aim_controller @@ -40,9 +41,9 @@ hero_hardware: board_serial_top_board: "D4-2BCA-2E47-76CD-23BC-0B78-684B" board_serial_bottom_board_one: "D4-7973-19A9-EA40-4A3E-306F-10F9" board_serial_bottom_board_two: "D4-3674-7174-8768-879E-E44A-3931" - bottom_yaw_motor_zero_point: 50656 + bottom_yaw_motor_zero_point: 52508 pitch_motor_zero_point: 63917 - top_yaw_motor_zero_point: 59095 + top_yaw_motor_zero_point: 58744 viewer_motor_zero_point: 3030 external_imu_port: /dev/ttyUSB0 left_front_zero_point: 4438 @@ -69,16 +70,29 @@ value_broadcaster: - /chassis/left_back_steering/control_torque - /chassis/right_back_steering/control_torque - /chassis/right_front_steering/control_torque - - /gimbal/bottom_yaw/angle + - /gimbal/pitch/control_torque + - /gimbal/pitch/velocity + - /gimbal/pitch/control_velocity + - /gimbal/pitch/control_angle_error + - /chassis/climber/left_back_motor/torque + - /chassis/climber/right_back_motor/torque + - /chassis/climber/left_back_motor/velocity + - /chassis/climber/right_back_motor/velocity + - /chassis/climber/right_back_motor/control_torque + - /chassis/climber/left_back_motor/control_torque climber_controller: ros__parameters: front_climber_velocity: 50.0 - back_climber_velocity: 10.0 - front_kp: 2.0 + back_climber_velocity: 20.0 + sync_coefficient: 0.3 + climb_timeout_limit: 800 + back_climber_burst_velocity: 20.0 + back_climber_burst_duration: 300 + front_kp: 1.0 front_ki: 0.0 front_kd: 0.5 - back_kp: 2.0 + back_kp: 1.0 back_ki: 0.0 back_kd: 0.0 @@ -121,18 +135,18 @@ pitch_angle_pid_controller: ros__parameters: measurement: /gimbal/pitch/control_angle_error control: /gimbal/pitch/control_velocity - kp: 28.00 + kp: 28.0 #28.00 ki: 0.0 - kd: 0.6 + kd: 0.6 #0.6 pitch_velocity_pid_controller: ros__parameters: measurement: /gimbal/pitch/velocity_imu setpoint: /gimbal/pitch/control_velocity control: /gimbal/pitch/control_torque - kp: 45.00 + kp: 45.0 #45.00 ki: 0.00 - kd: 1.00 + kd: 1.0 #1.00 gimbal_player_viewer_controller: ros__parameters: diff --git a/rmcs_ws/src/rmcs_core/CMakeLists.txt b/rmcs_ws/src/rmcs_core/CMakeLists.txt index ec80d3ed9..dd3c85799 100644 --- a/rmcs_ws/src/rmcs_core/CMakeLists.txt +++ b/rmcs_ws/src/rmcs_core/CMakeLists.txt @@ -17,8 +17,8 @@ include(FetchContent) set(BUILD_STATIC_LIBRMCS ON CACHE BOOL "Build static librmcs SDK" FORCE) FetchContent_Declare( librmcs - URL https://github.com/Alliance-Algorithm/librmcs/releases/download/v3.1.0b0/librmcs-sdk-src-3.1.0-beta.0.zip - URL_HASH SHA256=7f3203e8db8cb7954ae0ac2ee226f93e32b367b3956b1f0bb5c7ae203c2ccbef + URL https://github.com/Alliance-Algorithm/librmcs/releases/download/v3.0.0/librmcs-sdk-src-3.0.0.zip + URL_HASH SHA256=b39f51c21baacdcbf3f0176119b8850137a108b88a67e12395d37d89e5ef53e8 DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) FetchContent_MakeAvailable(librmcs) diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp index 3a5241879..b24c2f96a 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp @@ -1,18 +1,13 @@ #include "controller/pid/matrix_pid_calculator.hpp" #include "rmcs_msgs/switch.hpp" +#include #include #include -#include #include #include #include #include -/* - 所有的电机运动方向均以令底盘升高为正方向 - 暂时使用手动控制,遥控器键位不够用了,屏蔽自瞄和小陀螺模式 -*/ - namespace rmcs_core::controller::chassis { class ChassisClimberController : public rmcs_executor::Component @@ -32,6 +27,14 @@ class ChassisClimberController track_velocity_max_ = get_parameter("front_climber_velocity").as_double(); climber_back_control_velocity_abs_ = get_parameter("back_climber_velocity").as_double(); + sync_coefficient_ = get_parameter("sync_coefficient").as_double(); + climb_timeout_limit_ = get_parameter("climb_timeout_limit").as_int(); + + burst_velocity_abs_ = get_parameter("back_climber_burst_velocity").as_double(); + burst_duration_ = get_parameter("back_climber_burst_duration").as_int(); + + back_climber_block_count_ = 0; + back_climber_timer_ = 0; register_output( "/chassis/climber/left_front_motor/control_torque", climber_front_left_control_torque_, @@ -73,45 +76,63 @@ class ChassisClimberController if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::UP) { front_climber_enable_ = !front_climber_enable_; - } else if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::DOWN) { back_climber_dir_ = -1 * back_climber_dir_; - back_climber_block_count_ = 0; + reset_back_safety_counters(); } double track_control_velocity = front_climber_enable_ ? joystick_right_->x() * track_velocity_max_ : nan_; - double back_climber_control_velocity; - - if ((std::abs(*climber_back_left_torque_) > 0.1 - && std::abs(*climber_back_right_velocity_) < 0.1) - || (std::abs(*climber_back_left_velocity_) < 0.1 - && std::abs(*climber_back_right_torque_) > 0.1)) { - back_climber_block_count_++; - } dual_motor_sync_control( track_control_velocity, *climber_front_left_velocity_, *climber_front_right_velocity_, front_velocity_pid_calculator_, *climber_front_left_control_torque_, *climber_front_right_control_torque_); - if (back_climber_block_count_ >= 500) { - back_climber_control_velocity = 0; - // *climber_back_left_control_torque_ = 0; - // *climber_back_right_control_torque_ = 0; - // last_switch_left_ = switch_left; - // last_switch_right_ = switch_right; - // return; - } else { - back_climber_control_velocity = - climber_back_control_velocity_abs_ * back_climber_dir_; + double back_climber_control_velocity = 0.0; + + if (switch_left != Switch::DOWN) { + back_climber_timer_++; + bool force_zero_torque = false; + + if ((std::abs(*climber_back_left_torque_) > 0.1 + && std::abs(*climber_back_left_velocity_) < 0.1) + || (std::abs(*climber_back_right_torque_) > 0.1 + && std::abs(*climber_back_right_velocity_) < 0.1)) { + back_climber_block_count_++; + } + + if (back_climber_dir_ == -1) { + if (back_climber_timer_ < burst_duration_) { + back_climber_control_velocity = burst_velocity_abs_ * back_climber_dir_; + } else { + force_zero_torque = true; + } + } else { + back_climber_control_velocity = + climber_back_control_velocity_abs_ * back_climber_dir_; + } + + if (!force_zero_torque) { + if (back_climber_block_count_ >= 500) { + RCLCPP_WARN_THROTTLE(logger_, *get_clock(), 1000, "Back climber STALLED."); + back_climber_control_velocity = 0.0; + } else if (back_climber_timer_ >= climb_timeout_limit_) { + RCLCPP_WARN_THROTTLE(logger_, *get_clock(), 1000, "Back climber TIMEOUT."); + back_climber_control_velocity = 0.0; + } + + dual_motor_sync_control( + back_climber_control_velocity, *climber_back_left_velocity_, + *climber_back_right_velocity_, back_velocity_pid_calculator_, + *climber_back_left_control_torque_, *climber_back_right_control_torque_); + } else { + *climber_back_left_control_torque_ = 0.0; + *climber_back_right_control_torque_ = 0.0; + } } - - // dual_motor_sync_control( - // back_climber_control_velocity, *climber_back_left_velocity_, - // *climber_back_right_velocity_, back_velocity_pid_calculator_, - // *climber_back_left_control_torque_, *climber_back_right_control_torque_); } + last_switch_left_ = switch_left; last_switch_right_ = switch_right; } @@ -123,6 +144,12 @@ class ChassisClimberController *climber_back_left_control_torque_ = 0; *climber_back_right_control_torque_ = 0; front_climber_enable_ = false; + reset_back_safety_counters(); + } + + void reset_back_safety_counters() { + back_climber_block_count_ = 0; + back_climber_timer_ = 0; } void dual_motor_sync_control( @@ -130,13 +157,17 @@ class ChassisClimberController pid::MatrixPidCalculator<2>& pid_calculator, double& left_torque_out, double& right_torque_out) const { - Eigen::Vector2d setpoint_error{setpoint - left_velocity, setpoint - right_velocity}; + if (std::isnan(setpoint)) { + left_torque_out = nan_; + right_torque_out = nan_; + return; + } + Eigen::Vector2d setpoint_error{setpoint - left_velocity, setpoint - right_velocity}; Eigen::Vector2d relative_velocity{ left_velocity - right_velocity, right_velocity - left_velocity}; Eigen::Vector2d control_error = setpoint_error - sync_coefficient_ * relative_velocity; - auto control_torques = pid_calculator.update(control_error); left_torque_out = control_torques[0]; @@ -144,13 +175,19 @@ class ChassisClimberController } int back_climber_block_count_; + int back_climber_timer_; rclcpp::Logger logger_; static constexpr double nan_ = std::numeric_limits::quiet_NaN(); + double sync_coefficient_; + int64_t climb_timeout_limit_; + + double burst_velocity_abs_; + int64_t burst_duration_; bool front_climber_enable_ = false; - double back_climber_dir_ = -1; + double back_climber_dir_ = 1; double track_velocity_max_; double climber_back_control_velocity_abs_; @@ -182,4 +219,4 @@ class ChassisClimberController #include PLUGINLIB_EXPORT_CLASS( - rmcs_core::controller::chassis::ChassisClimberController, rmcs_executor::Component) \ No newline at end of file + rmcs_core::controller::chassis::ChassisClimberController, rmcs_executor::Component) diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_controller.cpp index da70a96f8..a1fbb6fee 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_controller.cpp @@ -19,7 +19,7 @@ class ChassisController : Node( get_component_name(), rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)) - , following_velocity_controller_(7.0, 0.0, 0.0) { + , following_velocity_controller_(10.0, 0.0, 1.2) { following_velocity_controller_.output_max = angular_velocity_max; following_velocity_controller_.output_min = -angular_velocity_max; @@ -58,8 +58,8 @@ class ChassisController using namespace rmcs_msgs; auto switch_right = *switch_right_; - auto switch_left = *switch_left_; - auto keyboard = *keyboard_; + auto switch_left = *switch_left_; + auto keyboard = *keyboard_; do { if ((switch_left == Switch::UNKNOWN || switch_right == Switch::UNKNOWN) @@ -74,14 +74,14 @@ class ChassisController if (mode == rmcs_msgs::ChassisMode::SPIN) { mode = rmcs_msgs::ChassisMode::STEP_DOWN; } else { - mode = rmcs_msgs::ChassisMode::SPIN; + mode = rmcs_msgs::ChassisMode::SPIN; spinning_forward_ = !spinning_forward_; } } else if (!last_keyboard_.c && keyboard.c) { if (mode == rmcs_msgs::ChassisMode::SPIN) { mode = rmcs_msgs::ChassisMode::AUTO; } else { - mode = rmcs_msgs::ChassisMode::SPIN; + mode = rmcs_msgs::ChassisMode::SPIN; spinning_forward_ = !spinning_forward_; } } else if (!last_keyboard_.x && keyboard.x) { @@ -100,8 +100,8 @@ class ChassisController } while (false); last_switch_right_ = switch_right; - last_switch_left_ = switch_left; - last_keyboard_ = keyboard; + last_switch_left_ = switch_left; + last_keyboard_ = keyboard; } void reset_all_controls() { @@ -112,7 +112,7 @@ class ChassisController void update_velocity_control() { auto translational_velocity = update_translational_velocity_control(); - auto angular_velocity = update_angular_velocity_control(); + auto angular_velocity = update_angular_velocity_control(); chassis_control_velocity_->vector << translational_velocity, angular_velocity; } @@ -133,7 +133,7 @@ class ChassisController } double update_angular_velocity_control() { - double angular_velocity = 0.0; + double angular_velocity = 0.0; double chassis_control_angle = nan; switch (*mode_) { @@ -171,7 +171,7 @@ class ChassisController angular_velocity = following_velocity_controller_.update(err); } break; } - *chassis_angle_ = 2 * std::numbers::pi - *gimbal_yaw_angle_; + *chassis_angle_ = 2 * std::numbers::pi - *gimbal_yaw_angle_; *chassis_control_angle_ = chassis_control_angle; return angular_velocity; @@ -202,7 +202,7 @@ class ChassisController // Maximum control velocities static constexpr double translational_velocity_max = 10.0; - static constexpr double angular_velocity_max = 16.0; + static constexpr double angular_velocity_max = 16.0; InputInterface joystick_right_; InputInterface joystick_left_; @@ -214,8 +214,8 @@ class ChassisController InputInterface rotary_knob_; rmcs_msgs::Switch last_switch_right_ = rmcs_msgs::Switch::UNKNOWN; - rmcs_msgs::Switch last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; - rmcs_msgs::Keyboard last_keyboard_ = rmcs_msgs::Keyboard::zero(); + rmcs_msgs::Switch last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; + rmcs_msgs::Keyboard last_keyboard_ = rmcs_msgs::Keyboard::zero(); InputInterface gimbal_yaw_angle_, gimbal_yaw_angle_error_; OutputInterface chassis_angle_, chassis_control_angle_; diff --git a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp index 4e74ddb06..b7d6ada53 100644 --- a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp +++ b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp @@ -180,7 +180,7 @@ class SteeringHero // std::numbers::pi / 2, Eigen::Vector3d::UnitZ()}; // Eigen::Vector3d mapping = pitch_link_to_bmi088_link * Eigen::Vector3d{1, 2, 3}; // std::cout << mapping << std::endl; - return std::make_tuple(x, y, z); + return std::make_tuple(-x, -y, z); }); } @@ -251,12 +251,12 @@ class SteeringHero builder.can2_transmit({ .can_id = 0x141, - .can_data = gimbal_top_yaw_motor_.generate_torque_command().as_bytes(), + .can_data = gimbal_top_yaw_motor_.generate_command().as_bytes(), }); builder.can2_transmit({ .can_id = 0x142, - .can_data = gimbal_pitch_motor_.generate_torque_command().as_bytes(), + .can_data = gimbal_pitch_motor_.generate_command().as_bytes(), }); } @@ -289,9 +289,7 @@ class SteeringHero gimbal_top_yaw_motor_.store_status(data.can_data); } else if (can_id == 0x142) { gimbal_pitch_motor_.store_status(data.can_data); - RCLCPP_INFO(logger_, "0x142 receive"); } - RCLCPP_INFO(logger_, "canid: %d", can_id); } void accelerometer_receive_callback( @@ -368,6 +366,22 @@ class SteeringHero .set_reversed() .set_reduction_ratio(2232. / 169.)); + chassis_front_climber_motor_[0].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + .set_reversed() + .set_reduction_ratio(19.)); + chassis_front_climber_motor_[1].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio(19.)); + chassis_back_climber_motor_[0].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + .enable_multi_turn_angle() + .set_reduction_ratio(19.)); + chassis_back_climber_motor_[1].configure( + device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + .set_reversed() + .enable_multi_turn_angle() + .set_reduction_ratio(19.)); + steering_hero.register_output( "/chassis/yaw/velocity_imu", chassis_yaw_velocity_imu_, 0); } @@ -475,17 +489,6 @@ class SteeringHero imu_.store_gyroscope_status(data.x, data.y, data.z); } - // test - std::chrono::steady_clock::time_point last_time_; - void calc_can_fps(double can_id) { - auto now = std::chrono::steady_clock::now(); - auto delta = - std::chrono::duration_cast(now - last_time_).count(); - - RCLCPP_INFO(logger_, "can id :%lf,fps:%ld", can_id, 1000000 / delta); - last_time_ = now; - } - rclcpp::Logger logger_; device::Bmi088 imu_; @@ -649,18 +652,6 @@ class SteeringHero rclcpp::Logger logger_; - // test - std::chrono::steady_clock::time_point last_time_; - void calc_can_fps(double can_id) { - auto now = std::chrono::steady_clock::now(); - auto delta = - std::chrono::duration_cast(now - last_time_).count(); - - RCLCPP_INFO(logger_, "can id :%lf,fps:%ld", can_id, 1000000 / delta); - last_time_ = now; - } - // test - void uart1_receive_callback(const librmcs::data::UartDataView& data) override { const auto* uart_data = data.uart_data.data(); referee_ring_buffer_receive_.emplace_back_n( From 901055a7c06a56a6fe24623377635a963039bc6b Mon Sep 17 00:00:00 2001 From: floatpigeon Date: Mon, 16 Mar 2026 06:50:23 +0800 Subject: [PATCH 05/12] Fixed friction direction error and Update putter controller without photogate --- .../rmcs_bringup/config/steering-hero.yaml | 3 +- .../chassis/chassis_climber_controller.cpp | 2 +- .../shooting/friction_wheel_controller.cpp | 4 +- .../controller/shooting/putter_controller.cpp | 103 ++++++++---------- .../controller/shooting/shooting_recorder.cpp | 15 ++- .../rmcs_core/src/hardware/steering-hero.cpp | 8 +- 6 files changed, 70 insertions(+), 65 deletions(-) diff --git a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml index aa125924b..5495b7910 100644 --- a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml +++ b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml @@ -25,6 +25,7 @@ rmcs_executor: - rmcs_core::controller::pid::PidController -> first_right_friction_velocity_pid_controller - rmcs_core::controller::pid::PidController -> second_left_friction_velocity_pid_controller - rmcs_core::controller::pid::PidController -> second_right_friction_velocity_pid_controller + # - rmcs_core::controller::shooting::ShootingRecorder -> shooting_recorder - rmcs_core::controller::chassis::ChassisController -> chassis_controller - rmcs_core::controller::chassis::ChassisPowerController -> chassis_power_controller @@ -171,7 +172,7 @@ friction_wheel_controller: friction_velocities: - 450.00 - 450.00 - - -535.00 + - 535.00 - 535.00 friction_soft_start_stop_time: 1.0 diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp index b24c2f96a..5d7384a33 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp @@ -155,7 +155,7 @@ class ChassisClimberController void dual_motor_sync_control( double setpoint, double left_velocity, double right_velocity, pid::MatrixPidCalculator<2>& pid_calculator, double& left_torque_out, - double& right_torque_out) const { + double& right_torque_out) { if (std::isnan(setpoint)) { left_torque_out = nan_; diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp index a83dc5f4d..e38437a44 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp @@ -150,8 +150,8 @@ class FrictionWheelController bool detect_friction_faulty() { for (size_t i = 0; i < friction_count_; i++) { - if (*friction_velocities_[i] < *friction_control_velocities_[i] * 0.5) - return true; + if (abs(*friction_velocities_[i]) < abs(*friction_control_velocities_[i] * 0.5)) + return false; } return false; } diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp index 4efdbf520..fbf857611 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp @@ -60,17 +60,17 @@ class PutterController bullet_feeder_angle_pid_.ki = 0.0; bullet_feeder_angle_pid_.kd = 2.0; - puttter_return_velocity_pid_.kp = 0.0015; - puttter_return_velocity_pid_.ki = 0.00005; - puttter_return_velocity_pid_.kd = 0.; - puttter_return_velocity_pid_.integral_max = 0.; - puttter_return_velocity_pid_.integral_min = -0.03; + putter_return_velocity_pid_.kp = 0.0015; + putter_return_velocity_pid_.ki = 0.00005; + putter_return_velocity_pid_.kd = 0.; + putter_return_velocity_pid_.integral_max = 0.; + putter_return_velocity_pid_.integral_min = -0.03; putter_velocity_pid_.kp = 0.004; putter_velocity_pid_.ki = 0.0001; putter_velocity_pid_.kd = 0.001; - puttter_return_velocity_pid_.integral_max = 0.03; - puttter_return_velocity_pid_.integral_min = 0.; + putter_velocity_pid_.integral_max = 0.03; + putter_velocity_pid_.integral_min = 0.; putter_return_angle_pid.kp = 0.0001; // putter_return_angle_pid.ki = 0.000001; @@ -103,20 +103,17 @@ class PutterController if (bullet_feeder_cool_down_ > 0) { bullet_feeder_cool_down_--; - // 冷却期前期:反转供弹轮以解除卡弹 - if (bullet_feeder_cool_down_ > 500) - *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update( - -bullet_feeder_angle_per_bullet_ - *bullet_feeder_velocity_); - else { - // 冷却期后期:停止控制 - bullet_feeder_velocity_pid_.reset(); - *bullet_feeder_control_torque_ = 0.0; - } - - bullet_feeder_angle_pid_.reset(); + // 使用角度环反转到“后退一格”的位置以解除卡弹 + double velocity_err = + bullet_feeder_angle_pid_.update( + bullet_feeder_control_angle_ - *bullet_feeder_angle_) + - *bullet_feeder_velocity_; + *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update(velocity_err); - if (!bullet_feeder_cool_down_) - RCLCPP_INFO(get_logger(), "Jamming Solved!"); + if (!bullet_feeder_cool_down_) { + RCLCPP_INFO(get_logger(), "Jamming Solved, Retrying..."); + set_preloading(); + } } else { // 正常运行模式:摩擦轮就绪时才允许发射 if (*friction_ready_) { @@ -135,30 +132,22 @@ class PutterController } if (shoot_stage_ == ShootStage::PRELOADING) { - if (last_preload_flag_) { - // 角度环控制模式:光电门触发后精确定位 - const auto angle_err = - bullet_feeder_control_angle_ - *bullet_feeder_angle_; - if (angle_err < 0.1) { - set_preloaded(); - } - double velocity_err = - bullet_feeder_angle_pid_.update( - bullet_feeder_control_angle_ - *bullet_feeder_angle_) - - *bullet_feeder_velocity_; - *bullet_feeder_control_torque_ = - bullet_feeder_velocity_pid_.update(velocity_err); - } else { - // 速度环控制模式:等待光电门触发 - if (*photoelectric_sensor_status_) { - RCLCPP_INFO(get_logger(), "Photoelectric Sensor Triggered"); - last_preload_flag_ = true; - bullet_feeder_control_angle_ = - *bullet_feeder_angle_ + bullet_feeder_angle_per_bullet_ * 1.; - } else - *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update( - low_latency_velocity_ - *bullet_feeder_velocity_); + // 盲拨模式:始终执行角度环定位 + if (std::isnan(bullet_feeder_control_angle_)) { + bullet_feeder_control_angle_ = + *bullet_feeder_angle_ + bullet_feeder_angle_per_bullet_; + last_preload_flag_ = true; + } + + const auto angle_err = bullet_feeder_control_angle_ - *bullet_feeder_angle_; + if (angle_err < 0.1) { + set_preloaded(); } + double velocity_err = + bullet_feeder_angle_pid_.update(angle_err) - *bullet_feeder_velocity_; + *bullet_feeder_control_torque_ = + bullet_feeder_velocity_pid_.update(velocity_err); + update_jam_detection(); } else { // 其他状态:角度环保持角度不变防止弹链退弹 @@ -186,14 +175,14 @@ class PutterController *putter_control_torque_ = 0.; set_preloading(); shooted = false; + } else { + *putter_control_torque_ = + putter_return_velocity_pid_.update(-80. - *putter_velocity_); } - - *putter_control_torque_ = - puttter_return_velocity_pid_.update(-80. - *putter_velocity_); } else { // 子弹未发出:继续推进 *putter_control_torque_ = - puttter_return_velocity_pid_.update(60. - *putter_velocity_); + putter_return_velocity_pid_.update(60. - *putter_velocity_); } } } else { @@ -207,7 +196,7 @@ class PutterController } } else { // 推杆未初始化:执行复位操作 - *putter_control_torque_ = puttter_return_velocity_pid_.update(-80. - *putter_velocity_); + *putter_control_torque_ = putter_return_velocity_pid_.update(-80. - *putter_velocity_); update_putter_jam_detection(); } @@ -237,7 +226,7 @@ class PutterController putter_initialized = false; putter_startpoint = nan_; - puttter_return_velocity_pid_.reset(); + putter_return_velocity_pid_.reset(); putter_velocity_pid_.reset(); putter_return_angle_pid.reset(); *putter_control_torque_ = nan_; @@ -252,6 +241,11 @@ class PutterController void set_preloading() { RCLCPP_INFO(get_logger(), "PRELOADING"); shoot_stage_ = ShootStage::PRELOADING; + // 盲拨方案:直接增加目标角度 + if (!std::isnan(bullet_feeder_control_angle_)) { + bullet_feeder_control_angle_ += bullet_feeder_angle_per_bullet_; + } + last_preload_flag_ = true; } void set_preloaded() { @@ -278,10 +272,8 @@ class PutterController bullet_feeder_faulty_count_++; else { bullet_feeder_faulty_count_ = 0; - if (last_preload_flag_) - set_preloaded(); - else - enter_jam_protection(); + RCLCPP_WARN(get_logger(), "Jam Detected! Reversing 60 degrees..."); + enter_jam_protection(); } } @@ -310,7 +302,8 @@ class PutterController } void enter_jam_protection() { - bullet_feeder_control_angle_ = nan_; + // 设置目标角度为当前角度后退 60 度(一格) + bullet_feeder_control_angle_ = *bullet_feeder_angle_ - bullet_feeder_angle_per_bullet_; bullet_feeder_cool_down_ = 1000; bullet_feeder_angle_pid_.reset(); bullet_feeder_velocity_pid_.reset(); @@ -357,7 +350,7 @@ class PutterController bool putter_initialized = false; int putter_faulty_count_ = 0; double putter_startpoint = nan_; - pid::PidCalculator puttter_return_velocity_pid_; + pid::PidCalculator putter_return_velocity_pid_; InputInterface putter_velocity_; pid::PidCalculator putter_velocity_pid_; diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp index c0ef5af23..4394bb8b0 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp @@ -96,7 +96,12 @@ class ShootingRecorder } v = *shoot_timestamp_; + static constexpr size_t max_velocities_size = 1000; + velocities.push_back(*initial_speed_); + if (velocities.size() > max_velocities_size) { + velocities.erase(velocities.begin()); + } analysis3(); @@ -160,9 +165,6 @@ class ShootingRecorder double v; double aim_velocity; - double start_time_ = 0.0; - double vv = 0.0; - private: static std::string timestamp_to_string(double timestamp) { auto time = static_cast(timestamp); @@ -189,7 +191,12 @@ class ShootingRecorder sort(velocities.begin(), velocities.end()); range_ = velocities.back() - velocities.front(); - range2_ = velocities[int(velocities.size() - 2)] - velocities[1]; + + if (velocities.size() >= 3) { + range2_ = velocities[int(velocities.size() - 2)] - velocities[1]; + } else { + range2_ = range_; + } velocity_max = velocities.back(); velocity_min = velocities.front(); diff --git a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp index b7d6ada53..64947ddce 100644 --- a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp +++ b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp @@ -147,11 +147,15 @@ class SteeringHero gimbal_friction_wheels_[0].configure( device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio(1.)); gimbal_friction_wheels_[1].configure( - device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio(1.)); + device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + .set_reversed() + .set_reduction_ratio(1.)); gimbal_friction_wheels_[2].configure( device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio(1.)); gimbal_friction_wheels_[3].configure( - device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio(1.)); + device::DjiMotor::Config{device::DjiMotor::Type::kM3508} + .set_reversed() + .set_reduction_ratio(1.)); gimbal_bullet_feeder_.configure( device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10} .set_reversed() From 6bbeed296952cfb6d2fd9a0666e6513a09faa50f Mon Sep 17 00:00:00 2001 From: floatpigeon Date: Tue, 17 Mar 2026 09:47:33 +0800 Subject: [PATCH 06/12] Auto Climber Demo! --- .../rmcs_bringup/config/steering-hero.yaml | 2 +- .../chassis/chassis_climber_controller.cpp | 187 +++++++++++++++++- .../controller/chassis/chassis_controller.cpp | 12 ++ .../gimbal/hero_gimbal_controller.cpp | 5 +- .../rmcs_core/src/hardware/steering-hero.cpp | 4 + 5 files changed, 200 insertions(+), 10 deletions(-) diff --git a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml index 5495b7910..d971bdf47 100644 --- a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml +++ b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml @@ -30,7 +30,7 @@ rmcs_executor: - rmcs_core::controller::chassis::ChassisController -> chassis_controller - rmcs_core::controller::chassis::ChassisPowerController -> chassis_power_controller - rmcs_core::controller::chassis::SteeringWheelController -> steering_wheel_controller - # - rmcs_core::controller::chassis::ChassisClimberController -> climber_controller + - rmcs_core::controller::chassis::ChassisClimberController -> climber_controller # - rmcs_auto_aim::AutoAimInitializer -> auto_aim_initializer # - rmcs_auto_aim::AutoAimController -> auto_aim_controller diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp index 5d7384a33..e8faeda0f 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp @@ -1,14 +1,20 @@ #include "controller/pid/matrix_pid_calculator.hpp" +#include "rmcs_msgs/keyboard.hpp" #include "rmcs_msgs/switch.hpp" +#include #include #include #include +#include #include #include #include #include namespace rmcs_core::controller::chassis { + +enum class AutoClimbState { IDLE, FRONT_UP, DASH, RETRACT }; + class ChassisClimberController : public rmcs_executor::Component , public rclcpp::Node { @@ -48,6 +54,7 @@ class ChassisClimberController register_output( "/chassis/climber/right_back_motor/control_torque", climber_back_right_control_torque_, nan_); + register_output("/chassis/climbing_forward_velocity", climbing_forward_velocity_, nan_); register_input("/chassis/climber/left_front_motor/velocity", climber_front_left_velocity_); register_input( @@ -61,6 +68,9 @@ class ChassisClimberController register_input("/remote/switch/right", switch_right_); register_input("/remote/switch/left", switch_left_); register_input("/remote/joystick/right", joystick_right_); + register_input("/remote/keyboard", keyboard_); + register_input("/remote/rotary_knob_switch", rotary_knob_switch_); + register_input("/chassis/pitch_imu", chassis_pitch_imu_); } void update() override { @@ -68,19 +78,166 @@ class ChassisClimberController auto switch_right = *switch_right_; auto switch_left = *switch_left_; + auto keyboard = *keyboard_; + auto rotary_knob_switch = *rotary_knob_switch_; + + bool rotary_knob_to_up = + (last_rotary_knob_switch_ != Switch::UP && rotary_knob_switch == Switch::UP); + bool rotary_knob_from_up = + (last_rotary_knob_switch_ == Switch::UP && rotary_knob_switch != Switch::UP); + RCLCPP_INFO(logger_, "%lf", *chassis_pitch_imu_); + // RCLCPP_INFO(logger_, "%hhu", static_cast(rotary_knob_switch)); + + if ((!last_keyboard_.g && keyboard.g) || rotary_knob_to_up) { + if (auto_climb_state_ == AutoClimbState::IDLE) { + auto_climb_state_ = AutoClimbState::FRONT_UP; + auto_climb_timer_ = 0; + climb_count_ = 0; + back_climber_block_count_ = 0; + auto_dash_started_ = false; + RCLCPP_INFO( + logger_, "Auto climb started by %s.", + rotary_knob_switch == Switch::UP ? "Rotary Knob" : "Keyboard G"); + } else { + auto_climb_state_ = AutoClimbState::IDLE; + reset_all_controls(); + RCLCPP_INFO(logger_, "Auto climb aborted (toggled again)."); + } + } else if (rotary_knob_from_up && auto_climb_state_ != AutoClimbState::IDLE) { + auto_climb_state_ = AutoClimbState::IDLE; + reset_all_controls(); + RCLCPP_INFO(logger_, "Auto climb aborted (rotary knob left UP)."); + } if ((switch_left == Switch::UNKNOWN || switch_right == Switch::UNKNOWN) || (switch_left == Switch::DOWN && switch_right == Switch::DOWN)) { reset_all_controls(); - } else if (switch_left != Switch::DOWN) { + auto_climb_state_ = AutoClimbState::IDLE; + } else if (auto_climb_state_ != AutoClimbState::IDLE) { + + auto_climb_timer_++; + double override_chassis_vx = nan_; + double track_control_velocity = 0.0; + double back_climber_control_velocity = 0.0; + + if (auto_climb_state_ == AutoClimbState::FRONT_UP) { + track_control_velocity = track_velocity_max_; // front climber active + override_chassis_vx = 1.0; // limit speed to leave distance + + double pitch = *chassis_pitch_imu_; + double target_pitch = + (climb_count_ == 0) ? 0.48 : 0.35; // ~25 deg for first, ~20 deg for second + + RCLCPP_INFO_THROTTLE( + logger_, *get_clock(), 500, "FRONT_UP (step %d): pitch=%.3f, target>%.3f", + climb_count_ + 1, pitch, target_pitch); - if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::UP) { - front_climber_enable_ = !front_climber_enable_; - } else if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::DOWN) { - back_climber_dir_ = -1 * back_climber_dir_; - reset_back_safety_counters(); + if (pitch > target_pitch) { + auto_climb_state_ = AutoClimbState::DASH; + auto_climb_timer_ = 0; + back_climber_block_count_ = 0; + auto_dash_started_ = false; + RCLCPP_INFO(logger_, "Auto climb dashing phase (step %d).", climb_count_ + 1); + } + } else if (auto_climb_state_ == AutoClimbState::DASH) { + // lower rear pole to push up + back_climber_control_velocity = climber_back_control_velocity_abs_; + + if (!auto_dash_started_) { + if ((std::abs(*climber_back_left_torque_) > 0.1 + && std::abs(*climber_back_left_velocity_) < 0.1) + || (std::abs(*climber_back_right_torque_) > 0.1 + && std::abs(*climber_back_right_velocity_) < 0.1)) { + back_climber_block_count_++; + } else { + back_climber_block_count_ = 0; + } + + if (back_climber_block_count_ > 50) { // 50 ticks = 0.1s confirm + auto_dash_started_ = true; + } + } + + if (auto_dash_started_) { + track_control_velocity = track_velocity_max_; // Dash: front climber running + override_chassis_vx = 3.0; // dash speed + } else { + track_control_velocity = 0.0; // Stop front climber, wait for rear pole + override_chassis_vx = 0.0; // Stop chassis completely while pushing up + } + + double pitch = *chassis_pitch_imu_; + RCLCPP_INFO_THROTTLE( + logger_, *get_clock(), 500, "DASH: pitch=%.3f, timer=%d, dash_started=%d", + pitch, auto_climb_timer_, auto_dash_started_); + + bool is_leveled = + auto_dash_started_ && (std::abs(pitch) < 0.1) && (auto_climb_timer_ > 500); + bool timeout = (auto_climb_timer_ > 3000); + + if (is_leveled || timeout) { + auto_climb_state_ = AutoClimbState::RETRACT; + auto_climb_timer_ = 0; + climb_count_++; + if (timeout) { + RCLCPP_WARN(logger_, "Auto climb DASH timeout! Forcing retract."); + } else { + RCLCPP_INFO(logger_, "Auto climb DASH completed (Leveled)."); + } + } + } else if (auto_climb_state_ == AutoClimbState::RETRACT) { + if (rotary_knob_switch == Switch::UP) { + track_control_velocity = track_velocity_max_; // Keep front climbers running + override_chassis_vx = 3.0; // Keep moving forward (dash) for next step + } else { + track_control_velocity = 0.0; + override_chassis_vx = 3.0; // Keep dash moving forward even if finishing + } + back_climber_control_velocity = + -climber_back_control_velocity_abs_ * 2.0; // retract faster + + RCLCPP_INFO_THROTTLE( + logger_, *get_clock(), 500, "RETRACT: timer=%d", auto_climb_timer_); + + if (auto_climb_timer_ > 1000) { // Retract for 1s + if (rotary_knob_switch == Switch::UP && climb_count_ < 2) { + auto_climb_state_ = AutoClimbState::FRONT_UP; + auto_climb_timer_ = 0; + back_climber_block_count_ = 0; + auto_dash_started_ = false; + RCLCPP_INFO( + logger_, "Auto climb RETRACT done, looping to FRONT_UP for step %d.", + climb_count_ + 1); + } else { + auto_climb_state_ = AutoClimbState::IDLE; + RCLCPP_INFO( + logger_, "Auto climb completed (finished %d steps).", climb_count_); + } + } } + *climbing_forward_velocity_ = override_chassis_vx; + + dual_motor_sync_control( + track_control_velocity, *climber_front_left_velocity_, + *climber_front_right_velocity_, front_velocity_pid_calculator_, + *climber_front_left_control_torque_, *climber_front_right_control_torque_); + + dual_motor_sync_control( + back_climber_control_velocity, *climber_back_left_velocity_, + *climber_back_right_velocity_, back_velocity_pid_calculator_, + *climber_back_left_control_torque_, *climber_back_right_control_torque_); + + } else if (switch_left != Switch::DOWN) { + *climbing_forward_velocity_ = nan_; + + // if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::UP) { + // front_climber_enable_ = !front_climber_enable_; + // } else if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::DOWN) { + // back_climber_dir_ = -1 * back_climber_dir_; + // reset_back_safety_counters(); + // } + double track_control_velocity = front_climber_enable_ ? joystick_right_->x() * track_velocity_max_ : nan_; @@ -135,6 +292,8 @@ class ChassisClimberController last_switch_left_ = switch_left; last_switch_right_ = switch_right; + last_keyboard_ = keyboard; + last_rotary_knob_switch_ = rotary_knob_switch; } private: @@ -144,6 +303,7 @@ class ChassisClimberController *climber_back_left_control_torque_ = 0; *climber_back_right_control_torque_ = 0; front_climber_enable_ = false; + *climbing_forward_velocity_ = nan_; reset_back_safety_counters(); } @@ -187,15 +347,22 @@ class ChassisClimberController int64_t burst_duration_; bool front_climber_enable_ = false; - double back_climber_dir_ = 1; + double back_climber_dir_ = -1; + + int climb_count_ = 0; double track_velocity_max_; double climber_back_control_velocity_abs_; + AutoClimbState auto_climb_state_ = AutoClimbState::IDLE; + int auto_climb_timer_ = 0; + bool auto_dash_started_ = false; + OutputInterface climber_front_left_control_torque_; OutputInterface climber_front_right_control_torque_; OutputInterface climber_back_left_control_torque_; OutputInterface climber_back_right_control_torque_; + OutputInterface climbing_forward_velocity_; InputInterface climber_front_left_velocity_; InputInterface climber_front_right_velocity_; @@ -208,9 +375,15 @@ class ChassisClimberController InputInterface switch_right_; InputInterface switch_left_; InputInterface joystick_right_; + InputInterface keyboard_; + InputInterface rotary_knob_switch_; + + InputInterface chassis_pitch_imu_; rmcs_msgs::Switch last_switch_right_ = rmcs_msgs::Switch::UNKNOWN; rmcs_msgs::Switch last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; + rmcs_msgs::Switch last_rotary_knob_switch_ = rmcs_msgs::Switch::UNKNOWN; + rmcs_msgs::Keyboard last_keyboard_ = rmcs_msgs::Keyboard::zero(); pid::MatrixPidCalculator<2> front_velocity_pid_calculator_, back_velocity_pid_calculator_; }; diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_controller.cpp index a1fbb6fee..af18ba43e 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_controller.cpp @@ -34,6 +34,7 @@ class ChassisController register_input("/gimbal/yaw/angle", gimbal_yaw_angle_, false); register_input("/gimbal/yaw/control_angle_error", gimbal_yaw_angle_error_, false); + register_input("/chassis/climbing_forward_velocity", climbing_forward_velocity_, nan); register_output("/chassis/angle", chassis_angle_, nan); register_output("/chassis/control_angle", chassis_control_angle_, nan); @@ -118,6 +119,10 @@ class ChassisController } Eigen::Vector2d update_translational_velocity_control() { + if (!std::isnan(*climbing_forward_velocity_)) { + return Eigen::Vector2d{*climbing_forward_velocity_, 0.0}; + } + auto keyboard = *keyboard_; Eigen::Vector2d keyboard_move{keyboard.w - keyboard.s, keyboard.a - keyboard.d}; @@ -133,6 +138,12 @@ class ChassisController } double update_angular_velocity_control() { + if (!std::isnan(*climbing_forward_velocity_)) { + *chassis_angle_ = 2 * std::numbers::pi - *gimbal_yaw_angle_; + *chassis_control_angle_ = nan; + return 0.0; + } + double angular_velocity = 0.0; double chassis_control_angle = nan; @@ -218,6 +229,7 @@ class ChassisController rmcs_msgs::Keyboard last_keyboard_ = rmcs_msgs::Keyboard::zero(); InputInterface gimbal_yaw_angle_, gimbal_yaw_angle_error_; + InputInterface climbing_forward_velocity_; OutputInterface chassis_angle_, chassis_control_angle_; OutputInterface mode_; diff --git a/rmcs_ws/src/rmcs_core/src/controller/gimbal/hero_gimbal_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/gimbal/hero_gimbal_controller.cpp index f20bd98fe..1c6dde8ba 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/gimbal/hero_gimbal_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/gimbal/hero_gimbal_controller.cpp @@ -65,8 +65,9 @@ class HeroGimbalController else gimbal_mode_keyboard_ = GimbalMode::IMU; } - *gimbal_mode_ = - *switch_right_ == Switch::UP ? GimbalMode::ENCODER : gimbal_mode_keyboard_; + // *gimbal_mode_ = + // *switch_right_ == Switch::UP ? GimbalMode::ENCODER : gimbal_mode_keyboard_; + *gimbal_mode_ = gimbal_mode_keyboard_; if (*gimbal_mode_ == GimbalMode::IMU) { auto angle_error = update_imu_control(); diff --git a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp index 64947ddce..847698c25 100644 --- a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp +++ b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp @@ -388,6 +388,8 @@ class SteeringHero steering_hero.register_output( "/chassis/yaw/velocity_imu", chassis_yaw_velocity_imu_, 0); + steering_hero.register_output( + "/chassis/pitch_imu", chassis_pitch_imu_, 0.0); } BottomBoard_one(const BottomBoard_one&) = delete; BottomBoard_one& operator=(const BottomBoard_one&) = delete; @@ -400,6 +402,7 @@ class SteeringHero imu_.update_status(); *chassis_yaw_velocity_imu_ = imu_.gz(); + *chassis_pitch_imu_ = -std::asin(2.0 * (imu_.q0() * imu_.q2() - imu_.q3() * imu_.q1())); chassis_front_climber_motor_[0].update_status(); chassis_front_climber_motor_[1].update_status(); @@ -502,6 +505,7 @@ class SteeringHero device::DjiMotor chassis_wheel_motors_[2]; OutputInterface chassis_yaw_velocity_imu_; + OutputInterface chassis_pitch_imu_; OutputInterface gimbal_yaw_velocity_imu_; OutputInterface gimbal_pitch_velocity_imu_; }; From 040b4c4fba47db1677f29201408d93112d340e4d Mon Sep 17 00:00:00 2001 From: floatpigeon Date: Wed, 18 Mar 2026 03:48:29 +0800 Subject: [PATCH 07/12] AUTO CLIMB STABLE --- .../rmcs_bringup/config/steering-hero.yaml | 10 +- .../chassis/chassis_climber_controller.cpp | 480 +++++++++++------- 2 files changed, 290 insertions(+), 200 deletions(-) diff --git a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml index d971bdf47..61946dff2 100644 --- a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml +++ b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml @@ -84,11 +84,15 @@ value_broadcaster: climber_controller: ros__parameters: - front_climber_velocity: 50.0 + front_climber_velocity: 60.0 back_climber_velocity: 20.0 + auto_climb_dash_velocity: 3.0 + auto_climb_support_retract_velocity: 30.0 sync_coefficient: 0.3 - climb_timeout_limit: 800 - back_climber_burst_velocity: 20.0 + climb_timeout_limit: 1000 + first_stair_approach_pitch: 0.485 + second_stair_approach_pitch: 0.35 + back_climber_burst_velocity: 15.0 back_climber_burst_duration: 300 front_kp: 1.0 front_ki: 0.0 diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp index e8faeda0f..76d0ffd02 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/chassis_climber_controller.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,7 +14,7 @@ namespace rmcs_core::controller::chassis { -enum class AutoClimbState { IDLE, FRONT_UP, DASH, RETRACT }; +enum class AutoClimbState { IDLE, APPROACH, SUPPORT_DEPLOY, DASH, SUPPORT_RETRACT }; class ChassisClimberController : public rmcs_executor::Component @@ -33,8 +34,13 @@ class ChassisClimberController track_velocity_max_ = get_parameter("front_climber_velocity").as_double(); climber_back_control_velocity_abs_ = get_parameter("back_climber_velocity").as_double(); + auto_climb_dash_velocity_ = get_parameter("auto_climb_dash_velocity").as_double(); + auto_climb_support_retract_velocity_abs_ = + get_parameter("auto_climb_support_retract_velocity").as_double(); sync_coefficient_ = get_parameter("sync_coefficient").as_double(); climb_timeout_limit_ = get_parameter("climb_timeout_limit").as_int(); + first_stair_approach_pitch_ = get_parameter("first_stair_approach_pitch").as_double(); + second_stair_approach_pitch_ = get_parameter("second_stair_approach_pitch").as_double(); burst_velocity_abs_ = get_parameter("back_climber_burst_velocity").as_double(); burst_duration_ = get_parameter("back_climber_burst_duration").as_int(); @@ -81,222 +87,267 @@ class ChassisClimberController auto keyboard = *keyboard_; auto rotary_knob_switch = *rotary_knob_switch_; + // RCLCPP_INFO(logger_, "%lf", *chassis_pitch_imu_); + bool rotary_knob_to_up = (last_rotary_knob_switch_ != Switch::UP && rotary_knob_switch == Switch::UP); bool rotary_knob_from_up = (last_rotary_knob_switch_ == Switch::UP && rotary_knob_switch != Switch::UP); - RCLCPP_INFO(logger_, "%lf", *chassis_pitch_imu_); - // RCLCPP_INFO(logger_, "%hhu", static_cast(rotary_knob_switch)); - if ((!last_keyboard_.g && keyboard.g) || rotary_knob_to_up) { + if ((switch_left == Switch::UNKNOWN || switch_right == Switch::UNKNOWN) + || (switch_left == Switch::DOWN && switch_right == Switch::DOWN)) { + reset_all_controls(); + } else { + handle_auto_climb_requests( + (!last_keyboard_.g && keyboard.g) || rotary_knob_to_up, rotary_knob_from_up, + rotary_knob_switch); + + if (auto_climb_state_ != AutoClimbState::IDLE) { + apply_auto_climb_control(update_auto_climb_control()); + } else if (switch_left != Switch::DOWN) { + update_manual_control(); + } + } + + last_switch_left_ = switch_left; + last_switch_right_ = switch_right; + last_keyboard_ = keyboard; + last_rotary_knob_switch_ = rotary_knob_switch; + } + +private: + struct AutoClimbControl { + double front_track_velocity = 0.0; + double back_climber_velocity = 0.0; + double override_chassis_vx = nan_; + }; + + void handle_auto_climb_requests( + bool start_requested, bool abort_by_rotary, rmcs_msgs::Switch rotary_knob_switch) { + + if (start_requested) { if (auto_climb_state_ == AutoClimbState::IDLE) { - auto_climb_state_ = AutoClimbState::FRONT_UP; - auto_climb_timer_ = 0; - climb_count_ = 0; - back_climber_block_count_ = 0; - auto_dash_started_ = false; - RCLCPP_INFO( - logger_, "Auto climb started by %s.", - rotary_knob_switch == Switch::UP ? "Rotary Knob" : "Keyboard G"); + start_auto_climb( + rotary_knob_switch == rmcs_msgs::Switch::UP ? "Rotary Knob" : "Keyboard G"); } else { - auto_climb_state_ = AutoClimbState::IDLE; - reset_all_controls(); - RCLCPP_INFO(logger_, "Auto climb aborted (toggled again)."); + abort_auto_climb("toggled again"); } - } else if (rotary_knob_from_up && auto_climb_state_ != AutoClimbState::IDLE) { - auto_climb_state_ = AutoClimbState::IDLE; - reset_all_controls(); - RCLCPP_INFO(logger_, "Auto climb aborted (rotary knob left UP)."); + } else if (abort_by_rotary && auto_climb_state_ != AutoClimbState::IDLE) { + abort_auto_climb("rotary knob left UP"); } + } - if ((switch_left == Switch::UNKNOWN || switch_right == Switch::UNKNOWN) - || (switch_left == Switch::DOWN && switch_right == Switch::DOWN)) { - reset_all_controls(); - auto_climb_state_ = AutoClimbState::IDLE; - } else if (auto_climb_state_ != AutoClimbState::IDLE) { - - auto_climb_timer_++; - double override_chassis_vx = nan_; - double track_control_velocity = 0.0; - double back_climber_control_velocity = 0.0; - - if (auto_climb_state_ == AutoClimbState::FRONT_UP) { - track_control_velocity = track_velocity_max_; // front climber active - override_chassis_vx = 1.0; // limit speed to leave distance - - double pitch = *chassis_pitch_imu_; - double target_pitch = - (climb_count_ == 0) ? 0.48 : 0.35; // ~25 deg for first, ~20 deg for second - - RCLCPP_INFO_THROTTLE( - logger_, *get_clock(), 500, "FRONT_UP (step %d): pitch=%.3f, target>%.3f", - climb_count_ + 1, pitch, target_pitch); - - if (pitch > target_pitch) { - auto_climb_state_ = AutoClimbState::DASH; - auto_climb_timer_ = 0; - back_climber_block_count_ = 0; - auto_dash_started_ = false; - RCLCPP_INFO(logger_, "Auto climb dashing phase (step %d).", climb_count_ + 1); - } - } else if (auto_climb_state_ == AutoClimbState::DASH) { - // lower rear pole to push up - back_climber_control_velocity = climber_back_control_velocity_abs_; - - if (!auto_dash_started_) { - if ((std::abs(*climber_back_left_torque_) > 0.1 - && std::abs(*climber_back_left_velocity_) < 0.1) - || (std::abs(*climber_back_right_torque_) > 0.1 - && std::abs(*climber_back_right_velocity_) < 0.1)) { - back_climber_block_count_++; - } else { - back_climber_block_count_ = 0; - } - - if (back_climber_block_count_ > 50) { // 50 ticks = 0.1s confirm - auto_dash_started_ = true; - } - } - - if (auto_dash_started_) { - track_control_velocity = track_velocity_max_; // Dash: front climber running - override_chassis_vx = 3.0; // dash speed - } else { - track_control_velocity = 0.0; // Stop front climber, wait for rear pole - override_chassis_vx = 0.0; // Stop chassis completely while pushing up - } - - double pitch = *chassis_pitch_imu_; - RCLCPP_INFO_THROTTLE( - logger_, *get_clock(), 500, "DASH: pitch=%.3f, timer=%d, dash_started=%d", - pitch, auto_climb_timer_, auto_dash_started_); - - bool is_leveled = - auto_dash_started_ && (std::abs(pitch) < 0.1) && (auto_climb_timer_ > 500); - bool timeout = (auto_climb_timer_ > 3000); - - if (is_leveled || timeout) { - auto_climb_state_ = AutoClimbState::RETRACT; - auto_climb_timer_ = 0; - climb_count_++; - if (timeout) { - RCLCPP_WARN(logger_, "Auto climb DASH timeout! Forcing retract."); - } else { - RCLCPP_INFO(logger_, "Auto climb DASH completed (Leveled)."); - } - } - } else if (auto_climb_state_ == AutoClimbState::RETRACT) { - if (rotary_knob_switch == Switch::UP) { - track_control_velocity = track_velocity_max_; // Keep front climbers running - override_chassis_vx = 3.0; // Keep moving forward (dash) for next step - } else { - track_control_velocity = 0.0; - override_chassis_vx = 3.0; // Keep dash moving forward even if finishing - } - back_climber_control_velocity = - -climber_back_control_velocity_abs_ * 2.0; // retract faster - - RCLCPP_INFO_THROTTLE( - logger_, *get_clock(), 500, "RETRACT: timer=%d", auto_climb_timer_); - - if (auto_climb_timer_ > 1000) { // Retract for 1s - if (rotary_knob_switch == Switch::UP && climb_count_ < 2) { - auto_climb_state_ = AutoClimbState::FRONT_UP; - auto_climb_timer_ = 0; - back_climber_block_count_ = 0; - auto_dash_started_ = false; - RCLCPP_INFO( - logger_, "Auto climb RETRACT done, looping to FRONT_UP for step %d.", - climb_count_ + 1); - } else { - auto_climb_state_ = AutoClimbState::IDLE; - RCLCPP_INFO( - logger_, "Auto climb completed (finished %d steps).", climb_count_); - } - } + void start_auto_climb(const char* source) { + auto_climb_continue_ = true; + auto_climb_stair_index_ = 0; + auto_climb_support_block_count_ = 0; + enter_auto_climb_state(AutoClimbState::APPROACH); + + RCLCPP_INFO(logger_, "Auto climb started by %s.", source); + } + + void abort_auto_climb(const char* reason) { + reset_all_controls(); + RCLCPP_INFO(logger_, "Auto climb aborted (%s).", reason); + } + + AutoClimbControl update_auto_climb_control() { + auto_climb_timer_++; + + switch (auto_climb_state_) { + case AutoClimbState::IDLE: return {}; + case AutoClimbState::APPROACH: return update_auto_climb_approach(); + case AutoClimbState::SUPPORT_DEPLOY: return update_auto_climb_support_deploy(); + case AutoClimbState::DASH: return update_auto_climb_dash(); + case AutoClimbState::SUPPORT_RETRACT: return update_auto_climb_support_retract(); + } + + return {}; + } + + AutoClimbControl update_auto_climb_approach() { + AutoClimbControl control{ + .front_track_velocity = track_velocity_max_, + .back_climber_velocity = 0.0, + .override_chassis_vx = kAutoClimbApproachVelocity, + }; + + double pitch = *chassis_pitch_imu_; + double target_pitch = auto_climb_stair_index_ == 0 ? first_stair_approach_pitch_ + : second_stair_approach_pitch_; + + RCLCPP_INFO_THROTTLE( + logger_, *get_clock(), 500, "APPROACH (step %d): pitch=%.3f, target>%.3f", + auto_climb_stair_index_ + 1, pitch, target_pitch); + + if (pitch > target_pitch) { + enter_auto_climb_state(AutoClimbState::SUPPORT_DEPLOY); + RCLCPP_INFO( + logger_, "Auto climb entering SUPPORT_DEPLOY (step %d).", + auto_climb_stair_index_ + 1); + } + + return control; + } + + AutoClimbControl update_auto_climb_support_deploy() { + AutoClimbControl control{ + .front_track_velocity = 0.0, + .back_climber_velocity = climber_back_control_velocity_abs_, + .override_chassis_vx = 0.2, + }; + + if (is_back_climber_blocked()) { + auto_climb_support_block_count_++; + } else { + auto_climb_support_block_count_ = 0; + } + + RCLCPP_INFO_THROTTLE( + logger_, *get_clock(), 500, "SUPPORT_DEPLOY (step %d): blocked_ticks=%d", + auto_climb_stair_index_ + 1, auto_climb_support_block_count_); + + if (auto_climb_support_block_count_ >= kAutoClimbSupportConfirmTicks) { + enter_auto_climb_state(AutoClimbState::DASH); + RCLCPP_INFO( + logger_, "Auto climb entering DASH (step %d).", auto_climb_stair_index_ + 1); + } + + return control; + } + + AutoClimbControl update_auto_climb_dash() { + AutoClimbControl control{ + .front_track_velocity = track_velocity_max_, + .back_climber_velocity = climber_back_control_velocity_abs_, + .override_chassis_vx = auto_climb_dash_velocity_, + }; + + double pitch = *chassis_pitch_imu_; + bool is_leveled = std::abs(pitch) < kAutoClimbLeveledPitchThreshold + && auto_climb_timer_ > kAutoClimbDashMinTicks; + bool timeout = auto_climb_timer_ > kAutoClimbDashTimeoutTicks; + + RCLCPP_INFO_THROTTLE( + logger_, *get_clock(), 500, "DASH (step %d): pitch=%.3f, timer=%d", + auto_climb_stair_index_ + 1, pitch, auto_climb_timer_); + + if (is_leveled || timeout) { + enter_auto_climb_state(AutoClimbState::SUPPORT_RETRACT); + + if (timeout) { + RCLCPP_WARN( + logger_, "Auto climb DASH timeout on step %d. Entering SUPPORT_RETRACT.", + auto_climb_stair_index_ + 1); + } else { + RCLCPP_INFO( + logger_, "Auto climb reached platform on step %d.", + auto_climb_stair_index_ + 1); } + } - *climbing_forward_velocity_ = override_chassis_vx; + return control; + } - dual_motor_sync_control( - track_control_velocity, *climber_front_left_velocity_, - *climber_front_right_velocity_, front_velocity_pid_calculator_, - *climber_front_left_control_torque_, *climber_front_right_control_torque_); + AutoClimbControl update_auto_climb_support_retract() { + AutoClimbControl control{ + .front_track_velocity = track_velocity_max_, + .back_climber_velocity = -auto_climb_support_retract_velocity_abs_, + .override_chassis_vx = auto_climb_dash_velocity_, + }; - dual_motor_sync_control( - back_climber_control_velocity, *climber_back_left_velocity_, - *climber_back_right_velocity_, back_velocity_pid_calculator_, - *climber_back_left_control_torque_, *climber_back_right_control_torque_); + RCLCPP_INFO_THROTTLE( + logger_, *get_clock(), 500, "SUPPORT_RETRACT (step %d): timer=%d", + auto_climb_stair_index_ + 1, auto_climb_timer_); - } else if (switch_left != Switch::DOWN) { - *climbing_forward_velocity_ = nan_; + if (auto_climb_timer_ > kAutoClimbSupportRetractTicks) { + bool has_next_stair = + auto_climb_continue_ && (auto_climb_stair_index_ + 1 < kAutoClimbMaxStairs); - // if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::UP) { - // front_climber_enable_ = !front_climber_enable_; - // } else if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::DOWN) { - // back_climber_dir_ = -1 * back_climber_dir_; - // reset_back_safety_counters(); - // } + if (has_next_stair) { + auto_climb_stair_index_++; + enter_auto_climb_state(AutoClimbState::APPROACH); + RCLCPP_INFO( + logger_, "Auto climb continuing to step %d.", auto_climb_stair_index_ + 1); + } else { + int finished_steps = auto_climb_stair_index_ + 1; + stop_auto_climb(); + RCLCPP_INFO(logger_, "Auto climb completed (finished %d steps).", finished_steps); + } + } - double track_control_velocity = - front_climber_enable_ ? joystick_right_->x() * track_velocity_max_ : nan_; + return control; + } - dual_motor_sync_control( - track_control_velocity, *climber_front_left_velocity_, - *climber_front_right_velocity_, front_velocity_pid_calculator_, - *climber_front_left_control_torque_, *climber_front_right_control_torque_); - - double back_climber_control_velocity = 0.0; - - if (switch_left != Switch::DOWN) { - back_climber_timer_++; - bool force_zero_torque = false; - - if ((std::abs(*climber_back_left_torque_) > 0.1 - && std::abs(*climber_back_left_velocity_) < 0.1) - || (std::abs(*climber_back_right_torque_) > 0.1 - && std::abs(*climber_back_right_velocity_) < 0.1)) { - back_climber_block_count_++; - } - - if (back_climber_dir_ == -1) { - if (back_climber_timer_ < burst_duration_) { - back_climber_control_velocity = burst_velocity_abs_ * back_climber_dir_; - } else { - force_zero_torque = true; - } - } else { - back_climber_control_velocity = - climber_back_control_velocity_abs_ * back_climber_dir_; - } - - if (!force_zero_torque) { - if (back_climber_block_count_ >= 500) { - RCLCPP_WARN_THROTTLE(logger_, *get_clock(), 1000, "Back climber STALLED."); - back_climber_control_velocity = 0.0; - } else if (back_climber_timer_ >= climb_timeout_limit_) { - RCLCPP_WARN_THROTTLE(logger_, *get_clock(), 1000, "Back climber TIMEOUT."); - back_climber_control_velocity = 0.0; - } - - dual_motor_sync_control( - back_climber_control_velocity, *climber_back_left_velocity_, - *climber_back_right_velocity_, back_velocity_pid_calculator_, - *climber_back_left_control_torque_, *climber_back_right_control_torque_); - } else { - *climber_back_left_control_torque_ = 0.0; - *climber_back_right_control_torque_ = 0.0; - } + void apply_auto_climb_control(const AutoClimbControl& control) { + *climbing_forward_velocity_ = control.override_chassis_vx; + + dual_motor_sync_control( + control.front_track_velocity, *climber_front_left_velocity_, + *climber_front_right_velocity_, front_velocity_pid_calculator_, + *climber_front_left_control_torque_, *climber_front_right_control_torque_); + + dual_motor_sync_control( + control.back_climber_velocity, *climber_back_left_velocity_, + *climber_back_right_velocity_, back_velocity_pid_calculator_, + *climber_back_left_control_torque_, *climber_back_right_control_torque_); + } + + void update_manual_control() { + *climbing_forward_velocity_ = nan_; + + // if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::UP) { + // front_climber_enable_ = !front_climber_enable_; + // } else if (last_switch_right_ == Switch::MIDDLE && switch_right == Switch::DOWN) { + // back_climber_dir_ = -1 * back_climber_dir_; + // reset_back_safety_counters(); + // } + + double track_control_velocity = + front_climber_enable_ ? joystick_right_->x() * track_velocity_max_ : nan_; + + dual_motor_sync_control( + track_control_velocity, *climber_front_left_velocity_, *climber_front_right_velocity_, + front_velocity_pid_calculator_, *climber_front_left_control_torque_, + *climber_front_right_control_torque_); + + double back_climber_control_velocity = 0.0; + back_climber_timer_++; + bool force_zero_torque = false; + + if (is_back_climber_blocked()) { + back_climber_block_count_++; + } + + if (back_climber_dir_ == -1) { + if (back_climber_timer_ < burst_duration_) { + back_climber_control_velocity = burst_velocity_abs_ * back_climber_dir_; + } else { + force_zero_torque = true; } + } else { + back_climber_control_velocity = climber_back_control_velocity_abs_ * back_climber_dir_; } - last_switch_left_ = switch_left; - last_switch_right_ = switch_right; - last_keyboard_ = keyboard; - last_rotary_knob_switch_ = rotary_knob_switch; + if (!force_zero_torque) { + if (back_climber_block_count_ >= 500) { + RCLCPP_WARN_THROTTLE(logger_, *get_clock(), 1000, "Back climber STALLED."); + back_climber_control_velocity = 0.0; + } else if (back_climber_timer_ >= climb_timeout_limit_) { + RCLCPP_WARN_THROTTLE(logger_, *get_clock(), 1000, "Back climber TIMEOUT."); + back_climber_control_velocity = 0.0; + } + + dual_motor_sync_control( + back_climber_control_velocity, *climber_back_left_velocity_, + *climber_back_right_velocity_, back_velocity_pid_calculator_, + *climber_back_left_control_torque_, *climber_back_right_control_torque_); + } else { + *climber_back_left_control_torque_ = 0.0; + *climber_back_right_control_torque_ = 0.0; + } } -private: void reset_all_controls() { *climber_front_left_control_torque_ = 0; *climber_front_right_control_torque_ = 0; @@ -304,14 +355,36 @@ class ChassisClimberController *climber_back_right_control_torque_ = 0; front_climber_enable_ = false; *climbing_forward_velocity_ = nan_; + stop_auto_climb(); reset_back_safety_counters(); } + void stop_auto_climb() { + auto_climb_state_ = AutoClimbState::IDLE; + auto_climb_timer_ = 0; + auto_climb_stair_index_ = 0; + auto_climb_continue_ = false; + auto_climb_support_block_count_ = 0; + } + + void enter_auto_climb_state(AutoClimbState state) { + auto_climb_state_ = state; + auto_climb_timer_ = 0; + auto_climb_support_block_count_ = 0; + } + void reset_back_safety_counters() { back_climber_block_count_ = 0; back_climber_timer_ = 0; } + bool is_back_climber_blocked() const { + return (std::abs(*climber_back_left_torque_) > kBackClimberBlockedTorqueThreshold + && std::abs(*climber_back_left_velocity_) < kBackClimberBlockedVelocityThreshold) + || (std::abs(*climber_back_right_torque_) > kBackClimberBlockedTorqueThreshold + && std::abs(*climber_back_right_velocity_) < kBackClimberBlockedVelocityThreshold); + } + void dual_motor_sync_control( double setpoint, double left_velocity, double right_velocity, pid::MatrixPidCalculator<2>& pid_calculator, double& left_torque_out, @@ -339,9 +412,20 @@ class ChassisClimberController rclcpp::Logger logger_; static constexpr double nan_ = std::numeric_limits::quiet_NaN(); + static constexpr double kAutoClimbApproachVelocity = 1.0; + static constexpr double kAutoClimbLeveledPitchThreshold = 0.1; + static constexpr double kBackClimberBlockedTorqueThreshold = 0.1; + static constexpr double kBackClimberBlockedVelocityThreshold = 0.1; + static constexpr int kAutoClimbSupportConfirmTicks = 50; + static constexpr int kAutoClimbDashMinTicks = 500; + static constexpr int kAutoClimbDashTimeoutTicks = 3000; + static constexpr int kAutoClimbSupportRetractTicks = 1000; + static constexpr int kAutoClimbMaxStairs = 2; double sync_coefficient_; int64_t climb_timeout_limit_; + double first_stair_approach_pitch_; + double second_stair_approach_pitch_; double burst_velocity_abs_; int64_t burst_duration_; @@ -349,14 +433,16 @@ class ChassisClimberController bool front_climber_enable_ = false; double back_climber_dir_ = -1; - int climb_count_ = 0; - double track_velocity_max_; double climber_back_control_velocity_abs_; + double auto_climb_dash_velocity_; + double auto_climb_support_retract_velocity_abs_; AutoClimbState auto_climb_state_ = AutoClimbState::IDLE; int auto_climb_timer_ = 0; - bool auto_dash_started_ = false; + int auto_climb_stair_index_ = 0; + int auto_climb_support_block_count_ = 0; + bool auto_climb_continue_ = false; OutputInterface climber_front_left_control_torque_; OutputInterface climber_front_right_control_torque_; From 7bf9b011ffaab63f285c202cf2c132a16a95491c Mon Sep 17 00:00:00 2001 From: zhzy-star <2807406212@qq.com> Date: Wed, 18 Mar 2026 03:01:09 +0800 Subject: [PATCH 08/12] Fixed putter controller : add photoelectric_sensor(demo) --- ...irmware-c_board-3.0.1-0.dev.4.gbaf538b.dfu | Bin 0 -> 95612 bytes .../rmcs_bringup/config/steering-hero.yaml | 1 + rmcs_ws/src/rmcs_core/CMakeLists.txt | 3 +- .../controller/shooting/putter_controller.cpp | 195 +++++++++++++----- .../controller/shooting/shooting_recorder.cpp | 54 ++--- .../rmcs_core/src/hardware/steering-hero.cpp | 23 ++- 6 files changed, 192 insertions(+), 84 deletions(-) create mode 100644 librmcs-firmware-c_board-3.0.1-0.dev.4.gbaf538b.dfu diff --git a/librmcs-firmware-c_board-3.0.1-0.dev.4.gbaf538b.dfu b/librmcs-firmware-c_board-3.0.1-0.dev.4.gbaf538b.dfu new file mode 100644 index 0000000000000000000000000000000000000000..c3e952e1b6d1f9d141b6f936f2de67159b810bd5 GIT binary patch literal 95612 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 literal 0 HcmV?d00001 diff --git a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml index 61946dff2..3d15b2eed 100644 --- a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml +++ b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml @@ -39,6 +39,7 @@ rmcs_executor: hero_hardware: ros__parameters: + dd: 7 board_serial_top_board: "D4-2BCA-2E47-76CD-23BC-0B78-684B" board_serial_bottom_board_one: "D4-7973-19A9-EA40-4A3E-306F-10F9" board_serial_bottom_board_two: "D4-3674-7174-8768-879E-E44A-3931" diff --git a/rmcs_ws/src/rmcs_core/CMakeLists.txt b/rmcs_ws/src/rmcs_core/CMakeLists.txt index dd3c85799..c576aca1d 100644 --- a/rmcs_ws/src/rmcs_core/CMakeLists.txt +++ b/rmcs_ws/src/rmcs_core/CMakeLists.txt @@ -17,8 +17,7 @@ include(FetchContent) set(BUILD_STATIC_LIBRMCS ON CACHE BOOL "Build static librmcs SDK" FORCE) FetchContent_Declare( librmcs - URL https://github.com/Alliance-Algorithm/librmcs/releases/download/v3.0.0/librmcs-sdk-src-3.0.0.zip - URL_HASH SHA256=b39f51c21baacdcbf3f0176119b8850137a108b88a67e12395d37d89e5ef53e8 + URL file:///workspaces/RMCS/develop_ws/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b.zip DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) FetchContent_MakeAvailable(librmcs) diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp index fbf857611..832e6e721 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp @@ -1,3 +1,7 @@ +#include +#include +#include + #include #include #include @@ -50,27 +54,27 @@ class PutterController last_preload_flag_ = false; - bullet_feeder_velocity_pid_.kp = 50.0; - bullet_feeder_velocity_pid_.ki = 10.0; + bullet_feeder_velocity_pid_.kp = 20.0; + bullet_feeder_velocity_pid_.ki = 5.0; bullet_feeder_velocity_pid_.kd = 0.0; - bullet_feeder_velocity_pid_.integral_max = 60.0; + bullet_feeder_velocity_pid_.integral_max = 40.0; bullet_feeder_velocity_pid_.integral_min = 0.0; bullet_feeder_angle_pid_.kp = 8.0; bullet_feeder_angle_pid_.ki = 0.0; bullet_feeder_angle_pid_.kd = 2.0; - putter_return_velocity_pid_.kp = 0.0015; - putter_return_velocity_pid_.ki = 0.00005; - putter_return_velocity_pid_.kd = 0.; - putter_return_velocity_pid_.integral_max = 0.; - putter_return_velocity_pid_.integral_min = -0.03; + puttter_return_velocity_pid_.kp = 0.0015; + puttter_return_velocity_pid_.ki = 0.00005; + puttter_return_velocity_pid_.kd = 0.; + puttter_return_velocity_pid_.integral_max = 0.; + puttter_return_velocity_pid_.integral_min = -0.03; putter_velocity_pid_.kp = 0.004; putter_velocity_pid_.ki = 0.0001; putter_velocity_pid_.kd = 0.001; - putter_velocity_pid_.integral_max = 0.03; - putter_velocity_pid_.integral_min = 0.; + puttter_return_velocity_pid_.integral_max = 0.03; + puttter_return_velocity_pid_.integral_min = 0.; putter_return_angle_pid.kp = 0.0001; // putter_return_angle_pid.ki = 0.000001; @@ -79,8 +83,16 @@ class PutterController register_output( "/gimbal/bullet_feeder/control_torque", bullet_feeder_control_torque_, nan_); register_output("/gimbal/putter/control_torque", putter_control_torque_, nan_); + register_output("/gimbal/shoot/delay_ms", shoot_delay_ms_, nan_); register_output("/gimbal/shooter/mode", shoot_mode_, rmcs_msgs::ShootMode::SINGLE); + + initialize_shoot_delay_log(); + } + + ~PutterController() override { + if (shoot_delay_log_stream_.is_open()) + shoot_delay_log_stream_.close(); } void update() override { @@ -103,17 +115,20 @@ class PutterController if (bullet_feeder_cool_down_ > 0) { bullet_feeder_cool_down_--; - // 使用角度环反转到“后退一格”的位置以解除卡弹 - double velocity_err = - bullet_feeder_angle_pid_.update( - bullet_feeder_control_angle_ - *bullet_feeder_angle_) - - *bullet_feeder_velocity_; - *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update(velocity_err); - - if (!bullet_feeder_cool_down_) { - RCLCPP_INFO(get_logger(), "Jamming Solved, Retrying..."); - set_preloading(); + // 冷却期前期:反转供弹轮以解除卡弹 + if (bullet_feeder_cool_down_ > 500) + *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update( + -bullet_feeder_angle_per_bullet_ - *bullet_feeder_velocity_); + else { + // 冷却期后期:停止控制 + bullet_feeder_velocity_pid_.reset(); + *bullet_feeder_control_torque_ = 0.0; } + + bullet_feeder_angle_pid_.reset(); + + if (!bullet_feeder_cool_down_) + RCLCPP_INFO(get_logger(), "Jamming Solved!"); } else { // 正常运行模式:摩擦轮就绪时才允许发射 if (*friction_ready_) { @@ -131,23 +146,37 @@ class PutterController } } - if (shoot_stage_ == ShootStage::PRELOADING) { - // 盲拨模式:始终执行角度环定位 - if (std::isnan(bullet_feeder_control_angle_)) { - bullet_feeder_control_angle_ = - *bullet_feeder_angle_ + bullet_feeder_angle_per_bullet_; - last_preload_flag_ = true; - } + if (*photoelectric_sensor_status_) { + trigger = true; + } - const auto angle_err = bullet_feeder_control_angle_ - *bullet_feeder_angle_; - if (angle_err < 0.1) { - set_preloaded(); + if (shoot_stage_ == ShootStage::PRELOADING) { + if (last_preload_flag_) { + // 角度环控制模式:光电门触发后精确定位 + const auto angle_err = + bullet_feeder_control_angle_ - *bullet_feeder_angle_; + if (angle_err < 0.1) { + set_preloaded(); + } + double velocity_err = + bullet_feeder_angle_pid_.update( + bullet_feeder_control_angle_ - *bullet_feeder_angle_) + - *bullet_feeder_velocity_; + *bullet_feeder_control_torque_ = + bullet_feeder_velocity_pid_.update(velocity_err); + } else { + // 速度环控制模式:等待光电门触发 + if (trigger) { + RCLCPP_INFO(get_logger(), "Photoelectric Sensor Triggered"); + last_preload_flag_ = true; + bullet_feeder_control_angle_ = + *bullet_feeder_angle_ + bullet_feeder_angle_per_bullet_ * 0.5; + } else + *bullet_feeder_control_torque_ = + bullet_feeder_velocity_pid_.update( + low_latency_velocity_ - *bullet_feeder_velocity_) + * 0.5; } - double velocity_err = - bullet_feeder_angle_pid_.update(angle_err) - *bullet_feeder_velocity_; - *bullet_feeder_control_torque_ = - bullet_feeder_velocity_pid_.update(velocity_err); - update_jam_detection(); } else { // 其他状态:角度环保持角度不变防止弹链退弹 @@ -164,6 +193,7 @@ class PutterController if (*bullet_fired_ || *putter_angle_ - putter_startpoint >= putter_stroke_) { shooted = true; + trigger = false; } update_putter_jam_detection(); @@ -175,14 +205,14 @@ class PutterController *putter_control_torque_ = 0.; set_preloading(); shooted = false; - } else { - *putter_control_torque_ = - putter_return_velocity_pid_.update(-80. - *putter_velocity_); } + + *putter_control_torque_ = + puttter_return_velocity_pid_.update(-80. - *putter_velocity_); } else { // 子弹未发出:继续推进 *putter_control_torque_ = - putter_return_velocity_pid_.update(60. - *putter_velocity_); + puttter_return_velocity_pid_.update(60. - *putter_velocity_); } } } else { @@ -196,10 +226,12 @@ class PutterController } } else { // 推杆未初始化:执行复位操作 - *putter_control_torque_ = putter_return_velocity_pid_.update(-80. - *putter_velocity_); + *putter_control_torque_ = puttter_return_velocity_pid_.update(-80. - *putter_velocity_); update_putter_jam_detection(); } + update_shoot_delay_measurement(*bullet_fired_); + // 保存当前状态用于下次比较 last_switch_right_ = switch_right; last_switch_left_ = switch_left; @@ -226,13 +258,15 @@ class PutterController putter_initialized = false; putter_startpoint = nan_; - putter_return_velocity_pid_.reset(); + puttter_return_velocity_pid_.reset(); putter_velocity_pid_.reset(); putter_return_angle_pid.reset(); *putter_control_torque_ = nan_; last_preload_flag_ = false; last_photoelectric_sensor_status_ = false; + shot_delay_pending_ = false; + last_bullet_fired_ = *bullet_fired_; bullet_feeder_faulty_count_ = 0; bullet_feeder_cool_down_ = 0; @@ -241,23 +275,67 @@ class PutterController void set_preloading() { RCLCPP_INFO(get_logger(), "PRELOADING"); shoot_stage_ = ShootStage::PRELOADING; - // 盲拨方案:直接增加目标角度 - if (!std::isnan(bullet_feeder_control_angle_)) { - bullet_feeder_control_angle_ += bullet_feeder_angle_per_bullet_; - } - last_preload_flag_ = true; } void set_preloaded() { RCLCPP_INFO(get_logger(), "PRELOADED"); shoot_stage_ = ShootStage::PRELOADED; last_preload_flag_ = false; - bullet_feeder_control_angle_ = *bullet_feeder_angle_ - 0.3; } void set_shooting() { RCLCPP_INFO(get_logger(), "SHOOTING"); shoot_stage_ = ShootStage::SHOOTING; + shoot_start_time_ = std::chrono::steady_clock::now(); + shot_delay_pending_ = true; + } + + void update_shoot_delay_measurement(bool bullet_fired_now) { + const bool bullet_fired_rising_edge = bullet_fired_now && !last_bullet_fired_; + + if (shot_delay_pending_ && bullet_fired_rising_edge) { + const auto now = std::chrono::steady_clock::now(); + const double delay_ms = + std::chrono::duration(now - shoot_start_time_).count(); + *shoot_delay_ms_ = delay_ms; + log_shoot_delay(delay_ms); + shot_delay_pending_ = false; + } + + // Shot ended without a bullet_fired edge: drop this sample. + if (shot_delay_pending_ && shoot_stage_ != ShootStage::SHOOTING) + shot_delay_pending_ = false; + + last_bullet_fired_ = bullet_fired_now; + } + + void initialize_shoot_delay_log() { + using namespace std::chrono; + const auto now = system_clock::now(); + const auto ms = duration_cast(now.time_since_epoch()).count(); + shoot_delay_log_path_ = "/robot_shoot/shoot_delay_" + std::to_string(ms) + ".log"; + shoot_delay_log_stream_.open(shoot_delay_log_path_); + + if (!shoot_delay_log_stream_.is_open()) { + RCLCPP_WARN( + get_logger(), "Failed to open shoot delay log file: %s", + shoot_delay_log_path_.c_str()); + return; + } + + shoot_delay_log_stream_ << "shot_index,delay_ms,timestamp_ms" << std::endl; + RCLCPP_INFO(get_logger(), "Shoot delay log file: %s", shoot_delay_log_path_.c_str()); + } + + void log_shoot_delay(double delay_ms) { + if (!shoot_delay_log_stream_.is_open()) + return; + + using namespace std::chrono; + const auto timestamp_ms = + duration_cast(system_clock::now().time_since_epoch()).count(); + shoot_delay_log_stream_ << shoot_delay_sample_count_++ << ',' << delay_ms << ',' + << timestamp_ms << std::endl; } void update_jam_detection() { @@ -272,8 +350,10 @@ class PutterController bullet_feeder_faulty_count_++; else { bullet_feeder_faulty_count_ = 0; - RCLCPP_WARN(get_logger(), "Jam Detected! Reversing 60 degrees..."); - enter_jam_protection(); + if (last_preload_flag_) + set_preloaded(); + else + enter_jam_protection(); } } @@ -302,8 +382,7 @@ class PutterController } void enter_jam_protection() { - // 设置目标角度为当前角度后退 60 度(一格) - bullet_feeder_control_angle_ = *bullet_feeder_angle_ - bullet_feeder_angle_per_bullet_; + bullet_feeder_control_angle_ = nan_; bullet_feeder_cool_down_ = 1000; bullet_feeder_angle_pid_.reset(); bullet_feeder_velocity_pid_.reset(); @@ -324,6 +403,7 @@ class PutterController InputInterface photoelectric_sensor_status_; bool last_photoelectric_sensor_status_; InputInterface bullet_fired_; + bool trigger{false}; bool shooted{false}; InputInterface friction_ready_; @@ -350,7 +430,7 @@ class PutterController bool putter_initialized = false; int putter_faulty_count_ = 0; double putter_startpoint = nan_; - pid::PidCalculator putter_return_velocity_pid_; + pid::PidCalculator puttter_return_velocity_pid_; InputInterface putter_velocity_; pid::PidCalculator putter_velocity_pid_; @@ -366,10 +446,19 @@ class PutterController InputInterface putter_angle_; pid::PidCalculator putter_return_angle_pid; OutputInterface putter_control_torque_; + OutputInterface shoot_delay_ms_; int bullet_feeder_faulty_count_ = 0; int bullet_feeder_cool_down_ = 0; + std::chrono::steady_clock::time_point shoot_start_time_{}; + bool shot_delay_pending_ = false; + bool last_bullet_fired_ = false; + + std::ofstream shoot_delay_log_stream_; + std::string shoot_delay_log_path_; + std::size_t shoot_delay_sample_count_ = 0; + OutputInterface shoot_mode_; }; @@ -377,4 +466,4 @@ class PutterController #include -PLUGINLIB_EXPORT_CLASS(rmcs_core::controller::shooting::PutterController, rmcs_executor::Component) \ No newline at end of file +PLUGINLIB_EXPORT_CLASS(rmcs_core::controller::shooting::PutterController, rmcs_executor::Component) diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp index 4394bb8b0..1144e734f 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp @@ -85,7 +85,7 @@ class ShootingRecorder switch (log_mode_) { case LogMode::TRIGGER: // It will be triggered by shooting action - if (*shoot_timestamp_ == last_shoot_timestamp_ || v == *shoot_timestamp_) + if (*shoot_timestamp_ == last_shoot_timestamp_) return; break; case LogMode::TIMING: @@ -94,39 +94,39 @@ class ShootingRecorder return; break; } - v = *shoot_timestamp_; + // v = *shoot_timestamp_; - static constexpr size_t max_velocities_size = 1000; + // static constexpr size_t max_velocities_size = 1000; - velocities.push_back(*initial_speed_); - if (velocities.size() > max_velocities_size) { - velocities.erase(velocities.begin()); - } - - analysis3(); + // velocities.push_back(*initial_speed_); + // if (velocities.size() > max_velocities_size) { + // velocities.erase(velocities.begin()); + // } - auto log_text = std::string{}; - auto timestamp = timestamp_to_string(*shoot_timestamp_); + // analysis3(); - if (friction_wheel_count_ == 2) { - log_text = fmt::format( - "{},{},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f}", *initial_speed_, - (int)velocities.size(), // - velocity_, excellence_rate_, pass_rate_, range_, range2_, velocity_max, - velocity_min); - } + // auto log_text = std::string{}; + // auto timestamp = timestamp_to_string(*shoot_timestamp_); + + // if (friction_wheel_count_ == 2) { + // log_text = fmt::format( + // "{},{},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f}", *initial_speed_, + // (int)velocities.size(), // + // velocity_, excellence_rate_, pass_rate_, range_, range2_, velocity_max, + // velocity_min); + // } - log_stream_ << log_text << std::endl; - RCLCPP_INFO(get_logger(), "%s", log_text.c_str()); + // log_stream_ << log_text << std::endl; + // RCLCPP_INFO(get_logger(), "%s", log_text.c_str()); - log_velocity = fmt::format("{:.3f}", *initial_speed_); - std::ofstream outFile("shoot_recorder", std::ios::app); - if (outFile.is_open()) { - outFile << log_velocity << std::endl; - outFile.close(); - } + // log_velocity = fmt::format("{:.3f}", *initial_speed_); + // std::ofstream outFile("shoot_recorder", std::ios::app); + // if (outFile.is_open()) { + // outFile << log_velocity << std::endl; + // outFile.close(); + // } - last_shoot_timestamp_ = *shoot_timestamp_; + // last_shoot_timestamp_ = *shoot_timestamp_; } private: diff --git a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp index 847698c25..24b5d3de3 100644 --- a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp +++ b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -165,6 +166,8 @@ class SteeringHero .set_reduction_ratio(1.) .enable_multi_turn_angle()); + dd = steering_hero.get_parameter("dd").as_int(); + steering_hero.register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_); steering_hero.register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_); @@ -262,6 +265,11 @@ class SteeringHero .can_id = 0x142, .can_data = gimbal_pitch_motor_.generate_command().as_bytes(), }); + + builder.gpio_digital_read({ + .channel = 7, + .falling_edge = true, + }); } private: @@ -296,6 +304,15 @@ class SteeringHero } } + void gpio_digital_read_result_callback( + const librmcs::data::GpioDigitalDataView& data) override { + *photoelectric_sensor_status_ = false; + if (data.channel == 7) { + *photoelectric_sensor_status_ = true; + RCLCPP_INFO(logger_, "trigger!"); + } + } + void accelerometer_receive_callback( const librmcs::data::AccelerometerDataView& data) override { imu_.store_accelerometer_status(data.x, data.y, data.z); @@ -307,6 +324,8 @@ class SteeringHero void putter_receive_callback(bool status) { *photoelectric_sensor_status_ = status; } + uint8_t dd; + rclcpp::Logger logger_; OutputInterface& tf_; @@ -324,6 +343,7 @@ class SteeringHero OutputInterface photoelectric_sensor_status_; OutputInterface camera_capturer_trigger_; OutputInterface camera_capturer_trigger_timestamp_; + std::atomic photoelectric_sensor_status_atomic{false}; }; class BottomBoard_one final : private librmcs::agent::CBoard { @@ -388,8 +408,7 @@ class SteeringHero steering_hero.register_output( "/chassis/yaw/velocity_imu", chassis_yaw_velocity_imu_, 0); - steering_hero.register_output( - "/chassis/pitch_imu", chassis_pitch_imu_, 0.0); + steering_hero.register_output("/chassis/pitch_imu", chassis_pitch_imu_, 0.0); } BottomBoard_one(const BottomBoard_one&) = delete; BottomBoard_one& operator=(const BottomBoard_one&) = delete; From cf68ff78146e31926194c3840717543a01effb9b Mon Sep 17 00:00:00 2001 From: zhzy-star <2807406212@qq.com> Date: Thu, 19 Mar 2026 06:08:13 +0800 Subject: [PATCH 09/12] Fixed dual_yaw; add new putter_controller. --- .../rmcs_bringup/config/steering-hero.yaml | 5 - .../controller/gimbal/dual_yaw_controller.cpp | 3 + .../gimbal/hero_gimbal_controller.cpp | 10 +- .../controller/shooting/putter_controller.cpp | 251 ++++++++++-------- .../rmcs_core/src/hardware/steering-hero.cpp | 4 - 5 files changed, 141 insertions(+), 132 deletions(-) diff --git a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml index 3d15b2eed..3ebcf3148 100644 --- a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml +++ b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml @@ -39,7 +39,6 @@ rmcs_executor: hero_hardware: ros__parameters: - dd: 7 board_serial_top_board: "D4-2BCA-2E47-76CD-23BC-0B78-684B" board_serial_bottom_board_one: "D4-7973-19A9-EA40-4A3E-306F-10F9" board_serial_bottom_board_two: "D4-3674-7174-8768-879E-E44A-3931" @@ -52,10 +51,6 @@ hero_hardware: right_front_zero_point: 7167 left_back_zero_point: 370 right_back_zero_point: 5156 - q0: 0.0 - q1: 0.0 - q2: 0.0 - q3: 1.0 # left_front_zero_point: 3473 # left_back_zero_point: 6124 # right_back_zero_point: 6157 diff --git a/rmcs_ws/src/rmcs_core/src/controller/gimbal/dual_yaw_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/gimbal/dual_yaw_controller.cpp index 20c092363..90ae2576c 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/gimbal/dual_yaw_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/gimbal/dual_yaw_controller.cpp @@ -44,6 +44,9 @@ class DualYawController register_output("/gimbal/top_yaw/control_torque", top_yaw_control_torque_, 0.0); register_output("/gimbal/bottom_yaw/control_torque", bottom_yaw_control_torque_, 0.0); + register_output("/gimbal/top_yaw/control_angle", top_yaw_control_angle_, nan_); + register_output( + "/gimbal/bottom_yaw/control_angle_shift", bottom_yaw_control_angle_shift_, nan_); status_component_ = create_partner_component(get_component_name() + "_status"); diff --git a/rmcs_ws/src/rmcs_core/src/controller/gimbal/hero_gimbal_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/gimbal/hero_gimbal_controller.cpp index 1c6dde8ba..40aba2081 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/gimbal/hero_gimbal_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/gimbal/hero_gimbal_controller.cpp @@ -65,9 +65,9 @@ class HeroGimbalController else gimbal_mode_keyboard_ = GimbalMode::IMU; } - // *gimbal_mode_ = - // *switch_right_ == Switch::UP ? GimbalMode::ENCODER : gimbal_mode_keyboard_; - *gimbal_mode_ = gimbal_mode_keyboard_; + *gimbal_mode_ = + *switch_right_ == Switch::UP ? GimbalMode::ENCODER : gimbal_mode_keyboard_; + // *gimbal_mode_ = gimbal_mode_keyboard_; if (*gimbal_mode_ == GimbalMode::IMU) { auto angle_error = update_imu_control(); @@ -122,7 +122,7 @@ class HeroGimbalController double yaw_shift = joystick_sensitivity * joystick_left_->y() + mouse_sensitivity * mouse_velocity_->y(); double pitch_shift = - -joystick_sensitivity * joystick_left_->x() - mouse_sensitivity * mouse_velocity_->x(); + -joystick_sensitivity * joystick_left_->x() + mouse_sensitivity * mouse_velocity_->x(); return imu_gimbal_solver.update( TwoAxisGimbalSolver::SetControlShift{yaw_shift, pitch_shift}); @@ -139,7 +139,7 @@ class HeroGimbalController double yaw_shift = joystick_sensitivity * joystick_left_->y() + mouse_yaw_sensitivity * mouse_velocity_->y(); double pitch_shift = -joystick_sensitivity * joystick_left_->x() - - mouse_pitch_sensitivity * mouse_velocity_->x(); + + mouse_pitch_sensitivity * mouse_velocity_->x(); return encoder_gimbal_solver.update( PreciseTwoAxisGimbalSolver::SetControlShift{yaw_shift, pitch_shift}); diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp index 832e6e721..02ab2dd86 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -40,6 +41,7 @@ class PutterController register_input("/gimbal/friction_ready", friction_ready_); register_input("/gimbal/bullet_feeder/angle", bullet_feeder_angle_); + register_input("/gimbal/bullet_feeder/torque", bullet_feeder_torque_); register_input("/gimbal/bullet_feeder/velocity", bullet_feeder_velocity_); register_input( @@ -54,27 +56,27 @@ class PutterController last_preload_flag_ = false; - bullet_feeder_velocity_pid_.kp = 20.0; - bullet_feeder_velocity_pid_.ki = 5.0; + bullet_feeder_velocity_pid_.kp = 50.0; + bullet_feeder_velocity_pid_.ki = 10.0; bullet_feeder_velocity_pid_.kd = 0.0; - bullet_feeder_velocity_pid_.integral_max = 40.0; + bullet_feeder_velocity_pid_.integral_max = 60.0; bullet_feeder_velocity_pid_.integral_min = 0.0; bullet_feeder_angle_pid_.kp = 8.0; bullet_feeder_angle_pid_.ki = 0.0; bullet_feeder_angle_pid_.kd = 2.0; - puttter_return_velocity_pid_.kp = 0.0015; - puttter_return_velocity_pid_.ki = 0.00005; - puttter_return_velocity_pid_.kd = 0.; - puttter_return_velocity_pid_.integral_max = 0.; - puttter_return_velocity_pid_.integral_min = -0.03; + putter_return_velocity_pid_.kp = 0.0015; + putter_return_velocity_pid_.ki = 0.00005; + putter_return_velocity_pid_.kd = 0.; + putter_return_velocity_pid_.integral_max = 0.; + putter_return_velocity_pid_.integral_min = -0.03; putter_velocity_pid_.kp = 0.004; putter_velocity_pid_.ki = 0.0001; putter_velocity_pid_.kd = 0.001; - puttter_return_velocity_pid_.integral_max = 0.03; - puttter_return_velocity_pid_.integral_min = 0.; + putter_velocity_pid_.integral_max = 0.03; + putter_velocity_pid_.integral_min = 0.; putter_return_angle_pid.kp = 0.0001; // putter_return_angle_pid.ki = 0.000001; @@ -83,14 +85,14 @@ class PutterController register_output( "/gimbal/bullet_feeder/control_torque", bullet_feeder_control_torque_, nan_); register_output("/gimbal/putter/control_torque", putter_control_torque_, nan_); - register_output("/gimbal/shoot/delay_ms", shoot_delay_ms_, nan_); register_output("/gimbal/shooter/mode", shoot_mode_, rmcs_msgs::ShootMode::SINGLE); + register_output("/gimbal/shoot/delay_ms", shoot_delay_ms_, nan_); initialize_shoot_delay_log(); } - ~PutterController() override { + ~PutterController() { if (shoot_delay_log_stream_.is_open()) shoot_delay_log_stream_.close(); } @@ -100,12 +102,14 @@ class PutterController const auto switch_left = *switch_left_; const auto mouse = *mouse_; const auto keyboard = *keyboard_; + const bool bullet_fired_now = *bullet_fired_; using namespace rmcs_msgs; if ((switch_left == Switch::UNKNOWN || switch_right == Switch::UNKNOWN) || (switch_left == Switch::DOWN && switch_right == Switch::DOWN)) { reset_all_controls(); + update_shoot_delay_measurement(bullet_fired_now); return; } @@ -115,20 +119,16 @@ class PutterController if (bullet_feeder_cool_down_ > 0) { bullet_feeder_cool_down_--; - // 冷却期前期:反转供弹轮以解除卡弹 - if (bullet_feeder_cool_down_ > 500) - *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update( - -bullet_feeder_angle_per_bullet_ - *bullet_feeder_velocity_); - else { - // 冷却期后期:停止控制 - bullet_feeder_velocity_pid_.reset(); - *bullet_feeder_control_torque_ = 0.0; - } - - bullet_feeder_angle_pid_.reset(); + // 使用角度环反转到“后退一格”的位置以解除卡弹 + double velocity_err = bullet_feeder_angle_pid_.update( + bullet_feeder_control_angle_ - *bullet_feeder_angle_) + - *bullet_feeder_velocity_; + *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update(velocity_err); - if (!bullet_feeder_cool_down_) - RCLCPP_INFO(get_logger(), "Jamming Solved!"); + if (!bullet_feeder_cool_down_) { + RCLCPP_INFO(get_logger(), "Jamming Solved, Retrying..."); + set_preloading(); + } } else { // 正常运行模式:摩擦轮就绪时才允许发射 if (*friction_ready_) { @@ -145,38 +145,27 @@ class PutterController set_shooting(); } } - - if (*photoelectric_sensor_status_) { - trigger = true; + if (shoot_stage_ == ShootStage::UPDATING) { + wait_bullet_ready(); } if (shoot_stage_ == ShootStage::PRELOADING) { - if (last_preload_flag_) { - // 角度环控制模式:光电门触发后精确定位 - const auto angle_err = - bullet_feeder_control_angle_ - *bullet_feeder_angle_; - if (angle_err < 0.1) { - set_preloaded(); - } - double velocity_err = - bullet_feeder_angle_pid_.update( - bullet_feeder_control_angle_ - *bullet_feeder_angle_) - - *bullet_feeder_velocity_; - *bullet_feeder_control_torque_ = - bullet_feeder_velocity_pid_.update(velocity_err); - } else { - // 速度环控制模式:等待光电门触发 - if (trigger) { - RCLCPP_INFO(get_logger(), "Photoelectric Sensor Triggered"); - last_preload_flag_ = true; - bullet_feeder_control_angle_ = - *bullet_feeder_angle_ + bullet_feeder_angle_per_bullet_ * 0.5; - } else - *bullet_feeder_control_torque_ = - bullet_feeder_velocity_pid_.update( - low_latency_velocity_ - *bullet_feeder_velocity_) - * 0.5; + // 盲拨模式:始终执行角度环定位 + if (std::isnan(bullet_feeder_control_angle_)) { + bullet_feeder_control_angle_ = + *bullet_feeder_angle_ + bullet_feeder_angle_per_bullet_; + last_preload_flag_ = true; + } + + const auto angle_err = bullet_feeder_control_angle_ - *bullet_feeder_angle_; + if (angle_err < 0.1) { + set_preloaded(); } + double velocity_err = + bullet_feeder_angle_pid_.update(angle_err) - *bullet_feeder_velocity_; + *bullet_feeder_control_torque_ = + bullet_feeder_velocity_pid_.update(velocity_err); + update_jam_detection(); } else { // 其他状态:角度环保持角度不变防止弹链退弹 @@ -193,7 +182,6 @@ class PutterController if (*bullet_fired_ || *putter_angle_ - putter_startpoint >= putter_stroke_) { shooted = true; - trigger = false; } update_putter_jam_detection(); @@ -203,16 +191,16 @@ class PutterController const auto angle_err = putter_startpoint - *putter_angle_; if (angle_err > -0.05) { *putter_control_torque_ = 0.; - set_preloading(); + set_updating(); shooted = false; + } else { + *putter_control_torque_ = + putter_return_velocity_pid_.update(-80. - *putter_velocity_); } - - *putter_control_torque_ = - puttter_return_velocity_pid_.update(-80. - *putter_velocity_); } else { // 子弹未发出:继续推进 *putter_control_torque_ = - puttter_return_velocity_pid_.update(60. - *putter_velocity_); + putter_return_velocity_pid_.update(60. - *putter_velocity_); } } } else { @@ -226,11 +214,11 @@ class PutterController } } else { // 推杆未初始化:执行复位操作 - *putter_control_torque_ = puttter_return_velocity_pid_.update(-80. - *putter_velocity_); + *putter_control_torque_ = putter_return_velocity_pid_.update(-80. - *putter_velocity_); update_putter_jam_detection(); } - update_shoot_delay_measurement(*bullet_fired_); + update_shoot_delay_measurement(bullet_fired_now); // 保存当前状态用于下次比较 last_switch_right_ = switch_right; @@ -258,23 +246,29 @@ class PutterController putter_initialized = false; putter_startpoint = nan_; - puttter_return_velocity_pid_.reset(); + putter_return_velocity_pid_.reset(); putter_velocity_pid_.reset(); putter_return_angle_pid.reset(); *putter_control_torque_ = nan_; last_preload_flag_ = false; last_photoelectric_sensor_status_ = false; - shot_delay_pending_ = false; - last_bullet_fired_ = *bullet_fired_; bullet_feeder_faulty_count_ = 0; bullet_feeder_cool_down_ = 0; + shot_delay_pending_ = false; + last_bullet_fired_ = false; + *shoot_delay_ms_ = nan_; } void set_preloading() { RCLCPP_INFO(get_logger(), "PRELOADING"); shoot_stage_ = ShootStage::PRELOADING; + // 盲拨方案:直接增加目标角度 + if (!std::isnan(bullet_feeder_control_angle_)) { + bullet_feeder_control_angle_ += bullet_feeder_angle_per_bullet_; + } + last_preload_flag_ = true; } void set_preloaded() { @@ -290,6 +284,55 @@ class PutterController shot_delay_pending_ = true; } + void set_updating() { + RCLCPP_INFO(get_logger(), "UPDATING"); + shoot_stage_ = ShootStage::UPDATING; + } + + void update_jam_detection() { + // RCLCPP_INFO(get_logger(), "%.2f --", *bullet_feeder_control_torque_); + if (*bullet_feeder_torque_ > 100 && *bullet_feeder_velocity_ < 0.1) { + bullet_feeder_faulty_count_++; + } else { + bullet_feeder_faulty_count_ = 0; + } + if (bullet_feeder_faulty_count_ >= 1000) + enter_jam_protection(); + } + + void update_putter_jam_detection() { + if ((*putter_control_torque_ > -0.03 && shoot_stage_ == ShootStage::PRELOADING) + || (*putter_control_torque_ < 0.05 && shoot_stage_ == ShootStage::SHOOTING) + || std::isnan(*putter_control_torque_)) { + putter_faulty_count_ = 0; + return; + } + + // 扭矩异常时累计故障计数 + if (putter_faulty_count_ < 500) + ++putter_faulty_count_; + else { + putter_faulty_count_ = 0; + if (shoot_stage_ != ShootStage::SHOOTING) { + // 非发射状态下检测到堵转:推杆已到位,设置为已初始化 + putter_initialized = true; + putter_startpoint = *putter_angle_; + } else { + // 发射状态下检测到堵转:认为子弹已发出 + shooted = true; + } + } + } + + void enter_jam_protection() { + // 设置目标角度为当前角度后退 60 度(一格) + bullet_feeder_control_angle_ = *bullet_feeder_angle_ - bullet_feeder_angle_per_bullet_; + bullet_feeder_cool_down_ = 1000; + bullet_feeder_angle_pid_.reset(); + bullet_feeder_velocity_pid_.reset(); + RCLCPP_INFO(get_logger(), "Jammed!"); + } + void update_shoot_delay_measurement(bool bullet_fired_now) { const bool bullet_fired_rising_edge = bullet_fired_now && !last_bullet_fired_; @@ -298,6 +341,8 @@ class PutterController const double delay_ms = std::chrono::duration(now - shoot_start_time_).count(); *shoot_delay_ms_ = delay_ms; + RCLCPP_INFO( + get_logger(), "Shoot delay[%zu]: %.3f ms", shoot_delay_sample_count_, delay_ms); log_shoot_delay(delay_ms); shot_delay_pending_ = false; } @@ -311,9 +356,18 @@ class PutterController void initialize_shoot_delay_log() { using namespace std::chrono; + const std::filesystem::path log_dir{"/robot_shoot"}; + std::error_code ec; + std::filesystem::create_directories(log_dir, ec); + if (ec) { + RCLCPP_WARN( + get_logger(), "Failed to create shoot delay log directory %s: %s", log_dir.c_str(), + ec.message().c_str()); + } + const auto now = system_clock::now(); const auto ms = duration_cast(now.time_since_epoch()).count(); - shoot_delay_log_path_ = "/robot_shoot/shoot_delay_" + std::to_string(ms) + ".log"; + shoot_delay_log_path_ = (log_dir / ("shoot_delay_" + std::to_string(ms) + ".log")).string(); shoot_delay_log_stream_.open(shoot_delay_log_path_); if (!shoot_delay_log_stream_.is_open()) { @@ -336,59 +390,18 @@ class PutterController duration_cast(system_clock::now().time_since_epoch()).count(); shoot_delay_log_stream_ << shoot_delay_sample_count_++ << ',' << delay_ms << ',' << timestamp_ms << std::endl; + shoot_delay_log_stream_.flush(); } - void update_jam_detection() { - // RCLCPP_INFO(get_logger(), "%.2f --", *bullet_feeder_control_torque_); - if (*bullet_feeder_control_torque_ < 300.0 || std::isnan(*bullet_feeder_control_torque_)) { - bullet_feeder_faulty_count_ = 0; - return; - } - - // 扭矩持续过大时累计故障计数 - if (bullet_feeder_faulty_count_ < 1000) - bullet_feeder_faulty_count_++; - else { - bullet_feeder_faulty_count_ = 0; - if (last_preload_flag_) - set_preloaded(); - else - enter_jam_protection(); - } - } - - void update_putter_jam_detection() { - if ((*putter_control_torque_ > -0.03 && shoot_stage_ == ShootStage::PRELOADING) - || (*putter_control_torque_ < 0.05 && shoot_stage_ == ShootStage::SHOOTING) - || std::isnan(*putter_control_torque_)) { - putter_faulty_count_ = 0; - return; - } - - // 扭矩异常时累计故障计数 - if (putter_faulty_count_ < 500) - ++putter_faulty_count_; - else { - putter_faulty_count_ = 0; - if (shoot_stage_ != ShootStage::SHOOTING) { - // 非发射状态下检测到堵转:推杆已到位,设置为已初始化 - putter_initialized = true; - putter_startpoint = *putter_angle_; - } else { - // 发射状态下检测到堵转:认为子弹已发出 - shooted = true; - } + void wait_bullet_ready() { + if (bullet_ready_count_ >= 600) { + bullet_ready_count_ = 0; + set_preloading(); + } else { + bullet_ready_count_++; } } - void enter_jam_protection() { - bullet_feeder_control_angle_ = nan_; - bullet_feeder_cool_down_ = 1000; - bullet_feeder_angle_pid_.reset(); - bullet_feeder_velocity_pid_.reset(); - RCLCPP_INFO(get_logger(), "Jammed!"); - } - static constexpr double nan_ = std::numeric_limits::quiet_NaN(); ///< 非数值常量 static constexpr double inf_ = std::numeric_limits::infinity(); ///< 无穷大常量 @@ -403,7 +416,6 @@ class PutterController InputInterface photoelectric_sensor_status_; bool last_photoelectric_sensor_status_; InputInterface bullet_fired_; - bool trigger{false}; bool shooted{false}; InputInterface friction_ready_; @@ -421,6 +433,7 @@ class PutterController bool overdrive_mode_ = false; InputInterface bullet_feeder_angle_; + InputInterface bullet_feeder_torque_; InputInterface bullet_feeder_velocity_; InputInterface control_bullet_allowance_limited_by_heat_; @@ -430,13 +443,13 @@ class PutterController bool putter_initialized = false; int putter_faulty_count_ = 0; double putter_startpoint = nan_; - pid::PidCalculator puttter_return_velocity_pid_; + pid::PidCalculator putter_return_velocity_pid_; InputInterface putter_velocity_; pid::PidCalculator putter_velocity_pid_; - enum class ShootStage { PRELOADING, PRELOADED, SHOOTING }; - ShootStage shoot_stage_ = ShootStage::PRELOADING; + enum class ShootStage { PRELOADING, PRELOADED, SHOOTING, UPDATING }; + ShootStage shoot_stage_ = ShootStage::PRELOADED; double bullet_feeder_control_angle_ = nan_; pid::PidCalculator bullet_feeder_velocity_pid_; @@ -446,10 +459,12 @@ class PutterController InputInterface putter_angle_; pid::PidCalculator putter_return_angle_pid; OutputInterface putter_control_torque_; - OutputInterface shoot_delay_ms_; int bullet_feeder_faulty_count_ = 0; int bullet_feeder_cool_down_ = 0; + int bullet_ready_count_ = 0; + + OutputInterface shoot_delay_ms_; std::chrono::steady_clock::time_point shoot_start_time_{}; bool shot_delay_pending_ = false; diff --git a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp index 24b5d3de3..976be1d23 100644 --- a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp +++ b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp @@ -166,8 +166,6 @@ class SteeringHero .set_reduction_ratio(1.) .enable_multi_turn_angle()); - dd = steering_hero.get_parameter("dd").as_int(); - steering_hero.register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_); steering_hero.register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_); @@ -324,8 +322,6 @@ class SteeringHero void putter_receive_callback(bool status) { *photoelectric_sensor_status_ = status; } - uint8_t dd; - rclcpp::Logger logger_; OutputInterface& tf_; From eafda5f05f5c130bd7e3b06a8732785dc9d62111 Mon Sep 17 00:00:00 2001 From: zhzy-star <2807406212@qq.com> Date: Sat, 21 Mar 2026 18:01:18 +0800 Subject: [PATCH 10/12] dd --- .../shooting/friction_wheel_controller.cpp | 2 +- .../controller/shooting/putter_controller.cpp | 14 ++--- .../controller/shooting/shooting_recorder.cpp | 54 +++++++++---------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp index e38437a44..879d9e534 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp @@ -169,7 +169,7 @@ class FrictionWheelController primary_friction_velocity_decrease_integral_ += differential; else { if (primary_friction_velocity_decrease_integral_ < -14.0 - && last_primary_friction_velocity_ < friction_working_velocities_[0] - 20.0) + && last_primary_friction_velocity_ < friction_working_velocities_[0] - 25.0) fired = true; primary_friction_velocity_decrease_integral_ = 0; diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp index 02ab2dd86..750326954 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp @@ -127,7 +127,7 @@ class PutterController if (!bullet_feeder_cool_down_) { RCLCPP_INFO(get_logger(), "Jamming Solved, Retrying..."); - set_preloading(); + set_preloaded(); } } else { // 正常运行模式:摩擦轮就绪时才允许发射 @@ -262,7 +262,7 @@ class PutterController } void set_preloading() { - RCLCPP_INFO(get_logger(), "PRELOADING"); + // RCLCPP_INFO(get_logger(), "PRELOADING"); shoot_stage_ = ShootStage::PRELOADING; // 盲拨方案:直接增加目标角度 if (!std::isnan(bullet_feeder_control_angle_)) { @@ -272,20 +272,20 @@ class PutterController } void set_preloaded() { - RCLCPP_INFO(get_logger(), "PRELOADED"); + // RCLCPP_INFO(get_logger(), "PRELOADED"); shoot_stage_ = ShootStage::PRELOADED; last_preload_flag_ = false; } void set_shooting() { - RCLCPP_INFO(get_logger(), "SHOOTING"); + // RCLCPP_INFO(get_logger(), "SHOOTING"); shoot_stage_ = ShootStage::SHOOTING; shoot_start_time_ = std::chrono::steady_clock::now(); shot_delay_pending_ = true; } void set_updating() { - RCLCPP_INFO(get_logger(), "UPDATING"); + // RCLCPP_INFO(get_logger(), "UPDATING"); shoot_stage_ = ShootStage::UPDATING; } @@ -341,8 +341,8 @@ class PutterController const double delay_ms = std::chrono::duration(now - shoot_start_time_).count(); *shoot_delay_ms_ = delay_ms; - RCLCPP_INFO( - get_logger(), "Shoot delay[%zu]: %.3f ms", shoot_delay_sample_count_, delay_ms); + // RCLCPP_INFO( + // get_logger(), "Shoot delay[%zu]: %.3f ms", shoot_delay_sample_count_, delay_ms); log_shoot_delay(delay_ms); shot_delay_pending_ = false; } diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp index 1144e734f..1e2039cd5 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp @@ -81,7 +81,7 @@ class ShootingRecorder // if (outFile.is_open()) { // outFile << log_text << std::endl; // outFile.close(); - // } + // } 评估摩擦轮质量 switch (log_mode_) { case LogMode::TRIGGER: // It will be triggered by shooting action @@ -94,39 +94,39 @@ class ShootingRecorder return; break; } - // v = *shoot_timestamp_; + v = *shoot_timestamp_; - // static constexpr size_t max_velocities_size = 1000; + static constexpr size_t max_velocities_size = 1000; - // velocities.push_back(*initial_speed_); - // if (velocities.size() > max_velocities_size) { - // velocities.erase(velocities.begin()); - // } + velocities.push_back(*initial_speed_); + if (velocities.size() > max_velocities_size) { + velocities.erase(velocities.begin()); + } - // analysis3(); + analysis3(); - // auto log_text = std::string{}; - // auto timestamp = timestamp_to_string(*shoot_timestamp_); - - // if (friction_wheel_count_ == 2) { - // log_text = fmt::format( - // "{},{},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f}", *initial_speed_, - // (int)velocities.size(), // - // velocity_, excellence_rate_, pass_rate_, range_, range2_, velocity_max, - // velocity_min); - // } + auto log_text = std::string{}; + auto timestamp = timestamp_to_string(*shoot_timestamp_); - // log_stream_ << log_text << std::endl; - // RCLCPP_INFO(get_logger(), "%s", log_text.c_str()); + if (friction_wheel_count_ == 4) { + log_text = fmt::format( + "{},{},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f}", *initial_speed_, + (int)velocities.size(), // + velocity_, excellence_rate_, pass_rate_, range_, range2_, velocity_max, + velocity_min); + } - // log_velocity = fmt::format("{:.3f}", *initial_speed_); - // std::ofstream outFile("shoot_recorder", std::ios::app); - // if (outFile.is_open()) { - // outFile << log_velocity << std::endl; - // outFile.close(); - // } + log_stream_ << log_text << std::endl; + RCLCPP_INFO(get_logger(), "%s", log_text.c_str()); // 记录每次射击的初速度和统计数据 + + log_velocity = fmt::format("{:.3f}", *initial_speed_); + std::ofstream outFile("shoot_recorder", std::ios::app); + if (outFile.is_open()) { + outFile << log_velocity << std::endl; + outFile.close(); + } - // last_shoot_timestamp_ = *shoot_timestamp_; + last_shoot_timestamp_ = *shoot_timestamp_; } private: From 8eb58c31755f93d4f3cbf3f6fd3df902d2785da6 Mon Sep 17 00:00:00 2001 From: zhzy-star <2807406212@qq.com> Date: Mon, 23 Mar 2026 20:24:24 +0800 Subject: [PATCH 11/12] fixed putter controller --- .../rmcs_bringup/config/steering-hero.yaml | 95 +- rmcs_ws/src/rmcs_bringup/config/test.yaml | 58 + rmcs_ws/src/rmcs_core/CMakeLists.txt | 3 +- rmcs_ws/src/rmcs_core/plugins.xml | 5 +- .../pid/friction_wheel_pid_recorder.cpp | 166 +++ .../shooting/friction_wheel_controller.cpp | 3 + .../controller/shooting/putter_controller.cpp | 81 +- .../controller/shooting/shooting_recorder.cpp | 20 +- .../rmcs_core/src/hardware/steering-hero.cpp | 10 +- tools/pid_tuning/README.md | 134 ++ .../friction_wheel_pid_recorder.example.yaml | 15 + tools/pid_tuning/identify_plant.py | 724 ++++++++++ tools/pid_tuning/tune_pid.py | 1280 +++++++++++++++++ 13 files changed, 2518 insertions(+), 76 deletions(-) create mode 100644 rmcs_ws/src/rmcs_bringup/config/test.yaml create mode 100644 rmcs_ws/src/rmcs_core/src/controller/pid/friction_wheel_pid_recorder.cpp create mode 100644 tools/pid_tuning/README.md create mode 100644 tools/pid_tuning/friction_wheel_pid_recorder.example.yaml create mode 100644 tools/pid_tuning/identify_plant.py create mode 100644 tools/pid_tuning/tune_pid.py diff --git a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml index 3ebcf3148..d58c57453 100644 --- a/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml +++ b/rmcs_ws/src/rmcs_bringup/config/steering-hero.yaml @@ -25,7 +25,7 @@ rmcs_executor: - rmcs_core::controller::pid::PidController -> first_right_friction_velocity_pid_controller - rmcs_core::controller::pid::PidController -> second_left_friction_velocity_pid_controller - rmcs_core::controller::pid::PidController -> second_right_friction_velocity_pid_controller - # - rmcs_core::controller::shooting::ShootingRecorder -> shooting_recorder + - rmcs_core::controller::shooting::ShootingRecorder -> shooting_recorder - rmcs_core::controller::chassis::ChassisController -> chassis_controller - rmcs_core::controller::chassis::ChassisPowerController -> chassis_power_controller @@ -43,14 +43,15 @@ hero_hardware: board_serial_bottom_board_one: "D4-7973-19A9-EA40-4A3E-306F-10F9" board_serial_bottom_board_two: "D4-3674-7174-8768-879E-E44A-3931" bottom_yaw_motor_zero_point: 52508 - pitch_motor_zero_point: 63917 + pitch_motor_zero_point: 40651 #35701 top_yaw_motor_zero_point: 58744 viewer_motor_zero_point: 3030 external_imu_port: /dev/ttyUSB0 - left_front_zero_point: 4438 - right_front_zero_point: 7167 - left_back_zero_point: 370 - right_back_zero_point: 5156 + bullet_feeder_motor_zero_point: 15111 + left_front_zero_point: 4516 + right_front_zero_point: 7190 + left_back_zero_point: 3841 + right_back_zero_point: 5159 # left_front_zero_point: 3473 # left_back_zero_point: 6124 # right_back_zero_point: 6157 @@ -59,24 +60,35 @@ hero_hardware: value_broadcaster: ros__parameters: forward_list: - - /chassis/left_front_steering/angle - - /chassis/left_back_steering/angle - - /chassis/right_back_steering/angle - - /chassis/right_front_steering/angle - - /chassis/left_front_steering/control_torque - - /chassis/left_back_steering/control_torque - - /chassis/right_back_steering/control_torque - - /chassis/right_front_steering/control_torque - - /gimbal/pitch/control_torque - - /gimbal/pitch/velocity - - /gimbal/pitch/control_velocity - - /gimbal/pitch/control_angle_error - - /chassis/climber/left_back_motor/torque - - /chassis/climber/right_back_motor/torque - - /chassis/climber/left_back_motor/velocity - - /chassis/climber/right_back_motor/velocity - - /chassis/climber/right_back_motor/control_torque - - /chassis/climber/left_back_motor/control_torque + - /gimbal/putter/velocity + - /gimbal/first_left_friction/velocity + - /gimbal/first_second_friction/velocity + - /gimbal/second_left_friction/velocity + - /gimbal/second_right_friction/velocity + - /gimbal/first_left_friction/control_torque + - /gimbal/first_second_friction/control_torque + - /gimbal/second_left_friction/control_torque + - /gimbal/second_right_friction/control_torque + # - /chassis/left_front_steering/angle + # - /chassis/left_back_steering/angle + # - /chassis/right_back_steering/angle + # - /chassis/right_front_steering/angle + # - /chassis/left_front_steering/control_torque + # - /chassis/left_back_steering/control_torque + # - /chassis/right_back_steering/control_torque + # - /chassis/right_front_steering/control_torque + # - /gimbal/pitch/control_torque + # - /gimbal/pitch/velocity + # - /gimbal/pitch/control_velocity + # - /gimbal/pitch/control_angle_error + # - /gimbal/bullet_feeder/phase_zero + # - /gimbal/bullet_feeder/alignment_error + # - /chassis/climber/left_back_motor/torque + # - /chassis/climber/right_back_motor/torque + # - /chassis/climber/left_back_motor/velocity + # - /chassis/climber/right_back_motor/velocity + # - /chassis/climber/right_back_motor/control_torque + # - /chassis/climber/left_back_motor/control_torque climber_controller: ros__parameters: @@ -136,7 +148,7 @@ pitch_angle_pid_controller: ros__parameters: measurement: /gimbal/pitch/control_angle_error control: /gimbal/pitch/control_velocity - kp: 28.0 #28.00 + kp: 30.0 #28.00 ki: 0.0 kd: 0.6 #0.6 @@ -145,9 +157,9 @@ pitch_velocity_pid_controller: measurement: /gimbal/pitch/velocity_imu setpoint: /gimbal/pitch/control_velocity control: /gimbal/pitch/control_torque - kp: 45.0 #45.00 + kp: 15.0 #45.00 ki: 0.00 - kd: 1.0 #1.00 + kd: 2.0 #1.00 gimbal_player_viewer_controller: ros__parameters: @@ -170,10 +182,10 @@ friction_wheel_controller: - /gimbal/second_left_friction - /gimbal/second_right_friction friction_velocities: - - 450.00 - - 450.00 - - 535.00 - - 535.00 + - 375.00 + - 375.00 + - 520.00 + - 520.00 friction_soft_start_stop_time: 1.0 heat_controller: @@ -183,44 +195,45 @@ heat_controller: shooting_recorder: ros__parameters: - friction_wheel_count: 2 - log_mode: 2 # 1: trigger, 2: timing + friction_wheel_count: 4 + aim_velocity: 11.8 + log_mode: 1 # 1: trigger, 2: timing first_left_friction_velocity_pid_controller: ros__parameters: measurement: /gimbal/first_left_friction/velocity setpoint: /gimbal/first_left_friction/control_velocity control: /gimbal/first_left_friction/control_torque - kp: 0.003436926 + kp: 0.0016 ki: 0.00 - kd: 0.009373434 + kd: 0.001 first_right_friction_velocity_pid_controller: ros__parameters: measurement: /gimbal/first_right_friction/velocity setpoint: /gimbal/first_right_friction/control_velocity control: /gimbal/first_right_friction/control_torque - kp: 0.003436926 + kp: 0.0016 ki: 0.00 - kd: 0.009373434 + kd: 0.001 second_left_friction_velocity_pid_controller: ros__parameters: measurement: /gimbal/second_left_friction/velocity setpoint: /gimbal/second_left_friction/control_velocity control: /gimbal/second_left_friction/control_torque - kp: 0.003436926 + kp: 0.001 ki: 0.00 - kd: 0.009373434 + kd: 0.001 second_right_friction_velocity_pid_controller: ros__parameters: measurement: /gimbal/second_right_friction/velocity setpoint: /gimbal/second_right_friction/control_velocity control: /gimbal/second_right_friction/control_torque - kp: 0.003436926 + kp: 0.001 ki: 0.00 - kd: 0.009373434 + kd: 0.00 steering_wheel_controller: ros__parameters: diff --git a/rmcs_ws/src/rmcs_bringup/config/test.yaml b/rmcs_ws/src/rmcs_bringup/config/test.yaml new file mode 100644 index 000000000..ebf295ae7 --- /dev/null +++ b/rmcs_ws/src/rmcs_bringup/config/test.yaml @@ -0,0 +1,58 @@ +rmcs_executor: + ros__parameters: + update_rate: 1000.0 + components: + - rmcs_core::hardware::SteeringHero -> hero_hardware + # - rmcs_core::controller::pid::PidController -> test_pid_controller + + # - rmcs_core::referee::Status -> referee_status + # - rmcs_core::referee::command::Interaction -> referee_interaction + # - rmcs_core::referee::command::interaction::Ui -> referee_ui + # - rmcs_core::referee::app::ui::Hero -> referee_ui_hero + # - rmcs_core::referee::Command -> referee_command + + # - rmcs_core::controller::gimbal::HeroGimbalController -> gimbal_controller + # - rmcs_core::controller::gimbal::DualYawController -> dual_yaw_controller + # - rmcs_core::controller::pid::ErrorPidController -> pitch_angle_pid_controller + # - rmcs_core::controller::pid::PidController -> pitch_velocity_pid_controller + + # - rmcs_core::controller::gimbal::PlayerViewer -> gimbal_player_viewer_controller + # - rmcs_core::controller::pid::ErrorPidController -> viewer_angle_pid_controller + + # - rmcs_core::controller::shooting::FrictionWheelController -> friction_wheel_controller + # - rmcs_core::controller::shooting::HeatController -> heat_controller + # - rmcs_core::controller::shooting::PutterController -> bullet_feeder_controller + # - rmcs_core::controller::pid::PidController -> first_left_friction_velocity_pid_controller + # - rmcs_core::controller::pid::PidController -> first_right_friction_velocity_pid_controller + # - rmcs_core::controller::pid::PidController -> second_left_friction_velocity_pid_controller + # - rmcs_core::controller::pid::PidController -> second_right_friction_velocity_pid_controller + # - rmcs_core::controller::shooting::ShootingRecorder -> shooting_recorder + + # - rmcs_core::controller::chassis::ChassisController -> chassis_controller + # - rmcs_core::controller::chassis::ChassisPowerController -> chassis_power_controller + # - rmcs_core::controller::chassis::SteeringWheelController -> steering_wheel_controller + # - rmcs_core::controller::chassis::ChassisClimberController -> climber_controller + + # - rmcs_auto_aim::AutoAimInitializer -> auto_aim_initializer + # - rmcs_auto_aim::AutoAimController -> auto_aim_controller + + # - rmcs_core::broadcaster::ValueBroadcaster -> value_broadcaster + +hero_hardware: + ros__parameters: + board_serial_bottom_board_two: "D4-3674-7174-8768-879E-E44A-3931" + test: 6.0 + bottom_yaw_motor_zero_point: 52508 + pitch_motor_zero_point: 35701 + top_yaw_motor_zero_point: 58744 + viewer_motor_zero_point: 3030 + external_imu_port: /dev/ttyUSB0 + bullet_feeder_motor_zero_point: 15111 + left_front_zero_point: 4433 + right_front_zero_point: 7177 + left_back_zero_point: 6919 + right_back_zero_point: 5120 + # left_front_zero_point: 3473 + # left_back_zero_point: 6124 + # right_back_zero_point: 6157 + # right_front_zero_point: 2723 diff --git a/rmcs_ws/src/rmcs_core/CMakeLists.txt b/rmcs_ws/src/rmcs_core/CMakeLists.txt index c576aca1d..579561c30 100644 --- a/rmcs_ws/src/rmcs_core/CMakeLists.txt +++ b/rmcs_ws/src/rmcs_core/CMakeLists.txt @@ -17,7 +17,8 @@ include(FetchContent) set(BUILD_STATIC_LIBRMCS ON CACHE BOOL "Build static librmcs SDK" FORCE) FetchContent_Declare( librmcs - URL file:///workspaces/RMCS/develop_ws/librmcs-sdk-src-3.0.1-0.dev.4.gbaf538b.zip + URL https://github.com/Alliance-Algorithm/librmcs/releases/download/v3.1.0b1/librmcs-sdk-src-3.1.0-beta.1.zip + URL_HASH SHA256=ec68a8b0e9441bd9e35b859139b3875c559944f7c0ecb4a1539bcdc75442fec4 DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) FetchContent_MakeAvailable(librmcs) diff --git a/rmcs_ws/src/rmcs_core/plugins.xml b/rmcs_ws/src/rmcs_core/plugins.xml index 9df7668b7..4ab6d82b1 100644 --- a/rmcs_ws/src/rmcs_core/plugins.xml +++ b/rmcs_ws/src/rmcs_core/plugins.xml @@ -62,6 +62,9 @@ Test plugin. + + Recorder for friction wheel PID tuning. + Test plugin. @@ -125,4 +128,4 @@ Test plugin. - \ No newline at end of file + diff --git a/rmcs_ws/src/rmcs_core/src/controller/pid/friction_wheel_pid_recorder.cpp b/rmcs_ws/src/rmcs_core/src/controller/pid/friction_wheel_pid_recorder.cpp new file mode 100644 index 000000000..366f66b62 --- /dev/null +++ b/rmcs_ws/src/rmcs_core/src/controller/pid/friction_wheel_pid_recorder.cpp @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace rmcs_core::controller::pid { + +class FrictionWheelPidRecorder + : public rmcs_executor::Component + , public rclcpp::Node { +public: + FrictionWheelPidRecorder() + : Node( + get_component_name(), + rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)) { + const auto wheels = + declare_parameter>("wheels", std::vector{}); + if (wheels.empty()) + throw std::runtime_error("Parameter \"wheels\" must not be empty"); + + min_setpoint_abs_ = declare_parameter("min_setpoint_abs", 10.0); + + const auto flush_every_n_samples = declare_parameter("flush_every_n_samples", 100); + if (flush_every_n_samples <= 0) + throw std::runtime_error("Parameter \"flush_every_n_samples\" must be positive"); + flush_every_n_samples_ = static_cast(flush_every_n_samples); + + const auto output_dir = std::filesystem::path{ + declare_parameter("output_dir", "/tmp/friction_pid_logs")}; + const auto output_name = declare_parameter("output_name", ""); + + wheel_count_ = wheels.size(); + wheel_ids_ = std::make_unique(wheel_count_); + setpoints_ = std::make_unique[]>(wheel_count_); + measurements_ = std::make_unique[]>(wheel_count_); + control_torques_ = std::make_unique[]>(wheel_count_); + run_ids_ = std::make_unique(wheel_count_); + sample_indices_ = std::make_unique(wheel_count_); + previously_enabled_ = std::make_unique(wheel_count_); + + for (size_t i = 0; i < wheel_count_; ++i) { + wheel_ids_[i] = wheel_id_from_topic(wheels[i]); + register_input(wheels[i] + "/control_velocity", setpoints_[i]); + register_input(wheels[i] + "/velocity", measurements_[i]); + register_input(wheels[i] + "/control_torque", control_torques_[i]); + + run_ids_[i] = 0; + sample_indices_[i] = 0; + previously_enabled_[i] = false; + } + + std::filesystem::create_directories(output_dir); + log_file_path_ = output_dir / resolve_output_name(output_name); + log_stream_.open(log_file_path_); + if (!log_stream_.is_open()) + throw std::runtime_error( + "Failed to open recorder output file: " + log_file_path_.string()); + + log_stream_.setf(std::ios::fixed); + log_stream_.precision(9); + log_stream_ + << "timestamp_us,wheel_id,run_id,sample_idx,setpoint_velocity,measured_velocity," + "control_torque,enabled\n"; + + RCLCPP_INFO(get_logger(), "FrictionWheelPidRecorder writing to %s", log_file_path_.c_str()); + } + + ~FrictionWheelPidRecorder() override { + if (log_stream_.is_open()) { + log_stream_.flush(); + log_stream_.close(); + } + } + + void update() override { + const auto timestamp_us = now_timestamp_us(); + + for (size_t i = 0; i < wheel_count_; ++i) { + const bool enabled = should_record(i); + if (enabled && !previously_enabled_[i]) { + ++run_ids_[i]; + sample_indices_[i] = 0; + } + + if (enabled) { + log_stream_ << timestamp_us << ',' << wheel_ids_[i] << ',' << run_ids_[i] << ',' + << sample_indices_[i]++ << ',' << *setpoints_[i] << ',' + << *measurements_[i] << ',' << *control_torques_[i] << ",1\n"; + if (++unflushed_samples_ >= flush_every_n_samples_) { + log_stream_.flush(); + unflushed_samples_ = 0; + } + } + + previously_enabled_[i] = enabled; + } + } + +private: + static std::string wheel_id_from_topic(std::string_view topic) { + auto pos = topic.find_last_of('/'); + if (pos == std::string_view::npos || pos + 1 >= topic.size()) + return std::string(topic); + return std::string(topic.substr(pos + 1)); + } + + static uint64_t now_timestamp_us() { + const auto now = std::chrono::steady_clock::now().time_since_epoch(); + return static_cast( + std::chrono::duration_cast(now).count()); + } + + static std::string resolve_output_name(const std::string& configured_name) { + if (!configured_name.empty()) + return configured_name; + + const auto now = std::chrono::system_clock::now().time_since_epoch(); + const auto ms = std::chrono::duration_cast(now).count(); + return "friction_wheel_pid_" + std::to_string(ms) + ".csv"; + } + + bool should_record(size_t index) const { + if (!setpoints_[index].ready() || !measurements_[index].ready() + || !control_torques_[index].ready()) + return false; + + const auto setpoint = *setpoints_[index]; + const auto measurement = *measurements_[index]; + const auto control_torque = *control_torques_[index]; + + return std::isfinite(setpoint) && std::isfinite(measurement) + && std::isfinite(control_torque) && std::abs(setpoint) >= min_setpoint_abs_; + } + + size_t wheel_count_ = 0; + double min_setpoint_abs_ = 10.0; + size_t flush_every_n_samples_ = 100; + size_t unflushed_samples_ = 0; + + std::filesystem::path log_file_path_; + std::ofstream log_stream_; + + std::unique_ptr wheel_ids_; + std::unique_ptr[]> setpoints_; + std::unique_ptr[]> measurements_; + std::unique_ptr[]> control_torques_; + std::unique_ptr run_ids_; + std::unique_ptr sample_indices_; + std::unique_ptr previously_enabled_; +}; + +} // namespace rmcs_core::controller::pid + +#include + +PLUGINLIB_EXPORT_CLASS( + rmcs_core::controller::pid::FrictionWheelPidRecorder, rmcs_executor::Component) diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp index 879d9e534..5aecea6b3 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/friction_wheel_controller.cpp @@ -90,6 +90,9 @@ class FrictionWheelController last_switch_left_ = switch_left; last_keyboard_ = keyboard; } + if (!friction_enabled_) { + reset_all_controls(); + } } private: diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp index 750326954..b1b621c13 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp @@ -1,9 +1,12 @@ #include + #include #include + #include #include + #include #include #include @@ -89,12 +92,12 @@ class PutterController register_output("/gimbal/shooter/mode", shoot_mode_, rmcs_msgs::ShootMode::SINGLE); register_output("/gimbal/shoot/delay_ms", shoot_delay_ms_, nan_); - initialize_shoot_delay_log(); + // initialize_shoot_delay_log(); } ~PutterController() { - if (shoot_delay_log_stream_.is_open()) - shoot_delay_log_stream_.close(); + /* if (shoot_delay_log_stream_.is_open()) + shoot_delay_log_stream_.close(); */ } void update() override { @@ -102,19 +105,20 @@ class PutterController const auto switch_left = *switch_left_; const auto mouse = *mouse_; const auto keyboard = *keyboard_; - const bool bullet_fired_now = *bullet_fired_; + // const bool bullet_fired_now = *bullet_fired_; using namespace rmcs_msgs; if ((switch_left == Switch::UNKNOWN || switch_right == Switch::UNKNOWN) || (switch_left == Switch::DOWN && switch_right == Switch::DOWN)) { reset_all_controls(); - update_shoot_delay_measurement(bullet_fired_now); + // update_shoot_delay_measurement(bullet_fired_now); return; } // 推杆已初始化后的正常控制流程 if (putter_initialized) { + // 供弹轮卡弹保护冷却期间的处理 if (bullet_feeder_cool_down_ > 0) { bullet_feeder_cool_down_--; @@ -126,8 +130,18 @@ class PutterController *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update(velocity_err); if (!bullet_feeder_cool_down_) { - RCLCPP_INFO(get_logger(), "Jamming Solved, Retrying..."); - set_preloaded(); + if (*photoelectric_sensor_status_) { + RCLCPP_INFO( + get_logger(), "Photoelectric Sensor Triggered during Jam Protection"); + *bullet_feeder_control_torque_ = 0.; + bullet_feeder_cool_down_ = 0; + // 认为是满链状态,直接设置为预装弹完成 + set_preloaded(); + } else { + RCLCPP_INFO(get_logger(), "Jamming Solved, Retrying..."); + set_preloading(); + // 认为是卡弹状态,继续保持在后退位置直到冷却结束 + } } } else { // 正常运行模式:摩擦轮就绪时才允许发射 @@ -167,6 +181,13 @@ class PutterController bullet_feeder_velocity_pid_.update(velocity_err); update_jam_detection(); + } else if (shoot_stage_ == ShootStage::SHOOTING) { + // 发射状态:供弹盘不发力 + if (!last_shoot_flag_) { + bullet_feeder_control_angle_ = *bullet_feeder_angle_; + last_shoot_flag_ = true; + } + *bullet_feeder_control_torque_ = 0; } else { // 其他状态:角度环保持角度不变防止弹链退弹 double velocity_err = @@ -179,8 +200,10 @@ class PutterController if (shoot_stage_ == ShootStage::SHOOTING) { // 发射状态:检测子弹是否发出 - if (*bullet_fired_ - || *putter_angle_ - putter_startpoint >= putter_stroke_) { + if (*putter_angle_ - putter_startpoint >= putter_stroke_) { + if (!shooted){ + RCLCPP_INFO(get_logger(),"%f", *putter_angle_); + } shooted = true; } @@ -193,6 +216,7 @@ class PutterController *putter_control_torque_ = 0.; set_updating(); shooted = false; + last_shoot_flag_ = false; } else { *putter_control_torque_ = putter_return_velocity_pid_.update(-80. - *putter_velocity_); @@ -218,7 +242,7 @@ class PutterController update_putter_jam_detection(); } - update_shoot_delay_measurement(bullet_fired_now); + // update_shoot_delay_measurement(bullet_fired_now); // 保存当前状态用于下次比较 last_switch_right_ = switch_right; @@ -233,6 +257,7 @@ class PutterController last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; last_mouse_ = rmcs_msgs::Mouse::zero(); last_keyboard_ = rmcs_msgs::Keyboard::zero(); + bullet_feeder_reset(); overdrive_mode_ = false; @@ -252,7 +277,6 @@ class PutterController *putter_control_torque_ = nan_; last_preload_flag_ = false; - last_photoelectric_sensor_status_ = false; bullet_feeder_faulty_count_ = 0; bullet_feeder_cool_down_ = 0; @@ -302,7 +326,8 @@ class PutterController void update_putter_jam_detection() { if ((*putter_control_torque_ > -0.03 && shoot_stage_ == ShootStage::PRELOADING) - || (*putter_control_torque_ < 0.05 && shoot_stage_ == ShootStage::SHOOTING) + || ((*putter_control_torque_ < 0.08) + && shoot_stage_ == ShootStage::SHOOTING) || std::isnan(*putter_control_torque_)) { putter_faulty_count_ = 0; return; @@ -319,11 +344,21 @@ class PutterController putter_startpoint = *putter_angle_; } else { // 发射状态下检测到堵转:认为子弹已发出 + RCLCPP_INFO(get_logger(),"dd"); shooted = true; } } } + void wait_bullet_ready() { + if (bullet_ready_count_ >= 600) { + bullet_ready_count_ = 0; + set_preloading(); + } else { + bullet_ready_count_++; + } + } + void enter_jam_protection() { // 设置目标角度为当前角度后退 60 度(一格) bullet_feeder_control_angle_ = *bullet_feeder_angle_ - bullet_feeder_angle_per_bullet_; @@ -333,6 +368,16 @@ class PutterController RCLCPP_INFO(get_logger(), "Jammed!"); } + void bullet_feeder_reset() {} + + static double normalize_feeder_phase(double angle) { + auto normalized = std::fmod(angle, bullet_feeder_angle_per_bullet_); + if (normalized < 0.0) + normalized += bullet_feeder_angle_per_bullet_; + return normalized; + } + + /* 记录发射延迟的测量逻辑: void update_shoot_delay_measurement(bool bullet_fired_now) { const bool bullet_fired_rising_edge = bullet_fired_now && !last_bullet_fired_; @@ -393,14 +438,7 @@ class PutterController shoot_delay_log_stream_.flush(); } - void wait_bullet_ready() { - if (bullet_ready_count_ >= 600) { - bullet_ready_count_ = 0; - set_preloading(); - } else { - bullet_ready_count_++; - } - } + */ static constexpr double nan_ = std::numeric_limits::quiet_NaN(); ///< 非数值常量 static constexpr double inf_ = std::numeric_limits::infinity(); ///< 无穷大常量 @@ -412,9 +450,9 @@ class PutterController static constexpr double max_bullet_feeder_control_torque_ = 0.1; static constexpr double bullet_feeder_angle_per_bullet_ = 2 * std::numbers::pi / 6; + static constexpr double bullet_feeder_offset = 0.1; InputInterface photoelectric_sensor_status_; - bool last_photoelectric_sensor_status_; InputInterface bullet_fired_; bool shooted{false}; @@ -439,6 +477,7 @@ class PutterController InputInterface control_bullet_allowance_limited_by_heat_; bool last_preload_flag_ = false; + bool last_shoot_flag_ = false; bool putter_initialized = false; int putter_faulty_count_ = 0; diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp index 1e2039cd5..80358510b 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/shooting_recorder.cpp @@ -26,16 +26,16 @@ class ShootingRecorder register_input("/referee/shooter/initial_speed", initial_speed_); register_input("/referee/shooter/shoot_timestamp", shoot_timestamp_); - if (friction_wheel_count_ == 2) { - const auto topic = std::array{ - "/gimbal/first_left_friction/control_velocity", - "/gimbal/first_right_friction/control_velocity", - }; - for (int i = 0; i < 2; i++) - register_input(topic[i], friction_wheels_velocity_[i]); - register_input("/gimbal/first_left_friction/velocity", friction_velocities_[0]); - register_input("/gimbal/first_right_friction/velocity", friction_velocities_[1]); - } + // if (friction_wheel_count_ == 4) { + // const auto topic = std::array{ + // "/gimbal/first_left_friction/control_velocity", + // "/gimbal/first_right_friction/control_velocity", + // }; + // for (int i = 0; i < 2; i++) + // register_input(topic[i], friction_wheels_velocity_[i]); + // register_input("/gimbal/first_left_friction/velocity", friction_velocities_[0]); + // register_input("/gimbal/first_right_friction/velocity", friction_velocities_[1]); + // } using namespace std::chrono; auto now = high_resolution_clock::now(); diff --git a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp index 976be1d23..0f16692da 100644 --- a/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp +++ b/rmcs_ws/src/rmcs_core/src/hardware/steering-hero.cpp @@ -92,6 +92,9 @@ class SteeringHero RCLCPP_INFO( get_logger(), "[gimbal calibration] New top yaw offset: %ld", top_board_->gimbal_top_yaw_motor_.calibrate_zero_point()); + RCLCPP_INFO( + get_logger(), "[gimbal calibration] New bullet feeder offset: %ld", + top_board_->gimbal_bullet_feeder_.calibrate_zero_point()); RCLCPP_INFO( get_logger(), "[chassis calibration] left front steering offset: %d", bottom_board_one_->chassis_steering_motors_[0].calibrate_zero_point()); @@ -159,6 +162,9 @@ class SteeringHero .set_reduction_ratio(1.)); gimbal_bullet_feeder_.configure( device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10} + .set_encoder_zero_point( + static_cast( + steering_hero.get_parameter("bullet_feeder_motor_zero_point").as_int())) .set_reversed() .enable_multi_turn_angle()); putter_motor_.configure( @@ -265,7 +271,7 @@ class SteeringHero }); builder.gpio_digital_read({ - .channel = 7, + .channel = 1, .falling_edge = true, }); } @@ -305,7 +311,7 @@ class SteeringHero void gpio_digital_read_result_callback( const librmcs::data::GpioDigitalDataView& data) override { *photoelectric_sensor_status_ = false; - if (data.channel == 7) { + if (data.channel == 1) { *photoelectric_sensor_status_ = true; RCLCPP_INFO(logger_, "trigger!"); } diff --git a/tools/pid_tuning/README.md b/tools/pid_tuning/README.md new file mode 100644 index 000000000..eab5cd214 --- /dev/null +++ b/tools/pid_tuning/README.md @@ -0,0 +1,134 @@ +# Friction Wheel PID Tuning + +This directory contains friction wheel PID tuning utilities and configuration snippets. + +## Recorder + +The recorder plugin class is: + +```text +rmcs_core::controller::pid::FrictionWheelPidRecorder +``` + +It records only when: + +- `control_velocity` is finite +- `velocity` is finite +- `control_torque` is finite +- `abs(control_velocity) >= min_setpoint_abs` + +Raw CSV columns: + +```text +timestamp_us,wheel_id,run_id,sample_idx,setpoint_velocity,measured_velocity,control_torque,enabled +``` + +Default output directory: + +```text +/tmp/friction_pid_logs +``` + +See `friction_wheel_pid_recorder.example.yaml` for a minimal configuration snippet. + +## Identification + +`identify_plant.py` estimates a discrete first-order-plus-dead-time model from the recorder CSV: + +```text +y[k+1] = a * y[k] + b * u[k-d] + c +``` + +Where: + +- `u` is `control_torque` +- `y` is `measured_velocity` +- `d` is the pure delay in samples + +The script scans candidate delays, fits each candidate with least squares, and writes: + +- `identification_summary.csv` +- one `*_model.json` per `wheel_id/run_id` +- one `*_fit.csv` per `wheel_id/run_id` + +Example: + +```bash +python3 tools/pid_tuning/identify_plant.py /tmp/friction_pid_logs/friction_wheel_pid_xxx.csv \ + --output-dir /tmp/friction_pid_identify +``` + +Common filters: + +```bash +python3 tools/pid_tuning/identify_plant.py /tmp/friction_pid_logs/friction_wheel_pid_xxx.csv \ + --wheel-id first_left_friction \ + --run-id 1 +``` + +Key options: + +- `--sample-time-ms`: fallback sample interval when timestamps are unavailable +- `--max-delay-ms`: maximum delay search range +- `--min-samples`: minimum segment length +- `--allow-unphysical`: export the best fit even if `a` or `b` is not physically plausible + +## Tuning + +`tune_pid.py` tunes only `kp` and `kd`. `ki` is always fixed to `0` so the offline controller matches the current friction wheel constraint. + +The simulated controller uses the same discrete update rule as `PidCalculator`: + +```text +u[k] = clamp(kp * e[k] + kd * (e[k] - e[k-1])) +``` + +Where: + +- `e[k] = setpoint_velocity[k] - measured_velocity[k]` +- `ki = 0` +- plant model comes from `identify_plant.py` + +Supported optimization methods: + +- `hybrid` (default): GA first, then GP-like Bayesian optimization refinement +- `ga`: genetic algorithm only +- `bo`: GP-like Bayesian optimization only + +Trace-driven tuning with the original recorded setpoint profile: + +```bash +python3 tools/pid_tuning/tune_pid.py /tmp/friction_pid_identify/first_left_friction_run_1_model.json \ + --trace-csv /tmp/friction_pid_logs/friction_wheel_pid_xxx.csv \ + --output-dir /tmp/friction_pid_tuning +``` + +Synthetic startup step tuning: + +```bash +python3 tools/pid_tuning/tune_pid.py /tmp/friction_pid_identify/first_left_friction_run_1_model.json \ + --step-setpoint 5000 \ + --output-limit 12 \ + --method hybrid +``` + +Batch tuning from an identification summary: + +```bash +python3 tools/pid_tuning/tune_pid.py /tmp/friction_pid_identify/identification_summary.csv \ + --trace-csv /tmp/friction_pid_logs/friction_wheel_pid_xxx.csv +``` + +Outputs: + +- `tuning_summary.csv` +- one `tuning_result.json` per `wheel_id/run_id` +- one `candidates.csv` per `wheel_id/run_id` +- one `best_response.csv` per `wheel_id/run_id` + +Common options: + +- `--seed-kp/--seed-kd`: baseline gains used for comparison +- `--kp-min/--kp-max`, `--kd-min/--kd-max`: search bounds +- `--output-limit` or `--output-min/--output-max`: controller output clamp +- `--trace-scope wheel`: reuse all runs of the same wheel instead of exact `run_id` diff --git a/tools/pid_tuning/friction_wheel_pid_recorder.example.yaml b/tools/pid_tuning/friction_wheel_pid_recorder.example.yaml new file mode 100644 index 000000000..8a7091123 --- /dev/null +++ b/tools/pid_tuning/friction_wheel_pid_recorder.example.yaml @@ -0,0 +1,15 @@ +rmcs_executor: + ros__parameters: + components: + - rmcs_core::controller::pid::FrictionWheelPidRecorder -> friction_wheel_pid_recorder + +friction_wheel_pid_recorder: + ros__parameters: + wheels: + - /gimbal/first_left_friction + - /gimbal/first_right_friction + - /gimbal/second_left_friction + - /gimbal/second_right_friction + output_dir: /tmp/friction_pid_logs + min_setpoint_abs: 10.0 + flush_every_n_samples: 100 diff --git a/tools/pid_tuning/identify_plant.py b/tools/pid_tuning/identify_plant.py new file mode 100644 index 000000000..2f50af2ea --- /dev/null +++ b/tools/pid_tuning/identify_plant.py @@ -0,0 +1,724 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import csv +import json +import math +import re +import sys +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Iterable + +import numpy as np + + +REQUIRED_COLUMNS = { + "timestamp_us", + "wheel_id", + "run_id", + "sample_idx", + "setpoint_velocity", + "measured_velocity", + "control_torque", +} + + +@dataclass(frozen=True) +class Sample: + timestamp_us: int + wheel_id: str + run_id: int + sample_idx: int + setpoint_velocity: float + measured_velocity: float + control_torque: float + + +@dataclass(frozen=True) +class Segment: + wheel_id: str + run_id: int + timestamp_us: np.ndarray + sample_idx: np.ndarray + setpoint_velocity: np.ndarray + measured_velocity: np.ndarray + control_torque: np.ndarray + dropped_rows: int + inferred_dt_s: float + sample_jitter_ratio: float + missing_samples: int + + +@dataclass(frozen=True) +class CandidateResult: + delay_samples: int + a: float + b: float + c: float + rollout_rmse: float + one_step_rmse: float + mae: float + fit_percent: float + physically_plausible: bool + predicted_velocity: np.ndarray + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Estimate a first-order-plus-dead-time plant model for friction wheel velocity loops " + "from recorder CSV." + ) + ) + parser.add_argument("input_csv", help="CSV produced by FrictionWheelPidRecorder") + parser.add_argument( + "--output-dir", + help="Directory for summary/model files. Default: /tmp/friction_pid_identify/", + ) + parser.add_argument("--wheel-id", help="Only process one wheel_id") + parser.add_argument("--run-id", type=int, help="Only process one run_id") + parser.add_argument( + "--sample-time-ms", + type=float, + default=1.0, + help="Fallback sample time in milliseconds when timestamps are missing or invalid", + ) + parser.add_argument( + "--max-delay-ms", + type=float, + default=25.0, + help="Maximum pure delay to scan in milliseconds", + ) + parser.add_argument( + "--max-delay-samples", + type=int, + help="Maximum pure delay to scan in samples. Overrides --max-delay-ms", + ) + parser.add_argument( + "--min-samples", + type=int, + default=80, + help="Minimum samples required for one segment", + ) + parser.add_argument( + "--min-speed-span", + type=float, + default=30.0, + help="Minimum measured velocity span required to consider a segment sufficiently excited", + ) + parser.add_argument( + "--min-torque-span", + type=float, + default=0.05, + help="Minimum control torque span required to consider a segment sufficiently excited", + ) + parser.add_argument( + "--allow-unphysical", + action="store_true", + help="Allow selecting candidates outside the expected 00 range if needed", + ) + parser.add_argument( + "--quiet", + action="store_true", + help="Only print output paths and fatal errors", + ) + return parser.parse_args() + + +def parse_float(value: str) -> float: + number = float(value) + if not math.isfinite(number): + raise ValueError("non-finite number") + return number + + +def parse_int(value: str) -> int: + return int(value) + + +def default_output_dir() -> Path: + stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + return Path("/tmp/friction_pid_identify") / stamp + + +def log(message: str, quiet: bool = False) -> None: + if not quiet: + print(message) + + +def load_samples(input_csv: Path, wheel_id: str | None, run_id: int | None) -> list[Sample]: + with input_csv.open("r", newline="") as handle: + reader = csv.DictReader(handle) + if reader.fieldnames is None: + raise ValueError("input CSV has no header") + + missing = REQUIRED_COLUMNS.difference(reader.fieldnames) + if missing: + raise ValueError(f"input CSV is missing columns: {', '.join(sorted(missing))}") + + samples: list[Sample] = [] + for row_index, row in enumerate(reader, start=2): + try: + enabled_value = row.get("enabled", "1").strip() + if enabled_value not in {"", "1", "true", "True", "TRUE"}: + continue + + sample = Sample( + timestamp_us=parse_int(row["timestamp_us"]), + wheel_id=row["wheel_id"].strip(), + run_id=parse_int(row["run_id"]), + sample_idx=parse_int(row["sample_idx"]), + setpoint_velocity=parse_float(row["setpoint_velocity"]), + measured_velocity=parse_float(row["measured_velocity"]), + control_torque=parse_float(row["control_torque"]), + ) + except Exception as exc: + raise ValueError(f"failed to parse row {row_index}: {exc}") from exc + + if wheel_id is not None and sample.wheel_id != wheel_id: + continue + if run_id is not None and sample.run_id != run_id: + continue + samples.append(sample) + + return samples + + +def group_samples(samples: Iterable[Sample]) -> dict[tuple[str, int], list[Sample]]: + grouped: dict[tuple[str, int], list[Sample]] = {} + for sample in samples: + grouped.setdefault((sample.wheel_id, sample.run_id), []).append(sample) + return grouped + + +def build_segment( + wheel_id: str, + run_id: int, + rows: list[Sample], + fallback_dt_s: float, +) -> Segment: + ordered = sorted(rows, key=lambda item: (item.sample_idx, item.timestamp_us)) + + filtered: list[Sample] = [] + dropped_rows = 0 + last_sample_idx: int | None = None + last_timestamp_us: int | None = None + for row in ordered: + if last_sample_idx is not None and row.sample_idx <= last_sample_idx: + dropped_rows += 1 + continue + if last_timestamp_us is not None and row.timestamp_us <= last_timestamp_us: + dropped_rows += 1 + continue + filtered.append(row) + last_sample_idx = row.sample_idx + last_timestamp_us = row.timestamp_us + + if not filtered: + raise ValueError(f"{wheel_id}/run{run_id}: no valid rows remain after ordering checks") + + timestamp_us = np.asarray([row.timestamp_us for row in filtered], dtype=np.int64) + sample_idx = np.asarray([row.sample_idx for row in filtered], dtype=np.int64) + setpoint_velocity = np.asarray([row.setpoint_velocity for row in filtered], dtype=np.float64) + measured_velocity = np.asarray([row.measured_velocity for row in filtered], dtype=np.float64) + control_torque = np.asarray([row.control_torque for row in filtered], dtype=np.float64) + + inferred_dt_s, sample_jitter_ratio = infer_sample_time(timestamp_us, fallback_dt_s) + missing_samples = count_missing_samples(sample_idx) + + return Segment( + wheel_id=wheel_id, + run_id=run_id, + timestamp_us=timestamp_us, + sample_idx=sample_idx, + setpoint_velocity=setpoint_velocity, + measured_velocity=measured_velocity, + control_torque=control_torque, + dropped_rows=dropped_rows, + inferred_dt_s=inferred_dt_s, + sample_jitter_ratio=sample_jitter_ratio, + missing_samples=missing_samples, + ) + + +def infer_sample_time(timestamp_us: np.ndarray, fallback_dt_s: float) -> tuple[float, float]: + if timestamp_us.size < 2: + return fallback_dt_s, 0.0 + + dt_us = np.diff(timestamp_us).astype(np.float64) + dt_us = dt_us[dt_us > 0.0] + if dt_us.size == 0: + return fallback_dt_s, 0.0 + + median_dt_us = float(np.median(dt_us)) + if not math.isfinite(median_dt_us) or median_dt_us <= 0.0: + return fallback_dt_s, 0.0 + + mad_us = float(np.median(np.abs(dt_us - median_dt_us))) + jitter_ratio = 0.0 if median_dt_us == 0.0 else mad_us / median_dt_us + return median_dt_us / 1e6, jitter_ratio + + +def count_missing_samples(sample_idx: np.ndarray) -> int: + if sample_idx.size < 2: + return 0 + diffs = np.diff(sample_idx) + return int(np.sum(np.clip(diffs - 1, 0, None))) + + +def excitation_ok(segment: Segment, min_speed_span: float, min_torque_span: float) -> tuple[bool, str]: + speed_span = percentile_span(segment.measured_velocity) + torque_span = percentile_span(segment.control_torque) + if speed_span < min_speed_span: + return False, f"measured velocity span too small ({speed_span:.3f} < {min_speed_span:.3f})" + if torque_span < min_torque_span: + return False, f"control torque span too small ({torque_span:.3f} < {min_torque_span:.3f})" + return True, "" + + +def percentile_span(values: np.ndarray) -> float: + if values.size == 0: + return 0.0 + return float(np.percentile(values, 95) - np.percentile(values, 5)) + + +def estimate_segment( + segment: Segment, + max_delay_samples: int, + allow_unphysical: bool, +) -> CandidateResult: + if segment.measured_velocity.size < 3: + raise ValueError(f"{segment.wheel_id}/run{segment.run_id}: too few samples to fit") + + best_result: CandidateResult | None = None + best_unphysical: CandidateResult | None = None + + for delay_samples in range(max_delay_samples + 1): + candidate = fit_delay_candidate(segment.measured_velocity, segment.control_torque, delay_samples) + if candidate is None: + continue + + if candidate.physically_plausible: + if best_result is None or compare_candidates(candidate, best_result): + best_result = candidate + elif best_unphysical is None or compare_candidates(candidate, best_unphysical): + best_unphysical = candidate + + if best_result is not None: + return best_result + if allow_unphysical and best_unphysical is not None: + return best_unphysical + if best_unphysical is not None: + raise ValueError( + f"{segment.wheel_id}/run{segment.run_id}: only unphysical candidates found; " + "rerun with --allow-unphysical to inspect them" + ) + raise ValueError(f"{segment.wheel_id}/run{segment.run_id}: no candidate could be fitted") + + +def fit_delay_candidate(y: np.ndarray, u: np.ndarray, delay_samples: int) -> CandidateResult | None: + sample_count = y.size + if sample_count - delay_samples < 3: + return None + + k = np.arange(delay_samples, sample_count - 1, dtype=np.int64) + if k.size < 3: + return None + + x = np.column_stack((y[k], u[k - delay_samples], np.ones(k.size, dtype=np.float64))) + target = y[k + 1] + + theta, *_ = np.linalg.lstsq(x, target, rcond=None) + a, b, c = (float(theta[0]), float(theta[1]), float(theta[2])) + if not all(math.isfinite(value) for value in (a, b, c)): + return None + + one_step = x @ theta + one_step_rmse = rmse(target, one_step) + + predicted = np.zeros_like(y) + predicted[0] = y[0] + for index in range(sample_count - 1): + delayed_u = u[index - delay_samples] if index >= delay_samples else 0.0 + predicted[index + 1] = a * predicted[index] + b * delayed_u + c + + actual_tail = y[1:] + predicted_tail = predicted[1:] + if actual_tail.size == 0: + return None + + rollout_rmse = rmse(actual_tail, predicted_tail) + mae = float(np.mean(np.abs(actual_tail - predicted_tail))) + fit_percent = normalized_fit_percent(actual_tail, predicted_tail) + physically_plausible = (0.0 < a < 1.0) and (b > 0.0) + + return CandidateResult( + delay_samples=delay_samples, + a=a, + b=b, + c=c, + rollout_rmse=rollout_rmse, + one_step_rmse=one_step_rmse, + mae=mae, + fit_percent=fit_percent, + physically_plausible=physically_plausible, + predicted_velocity=predicted, + ) + + +def compare_candidates(left: CandidateResult, right: CandidateResult) -> bool: + if left.rollout_rmse < right.rollout_rmse - 1e-9: + return True + if math.isclose(left.rollout_rmse, right.rollout_rmse, rel_tol=0.0, abs_tol=1e-9): + if left.fit_percent > right.fit_percent + 1e-9: + return True + if math.isclose(left.fit_percent, right.fit_percent, rel_tol=0.0, abs_tol=1e-9): + return left.delay_samples < right.delay_samples + return False + + +def rmse(actual: np.ndarray, predicted: np.ndarray) -> float: + return float(np.sqrt(np.mean(np.square(actual - predicted)))) + + +def normalized_fit_percent(actual: np.ndarray, predicted: np.ndarray) -> float: + centered = actual - np.mean(actual) + denominator = float(np.linalg.norm(centered)) + if denominator <= 0.0: + return 0.0 + numerator = float(np.linalg.norm(actual - predicted)) + return 100.0 * max(0.0, 1.0 - numerator / denominator) + + +def continuous_params(candidate: CandidateResult, dt_s: float) -> tuple[float | None, float | None, float | None]: + a = candidate.a + if not (0.0 < a < 1.0): + return None, None, None + + one_minus_a = 1.0 - a + if one_minus_a <= 0.0: + return None, None, None + + time_constant_s = -dt_s / math.log(a) + dc_gain = candidate.b / one_minus_a + velocity_bias = candidate.c / one_minus_a + return time_constant_s, dc_gain, velocity_bias + + +def sanitize_token(value: str) -> str: + sanitized = re.sub(r"[^A-Za-z0-9_.-]+", "_", value.strip()) + return sanitized or "unknown" + + +def write_prediction_csv( + output_path: Path, + segment: Segment, + candidate: CandidateResult, +) -> None: + with output_path.open("w", newline="") as handle: + writer = csv.writer(handle) + writer.writerow( + [ + "timestamp_us", + "sample_idx", + "wheel_id", + "run_id", + "setpoint_velocity", + "measured_velocity", + "predicted_velocity", + "control_torque", + "residual", + ] + ) + residual = segment.measured_velocity - candidate.predicted_velocity + for index in range(segment.measured_velocity.size): + writer.writerow( + [ + int(segment.timestamp_us[index]), + int(segment.sample_idx[index]), + segment.wheel_id, + segment.run_id, + float(segment.setpoint_velocity[index]), + float(segment.measured_velocity[index]), + float(candidate.predicted_velocity[index]), + float(segment.control_torque[index]), + float(residual[index]), + ] + ) + + +def write_model_json( + output_path: Path, + segment: Segment, + candidate: CandidateResult, + time_constant_s: float | None, + dc_gain: float | None, + velocity_bias: float | None, + excitation_message: str, +) -> None: + payload = { + "wheel_id": segment.wheel_id, + "run_id": segment.run_id, + "sample_count": int(segment.measured_velocity.size), + "dropped_rows": segment.dropped_rows, + "missing_samples": segment.missing_samples, + "inferred_dt_s": segment.inferred_dt_s, + "sample_jitter_ratio": segment.sample_jitter_ratio, + "excitation_warning": excitation_message or None, + "model_type": "first_order_plus_dead_time_discrete", + "discrete": { + "a": candidate.a, + "b": candidate.b, + "c": candidate.c, + "delay_samples": candidate.delay_samples, + }, + "continuous": { + "time_constant_s": time_constant_s, + "dc_gain": dc_gain, + "velocity_bias": velocity_bias, + "delay_s": candidate.delay_samples * segment.inferred_dt_s, + }, + "fit": { + "rollout_rmse": candidate.rollout_rmse, + "one_step_rmse": candidate.one_step_rmse, + "mae": candidate.mae, + "fit_percent": candidate.fit_percent, + "physically_plausible": candidate.physically_plausible, + }, + } + + output_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + + +def write_summary_csv(output_path: Path, rows: list[dict[str, object]]) -> None: + fieldnames = [ + "wheel_id", + "run_id", + "status", + "message", + "sample_count", + "dropped_rows", + "missing_samples", + "inferred_dt_s", + "sample_jitter_ratio", + "speed_span", + "torque_span", + "delay_samples", + "delay_s", + "a", + "b", + "c", + "time_constant_s", + "dc_gain", + "velocity_bias", + "rollout_rmse", + "one_step_rmse", + "mae", + "fit_percent", + "model_json", + "prediction_csv", + ] + + with output_path.open("w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + for row in rows: + writer.writerow(row) + + +def main() -> int: + args = parse_args() + + input_csv = Path(args.input_csv) + if not input_csv.is_file(): + print(f"input CSV does not exist: {input_csv}", file=sys.stderr) + return 2 + + fallback_dt_s = args.sample_time_ms / 1000.0 + if fallback_dt_s <= 0.0: + print("--sample-time-ms must be positive", file=sys.stderr) + return 2 + + output_dir = Path(args.output_dir) if args.output_dir else default_output_dir() + output_dir.mkdir(parents=True, exist_ok=True) + + try: + samples = load_samples(input_csv, args.wheel_id, args.run_id) + except Exception as exc: + print(f"failed to load CSV: {exc}", file=sys.stderr) + return 2 + + if not samples: + print("no samples matched the selected wheel/run filters", file=sys.stderr) + return 2 + + grouped = group_samples(samples) + summary_rows: list[dict[str, object]] = [] + success_count = 0 + + for (wheel_id, run_id), rows in sorted(grouped.items()): + base_summary = { + "wheel_id": wheel_id, + "run_id": run_id, + "model_json": "", + "prediction_csv": "", + } + + try: + segment = build_segment(wheel_id, run_id, rows, fallback_dt_s) + except Exception as exc: + summary_rows.append( + { + **base_summary, + "status": "error", + "message": str(exc), + } + ) + continue + + if segment.measured_velocity.size < args.min_samples: + summary_rows.append( + { + **base_summary, + "status": "skipped", + "message": ( + f"too few samples ({segment.measured_velocity.size} < {args.min_samples})" + ), + "sample_count": int(segment.measured_velocity.size), + "dropped_rows": segment.dropped_rows, + "missing_samples": segment.missing_samples, + "inferred_dt_s": segment.inferred_dt_s, + "sample_jitter_ratio": segment.sample_jitter_ratio, + "speed_span": percentile_span(segment.measured_velocity), + "torque_span": percentile_span(segment.control_torque), + } + ) + continue + + excitation_good, excitation_message = excitation_ok( + segment, + min_speed_span=args.min_speed_span, + min_torque_span=args.min_torque_span, + ) + if not excitation_good: + summary_rows.append( + { + **base_summary, + "status": "skipped", + "message": excitation_message, + "sample_count": int(segment.measured_velocity.size), + "dropped_rows": segment.dropped_rows, + "missing_samples": segment.missing_samples, + "inferred_dt_s": segment.inferred_dt_s, + "sample_jitter_ratio": segment.sample_jitter_ratio, + "speed_span": percentile_span(segment.measured_velocity), + "torque_span": percentile_span(segment.control_torque), + } + ) + continue + + if args.max_delay_samples is not None: + max_delay_samples = max(0, args.max_delay_samples) + else: + max_delay_samples = max(0, int(round((args.max_delay_ms / 1000.0) / segment.inferred_dt_s))) + + try: + candidate = estimate_segment( + segment=segment, + max_delay_samples=max_delay_samples, + allow_unphysical=args.allow_unphysical, + ) + except Exception as exc: + summary_rows.append( + { + **base_summary, + "status": "error", + "message": str(exc), + "sample_count": int(segment.measured_velocity.size), + "dropped_rows": segment.dropped_rows, + "missing_samples": segment.missing_samples, + "inferred_dt_s": segment.inferred_dt_s, + "sample_jitter_ratio": segment.sample_jitter_ratio, + "speed_span": percentile_span(segment.measured_velocity), + "torque_span": percentile_span(segment.control_torque), + } + ) + continue + + time_constant_s, dc_gain, velocity_bias = continuous_params(candidate, segment.inferred_dt_s) + + wheel_token = sanitize_token(wheel_id) + run_token = f"run_{run_id}" + model_json_path = output_dir / f"{wheel_token}_{run_token}_model.json" + prediction_csv_path = output_dir / f"{wheel_token}_{run_token}_fit.csv" + + write_model_json( + model_json_path, + segment, + candidate, + time_constant_s, + dc_gain, + velocity_bias, + excitation_message="" if excitation_good else excitation_message, + ) + write_prediction_csv(prediction_csv_path, segment, candidate) + + speed_span = percentile_span(segment.measured_velocity) + torque_span = percentile_span(segment.control_torque) + summary_rows.append( + { + **base_summary, + "status": "ok", + "message": "" if candidate.physically_plausible else "selected unphysical candidate", + "sample_count": int(segment.measured_velocity.size), + "dropped_rows": segment.dropped_rows, + "missing_samples": segment.missing_samples, + "inferred_dt_s": segment.inferred_dt_s, + "sample_jitter_ratio": segment.sample_jitter_ratio, + "speed_span": speed_span, + "torque_span": torque_span, + "delay_samples": candidate.delay_samples, + "delay_s": candidate.delay_samples * segment.inferred_dt_s, + "a": candidate.a, + "b": candidate.b, + "c": candidate.c, + "time_constant_s": time_constant_s, + "dc_gain": dc_gain, + "velocity_bias": velocity_bias, + "rollout_rmse": candidate.rollout_rmse, + "one_step_rmse": candidate.one_step_rmse, + "mae": candidate.mae, + "fit_percent": candidate.fit_percent, + "model_json": str(model_json_path), + "prediction_csv": str(prediction_csv_path), + } + ) + + success_count += 1 + log( + ( + f"[ok] {wheel_id}/run{run_id}: delay={candidate.delay_samples} samples, " + f"tau={time_constant_s if time_constant_s is not None else float('nan'):.6f}s, " + f"gain={dc_gain if dc_gain is not None else float('nan'):.6f}, " + f"fit={candidate.fit_percent:.2f}%" + ), + quiet=args.quiet, + ) + + summary_path = output_dir / "identification_summary.csv" + write_summary_csv(summary_path, summary_rows) + + if success_count == 0: + print(f"no segment was successfully identified; summary: {summary_path}", file=sys.stderr) + return 1 + + log(f"summary: {summary_path}", quiet=args.quiet) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/pid_tuning/tune_pid.py b/tools/pid_tuning/tune_pid.py new file mode 100644 index 000000000..6bd61a3a7 --- /dev/null +++ b/tools/pid_tuning/tune_pid.py @@ -0,0 +1,1280 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import csv +import json +import math +import random +import sys +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Iterable + +import numpy as np + +from identify_plant import build_segment, group_samples, load_samples, sanitize_token + + +CURRENT_DEFAULT_KP = 0.003436926 +CURRENT_DEFAULT_KD = 0.009373434 + + +@dataclass(frozen=True) +class PlantModel: + wheel_id: str + run_id: int + dt_s: float + a: float + b: float + c: float + delay_samples: int + source_path: Path + fit_percent: float | None + + +@dataclass(frozen=True) +class TraceData: + wheel_id: str + run_id: int + dt_s: float + setpoint: np.ndarray + initial_velocity: float + observed_velocity: np.ndarray | None + observed_control: np.ndarray | None + source_name: str + + +@dataclass(frozen=True) +class SimulationResult: + velocity: np.ndarray + control: np.ndarray + diverged: bool + + +@dataclass(frozen=True) +class Metrics: + cost: float + mae_norm: float + rmse_norm: float + itae_norm: float + overshoot_norm: float + steady_error_norm: float + final_error_norm: float + settle_ratio: float + control_rms_norm: float + control_tv_norm: float + saturation_ratio: float + + +@dataclass(frozen=True) +class EvaluationRecord: + kp: float + kd: float + cost: float + mae_norm: float + rmse_norm: float + itae_norm: float + overshoot_norm: float + steady_error_norm: float + final_error_norm: float + settle_ratio: float + control_rms_norm: float + control_tv_norm: float + saturation_ratio: float + method: str + evaluation_index: int + + +@dataclass(frozen=True) +class TuningOutcome: + model: PlantModel + trace_count: int + output_min: float + output_max: float + best: EvaluationRecord + baseline: EvaluationRecord + best_simulation: SimulationResult + best_trace: TraceData + output_dir: Path + + +class ObjectiveEvaluator: + def __init__( + self, + model: PlantModel, + traces: list[TraceData], + output_min: float, + output_max: float, + settle_band_ratio: float, + quiet: bool, + ) -> None: + self.model = model + self.traces = traces + self.output_min = output_min + self.output_max = output_max + self.settle_band_ratio = settle_band_ratio + self.quiet = quiet + self.cache: dict[tuple[float, float], EvaluationRecord] = {} + self.records: list[EvaluationRecord] = [] + self.best_record: EvaluationRecord | None = None + + def evaluate(self, kp: float, kd: float, method: str) -> EvaluationRecord: + kp = float(kp) + kd = float(kd) + key = (round(kp, 15), round(kd, 15)) + cached = self.cache.get(key) + if cached is not None: + return cached + + trace_metrics: list[Metrics] = [] + for trace in self.traces: + simulation = simulate_closed_loop( + model=self.model, + trace=trace, + kp=kp, + kd=kd, + output_min=self.output_min, + output_max=self.output_max, + ) + metrics = compute_metrics( + trace=trace, + simulation=simulation, + settle_band_ratio=self.settle_band_ratio, + output_min=self.output_min, + output_max=self.output_max, + ) + trace_metrics.append(metrics) + + record = EvaluationRecord( + kp=kp, + kd=kd, + cost=mean_metric(trace_metrics, "cost"), + mae_norm=mean_metric(trace_metrics, "mae_norm"), + rmse_norm=mean_metric(trace_metrics, "rmse_norm"), + itae_norm=mean_metric(trace_metrics, "itae_norm"), + overshoot_norm=mean_metric(trace_metrics, "overshoot_norm"), + steady_error_norm=mean_metric(trace_metrics, "steady_error_norm"), + final_error_norm=mean_metric(trace_metrics, "final_error_norm"), + settle_ratio=mean_metric(trace_metrics, "settle_ratio"), + control_rms_norm=mean_metric(trace_metrics, "control_rms_norm"), + control_tv_norm=mean_metric(trace_metrics, "control_tv_norm"), + saturation_ratio=mean_metric(trace_metrics, "saturation_ratio"), + method=method, + evaluation_index=len(self.records), + ) + + self.records.append(record) + self.cache[key] = record + if self.best_record is None or record.cost < self.best_record.cost: + self.best_record = record + if not self.quiet: + print( + ( + f"[best] {self.model.wheel_id}/run{self.model.run_id}: " + f"kp={record.kp:.9g}, kd={record.kd:.9g}, cost={record.cost:.6f}" + ) + ) + return record + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Tune friction wheel velocity PID gains offline from identified plant models. " + "The discrete control law matches rmcs_core::controller::pid::PidCalculator " + "with ki fixed to 0." + ) + ) + parser.add_argument( + "model_input", + help="Either one *_model.json from identify_plant.py or identification_summary.csv", + ) + parser.add_argument( + "--trace-csv", + help=( + "Recorder CSV from FrictionWheelPidRecorder. If provided, the original setpoint trace " + "is reused for closed-loop evaluation." + ), + ) + parser.add_argument("--wheel-id", help="Only tune one wheel_id") + parser.add_argument("--run-id", type=int, help="Only tune one run_id") + parser.add_argument( + "--trace-scope", + choices=("exact", "wheel"), + default="exact", + help="How to match trace segments to a model when --trace-csv is provided", + ) + parser.add_argument( + "--sample-time-ms", + type=float, + default=1.0, + help="Fallback sample interval for trace CSV parsing when timestamps are invalid", + ) + parser.add_argument( + "--method", + choices=("hybrid", "ga", "bo"), + default="hybrid", + help="Optimization method", + ) + parser.add_argument("--kp-min", type=float, default=1e-6, help="Lower bound for kp") + parser.add_argument("--kp-max", type=float, default=1.0, help="Upper bound for kp") + parser.add_argument("--kd-min", type=float, default=1e-8, help="Lower bound for kd") + parser.add_argument("--kd-max", type=float, default=1.0, help="Upper bound for kd") + parser.add_argument( + "--seed-kp", + type=float, + default=CURRENT_DEFAULT_KP, + help="Initial/reference kp to compare against", + ) + parser.add_argument( + "--seed-kd", + type=float, + default=CURRENT_DEFAULT_KD, + help="Initial/reference kd to compare against", + ) + parser.add_argument( + "--ga-population", + type=int, + default=36, + help="GA population size", + ) + parser.add_argument( + "--ga-generations", + type=int, + default=20, + help="GA generation count", + ) + parser.add_argument( + "--bo-initial", + type=int, + default=12, + help="BO random warmup samples", + ) + parser.add_argument( + "--bo-iterations", + type=int, + default=24, + help="BO surrogate iterations", + ) + parser.add_argument( + "--random-seed", + type=int, + default=42, + help="Random seed for deterministic tuning runs", + ) + parser.add_argument( + "--output-limit", + type=float, + help="Symmetric control limit. Equivalent to --output-min=-x --output-max=x", + ) + parser.add_argument("--output-min", type=float, help="Lower control clamp") + parser.add_argument("--output-max", type=float, help="Upper control clamp") + parser.add_argument( + "--infer-output-percentile", + type=float, + default=99.5, + help="Percentile used to infer |control_torque| limit from --trace-csv", + ) + parser.add_argument( + "--estimated-output-factor", + type=float, + default=2.5, + help="Fallback multiplier when output limit is estimated from target speed and plant gain", + ) + parser.add_argument( + "--step-setpoint", + type=float, + help="Target speed for synthetic step evaluation when --trace-csv is not provided", + ) + parser.add_argument( + "--step-duration-ms", + type=float, + default=1200.0, + help="Synthetic step hold duration in milliseconds", + ) + parser.add_argument( + "--pre-roll-ms", + type=float, + default=30.0, + help="Synthetic zero-input lead-in before the step", + ) + parser.add_argument( + "--settle-band-ratio", + type=float, + default=0.05, + help="Relative settling band used by the objective", + ) + parser.add_argument( + "--output-dir", + help="Directory for tuning outputs. Default: /tmp/friction_pid_tuning/", + ) + parser.add_argument( + "--quiet", + action="store_true", + help="Suppress progress output", + ) + return parser.parse_args() + + +def default_output_dir() -> Path: + stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + return Path("/tmp/friction_pid_tuning") / stamp + + +def load_models(model_input: Path, wheel_id: str | None, run_id: int | None) -> list[PlantModel]: + if not model_input.is_file(): + raise ValueError(f"model input does not exist: {model_input}") + + if model_input.suffix.lower() == ".json": + models = [load_model_json(model_input)] + else: + models = load_models_from_summary(model_input) + + filtered = [ + model + for model in models + if (wheel_id is None or model.wheel_id == wheel_id) + and (run_id is None or model.run_id == run_id) + ] + if not filtered: + raise ValueError("no model matched the selected wheel/run filters") + return filtered + + +def load_model_json(path: Path) -> PlantModel: + payload = json.loads(path.read_text()) + discrete = payload["discrete"] + fit = payload.get("fit", {}) + dt_s = float(payload["inferred_dt_s"]) + if not math.isfinite(dt_s) or dt_s <= 0.0: + raise ValueError(f"{path}: invalid inferred_dt_s") + + return PlantModel( + wheel_id=str(payload["wheel_id"]), + run_id=int(payload["run_id"]), + dt_s=dt_s, + a=float(discrete["a"]), + b=float(discrete["b"]), + c=float(discrete["c"]), + delay_samples=int(discrete["delay_samples"]), + source_path=path, + fit_percent=float(fit["fit_percent"]) if "fit_percent" in fit else None, + ) + + +def load_models_from_summary(path: Path) -> list[PlantModel]: + models: list[PlantModel] = [] + with path.open("r", newline="") as handle: + reader = csv.DictReader(handle) + for row in reader: + if row.get("status") != "ok": + continue + model_json = (row.get("model_json") or "").strip() + if not model_json: + continue + model_path = Path(model_json) + if not model_path.is_absolute(): + model_path = (path.parent / model_path).resolve() + models.append(load_model_json(model_path)) + + if not models: + raise ValueError(f"{path}: no usable model_json rows found in summary") + return models + + +def load_trace_map(trace_csv: Path, fallback_dt_s: float) -> dict[tuple[str, int], TraceData]: + samples = load_samples(trace_csv, None, None) + grouped = group_samples(samples) + trace_map: dict[tuple[str, int], TraceData] = {} + for (wheel_id, run_id), rows in grouped.items(): + segment = build_segment(wheel_id, run_id, rows, fallback_dt_s) + trace_map[(wheel_id, run_id)] = TraceData( + wheel_id=wheel_id, + run_id=run_id, + dt_s=segment.inferred_dt_s, + setpoint=segment.setpoint_velocity.copy(), + initial_velocity=float(segment.measured_velocity[0]) if segment.measured_velocity.size else 0.0, + observed_velocity=segment.measured_velocity.copy(), + observed_control=segment.control_torque.copy(), + source_name=f"{trace_csv}:{wheel_id}/run{run_id}", + ) + return trace_map + + +def select_traces( + model: PlantModel, + trace_map: dict[tuple[str, int], TraceData] | None, + trace_scope: str, + step_setpoint: float | None, + step_duration_ms: float, + pre_roll_ms: float, +) -> list[TraceData]: + if trace_map: + exact = trace_map.get((model.wheel_id, model.run_id)) + if exact is not None: + return [exact] + if trace_scope == "wheel": + same_wheel = [ + trace + for trace in trace_map.values() + if trace.wheel_id == model.wheel_id + ] + if same_wheel: + return sorted(same_wheel, key=lambda trace: trace.run_id) + raise ValueError( + f"no trace matched {model.wheel_id}/run{model.run_id}; " + "provide the corresponding recorder CSV or switch to synthetic step mode" + ) + + if step_setpoint is None: + raise ValueError( + f"{model.wheel_id}/run{model.run_id}: --step-setpoint is required when --trace-csv is absent" + ) + return [make_step_trace(model, step_setpoint, step_duration_ms, pre_roll_ms)] + + +def make_step_trace( + model: PlantModel, + step_setpoint: float, + step_duration_ms: float, + pre_roll_ms: float, +) -> TraceData: + pre_samples = max(1, int(round((pre_roll_ms / 1000.0) / model.dt_s))) + hold_samples = max(10, int(round((step_duration_ms / 1000.0) / model.dt_s))) + total = pre_samples + hold_samples + setpoint = np.zeros(total, dtype=np.float64) + setpoint[pre_samples:] = step_setpoint + + return TraceData( + wheel_id=model.wheel_id, + run_id=model.run_id, + dt_s=model.dt_s, + setpoint=setpoint, + initial_velocity=0.0, + observed_velocity=None, + observed_control=None, + source_name=f"synthetic_step:{model.wheel_id}/run{model.run_id}", + ) + + +def infer_output_bounds( + model: PlantModel, + traces: list[TraceData], + output_limit: float | None, + output_min: float | None, + output_max: float | None, + infer_output_percentile: float, + estimated_output_factor: float, +) -> tuple[float, float]: + if output_limit is not None: + limit = abs(output_limit) + if limit <= 0.0: + raise ValueError("--output-limit must be positive") + return -limit, limit + + if output_min is not None or output_max is not None: + if output_min is None or output_max is None: + raise ValueError("--output-min and --output-max must be set together") + if output_min >= output_max: + raise ValueError("--output-min must be smaller than --output-max") + return float(output_min), float(output_max) + + observed_controls = [ + trace.observed_control + for trace in traces + if trace.observed_control is not None and trace.observed_control.size > 0 + ] + if observed_controls: + merged = np.concatenate(observed_controls) + limit = float(np.percentile(np.abs(merged), infer_output_percentile)) + limit = max(limit, 1e-6) + return -limit, limit + + dc_gain = plant_dc_gain(model) + target_mag = max(trace_target_magnitude(trace) for trace in traces) + if dc_gain is None or abs(dc_gain) < 1e-9: + estimated_limit = 10.0 + else: + estimated_hold = target_mag / abs(dc_gain) + estimated_limit = max(estimated_output_factor * estimated_hold, 1.0) + return -estimated_limit, estimated_limit + + +def plant_dc_gain(model: PlantModel) -> float | None: + denominator = 1.0 - model.a + if abs(denominator) < 1e-12: + return None + return model.b / denominator + + +def trace_target_magnitude(trace: TraceData) -> float: + if trace.setpoint.size == 0: + return 1.0 + return max(float(np.percentile(np.abs(trace.setpoint), 95)), 1.0) + + +def simulate_closed_loop( + model: PlantModel, + trace: TraceData, + kp: float, + kd: float, + output_min: float, + output_max: float, +) -> SimulationResult: + sample_count = trace.setpoint.size + velocity = np.zeros(sample_count, dtype=np.float64) + control = np.zeros(sample_count, dtype=np.float64) + velocity[0] = trace.initial_velocity + + diverged = False + last_err = math.nan + divergence_limit = max(50.0 * trace_target_magnitude(trace), 1e6) + + for index in range(sample_count): + err = float(trace.setpoint[index] - velocity[index]) + command = kp * err + if not math.isnan(last_err): + command += kd * (err - last_err) + last_err = err + command = min(max(command, output_min), output_max) + control[index] = command + + if index + 1 >= sample_count: + continue + + delayed_u = control[index - model.delay_samples] if index >= model.delay_samples else 0.0 + next_velocity = model.a * velocity[index] + model.b * delayed_u + model.c + if not math.isfinite(next_velocity) or abs(next_velocity) > divergence_limit: + diverged = True + velocity[index + 1 :] = divergence_limit * np.sign(next_velocity if math.isfinite(next_velocity) else 1.0) + break + velocity[index + 1] = next_velocity + + return SimulationResult(velocity=velocity, control=control, diverged=diverged) + + +def compute_metrics( + trace: TraceData, + simulation: SimulationResult, + settle_band_ratio: float, + output_min: float, + output_max: float, +) -> Metrics: + setpoint = trace.setpoint + velocity = simulation.velocity + control = simulation.control + scale = trace_target_magnitude(trace) + error = setpoint - velocity + normalized_error = error / scale + + mae_norm = float(np.mean(np.abs(normalized_error))) + rmse_norm = float(np.sqrt(np.mean(np.square(normalized_error)))) + + time_vector = np.linspace(0.0, 1.0, setpoint.size, endpoint=True) + itae_norm = float(np.mean(time_vector * np.abs(normalized_error))) + + active_mask = np.abs(setpoint) >= 0.1 * scale + if not np.any(active_mask): + active_mask = np.ones(setpoint.size, dtype=bool) + active_indices = np.where(active_mask)[0] + active_start = int(active_indices[0]) + + plateau_mask = active_mask & ( + np.abs(np.diff(setpoint, prepend=setpoint[0])) <= 0.005 * scale + ) + if np.count_nonzero(plateau_mask) < max(5, setpoint.size // 20): + plateau_mask = np.zeros(setpoint.size, dtype=bool) + plateau_mask[max(active_start, int(setpoint.size * 0.8)) :] = True + plateau_indices = np.where(plateau_mask)[0] + + if plateau_indices.size > 0: + steady_error_norm = float(np.mean(np.abs(normalized_error[plateau_indices]))) + overshoot_norm = ( + float(np.max(np.maximum(0.0, np.abs(velocity[plateau_indices]) - np.abs(setpoint[plateau_indices])))) + / scale + ) + else: + steady_error_norm = float(np.mean(np.abs(normalized_error[-max(1, setpoint.size // 10) :]))) + overshoot_norm = ( + float(np.max(np.maximum(0.0, np.abs(velocity[active_mask]) - np.abs(setpoint[active_mask])))) + / scale + ) + + band = np.maximum(settle_band_ratio * np.abs(setpoint), settle_band_ratio * scale) + outside = np.where(active_mask & (np.abs(error) > band))[0] + if outside.size == 0: + settle_ratio = 0.0 + else: + denominator = max(1, (setpoint.size - 1) - active_start) + settle_ratio = float((outside[-1] - active_start) / denominator) + + final_error_norm = float(abs(error[-1]) / scale) + + finite_limits = math.isfinite(output_min) and math.isfinite(output_max) + if finite_limits: + control_scale = max(abs(output_min), abs(output_max), 1.0) + saturation_margin = 0.01 * control_scale + saturation_ratio = float( + np.mean((control <= output_min + saturation_margin) | (control >= output_max - saturation_margin)) + ) + else: + control_scale = max(float(np.percentile(np.abs(control), 95)), 1.0) + saturation_ratio = 0.0 + + control_rms_norm = float(np.sqrt(np.mean(np.square(control / control_scale)))) + if control.size >= 2: + control_tv_norm = float(np.mean(np.abs(np.diff(control))) / control_scale) + else: + control_tv_norm = 0.0 + + divergence_penalty = 50.0 if simulation.diverged else 0.0 + cost = ( + 4.0 * mae_norm + + 2.5 * rmse_norm + + 2.0 * itae_norm + + 3.5 * overshoot_norm + + 2.5 * steady_error_norm + + 2.0 * final_error_norm + + 1.2 * settle_ratio + + 0.25 * control_rms_norm + + 0.6 * control_tv_norm + + 1.5 * saturation_ratio + + divergence_penalty + ) + + return Metrics( + cost=cost, + mae_norm=mae_norm, + rmse_norm=rmse_norm, + itae_norm=itae_norm, + overshoot_norm=overshoot_norm, + steady_error_norm=steady_error_norm, + final_error_norm=final_error_norm, + settle_ratio=settle_ratio, + control_rms_norm=control_rms_norm, + control_tv_norm=control_tv_norm, + saturation_ratio=saturation_ratio, + ) + + +def mean_metric(metrics: Iterable[Metrics], field_name: str) -> float: + values = [getattr(metric, field_name) for metric in metrics] + return float(sum(values) / len(values)) + + +def encode_params(kp: float, kd: float) -> np.ndarray: + return np.asarray([math.log10(kp), math.log10(kd)], dtype=np.float64) + + +def decode_params(encoded: np.ndarray) -> tuple[float, float]: + return 10.0 ** float(encoded[0]), 10.0 ** float(encoded[1]) + + +def sample_encoded(bounds: tuple[np.ndarray, np.ndarray], rng: random.Random) -> np.ndarray: + lower, upper = bounds + return np.asarray( + [rng.uniform(float(lower[0]), float(upper[0])), rng.uniform(float(lower[1]), float(upper[1]))], + dtype=np.float64, + ) + + +def clamp_encoded(encoded: np.ndarray, bounds: tuple[np.ndarray, np.ndarray]) -> np.ndarray: + lower, upper = bounds + return np.minimum(np.maximum(encoded, lower), upper) + + +def build_seed_points( + bounds: tuple[np.ndarray, np.ndarray], + seed_kp: float, + seed_kd: float, + model: PlantModel, + traces: list[TraceData], + output_min: float, + output_max: float, +) -> list[np.ndarray]: + lower, upper = bounds + seeds: list[np.ndarray] = [] + + def append_point(kp: float, kd: float) -> None: + kp = min(max(kp, 10.0 ** float(lower[0])), 10.0 ** float(upper[0])) + kd = min(max(kd, 10.0 ** float(lower[1])), 10.0 ** float(upper[1])) + encoded = encode_params(kp, kd) + for existing in seeds: + if np.allclose(existing, encoded, atol=1e-12, rtol=0.0): + return + seeds.append(encoded) + + append_point(seed_kp, seed_kd) + append_point(seed_kp * 0.5, seed_kd * 0.5) + append_point(seed_kp * 2.0, seed_kd * 2.0) + append_point(seed_kp * 0.5, seed_kd * 2.0) + append_point(seed_kp * 2.0, max(seed_kd * 0.5, 1e-12)) + + dc_gain = plant_dc_gain(model) + target_mag = max(trace_target_magnitude(trace) for trace in traces) + output_limit = max(abs(output_min), abs(output_max), 1.0) + if dc_gain is not None and abs(dc_gain) > 1e-9: + proportional_guess = min(max(0.35 * output_limit / target_mag, 1e-12), 10.0 ** float(upper[0])) + derivative_guess = min(max(proportional_guess * max(1.0, model.delay_samples), 1e-12), 10.0 ** float(upper[1])) + append_point(proportional_guess, derivative_guess) + append_point(0.75 * proportional_guess, 0.5 * derivative_guess) + + append_point(10.0 ** float(lower[0]), 10.0 ** float(lower[1])) + return seeds + + +def run_ga( + evaluator: ObjectiveEvaluator, + bounds: tuple[np.ndarray, np.ndarray], + population_size: int, + generations: int, + seed_points: list[np.ndarray], + rng: random.Random, + quiet: bool, +) -> None: + lower, upper = bounds + population: list[np.ndarray] = [point.copy() for point in seed_points[:population_size]] + while len(population) < population_size: + population.append(sample_encoded(bounds, rng)) + + for generation in range(generations): + scored: list[tuple[EvaluationRecord, np.ndarray]] = [] + for encoded in population: + kp, kd = decode_params(encoded) + scored.append((evaluator.evaluate(kp, kd, method="ga"), encoded)) + scored.sort(key=lambda item: item[0].cost) + + if not quiet: + best_record = scored[0][0] + print( + ( + f"[ga] generation={generation + 1}/{generations} " + f"best_cost={best_record.cost:.6f} " + f"kp={best_record.kp:.9g} kd={best_record.kd:.9g}" + ) + ) + + elite_count = max(2, population_size // 5) + survivors = [encoded.copy() for _, encoded in scored[:elite_count]] + + next_population: list[np.ndarray] = survivors[:] + mutation_sigma = max(0.03, 0.18 * (1.0 - generation / max(1, generations))) + tournament_pool = [encoded for _, encoded in scored[: max(elite_count * 2, 4)]] + + while len(next_population) < population_size: + parent_a = rng.choice(tournament_pool) + parent_b = rng.choice(tournament_pool) + alpha = rng.uniform(0.2, 0.8) + child = alpha * parent_a + (1.0 - alpha) * parent_b + if rng.random() < 0.85: + child = child + np.asarray( + [rng.gauss(0.0, mutation_sigma), rng.gauss(0.0, mutation_sigma)], + dtype=np.float64, + ) + if rng.random() < 0.12: + child = sample_encoded(bounds, rng) + child = clamp_encoded(child, bounds) + next_population.append(child) + + population = next_population + + # Evaluate the final population once more so the final offspring enter the archive. + for encoded in population: + kp, kd = decode_params(encoded) + evaluator.evaluate(kp, kd, method="ga") + + +def kernel_rbf(x1: np.ndarray, x2: np.ndarray, length_scale: float) -> np.ndarray: + diff = x1[:, None, :] - x2[None, :, :] + dist_sq = np.sum(np.square(diff), axis=2) + return np.exp(-0.5 * dist_sq / (length_scale * length_scale)) + + +def normalize_encoded(encoded: np.ndarray, bounds: tuple[np.ndarray, np.ndarray]) -> np.ndarray: + lower, upper = bounds + return (encoded - lower) / np.maximum(upper - lower, 1e-12) + + +def run_bo( + evaluator: ObjectiveEvaluator, + bounds: tuple[np.ndarray, np.ndarray], + initial_samples: int, + iterations: int, + seed_points: list[np.ndarray], + rng: random.Random, + quiet: bool, +) -> None: + archive_encoded: list[np.ndarray] = [] + archive_costs: list[float] = [] + + def register_point(encoded: np.ndarray, method: str) -> None: + kp, kd = decode_params(encoded) + record = evaluator.evaluate(kp, kd, method=method) + archive_encoded.append(encoded.copy()) + archive_costs.append(record.cost) + + seen: set[tuple[float, float]] = set() + + def add_if_new(encoded: np.ndarray, method: str) -> None: + key = (round(float(encoded[0]), 12), round(float(encoded[1]), 12)) + if key in seen: + return + seen.add(key) + register_point(encoded, method) + + for encoded in seed_points[:initial_samples]: + add_if_new(clamp_encoded(encoded, bounds), "bo") + + while len(archive_encoded) < initial_samples: + add_if_new(sample_encoded(bounds, rng), "bo") + + for iteration in range(iterations): + x_train = np.asarray(archive_encoded, dtype=np.float64) + y_train = np.asarray(archive_costs, dtype=np.float64) + x_norm = normalize_encoded(x_train, bounds) + + length_scale = 0.22 + jitter = 1e-6 + k_xx = kernel_rbf(x_norm, x_norm, length_scale) + jitter * np.eye(x_norm.shape[0]) + centered = y_train - np.mean(y_train) + alpha = np.linalg.solve(k_xx, centered) + inverse_k = np.linalg.inv(k_xx) + + candidate_pool = [sample_encoded(bounds, rng) for _ in range(2048)] + if evaluator.best_record is not None: + best_encoded = encode_params(evaluator.best_record.kp, evaluator.best_record.kd) + for _ in range(256): + local = best_encoded + np.asarray( + [rng.gauss(0.0, 0.06), rng.gauss(0.0, 0.06)], + dtype=np.float64, + ) + candidate_pool.append(clamp_encoded(local, bounds)) + + candidate_matrix = np.asarray(candidate_pool, dtype=np.float64) + candidate_norm = normalize_encoded(candidate_matrix, bounds) + + k_star = kernel_rbf(x_norm, candidate_norm, length_scale) + mean = np.mean(y_train) + k_star.T @ alpha + variance = np.maximum(0.0, 1.0 - np.sum((k_star.T @ inverse_k) * k_star.T, axis=1)) + sigma = np.sqrt(variance) + beta = max(0.35, 1.75 - 1.2 * (iteration / max(1, iterations))) + acquisition = mean - beta * sigma + + ordering = np.argsort(acquisition) + chosen: np.ndarray | None = None + for idx in ordering: + encoded = candidate_matrix[int(idx)] + key = (round(float(encoded[0]), 12), round(float(encoded[1]), 12)) + if key not in seen: + chosen = encoded + break + if chosen is None: + chosen = sample_encoded(bounds, rng) + + add_if_new(chosen, "bo") + + if not quiet and evaluator.best_record is not None: + best = evaluator.best_record + print( + ( + f"[bo] iteration={iteration + 1}/{iterations} " + f"best_cost={best.cost:.6f} " + f"kp={best.kp:.9g} kd={best.kd:.9g}" + ) + ) + + +def tune_one_model( + model: PlantModel, + traces: list[TraceData], + args: argparse.Namespace, + rng: random.Random, + output_dir: Path, +) -> TuningOutcome: + output_min, output_max = infer_output_bounds( + model=model, + traces=traces, + output_limit=args.output_limit, + output_min=args.output_min, + output_max=args.output_max, + infer_output_percentile=args.infer_output_percentile, + estimated_output_factor=args.estimated_output_factor, + ) + + if args.kp_min <= 0.0 or args.kd_min <= 0.0: + raise ValueError("--kp-min and --kd-min must be strictly positive for log-scale search") + if args.kp_min >= args.kp_max or args.kd_min >= args.kd_max: + raise ValueError("kp/kd bounds must satisfy min < max") + + bounds = ( + np.asarray([math.log10(args.kp_min), math.log10(args.kd_min)], dtype=np.float64), + np.asarray([math.log10(args.kp_max), math.log10(args.kd_max)], dtype=np.float64), + ) + + evaluator = ObjectiveEvaluator( + model=model, + traces=traces, + output_min=output_min, + output_max=output_max, + settle_band_ratio=args.settle_band_ratio, + quiet=args.quiet, + ) + + seeds = build_seed_points( + bounds=bounds, + seed_kp=args.seed_kp, + seed_kd=args.seed_kd, + model=model, + traces=traces, + output_min=output_min, + output_max=output_max, + ) + + baseline = evaluator.evaluate(args.seed_kp, args.seed_kd, method="baseline") + + if args.method in {"ga", "hybrid"}: + run_ga( + evaluator=evaluator, + bounds=bounds, + population_size=args.ga_population, + generations=args.ga_generations, + seed_points=seeds, + rng=rng, + quiet=args.quiet, + ) + + if args.method in {"bo", "hybrid"}: + if args.method == "bo": + bo_seeds = seeds + else: + sorted_records = sorted(evaluator.records, key=lambda record: record.cost) + bo_seeds = [encode_params(record.kp, record.kd) for record in sorted_records[: max(8, args.bo_initial)]] + bo_seeds.extend(seeds) + + run_bo( + evaluator=evaluator, + bounds=bounds, + initial_samples=args.bo_initial, + iterations=args.bo_iterations, + seed_points=bo_seeds, + rng=rng, + quiet=args.quiet, + ) + + if evaluator.best_record is None: + raise RuntimeError(f"{model.wheel_id}/run{model.run_id}: no candidate was evaluated") + + best = evaluator.best_record + best_trace = traces[0] + best_simulation = simulate_closed_loop( + model=model, + trace=best_trace, + kp=best.kp, + kd=best.kd, + output_min=output_min, + output_max=output_max, + ) + + tune_output_dir = output_dir / f"{sanitize_token(model.wheel_id)}_run_{model.run_id}" + tune_output_dir.mkdir(parents=True, exist_ok=True) + + write_candidates_csv(tune_output_dir / "candidates.csv", evaluator.records) + write_response_csv( + tune_output_dir / "best_response.csv", + trace=best_trace, + simulation=best_simulation, + output_min=output_min, + output_max=output_max, + ) + write_tuning_json( + tune_output_dir / "tuning_result.json", + model=model, + trace_count=len(traces), + output_min=output_min, + output_max=output_max, + baseline=baseline, + best=best, + method=args.method, + ) + + return TuningOutcome( + model=model, + trace_count=len(traces), + output_min=output_min, + output_max=output_max, + best=best, + baseline=baseline, + best_simulation=best_simulation, + best_trace=best_trace, + output_dir=tune_output_dir, + ) + + +def write_candidates_csv(output_path: Path, records: list[EvaluationRecord]) -> None: + fieldnames = [ + "evaluation_index", + "method", + "kp", + "kd", + "cost", + "mae_norm", + "rmse_norm", + "itae_norm", + "overshoot_norm", + "steady_error_norm", + "final_error_norm", + "settle_ratio", + "control_rms_norm", + "control_tv_norm", + "saturation_ratio", + ] + ordered = sorted(records, key=lambda record: (record.cost, record.evaluation_index)) + with output_path.open("w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + for record in ordered: + writer.writerow( + { + "evaluation_index": record.evaluation_index, + "method": record.method, + "kp": record.kp, + "kd": record.kd, + "cost": record.cost, + "mae_norm": record.mae_norm, + "rmse_norm": record.rmse_norm, + "itae_norm": record.itae_norm, + "overshoot_norm": record.overshoot_norm, + "steady_error_norm": record.steady_error_norm, + "final_error_norm": record.final_error_norm, + "settle_ratio": record.settle_ratio, + "control_rms_norm": record.control_rms_norm, + "control_tv_norm": record.control_tv_norm, + "saturation_ratio": record.saturation_ratio, + } + ) + + +def write_response_csv( + output_path: Path, + trace: TraceData, + simulation: SimulationResult, + output_min: float, + output_max: float, +) -> None: + saturation_margin = 0.01 * max(abs(output_min), abs(output_max), 1.0) + with output_path.open("w", newline="") as handle: + writer = csv.writer(handle) + writer.writerow( + [ + "sample_idx", + "time_s", + "setpoint_velocity", + "simulated_velocity", + "simulated_control", + "saturated", + "observed_velocity", + "observed_control", + ] + ) + for index in range(trace.setpoint.size): + simulated_control = float(simulation.control[index]) + saturated = int( + (simulated_control <= output_min + saturation_margin) + or (simulated_control >= output_max - saturation_margin) + ) + writer.writerow( + [ + index, + index * trace.dt_s, + float(trace.setpoint[index]), + float(simulation.velocity[index]), + simulated_control, + saturated, + "" if trace.observed_velocity is None else float(trace.observed_velocity[index]), + "" if trace.observed_control is None else float(trace.observed_control[index]), + ] + ) + + +def write_tuning_json( + output_path: Path, + model: PlantModel, + trace_count: int, + output_min: float, + output_max: float, + baseline: EvaluationRecord, + best: EvaluationRecord, + method: str, +) -> None: + payload = { + "wheel_id": model.wheel_id, + "run_id": model.run_id, + "trace_count": trace_count, + "method": method, + "ki": 0.0, + "output_min": output_min, + "output_max": output_max, + "plant_model": { + "source_path": str(model.source_path), + "dt_s": model.dt_s, + "a": model.a, + "b": model.b, + "c": model.c, + "delay_samples": model.delay_samples, + "fit_percent": model.fit_percent, + }, + "baseline": record_to_dict(baseline), + "best": record_to_dict(best), + "improvement": { + "absolute_cost_reduction": baseline.cost - best.cost, + "relative_cost_reduction": 0.0 if baseline.cost == 0.0 else (baseline.cost - best.cost) / baseline.cost, + }, + } + output_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + + +def record_to_dict(record: EvaluationRecord) -> dict[str, float | str | int]: + return { + "kp": record.kp, + "ki": 0.0, + "kd": record.kd, + "cost": record.cost, + "mae_norm": record.mae_norm, + "rmse_norm": record.rmse_norm, + "itae_norm": record.itae_norm, + "overshoot_norm": record.overshoot_norm, + "steady_error_norm": record.steady_error_norm, + "final_error_norm": record.final_error_norm, + "settle_ratio": record.settle_ratio, + "control_rms_norm": record.control_rms_norm, + "control_tv_norm": record.control_tv_norm, + "saturation_ratio": record.saturation_ratio, + "method": record.method, + "evaluation_index": record.evaluation_index, + } + + +def write_summary_csv(output_path: Path, outcomes: list[TuningOutcome]) -> None: + fieldnames = [ + "wheel_id", + "run_id", + "trace_count", + "method", + "seed_kp", + "seed_kd", + "best_kp", + "best_kd", + "baseline_cost", + "best_cost", + "absolute_cost_reduction", + "relative_cost_reduction", + "output_min", + "output_max", + "result_dir", + ] + with output_path.open("w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + for outcome in outcomes: + baseline = outcome.baseline + best = outcome.best + writer.writerow( + { + "wheel_id": outcome.model.wheel_id, + "run_id": outcome.model.run_id, + "trace_count": outcome.trace_count, + "method": best.method, + "seed_kp": baseline.kp, + "seed_kd": baseline.kd, + "best_kp": best.kp, + "best_kd": best.kd, + "baseline_cost": baseline.cost, + "best_cost": best.cost, + "absolute_cost_reduction": baseline.cost - best.cost, + "relative_cost_reduction": 0.0 if baseline.cost == 0.0 else (baseline.cost - best.cost) / baseline.cost, + "output_min": outcome.output_min, + "output_max": outcome.output_max, + "result_dir": str(outcome.output_dir), + } + ) + + +def main() -> int: + args = parse_args() + model_input = Path(args.model_input) + output_dir = Path(args.output_dir) if args.output_dir else default_output_dir() + output_dir.mkdir(parents=True, exist_ok=True) + + if args.sample_time_ms <= 0.0: + print("--sample-time-ms must be positive", file=sys.stderr) + return 2 + if args.step_duration_ms <= 0.0 or args.pre_roll_ms < 0.0: + print("--step-duration-ms must be positive and --pre-roll-ms must be non-negative", file=sys.stderr) + return 2 + + try: + models = load_models(model_input, args.wheel_id, args.run_id) + except Exception as exc: + print(f"failed to load model input: {exc}", file=sys.stderr) + return 2 + + trace_map = None + if args.trace_csv: + try: + trace_map = load_trace_map(Path(args.trace_csv), args.sample_time_ms / 1000.0) + except Exception as exc: + print(f"failed to load trace CSV: {exc}", file=sys.stderr) + return 2 + + rng = random.Random(args.random_seed) + outcomes: list[TuningOutcome] = [] + + for model in models: + try: + traces = select_traces( + model=model, + trace_map=trace_map, + trace_scope=args.trace_scope, + step_setpoint=args.step_setpoint, + step_duration_ms=args.step_duration_ms, + pre_roll_ms=args.pre_roll_ms, + ) + outcome = tune_one_model( + model=model, + traces=traces, + args=args, + rng=rng, + output_dir=output_dir, + ) + except Exception as exc: + print(f"[error] {model.wheel_id}/run{model.run_id}: {exc}", file=sys.stderr) + continue + + outcomes.append(outcome) + if not args.quiet: + print( + ( + f"[ok] {model.wheel_id}/run{model.run_id}: " + f"kp={outcome.best.kp:.9g}, kd={outcome.best.kd:.9g}, " + f"cost={outcome.best.cost:.6f}, " + f"baseline={outcome.baseline.cost:.6f}, " + f"dir={outcome.output_dir}" + ) + ) + + if not outcomes: + print("no model was successfully tuned", file=sys.stderr) + return 1 + + summary_path = output_dir / "tuning_summary.csv" + write_summary_csv(summary_path, outcomes) + if not args.quiet: + print(f"summary: {summary_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 65e7038de9b47ddad060b6c104a840cab40bdbe0 Mon Sep 17 00:00:00 2001 From: zhzy-star <2807406212@qq.com> Date: Mon, 23 Mar 2026 23:07:37 +0800 Subject: [PATCH 12/12] Update putter_controller: demo --- .../controller/shooting/putter_controller.cpp | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp index b1b621c13..fd6455771 100644 --- a/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp +++ b/rmcs_ws/src/rmcs_core/src/controller/shooting/putter_controller.cpp @@ -130,18 +130,8 @@ class PutterController *bullet_feeder_control_torque_ = bullet_feeder_velocity_pid_.update(velocity_err); if (!bullet_feeder_cool_down_) { - if (*photoelectric_sensor_status_) { - RCLCPP_INFO( - get_logger(), "Photoelectric Sensor Triggered during Jam Protection"); - *bullet_feeder_control_torque_ = 0.; - bullet_feeder_cool_down_ = 0; - // 认为是满链状态,直接设置为预装弹完成 - set_preloaded(); - } else { - RCLCPP_INFO(get_logger(), "Jamming Solved, Retrying..."); - set_preloading(); - // 认为是卡弹状态,继续保持在后退位置直到冷却结束 - } + RCLCPP_INFO(get_logger(), "Jamming Solved, Retrying..."); + set_preloading(); } } else { // 正常运行模式:摩擦轮就绪时才允许发射 @@ -156,7 +146,14 @@ class PutterController if ( // *control_bullet_allowance_limited_by_heat_ > 0 && shoot_stage_ == ShootStage::PRELOADED) - set_shooting(); + if (last_photoelectric_sensor_status_ + && *photoelectric_sensor_status_) { + RCLCPP_INFO( + get_logger(), "Photoelectric sensor triggered, shooting!"); + set_shooting(); + } else { + set_preloading(); + } } } if (shoot_stage_ == ShootStage::UPDATING) { @@ -182,12 +179,10 @@ class PutterController update_jam_detection(); } else if (shoot_stage_ == ShootStage::SHOOTING) { - // 发射状态:供弹盘不发力 - if (!last_shoot_flag_) { - bullet_feeder_control_angle_ = *bullet_feeder_angle_; - last_shoot_flag_ = true; - } *bullet_feeder_control_torque_ = 0; + RCLCPP_INFO( + get_logger(), "Shooting! Angle: %.2f, Velocity: %.2f", + *bullet_feeder_angle_, *bullet_feeder_velocity_); } else { // 其他状态:角度环保持角度不变防止弹链退弹 double velocity_err = @@ -201,9 +196,10 @@ class PutterController if (shoot_stage_ == ShootStage::SHOOTING) { // 发射状态:检测子弹是否发出 if (*putter_angle_ - putter_startpoint >= putter_stroke_) { - if (!shooted){ - RCLCPP_INFO(get_logger(),"%f", *putter_angle_); + if (!shooted) { + RCLCPP_INFO(get_logger(), "%f", *putter_angle_); } + last_photoelectric_sensor_status_ = false; shooted = true; } @@ -216,7 +212,6 @@ class PutterController *putter_control_torque_ = 0.; set_updating(); shooted = false; - last_shoot_flag_ = false; } else { *putter_control_torque_ = putter_return_velocity_pid_.update(-80. - *putter_velocity_); @@ -257,6 +252,7 @@ class PutterController last_switch_left_ = rmcs_msgs::Switch::UNKNOWN; last_mouse_ = rmcs_msgs::Mouse::zero(); last_keyboard_ = rmcs_msgs::Keyboard::zero(); + last_photoelectric_sensor_status_ = false; bullet_feeder_reset(); overdrive_mode_ = false; @@ -287,6 +283,9 @@ class PutterController void set_preloading() { // RCLCPP_INFO(get_logger(), "PRELOADING"); + if (*photoelectric_sensor_status_) { + last_photoelectric_sensor_status_ = true; + } shoot_stage_ = ShootStage::PRELOADING; // 盲拨方案:直接增加目标角度 if (!std::isnan(bullet_feeder_control_angle_)) { @@ -297,6 +296,9 @@ class PutterController void set_preloaded() { // RCLCPP_INFO(get_logger(), "PRELOADED"); + if (*photoelectric_sensor_status_) { + last_photoelectric_sensor_status_ = true; + } shoot_stage_ = ShootStage::PRELOADED; last_preload_flag_ = false; } @@ -320,14 +322,13 @@ class PutterController } else { bullet_feeder_faulty_count_ = 0; } - if (bullet_feeder_faulty_count_ >= 1000) + if (bullet_feeder_faulty_count_ >= 500) enter_jam_protection(); } void update_putter_jam_detection() { if ((*putter_control_torque_ > -0.03 && shoot_stage_ == ShootStage::PRELOADING) - || ((*putter_control_torque_ < 0.08) - && shoot_stage_ == ShootStage::SHOOTING) + || ((*putter_control_torque_ < 0.08) && shoot_stage_ == ShootStage::SHOOTING) || std::isnan(*putter_control_torque_)) { putter_faulty_count_ = 0; return; @@ -344,7 +345,7 @@ class PutterController putter_startpoint = *putter_angle_; } else { // 发射状态下检测到堵转:认为子弹已发出 - RCLCPP_INFO(get_logger(),"dd"); + RCLCPP_INFO(get_logger(), "dd"); shooted = true; } } @@ -455,6 +456,7 @@ class PutterController InputInterface photoelectric_sensor_status_; InputInterface bullet_fired_; bool shooted{false}; + bool last_photoelectric_sensor_status_ = false; InputInterface friction_ready_; @@ -477,7 +479,6 @@ class PutterController InputInterface control_bullet_allowance_limited_by_heat_; bool last_preload_flag_ = false; - bool last_shoot_flag_ = false; bool putter_initialized = false; int putter_faulty_count_ = 0; @@ -488,7 +489,7 @@ class PutterController pid::PidCalculator putter_velocity_pid_; enum class ShootStage { PRELOADING, PRELOADED, SHOOTING, UPDATING }; - ShootStage shoot_stage_ = ShootStage::PRELOADED; + ShootStage shoot_stage_ = ShootStage::PRELOADING; double bullet_feeder_control_angle_ = nan_; pid::PidCalculator bullet_feeder_velocity_pid_;