diff --git a/.clang-format b/.clang-format
index 994474914..5c1eec782 100644
--- a/.clang-format
+++ b/.clang-format
@@ -13,27 +13,25 @@ MaxEmptyLinesToKeep: 1
AlignAfterOpenBracket: AlwaysBreak
-AlignArrayOfStructures: Right
-
AlignConsecutiveAssignments:
- Enabled: false
+ Enabled: false
AlignConsecutiveBitFields:
- Enabled: true
- AcrossEmptyLines: false
- AcrossComments: false
+ Enabled: true
+ AcrossEmptyLines: false
+ AcrossComments: false
AlignConsecutiveDeclarations:
- Enabled: false
+ Enabled: false
AlignConsecutiveMacros:
- Enabled: true
- AcrossEmptyLines: false
- AcrossComments: false
+ Enabled: true
+ AcrossEmptyLines: false
+ AcrossComments: false
# AlignConsecutiveShortCaseStatements:
# Enabled: false
AlignEscapedNewlines: Left
AlignOperands: AlignAfterOperator
AlignTrailingComments:
- Kind: Always
- OverEmptyLines: 64
+ Kind: Always
+ OverEmptyLines: 64
PointerAlignment: Left
AllowAllArgumentsOnNextLine: true
diff --git a/.script/remote-status b/.script/remote-status
new file mode 100755
index 000000000..f5805e05c
--- /dev/null
+++ b/.script/remote-status
@@ -0,0 +1,65 @@
+#!/bin/env bash
+
+set -euo pipefail
+
+# 定义需要查询的服务列表
+services=(
+ "/rmcs/service/robot_status"
+)
+
+# 使用 bash 远程执行,并传递服务列表作为参数
+ssh remote bash -s -- "${services[@]}" <<'EOF'
+set -euo pipefail
+
+# 1. 环境准备
+set +u
+if [ -f ~/env_setup.bash ]; then
+ source ~/env_setup.bash
+fi
+set -u
+
+# 获取所有可用服务列表
+all_services="$(ros2 service list 2>/dev/null || true)"
+input_services=("${@}")
+
+# 2. 定义单个服务查询函数
+call_status_service() {
+ local service="$1"
+
+ if ! printf "%s\n" "$all_services" | grep -Fxq "$service"; then
+ printf "[warn] service not found: %s\n\n" "$service"
+ return
+ fi
+
+ printf "=== %s ===\n" "$service"
+ local raw
+ raw="$(ros2 service call "$service" std_srvs/srv/Trigger "{}" 2>&1 || true)"
+
+ local msg
+ msg="$(printf "%s\n" "$raw" | sed -n "s/.*message='\(.*\)'.*/\1/p")"
+
+ if [[ -z "$msg" ]]; then
+ printf "[warn] failed to parse message: %s\n%s\n" "$service" "$raw"
+ else
+ printf "%b\n" "$msg"
+ fi
+ printf "\n"
+}
+
+# 3. 并发执行逻辑
+tmp_dir=$(mktemp -d)
+trap 'rm -rf "${tmp_dir}"' EXIT
+
+for i in "${!input_services[@]}"; do
+ call_status_service "${input_services[$i]}" > "${tmp_dir}/${i}" 2>&1 &
+done
+
+wait
+
+# 4. 按原始顺序汇总输出
+for i in "${!input_services[@]}"; do
+ if [ -f "${tmp_dir}/${i}" ]; then
+ cat "${tmp_dir}/${i}"
+ fi
+done
+EOF
diff --git a/Dockerfile b/Dockerfile
index 5fce6b00f..f35ba3ae3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -30,8 +30,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libceres-dev \
ros-$ROS_DISTRO-rviz2 \
ros-$ROS_DISTRO-foxglove-bridge \
- dotnet-sdk-8.0 \
ros-$ROS_DISTRO-pcl-ros ros-$ROS_DISTRO-pcl-conversions ros-$ROS_DISTRO-pcl-msgs && \
+ ros-$ROS_DISTRO-navigation2 ros-$ROS_DISTRO-nav2-msgs \
+ lua5.4 liblua5.4-0 liblua5.4-dev && \
apt-get autoremove -y && apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/*
diff --git a/rmcs_ws/src/rmcs_bringup/config/sentry.yaml b/rmcs_ws/src/rmcs_bringup/config/sentry.yaml
new file mode 100644
index 000000000..85e636c1c
--- /dev/null
+++ b/rmcs_ws/src/rmcs_bringup/config/sentry.yaml
@@ -0,0 +1,164 @@
+rmcs_executor:
+ ros__parameters:
+ update_rate: 1000.0
+ components:
+ - rmcs_core::hardware::Sentry -> sentry_hardware
+
+ - rmcs_core::referee::Status -> referee_status
+ - rmcs_core::referee::Command -> referee_command
+
+ - rmcs_core::referee::command::Interaction -> referee_interaction
+ - rmcs_core::referee::command::interaction::Ui -> referee_ui
+
+ - rmcs_core::controller::gimbal::EccentricDualYaw -> gimbal_controller
+
+ # - rmcs_core::broadcaster::TfBroadcaster -> tf_broadcaster
+
+ - rmcs_core::controller::shooting::FrictionWheelController -> friction_wheel_controller
+ - rmcs_core::controller::shooting::HeatController -> heat_controller
+ - rmcs_core::controller::shooting::BulletFeederController17mm -> bullet_feeder_controller
+ - rmcs_core::controller::pid::PidController -> left_friction_velocity_pid_controller
+ - rmcs_core::controller::pid::PidController -> right_friction_velocity_pid_controller
+ - rmcs_core::controller::pid::PidController -> bullet_feeder_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::navigation::Navigation -> rmcs_navigation
+
+# The positive direction is the one that battery exists
+sentry_hardware:
+ ros__parameters:
+ board_serial_top_board: "af-da30"
+ board_serial_bottom_board: "d4-2184"
+ board_serial_gimbal_board: "d4-1d2b"
+ bottom_yaw_motor_zero_point: 26163
+ top_yaw_motor_zero_point: 4540
+ pitch_motor_zero_point: 1446
+ left_front_zero_point: 7131
+ left_back_zero_point: 3740
+ right_back_zero_point: 1327
+ right_front_zero_point: 5147
+
+rmcs_navigation:
+ ros__parameters:
+ # 策略名称:
+ # - fast-push-output "速推前哨站"
+ # - kill-robots "杀伤优先"
+ decision: "fast-push-output"
+ command_vel_name: "/cmd_vel"
+ mock_context: false
+ endpoint: "test"
+ enable_goal_topic_forward: true
+
+gimbal_controller:
+ ros__parameters:
+ upper_limit: -0.39518
+ lower_limit: 0.36
+
+ top_yaw_angle_kp: 30.0
+ top_yaw_angle_ki: 0.0
+ top_yaw_angle_kd: 0.0
+ top_yaw_velocity_kp: 2.160
+ top_yaw_velocity_ki: 0.0
+ top_yaw_velocity_kd: 0.0
+
+ bottom_yaw_angle_kp: 8.8
+ bottom_yaw_angle_ki: 0.0
+ bottom_yaw_angle_kd: 0.0
+ bottom_yaw_velocity_kp: 22.49
+ bottom_yaw_velocity_ki: 0.0
+ bottom_yaw_velocity_kd: 0.0
+
+ pitch_angle_kp: 40.0
+ pitch_angle_ki: 0.0
+ pitch_angle_kd: 0.01
+ pitch_velocity_kp: 2.5
+ pitch_velocity_ki: 0.0
+ pitch_velocity_kd: 0.0
+
+ k_top_to_bottom: -1.0
+
+ bottom_yaw_viscous_ff_gain: 0.002495
+ # bottom_yaw_coulomb_ff_gain: 0.457343
+ # bottom_yaw_coulomb_ff_tanh_gain: 100.0
+ top_yaw_viscous_ff_gain: 0.231
+ # top_yaw_coulomb_ff_gain: 1.12
+ # top_yaw_coulomb_ff_tanh_gain: 100.0
+ pitch_viscous_ff_gain: 0.33
+ # pitch_coulomb_ff_gain: 0.95
+ # pitch_coulomb_ff_tanh_gain: 100.0
+ pitch_gravity_ff_gain: 2.128
+ pitch_gravity_ff_phase: 1.438
+
+chassis_controller:
+ ros__parameters:
+ navigation_velocity_scale: 1.0
+
+friction_wheel_controller:
+ ros__parameters:
+ friction_wheels:
+ - /gimbal/left_friction
+ - /gimbal/right_friction
+ friction_velocities:
+ - 630.0
+ - 630.0
+ friction_soft_start_stop_time: 1.0
+
+heat_controller:
+ ros__parameters:
+ heat_per_shot: 10000
+ reserved_heat: 10000
+
+bullet_feeder_controller:
+ ros__parameters:
+ bullets_per_feeder_turn: 9.0
+ shot_frequency: 28.0
+ safe_shot_frequency: 10.0
+ eject_frequency: 15.0
+ eject_time: 0.15
+ deep_eject_frequency: 15.0
+ deep_eject_time: 0.20
+ single_shot_max_stop_delay: 2.0
+
+left_friction_velocity_pid_controller:
+ ros__parameters:
+ measurement: /gimbal/left_friction/velocity
+ setpoint: /gimbal/left_friction/control_velocity
+ control: /gimbal/left_friction/control_torque
+ kp: 0.003436926
+ ki: 0.00
+ kd: 0.009373434
+
+right_friction_velocity_pid_controller:
+ ros__parameters:
+ measurement: /gimbal/right_friction/velocity
+ setpoint: /gimbal/right_friction/control_velocity
+ control: /gimbal/right_friction/control_torque
+ kp: 0.003436926
+ ki: 0.00
+ kd: 0.009373434
+
+bullet_feeder_velocity_pid_controller:
+ ros__parameters:
+ measurement: /gimbal/bullet_feeder/velocity
+ setpoint: /gimbal/bullet_feeder/control_velocity
+ control: /gimbal/bullet_feeder/control_torque
+ kp: 0.283
+ ki: 0.0
+ kd: 0.0
+
+steering_wheel_controller:
+ ros__parameters:
+ mess: 22.0
+ moment_of_inertia: 0.77852676
+ vehicle_radius: 0.26870058
+ wheel_radius: 0.055
+ friction_coefficient: 0.666
+ k1: 2.958580e+00
+ k2: 3.082190e-03
+ no_load_power: 11.37
+ chassis_translation_kp: 8.0
+ chassis_translation_ki: 0.0
+ chassis_translation_kd: 0.0
diff --git a/rmcs_ws/src/rmcs_core/CMakeLists.txt b/rmcs_ws/src/rmcs_core/CMakeLists.txt
index dd3c85799..36e7e5706 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.0/librmcs-sdk-src-3.1.0.zip
+ URL_HASH SHA256=07107e251745ddb23f7b3e39edec5d6910be1a197025d167ec9849c5c80dd954
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(librmcs)
diff --git a/rmcs_ws/src/rmcs_core/package.xml b/rmcs_ws/src/rmcs_core/package.xml
index 410745520..4312d3341 100644
--- a/rmcs_ws/src/rmcs_core/package.xml
+++ b/rmcs_ws/src/rmcs_core/package.xml
@@ -11,6 +11,7 @@
rclcpp
std_msgs
+ std_srvs
pluginlib
tf2
tf2_ros
diff --git a/rmcs_ws/src/rmcs_core/plugins.xml b/rmcs_ws/src/rmcs_core/plugins.xml
index f7847151c..db9ff7c45 100644
--- a/rmcs_ws/src/rmcs_core/plugins.xml
+++ b/rmcs_ws/src/rmcs_core/plugins.xml
@@ -2,6 +2,7 @@
+
@@ -10,6 +11,7 @@
+
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..739b58592 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
@@ -145,4 +145,4 @@ class DualYawController
#include
-PLUGINLIB_EXPORT_CLASS(rmcs_core::controller::gimbal::DualYawController, rmcs_executor::Component)
\ No newline at end of file
+PLUGINLIB_EXPORT_CLASS(rmcs_core::controller::gimbal::DualYawController, rmcs_executor::Component)
diff --git a/rmcs_ws/src/rmcs_core/src/controller/gimbal/eccentric_dual_yaw.cpp b/rmcs_ws/src/rmcs_core/src/controller/gimbal/eccentric_dual_yaw.cpp
new file mode 100644
index 000000000..4e6c4e72d
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/controller/gimbal/eccentric_dual_yaw.cpp
@@ -0,0 +1,308 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "controller/pid/pid_calculator.hpp"
+
+namespace rmcs_core::controller::gimbal {
+using namespace rmcs_description;
+
+class EccentricDualYaw
+ : public rmcs_executor::Component
+ , public rclcpp::Node {
+public:
+ EccentricDualYaw()
+ : Node{
+ get_component_name(),
+ rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)} {}
+
+ auto before_updating() -> void override {
+ enter_disabled_state();
+ previous_actual_yaw_ = current_barrel_yaw_pitch().first;
+ previous_yaw_timestamp_ = *input_.timestamp;
+ }
+
+ auto update() -> void override {
+ const auto actual_yaw_pitch = current_barrel_yaw_pitch();
+ *output_.yaw_angle = *input_.bottom_yaw_angle;
+ *output_.yaw_velocity = compute_actual_yaw_velocity(actual_yaw_pitch.first);
+
+ if (!input_.enable_control()) {
+ enter_disabled_state();
+ return;
+ }
+
+ const double yaw_shift = kJoystickSensitivity * input_.joystick_left->y()
+ + kMouseSensitivity * input_.mouse_velocity->y();
+ const double pitch_shift = -kJoystickSensitivity * input_.joystick_left->x()
+ - kMouseSensitivity * input_.mouse_velocity->x();
+
+ manual_bottom_yaw_target_ = limit_rad(manual_bottom_yaw_target_ + yaw_shift);
+ manual_pitch_target_ =
+ std::clamp(manual_pitch_target_ + pitch_shift, upper_limit_, lower_limit_);
+
+ const auto manual_target = ControlTarget{
+ .bottom_yaw = {.target = manual_bottom_yaw_target_},
+ .top_yaw = {.target = 0.0},
+ .pitch = {.target = manual_pitch_target_},
+ };
+ apply_control(manual_target);
+ }
+
+private:
+ static constexpr double kNaN = std::numeric_limits::quiet_NaN();
+ static constexpr double kJoystickSensitivity = 0.006;
+ static constexpr double kMouseSensitivity = 0.5;
+
+ const double upper_limit_{get_parameter("upper_limit").as_double()};
+ const double lower_limit_{get_parameter("lower_limit").as_double()};
+
+ const double bottom_yaw_viscous_ff_gain_{get_parameter_or("bottom_yaw_viscous_ff_gain", 0.0)};
+ const double bottom_yaw_coulomb_ff_gain_{get_parameter_or("bottom_yaw_coulomb_ff_gain", 0.0)};
+ const double bottom_yaw_coulomb_ff_tanh_gain_{
+ get_parameter_or("bottom_yaw_coulomb_ff_tanh_gain", 100.0)};
+ const double k_top_to_bottom_{get_parameter_or("k_top_to_bottom", 0.0)};
+ const double top_yaw_viscous_ff_gain_{get_parameter_or("top_yaw_viscous_ff_gain", 0.0)};
+ const double top_yaw_coulomb_ff_gain_{get_parameter_or("top_yaw_coulomb_ff_gain", 0.0)};
+ const double top_yaw_coulomb_ff_tanh_gain_{
+ get_parameter_or("top_yaw_coulomb_ff_tanh_gain", 100.0)};
+ const double pitch_viscous_ff_gain_{get_parameter_or("pitch_viscous_ff_gain", 0.0)};
+ const double pitch_coulomb_ff_gain_{get_parameter_or("pitch_coulomb_ff_gain", 0.0)};
+ const double pitch_coulomb_ff_tanh_gain_{get_parameter_or("pitch_coulomb_ff_tanh_gain", 100.0)};
+ const double pitch_gravity_ff_gain_{get_parameter_or("pitch_gravity_ff_gain", 0.0)};
+ const double pitch_gravity_ff_phase_{get_parameter_or("pitch_gravity_ff_phase", 0.0)};
+
+ struct AxisCommand {
+ double target = 0.0;
+ double velocity_ff = 0.0;
+ double acceleration_ff = 0.0;
+ };
+ struct ControlTarget {
+ AxisCommand bottom_yaw;
+ AxisCommand top_yaw;
+ AxisCommand pitch;
+ };
+
+ struct Input {
+ explicit Input(rmcs_executor::Component& component) {
+ component.register_input("/remote/joystick/left", joystick_left);
+ component.register_input("/remote/switch/right", switch_right);
+ component.register_input("/remote/switch/left", switch_left);
+ component.register_input("/remote/mouse/velocity", mouse_velocity);
+
+ component.register_input("/predefined/timestamp", timestamp);
+ component.register_input("/tf", tf);
+
+ component.register_input("/gimbal/top_yaw/angle", top_yaw_angle);
+ component.register_input("/gimbal/top_yaw/velocity", top_yaw_velocity);
+ component.register_input("/gimbal/bottom_yaw/angle", bottom_yaw_angle);
+ component.register_input("/gimbal/bottom_yaw/velocity", bottom_yaw_velocity);
+ component.register_input("/gimbal/pitch/angle", pitch_angle);
+ component.register_input("/gimbal/pitch/velocity", pitch_velocity);
+ component.register_input("/chassis/yaw/velocity_imu", chassis_yaw_velocity_imu);
+ }
+
+ auto enable_control() const noexcept -> bool {
+ using namespace rmcs_msgs;
+ if ((*switch_left == Switch::UNKNOWN || *switch_right == Switch::UNKNOWN)
+ || (*switch_left == Switch::DOWN && *switch_right == Switch::DOWN)) {
+ return false;
+ }
+ return true;
+ }
+
+ InputInterface joystick_left;
+ InputInterface switch_right;
+ InputInterface switch_left;
+ InputInterface mouse_velocity;
+
+ InputInterface timestamp;
+ InputInterface tf;
+
+ InputInterface top_yaw_angle;
+ InputInterface top_yaw_velocity;
+ InputInterface bottom_yaw_angle;
+ InputInterface bottom_yaw_velocity;
+ InputInterface pitch_angle;
+ InputInterface pitch_velocity;
+ InputInterface chassis_yaw_velocity_imu;
+ } input_{*this};
+
+ struct Output {
+ explicit Output(rmcs_executor::Component& component) {
+ component.register_output(
+ "/gimbal/top_yaw/control_torque", top_yaw_control_torque, kNaN);
+ component.register_output(
+ "/gimbal/bottom_yaw/control_torque", bottom_yaw_control_torque, kNaN);
+ component.register_output("/gimbal/pitch/control_torque", pitch_control_torque, kNaN);
+
+ component.register_output(
+ "/gimbal/yaw/control_angle_error", yaw_control_angle_error, kNaN);
+ component.register_output("/gimbal/yaw/angle", yaw_angle, 0.0);
+ component.register_output("/gimbal/yaw/velocity", yaw_velocity, 0.0);
+ }
+
+ OutputInterface top_yaw_control_torque;
+ OutputInterface bottom_yaw_control_torque;
+ OutputInterface pitch_control_torque;
+
+ OutputInterface yaw_control_angle_error;
+ OutputInterface yaw_angle;
+ OutputInterface yaw_velocity;
+ } output_{*this};
+
+ pid::PidCalculator top_yaw_angle_pid_{pid::make_pid_calculator(*this, "top_yaw_angle_")};
+ pid::PidCalculator top_yaw_velocity_pid_{pid::make_pid_calculator(*this, "top_yaw_velocity_")};
+ pid::PidCalculator bottom_yaw_angle_pid_{pid::make_pid_calculator(*this, "bottom_yaw_angle_")};
+ pid::PidCalculator bottom_yaw_velocity_pid_{
+ pid::make_pid_calculator(*this, "bottom_yaw_velocity_")};
+ pid::PidCalculator pitch_angle_pid_{pid::make_pid_calculator(*this, "pitch_angle_")};
+ pid::PidCalculator pitch_velocity_pid_{pid::make_pid_calculator(*this, "pitch_velocity_")};
+
+ double manual_bottom_yaw_target_ = 0.0;
+ double manual_pitch_target_ = 0.0;
+ double previous_actual_yaw_ = 0.0;
+ std::chrono::steady_clock::time_point previous_yaw_timestamp_{};
+
+ static constexpr auto limit_rad(double angle) -> double {
+ constexpr double kPi = std::numbers::pi_v;
+ while (angle > kPi)
+ angle -= 2.0 * kPi;
+ while (angle <= -kPi)
+ angle += 2.0 * kPi;
+ return angle;
+ }
+
+ auto reset_all_controls() -> void {
+ top_yaw_angle_pid_.reset();
+ top_yaw_velocity_pid_.reset();
+ bottom_yaw_angle_pid_.reset();
+ bottom_yaw_velocity_pid_.reset();
+ pitch_angle_pid_.reset();
+ pitch_velocity_pid_.reset();
+
+ *output_.top_yaw_control_torque = kNaN;
+ *output_.bottom_yaw_control_torque = kNaN;
+ *output_.pitch_control_torque = kNaN;
+ }
+
+ auto enter_disabled_state() -> void {
+ reset_all_controls();
+
+ manual_bottom_yaw_target_ = current_bottom_world_yaw();
+ manual_pitch_target_ =
+ std::clamp(limit_rad(*input_.pitch_angle), upper_limit_, lower_limit_);
+
+ *output_.yaw_control_angle_error = kNaN;
+ }
+
+ auto compute_actual_yaw_velocity(double actual_yaw) -> double {
+ const auto now = *input_.timestamp;
+ const double dt = std::chrono::duration(now - previous_yaw_timestamp_).count();
+ double velocity = 0.0;
+ if (dt > 1e-6)
+ velocity = limit_rad(actual_yaw - previous_actual_yaw_) / dt;
+ previous_actual_yaw_ = actual_yaw;
+ previous_yaw_timestamp_ = now;
+ return velocity;
+ }
+
+ auto current_barrel_yaw_pitch() const -> std::pair {
+ auto direction = fast_tf::cast(
+ PitchLink::DirectionVector{Eigen::Vector3d::UnitX()}, *input_.tf);
+ Eigen::Vector3d vector = *direction;
+ if (vector.norm() > 1e-9)
+ vector.normalize();
+ else
+ vector = Eigen::Vector3d::UnitX();
+ const double xy_norm = std::hypot(vector.x(), vector.y());
+ return {std::atan2(vector.y(), vector.x()), std::atan2(-vector.z(), xy_norm)};
+ }
+
+ auto current_bottom_world_yaw() const -> double {
+ auto direction = fast_tf::cast(
+ BottomYawLink::DirectionVector{Eigen::Vector3d::UnitX()}, *input_.tf);
+ Eigen::Vector3d vector = *direction;
+ vector.z() = 0.0;
+ if (vector.norm() > 1e-9)
+ vector.normalize();
+ else
+ vector = Eigen::Vector3d::UnitX();
+ return std::atan2(vector.y(), vector.x());
+ }
+
+ auto apply_control(const ControlTarget& target) -> void {
+
+ constexpr auto friction_feedforward = [](double viscous_gain, double coulomb_gain,
+ double tanh_gain, double velocity) -> double {
+ return (viscous_gain * velocity) + (coulomb_gain * std::tanh(tanh_gain * velocity));
+ };
+
+ const double current_bottom_angle = current_bottom_world_yaw();
+ const double current_bottom_velocity =
+ *input_.bottom_yaw_velocity + *input_.chassis_yaw_velocity_imu;
+ const double current_top_angle = limit_rad(*input_.top_yaw_angle);
+ const double current_pitch_angle = limit_rad(*input_.pitch_angle);
+
+ const double bottom_yaw_error = limit_rad(target.bottom_yaw.target - current_bottom_angle);
+ const double top_yaw_error = limit_rad(target.top_yaw.target - current_top_angle);
+ const double pitch_error = limit_rad(target.pitch.target - current_pitch_angle);
+
+ const double bottom_velocity_ref =
+ bottom_yaw_angle_pid_.update(bottom_yaw_error) + target.bottom_yaw.velocity_ff;
+ const double top_velocity_ref =
+ top_yaw_angle_pid_.update(top_yaw_error) + target.top_yaw.velocity_ff;
+ const double pitch_velocity_ref =
+ pitch_angle_pid_.update(pitch_error) + target.pitch.velocity_ff;
+
+ const double bottom_world_velocity_ff =
+ target.bottom_yaw.velocity_ff + *input_.chassis_yaw_velocity_imu;
+ const double top_yaw_continuous_torque_ff =
+ target.top_yaw.acceleration_ff + top_yaw_viscous_ff_gain_ * target.top_yaw.velocity_ff;
+ const double bottom_yaw_torque_ff =
+ target.bottom_yaw.acceleration_ff
+ + friction_feedforward(
+ bottom_yaw_viscous_ff_gain_, bottom_yaw_coulomb_ff_gain_,
+ bottom_yaw_coulomb_ff_tanh_gain_, bottom_world_velocity_ff)
+ - k_top_to_bottom_ * top_yaw_continuous_torque_ff;
+
+ const double top_yaw_torque_ff = target.top_yaw.acceleration_ff
+ + friction_feedforward(
+ top_yaw_viscous_ff_gain_, top_yaw_coulomb_ff_gain_,
+ top_yaw_coulomb_ff_tanh_gain_, top_velocity_ref);
+ const double pitch_torque_ff =
+ target.pitch.acceleration_ff
+ + friction_feedforward(
+ pitch_viscous_ff_gain_, pitch_coulomb_ff_gain_, pitch_coulomb_ff_tanh_gain_,
+ pitch_velocity_ref)
+ + pitch_gravity_ff_gain_ * std::sin(current_pitch_angle - pitch_gravity_ff_phase_);
+
+ *output_.bottom_yaw_control_torque =
+ bottom_yaw_velocity_pid_.update(bottom_velocity_ref - current_bottom_velocity)
+ + bottom_yaw_torque_ff;
+ *output_.top_yaw_control_torque =
+ top_yaw_velocity_pid_.update(top_velocity_ref - *input_.top_yaw_velocity)
+ + top_yaw_torque_ff;
+ *output_.pitch_control_torque =
+ pitch_velocity_pid_.update(pitch_velocity_ref - *input_.pitch_velocity)
+ + pitch_torque_ff;
+
+ *output_.yaw_control_angle_error = bottom_yaw_error;
+ }
+};
+
+} // namespace rmcs_core::controller::gimbal
+
+#include
+
+PLUGINLIB_EXPORT_CLASS(rmcs_core::controller::gimbal::EccentricDualYaw, rmcs_executor::Component)
diff --git a/rmcs_ws/src/rmcs_core/src/controller/pid/pid_calculator.hpp b/rmcs_ws/src/rmcs_core/src/controller/pid/pid_calculator.hpp
index 4eba4ff4d..2951f53f8 100644
--- a/rmcs_ws/src/rmcs_core/src/controller/pid/pid_calculator.hpp
+++ b/rmcs_ws/src/rmcs_core/src/controller/pid/pid_calculator.hpp
@@ -4,6 +4,10 @@
#include
#include
+#include
+#include
+
+#include
namespace rmcs_core::controller::pid {
@@ -21,7 +25,7 @@ class PidCalculator {
virtual ~PidCalculator() = default;
void reset() {
- last_err_ = nan;
+ last_err_ = nan;
err_integral_ = 0;
}
@@ -57,4 +61,32 @@ class PidCalculator {
double last_err_, err_integral_;
};
-} // namespace rmcs_core::controller::pid
\ No newline at end of file
+inline auto make_pid_calculator(
+ rclcpp::Node& node, const std::string& prefix, //
+ std::optional kp_default = std::nullopt,
+ std::optional ki_default = std::nullopt,
+ std::optional kd_default = std::nullopt) {
+
+ const auto parameter_or_default =
+ [&node](const std::string& name, std::optional default_value) {
+ if (default_value.has_value() && !node.has_parameter(name))
+ node.declare_parameter(name, *default_value);
+ return node.get_parameter(name).as_double();
+ };
+
+ auto calculator = PidCalculator{
+ parameter_or_default(prefix + "kp", kp_default),
+ parameter_or_default(prefix + "ki", ki_default),
+ parameter_or_default(prefix + "kd", kd_default),
+ };
+
+ node.get_parameter(prefix + "integral_min", calculator.integral_min);
+ node.get_parameter(prefix + "integral_max", calculator.integral_max);
+ node.get_parameter(prefix + "integral_split_min", calculator.integral_split_min);
+ node.get_parameter(prefix + "integral_split_max", calculator.integral_split_max);
+ node.get_parameter(prefix + "output_min", calculator.output_min);
+ node.get_parameter(prefix + "output_max", calculator.output_max);
+ return calculator;
+}
+
+} // namespace rmcs_core::controller::pid
diff --git a/rmcs_ws/src/rmcs_core/src/hardware/device/dji_motor.hpp b/rmcs_ws/src/rmcs_core/src/hardware/device/dji_motor.hpp
index bb3f1c932..d1c56cbfd 100644
--- a/rmcs_ws/src/rmcs_core/src/hardware/device/dji_motor.hpp
+++ b/rmcs_ws/src/rmcs_core/src/hardware/device/dji_motor.hpp
@@ -200,6 +200,8 @@ class DjiMotor {
return encoder_zero_point_;
}
+ int last_raw_angle() const { return last_raw_angle_; }
+
double angle() const { return angle_; }
double velocity() const { return velocity_; }
double torque() const { return torque_; }
diff --git a/rmcs_ws/src/rmcs_core/src/hardware/device/lk_motor.hpp b/rmcs_ws/src/rmcs_core/src/hardware/device/lk_motor.hpp
index 68b2e4f65..c11061c23 100644
--- a/rmcs_ws/src/rmcs_core/src/hardware/device/lk_motor.hpp
+++ b/rmcs_ws/src/rmcs_core/src/hardware/device/lk_motor.hpp
@@ -203,6 +203,8 @@ class LkMotor {
return encoder_zero_point_;
}
+ int64_t last_raw_angle() const { return last_raw_angle_; }
+
double angle() const { return angle_; }
double velocity() const { return velocity_; }
double torque() const { return torque_; }
diff --git a/rmcs_ws/src/rmcs_core/src/hardware/sentry.cpp b/rmcs_ws/src/rmcs_core/src/hardware/sentry.cpp
new file mode 100644
index 000000000..9d917a2b1
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/hardware/sentry.cpp
@@ -0,0 +1,500 @@
+#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"
+#include "hardware/device/supercap.hpp"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace rmcs_core::hardware {
+
+class Sentry
+ : public rmcs_executor::Component
+ , public rclcpp::Node {
+public:
+ Sentry()
+ : Node(
+ get_component_name(),
+ rclcpp::NodeOptions().automatically_declare_parameters_from_overrides(true)) {
+
+ register_input("/predefined/timestamp", timestamp_);
+ register_output("/tf", tf_);
+
+ // For command: remote-status
+ using Srv = std_srvs::srv::Trigger;
+ status_service_ = create_service(
+ "/rmcs/service/robot_status",
+ [this](const Srv::Request::SharedPtr&, const Srv::Response::SharedPtr& response) {
+ status_service_callback(response);
+ });
+
+ 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());
+
+ gimbal_board_ =
+ std::make_unique(get_parameter("board_serial_gimbal_board").as_string());
+
+ tf_->set_transform(
+ Eigen::Translation3d{0.08, 0.0, 0.0});
+ tf_->set_transform(
+ Eigen::Translation3d{0.07128, 0.0, 0.0481});
+ }
+
+ auto update() -> void override {
+ top_board_->update();
+ bottom_board_->update();
+ gimbal_board_->update();
+ tf_->set_transform(
+ gimbal_board_->imu_pose().conjugate());
+ }
+
+private:
+ class GimbalBoard final : private librmcs::agent::CBoard {
+ public:
+ explicit GimbalBoard(std::string_view board_serial = {})
+ : librmcs::agent::CBoard(board_serial) {
+ bmi088_.set_coordinate_mapping(
+ [](double x, double y, double z) { return std::make_tuple(y, -x, z); });
+ }
+
+ GimbalBoard(const GimbalBoard&) = delete;
+ GimbalBoard& operator=(const GimbalBoard&) = delete;
+ GimbalBoard(GimbalBoard&&) = delete;
+ GimbalBoard& operator=(GimbalBoard&&) = delete;
+
+ ~GimbalBoard() override = default;
+
+ auto update() -> void {
+ bmi088_.update_status();
+ imu_pose_ = Eigen::Quaterniond{bmi088_.q0(), bmi088_.q1(), bmi088_.q2(), bmi088_.q3()};
+ }
+
+ auto imu_pose() const -> Eigen::Quaterniond { return imu_pose_; }
+
+ private:
+ auto accelerometer_receive_callback(const librmcs::data::AccelerometerDataView& data)
+ -> void override {
+ bmi088_.store_accelerometer_status(data.x, data.y, data.z);
+ }
+
+ auto gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data)
+ -> void override {
+ bmi088_.store_gyroscope_status(data.x, data.y, data.z);
+ }
+
+ device::Bmi088 bmi088_{1000, 0.2, 0.0};
+ Eigen::Quaterniond imu_pose_ = Eigen::Quaterniond::Identity();
+ };
+
+ class TopBoard final : private librmcs::agent::RmcsBoardLite {
+ friend class Sentry;
+
+ public:
+ explicit TopBoard(
+ Sentry& sentry, rmcs_executor::Component& sentry_command,
+ std::string_view board_serial = {}, librmcs::agent::AdvancedOptions options = {})
+ : librmcs::agent::RmcsBoardLite(board_serial, options)
+ , tf_(sentry.tf_)
+ , bmi088_(1000, 0.2, 0.0)
+ , gimbal_pitch_motor_(sentry, sentry_command, "/gimbal/pitch")
+ , gimbal_top_yaw_motor_(sentry, sentry_command, "/gimbal/top_yaw")
+ , gimbal_bullet_feeder_(sentry, sentry_command, "/gimbal/bullet_feeder")
+ , gimbal_left_friction_(sentry, sentry_command, "/gimbal/left_friction")
+ , gimbal_right_friction_(sentry, sentry_command, "/gimbal/right_friction") {
+ gimbal_pitch_motor_.configure(
+ device::LkMotor::Config{device::LkMotor::Type::kMG4010Ei10}.set_encoder_zero_point(
+ static_cast(sentry.get_parameter("pitch_motor_zero_point").as_int())));
+
+ gimbal_top_yaw_motor_.configure(
+ device::LkMotor::Config{device::LkMotor::Type::kMG4010Ei10}.set_encoder_zero_point(
+ static_cast(sentry.get_parameter("top_yaw_motor_zero_point").as_int())));
+
+ gimbal_bullet_feeder_.configure(
+ device::DjiMotor::Config{device::DjiMotor::Type::kM3508}
+ .enable_multi_turn_angle()
+ .set_reversed()
+ .set_reduction_ratio(19 * 2));
+
+ gimbal_left_friction_.configure(
+ device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio(1.));
+ gimbal_right_friction_.configure(
+ device::DjiMotor::Config{device::DjiMotor::Type::kM3508}
+ .set_reduction_ratio(1.)
+ .set_reversed());
+
+ sentry.register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_bmi088_);
+ sentry.register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_bmi088_);
+
+ bmi088_.set_coordinate_mapping(
+ [](double x, double y, double z) { return std::make_tuple(-x, -y, z); });
+ }
+
+ auto update() -> void {
+ gimbal_top_yaw_motor_.update_status();
+ gimbal_pitch_motor_.update_status();
+
+ const auto pitch_angle =
+ std::remainder(gimbal_pitch_motor_.angle(), 2.0 * std::numbers::pi_v);
+
+ bmi088_.update_status();
+ const Eigen::Quaterniond gimbal_bmi088_pose{
+ bmi088_.q0(), bmi088_.q1(), bmi088_.q2(), bmi088_.q3()};
+
+ tf_->set_transform(
+ gimbal_bmi088_pose.conjugate());
+
+ *gimbal_yaw_velocity_bmi088_ = bmi088_.gz();
+ *gimbal_pitch_velocity_bmi088_ = bmi088_.gy();
+
+ gimbal_bullet_feeder_.update_status();
+ gimbal_left_friction_.update_status();
+ gimbal_right_friction_.update_status();
+
+ tf_->set_state(
+ gimbal_top_yaw_motor_.angle());
+ tf_->set_state(pitch_angle);
+ }
+
+ auto command_update() -> void {
+ auto builder = start_transmit();
+
+ builder.can0_transmit({
+ .can_id = 0x200,
+ .can_data =
+ device::CanPacket8{
+ gimbal_right_friction_.generate_command(),
+ gimbal_left_friction_.generate_command(),
+ device::CanPacket8::PaddingQuarter{},
+ gimbal_bullet_feeder_.generate_command(),
+ }
+ .as_bytes(),
+ });
+
+ builder.can3_transmit({
+ .can_id = 0x141,
+ .can_data = gimbal_top_yaw_motor_.generate_torque_command().as_bytes(),
+ });
+
+ builder.can2_transmit({
+ .can_id = 0x141,
+ .can_data = gimbal_pitch_motor_.generate_torque_command().as_bytes(),
+ });
+ }
+
+ private:
+ auto can0_receive_callback(const librmcs::data::CanDataView& data) -> void override {
+ if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]]
+ return;
+ auto can_id = data.can_id;
+ if (can_id == 0x202) {
+ gimbal_left_friction_.store_status(data.can_data);
+ } else if (can_id == 0x201) {
+ gimbal_right_friction_.store_status(data.can_data);
+ } else if (can_id == 0x204) {
+ gimbal_bullet_feeder_.store_status(data.can_data);
+ }
+ }
+
+ auto can2_receive_callback(const librmcs::data::CanDataView& data) -> void override {
+ if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]]
+ return;
+ auto can_id = data.can_id;
+ if (can_id == 0x141)
+ gimbal_pitch_motor_.store_status(data.can_data);
+ }
+
+ auto can3_receive_callback(const librmcs::data::CanDataView& data) -> void 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(data.can_data);
+ }
+
+ auto accelerometer_receive_callback(const librmcs::data::AccelerometerDataView& data)
+ -> void override {
+ bmi088_.store_accelerometer_status(data.x, data.y, data.z);
+ }
+
+ auto gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data)
+ -> void override {
+ bmi088_.store_gyroscope_status(data.x, data.y, data.z);
+ }
+
+ OutputInterface& tf_;
+
+ OutputInterface gimbal_yaw_velocity_bmi088_;
+ OutputInterface gimbal_pitch_velocity_bmi088_;
+
+ device::Bmi088 bmi088_;
+ device::LkMotor gimbal_pitch_motor_;
+ device::LkMotor gimbal_top_yaw_motor_;
+ device::DjiMotor gimbal_bullet_feeder_;
+
+ device::DjiMotor gimbal_left_friction_;
+ device::DjiMotor gimbal_right_friction_;
+ };
+
+ class BottomBoard final : private librmcs::agent::CBoard {
+ friend class Sentry;
+
+ public:
+ explicit BottomBoard(
+ Sentry& sentry, rmcs_executor::Component& sentry_command,
+ std::string_view board_serial = {})
+ : librmcs::agent::CBoard(board_serial)
+ , imu_(1000, 0.2, 0.0)
+ , tf_(sentry.tf_)
+ , dr16_(sentry)
+ , gimbal_bottom_yaw_motor_(sentry, sentry_command, "/gimbal/bottom_yaw")
+ , chassis_wheel_motors_(
+ {sentry, sentry_command, "/chassis/left_front_wheel"},
+ {sentry, sentry_command, "/chassis/left_back_wheel"},
+ {sentry, sentry_command, "/chassis/right_back_wheel"},
+ {sentry, sentry_command, "/chassis/right_front_wheel"})
+ , chassis_steer_motors_(
+ {sentry, sentry_command, "/chassis/left_front_steering"},
+ {sentry, sentry_command, "/chassis/left_back_steering"},
+ {sentry, sentry_command, "/chassis/right_back_steering"},
+ {sentry, sentry_command, "/chassis/right_front_steering"})
+ , supercap_(sentry, sentry_command) {
+ sentry.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;
+ };
+
+ const auto zero_point = sentry.get_parameter("bottom_yaw_motor_zero_point").as_int();
+ gimbal_bottom_yaw_motor_.configure(
+ device::LkMotor::Config{device::LkMotor::Type::kMG6012Ei8}
+ .set_reversed()
+ .set_encoder_zero_point(static_cast(zero_point)));
+
+ for (auto& motor : chassis_wheel_motors_) {
+ motor.configure(
+ device::DjiMotor::Config{device::DjiMotor::Type::kM3508}
+ .set_reduction_ratio(11.)
+ .enable_multi_turn_angle()
+ .set_reversed());
+ }
+
+ constexpr auto kSteerNames = std::array{
+ "right_back_zero_point",
+ "right_front_zero_point",
+ "left_front_zero_point",
+ "left_back_zero_point",
+ };
+ for (auto&& [motor, name] : std::views::zip(chassis_steer_motors_, kSteerNames)) {
+ const auto zero_point = sentry.get_parameter(name).as_int();
+ motor.configure(
+ device::DjiMotor::Config{device::DjiMotor::Type::kGM6020}
+ .set_reversed()
+ .set_encoder_zero_point(static_cast(zero_point))
+ .enable_multi_turn_angle());
+ }
+
+ sentry.register_output("/chassis/yaw/velocity_imu", chassis_yaw_velocity_imu_, 0);
+ }
+
+ auto update() -> void {
+ imu_.update_status();
+ *chassis_yaw_velocity_imu_ = imu_.gz();
+ supercap_.update_status();
+
+ for (auto& motor : chassis_wheel_motors_)
+ motor.update_status();
+ for (auto& motor : chassis_steer_motors_)
+ motor.update_status();
+
+ dr16_.update_status();
+ gimbal_bottom_yaw_motor_.update_status();
+ tf_->set_state(
+ gimbal_bottom_yaw_motor_.angle());
+ }
+
+ auto command_update() -> void {
+ using namespace device;
+
+ auto builder = start_transmit();
+ builder.can1_transmit({
+ .can_id = 0x141,
+ .can_data = gimbal_bottom_yaw_motor_.generate_command().as_bytes(),
+ });
+
+ auto cache = CanPacket8{};
+ auto generate = [&](std::uint32_t id, std::ranges::range auto& motors, auto... args) {
+ auto command = [&](T arg) {
+ if constexpr (std::same_as) {
+ return arg;
+ } else {
+ const auto valid = arg >= 0 && arg < 4;
+ return valid ? motors[arg].generate_command()
+ : CanPacket8::PaddingQuarter{};
+ }
+ };
+ cache = CanPacket8{command(args)...};
+ return librmcs::data::CanDataView{.can_id = id, .can_data = cache.as_bytes()};
+ };
+
+ if (can_transmission_mode_) {
+ builder.can1_transmit(generate(0x200, chassis_wheel_motors_, 1, 0, -1, -1))
+ .can2_transmit(generate(0x200, chassis_wheel_motors_, -1, 2, -1, 3));
+ } else {
+ builder.can1_transmit(generate(0x1FE, chassis_steer_motors_, 1, 0, -1, -1))
+ .can2_transmit(generate(
+ 0x1FE, chassis_steer_motors_, 2, 3, -1, supercap_.generate_command()));
+ }
+ can_transmission_mode_ = !can_transmission_mode_;
+ }
+
+ private:
+ auto dbus_receive_callback(const librmcs::data::UartDataView& data) -> void override {
+ dr16_.store_status(data.uart_data.data(), data.uart_data.size());
+ }
+
+ auto can1_receive_callback(const librmcs::data::CanDataView& data) -> void 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(data.can_data);
+ else if (can_id == 0x202)
+ chassis_wheel_motors_[0].store_status(data.can_data);
+ else if (can_id == 0x205)
+ chassis_steer_motors_[1].store_status(data.can_data);
+ else if (can_id == 0x206)
+ chassis_steer_motors_[0].store_status(data.can_data);
+ else if (can_id == 0x141)
+ gimbal_bottom_yaw_motor_.store_status(data.can_data);
+ }
+
+ auto can2_receive_callback(const librmcs::data::CanDataView& data) -> void override {
+ if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]]
+ return;
+ auto can_id = data.can_id;
+ if (can_id == 0x202)
+ chassis_wheel_motors_[2].store_status(data.can_data);
+ else if (can_id == 0x204)
+ chassis_wheel_motors_[3].store_status(data.can_data);
+ else if (can_id == 0x205)
+ chassis_steer_motors_[2].store_status(data.can_data);
+ else if (can_id == 0x206)
+ chassis_steer_motors_[3].store_status(data.can_data);
+ else if (can_id == 0x300)
+ supercap_.store_status(data.can_data);
+ }
+
+ auto uart1_receive_callback(const librmcs::data::UartDataView& data) -> void 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());
+ }
+
+ auto accelerometer_receive_callback(const librmcs::data::AccelerometerDataView& data)
+ -> void override {
+ imu_.store_accelerometer_status(data.x, data.y, data.z);
+ }
+
+ auto gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data)
+ -> void override {
+ imu_.store_gyroscope_status(data.x, data.y, data.z);
+ }
+
+ bool can_transmission_mode_ = true;
+ device::Bmi088 imu_;
+ OutputInterface& tf_;
+
+ device::Dr16 dr16_;
+ device::LkMotor gimbal_bottom_yaw_motor_;
+ device::DjiMotor chassis_wheel_motors_[4];
+ device::DjiMotor chassis_steer_motors_[4];
+ device::Supercap supercap_;
+
+ rmcs_utility::RingBuffer referee_ring_buffer_receive_{256};
+ OutputInterface referee_serial_;
+ OutputInterface chassis_yaw_velocity_imu_;
+ };
+
+ struct CommandTransmitter : public rmcs_executor::Component {
+ std::function fn;
+
+ template
+ explicit CommandTransmitter(Fn&& fn)
+ : fn{std::forward(fn)} {}
+
+ auto update() -> void override { fn(); }
+ };
+
+ auto status_service_callback(const std::shared_ptr& response)
+ -> void {
+ response->success = true;
+
+ auto feedback_message = std::ostringstream{};
+ auto text = [&](std::format_string format, Args&&... args) {
+ std::println(feedback_message, format, std::forward(args)...);
+ };
+
+ text("Gimbal Status");
+ text("- Bottom Yaw: {}", bottom_board_->gimbal_bottom_yaw_motor_.last_raw_angle());
+ text("- Top Yaw: {}", top_board_->gimbal_top_yaw_motor_.last_raw_angle());
+ text("- Pitch Angle: {}", top_board_->gimbal_pitch_motor_.last_raw_angle());
+
+ text("Chassis Status");
+ constexpr auto kPosition =
+ std::array{"right back", "right front", "left front", "left back"};
+ constexpr auto kMaxLength =
+ std::ranges::max_element(kPosition, {}, &std::string_view::size)->size();
+
+ for (auto&& [index, motor] :
+ std::views::zip(kPosition, bottom_board_->chassis_steer_motors_)) {
+ text("- {:{}}: {}", index, kMaxLength, motor.last_raw_angle());
+ }
+
+ response->message = feedback_message.str();
+ }
+
+ auto command_update() -> void {
+ top_board_->command_update();
+ bottom_board_->command_update();
+ }
+ std::shared_ptr command_component_{
+ create_partner_component(
+ get_component_name() + "_command", [this] { command_update(); })};
+
+ InputInterface timestamp_;
+ OutputInterface tf_;
+
+ std::unique_ptr gimbal_board_;
+ std::unique_ptr top_board_;
+ std::unique_ptr bottom_board_;
+
+ std::shared_ptr> status_service_;
+};
+
+} // namespace rmcs_core::hardware
+
+#include
+
+PLUGINLIB_EXPORT_CLASS(rmcs_core::hardware::Sentry, rmcs_executor::Component)
diff --git a/rmcs_ws/src/rmcs_description/include/rmcs_description/sentry_description.hpp b/rmcs_ws/src/rmcs_description/include/rmcs_description/sentry_description.hpp
new file mode 100644
index 000000000..2fc9d17be
--- /dev/null
+++ b/rmcs_ws/src/rmcs_description/include/rmcs_description/sentry_description.hpp
@@ -0,0 +1,198 @@
+#pragma once
+
+#include
+
+#include
+
+#include
+
+namespace rmcs_description {
+
+struct BaseLink : fast_tf::Link {
+ static constexpr char name[] = "base_link";
+};
+
+struct BottomYawLink : fast_tf::Link {
+ static constexpr char name[] = "bottom_yaw_link";
+};
+using YawLink = BottomYawLink;
+
+struct TopYawLink : fast_tf::Link {
+ static constexpr char name[] = "top_yaw_link";
+};
+
+struct PitchLink : fast_tf::Link {
+ static constexpr char name[] = "pitch_link";
+};
+
+struct MuzzleLink : fast_tf::Link {
+ static constexpr char name[] = "muzzle_link";
+};
+
+struct CameraLink : fast_tf::Link {
+ static constexpr char name[] = "camera_link";
+};
+
+struct TransmitterLink : fast_tf::Link {
+ static constexpr char name[] = "transmitter_link";
+};
+
+struct OdomImu : fast_tf::Link {
+ static constexpr char name[] = "odom_imu";
+};
+
+struct OdomGimbalImu : fast_tf::Link {
+ static constexpr char name[] = "odom_gimbal_imu";
+};
+
+struct GimbalCenterLink : fast_tf::Link {
+ static constexpr char name[] = "gimbal_center_link";
+};
+
+struct LeftFrontWheelLink : fast_tf::Link {
+ static constexpr char name[] = "left_front_wheel_link";
+};
+struct LeftBackWheelLink : fast_tf::Link {
+ static constexpr char name[] = "left_back_wheel_link";
+};
+struct RightBackWheelLink : fast_tf::Link {
+ static constexpr char name[] = "right_back_wheel_link";
+};
+struct RightFrontWheelLink : fast_tf::Link {
+ static constexpr char name[] = "right_front_wheel_link";
+};
+
+} // namespace rmcs_description
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::BaseLink;
+ Eigen::Translation3d transform = Eigen::Translation3d::Identity();
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::GimbalCenterLink;
+
+ void set_state(double angle) { angle_ = angle; }
+ auto get_transform() const { return Eigen::AngleAxisd{angle_, Eigen::Vector3d::UnitZ()}; }
+
+private:
+ double angle_ = 0.0;
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::BottomYawLink;
+
+ void set_transform(const Eigen::Translation3d& translation) { translation_ = translation; }
+
+ void set_state(double angle) { angle_ = angle; }
+
+ auto get_transform() const {
+ Eigen::Isometry3d transform = Eigen::Isometry3d::Identity();
+ transform *= translation_;
+ transform *= Eigen::AngleAxisd{angle_, Eigen::Vector3d::UnitZ()};
+ return transform;
+ }
+
+private:
+ Eigen::Translation3d translation_ = Eigen::Translation3d::Identity();
+ double angle_ = 0.0;
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::TopYawLink;
+
+ void set_state(double angle) { angle_ = angle; }
+ auto get_transform() const { return Eigen::AngleAxisd{angle_, Eigen::Vector3d::UnitY()}; }
+
+private:
+ double angle_ = 0.0;
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::PitchLink;
+ Eigen::Translation3d transform = Eigen::Translation3d::Identity();
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::PitchLink;
+ Eigen::Translation3d transform = Eigen::Translation3d::Identity();
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::PitchLink;
+ Eigen::Isometry3d transform = Eigen::Isometry3d::Identity();
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::BottomYawLink;
+ Eigen::Quaterniond transform = Eigen::Quaterniond::Identity();
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::PitchLink;
+ Eigen::Quaterniond transform = Eigen::Quaterniond::Identity();
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::BaseLink;
+ Eigen::Isometry3d transform = Eigen::Isometry3d::Identity();
+ void set_state(double angle) {
+ auto rotation = Eigen::AngleAxisd{std::numbers::pi / 4, Eigen::Vector3d::UnitZ()}
+ * Eigen::AngleAxisd{angle, Eigen::Vector3d::UnitX()};
+ transform.linear() = rotation.matrix();
+ }
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::BaseLink;
+ Eigen::Isometry3d transform = Eigen::Isometry3d::Identity();
+ void set_state(double angle) {
+ auto rotation = Eigen::AngleAxisd{std::numbers::pi / 4 * 3, Eigen::Vector3d::UnitZ()}
+ * Eigen::AngleAxisd{angle, Eigen::Vector3d::UnitX()};
+ transform.linear() = rotation.matrix();
+ }
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::BaseLink;
+ Eigen::Isometry3d transform = Eigen::Isometry3d::Identity();
+ void set_state(double angle) {
+ auto rotation = Eigen::AngleAxisd{-std::numbers::pi / 4 * 3, Eigen::Vector3d::UnitZ()}
+ * Eigen::AngleAxisd{angle, Eigen::Vector3d::UnitX()};
+ transform.linear() = rotation.matrix();
+ }
+};
+
+template <>
+struct fast_tf::Joint : fast_tf::ModificationTrackable {
+ using Parent = rmcs_description::BaseLink;
+ Eigen::Isometry3d transform = Eigen::Isometry3d::Identity();
+ void set_state(double angle) {
+ auto rotation = Eigen::AngleAxisd{-std::numbers::pi / 4, Eigen::Vector3d::UnitZ()}
+ * Eigen::AngleAxisd{angle, Eigen::Vector3d::UnitX()};
+ transform.linear() = rotation.matrix();
+ }
+};
+
+namespace rmcs_description {
+
+using Tf = fast_tf::JointCollection<
+ GimbalCenterLink, BottomYawLink, TopYawLink, PitchLink, MuzzleLink, TransmitterLink, CameraLink,
+ OdomImu, OdomGimbalImu, LeftFrontWheelLink, LeftBackWheelLink, RightBackWheelLink,
+ RightFrontWheelLink>;
+
+using SentryTf = Tf;
+
+} // namespace rmcs_description