diff --git a/.env b/.env
new file mode 100644
index 00000000..c28fb80f
--- /dev/null
+++ b/.env
@@ -0,0 +1,3 @@
+CONTAINER_USER=ubuntu
+CONTAINER_HOME=/home/${CONTAINER_USER}
+HOST_NVIM_DIR=${HOME}/.config/nvim
diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml
new file mode 100644
index 00000000..71c5538f
--- /dev/null
+++ b/.github/workflows/update-image.yml
@@ -0,0 +1,49 @@
+name: Build and Push RMCS Images
+
+on:
+ workflow_dispatch:
+ push:
+ paths:
+ - 'Dockerfile'
+ branches:
+ - main
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Set up SSH keys
+ run: |
+ echo "${{ secrets.CONTAINER_ID_RSA }}" > .ssh/id_rsa
+ echo "${{ secrets.CONTAINER_ID_RSA_PUB }}" > .ssh/id_rsa.pub
+ chmod 600 .ssh/id_rsa
+ chmod 644 .ssh/id_rsa.pub
+
+ - name: Build and push rmcs-develop:latest
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ target: rmcs-develop
+ tags: qzhhhi/rmcs-develop:latest
+
+ - name: Build and push rmcs-runtime:latest
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ target: rmcs-runtime
+ tags: qzhhhi/rmcs-runtime:latest
diff --git a/.gitmodules b/.gitmodules
index 1938ee6e..d4d71be2 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -9,4 +9,4 @@
url = https://github.com/Alliance-Algorithm/ros2-hikcamera.git
[submodule "rmcs_ws/src/serial"]
path = rmcs_ws/src/serial
- url = git@github.com:Alliance-Algorithm/ros2-serial.git
+ url = https://github.com/Alliance-Algorithm/ros2-serial.git
diff --git a/.script/build-rmcs b/.script/build-rmcs
index 08d7198e..ef5d85ff 100755
--- a/.script/build-rmcs
+++ b/.script/build-rmcs
@@ -2,6 +2,6 @@
source /opt/ros/jazzy/setup.bash
-cd /workspaces/RMCS/rmcs_ws
+cd ${RMCS_PATH}/rmcs_ws
-colcon build --symlink-install --merge-install
+colcon build --symlink-install --merge-install $@
diff --git a/.script/clean-rmcs b/.script/clean-rmcs
index cab27019..3227fd82 100755
--- a/.script/clean-rmcs
+++ b/.script/clean-rmcs
@@ -1,3 +1,4 @@
#! /bin/bash
-rm -rf /workspaces/RMCS/rmcs_ws/build /workspaces/RMCS/rmcs_ws/install /workspaces/RMCS/rmcs_ws/log
\ No newline at end of file
+rm -rf ${RMCS_PATH}/rmcs_ws/build ${RMCS_PATH}/rmcs_ws/install ${RMCS_PATH}/rmcs_ws/log
+
diff --git a/.script/complete/_play-autoaim b/.script/complete/_play-autoaim
new file mode 100644
index 00000000..7365b535
--- /dev/null
+++ b/.script/complete/_play-autoaim
@@ -0,0 +1,6 @@
+#compdef play-autoaim
+
+_arguments \
+ '--user[指定远程用户名]:用户名:' \
+ '--remote[从设备拉取 SDP 到容器]' \
+ '--no-copy[跳过容器到宿主机的拷贝]'
diff --git a/.script/complete/_set-remote b/.script/complete/_set-remote
new file mode 100644
index 00000000..e5599c3f
--- /dev/null
+++ b/.script/complete/_set-remote
@@ -0,0 +1,22 @@
+#compdef set-remote
+
+_remote_hosts() {
+ local hosts=(
+ "169.254.233.233"
+ "alliance-sentry.local"
+ "alliance-infantry.local"
+ "alliance-hero.local"
+ )
+
+ # 从 ~/.ssh/config 提取最近使用的 HostName
+ if [[ -f ~/.ssh/config ]]; then
+ local extracted
+ extracted=(${(f)"$(grep -A1 'Host remote' ~/.ssh/config | grep HostName | awk '{print $2}')"})
+ hosts+=(${extracted})
+ fi
+
+ compadd -- $hosts
+}
+
+_arguments \
+ '1:远程主机地址:_remote_hosts'
diff --git a/.script/play-autoaim b/.script/play-autoaim
new file mode 100755
index 00000000..8f367049
--- /dev/null
+++ b/.script/play-autoaim
@@ -0,0 +1,69 @@
+#!/bin/sh
+
+set -e
+
+MONITOR_USER=""
+MONITOR_PLAYER="vlc"
+MONITOR_HOST="localhost"
+
+SDP_PATH="/tmp/auto_aim.sdp"
+
+USE_REMOTE=false
+SKIP_COPY=false
+
+# 参数解析
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --user)
+ shift
+ [ -z "$1" ] && echo "❌ --user 参数缺失" && exit 1
+ MONITOR_USER="$1"
+ ;;
+ --remote)
+ USE_REMOTE=true
+ ;;
+ --no-copy)
+ SKIP_COPY=true
+ ;;
+ *)
+ echo "未知参数: $1"
+ echo "用法: $0 --user <用户名> [--remote] [--no-copy]"
+ exit 1
+ ;;
+ esac
+ shift
+done
+
+if [ -z "$MONITOR_USER" ]; then
+ echo "❌ 缺少 --user 参数"
+ echo "用法: play-autoaim --user <用户名> [--remote] [--no-copy]"
+ exit 1
+fi
+
+if [ "$USE_REMOTE" = true ]; then
+ scp remote:${SDP_PATH} ${SDP_PATH}
+ if [ $? -ne 0 ]; then
+ echo " 从 remote 拷贝失败。是否继续?(y/n)"
+ read -r answer
+ case "$answer" in
+ [Yy]*) echo " 继续执行后续操作…" ;;
+ *)
+ echo " 已取消。"
+ exit 1
+ ;;
+ esac
+ fi
+fi
+
+if [ "$SKIP_COPY" = false ]; then
+ scp "${SDP_PATH}" "${MONITOR_USER}@${MONITOR_HOST}:${SDP_PATH}"
+ if [ $? -ne 0 ]; then
+ echo " scp 拷贝失败"
+ exit 1
+ fi
+ echo " 文件已拷贝到宿主机:${SDP_PATH}"
+else
+ echo " 跳过拷贝,直接打开远程 SDP"
+fi
+
+ssh -f "${MONITOR_USER}@${MONITOR_HOST}" "DISPLAY=:0 ${MONITOR_PLAYER} '${SDP_PATH}' --network-caching=50"
diff --git a/.script/sync-remote b/.script/sync-remote
index 60a2b816..7d62782b 100755
--- a/.script/sync-remote
+++ b/.script/sync-remote
@@ -8,7 +8,7 @@ import subprocess
from colorama import Fore, Style
-SRC_DIR = "/workspaces/RMCS/rmcs_ws/install"
+SRC_DIR = os.getenv("RMCS_PATH") + "/rmcs_ws/install"
DST_DIR = "ssh://remote//rmcs_install"
SOCKET_PATH = "/tmp/sync-remote"
diff --git a/.script/template/env_setup.bash b/.script/template/env_setup.bash
index aa904464..f8ab1dd2 100644
--- a/.script/template/env_setup.bash
+++ b/.script/template/env_setup.bash
@@ -1,13 +1,15 @@
#!/bin/bash
export ROS_LOCALHOST_ONLY=1
+export RCUTILS_COLORIZED_OUTPUT=1
+export RMCS_PATH="/workspaces/RMCS"
source /opt/ros/jazzy/setup.bash
if [ -f "/rmcs_install/local_setup.bash" ]; then
source /rmcs_install/local_setup.bash
-elif [ -f "/workspaces/RMCS/rmcs_ws/install/local_setup.bash" ]; then
- source /workspaces/RMCS/rmcs_ws/install/local_setup.bash
+elif [ -f "${RMCS_PATH}/rmcs_ws/install/local_setup.bash" ]; then
+ source ${RMCS_PATH}/rmcs_ws/install/local_setup.bash
fi
export RMCS_ROBOT_TYPE=""
diff --git a/.script/template/env_setup.zsh b/.script/template/env_setup.zsh
index 518afb8f..2f7a0ee6 100644
--- a/.script/template/env_setup.zsh
+++ b/.script/template/env_setup.zsh
@@ -1,16 +1,22 @@
#!/bin/bash
+export RCUTILS_COLORIZED_OUTPUT=1
export ROS_LOCALHOST_ONLY=1
+export RMCS_PATH="/workspaces/RMCS"
source /opt/ros/jazzy/setup.zsh
if [ -f "/rmcs_install/local_setup.zsh" ]; then
source /rmcs_install/local_setup.zsh
-elif [ -f "/workspaces/RMCS/rmcs_ws/install/local_setup.zsh" ]; then
- source /workspaces/RMCS/rmcs_ws/install/local_setup.zsh
+elif [ -f "${RMCS_PATH}/rmcs_ws/install/local_setup.zsh" ]; then
+ source ${RMCS_PATH}/rmcs_ws/install/local_setup.zsh
fi
eval "$(register-python-argcomplete ros2)"
eval "$(register-python-argcomplete colcon)"
export RMCS_ROBOT_TYPE=""
+
+fpath=(${RMCS_PATH}/.script/complete $fpath)
+autoload -Uz compinit
+compinit
diff --git a/Dockerfile b/Dockerfile
index 37bf1097..7fe52df3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,6 +18,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
vim wget curl unzip \
zsh screen tmux \
usbutils net-tools iputils-ping \
+ gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
ripgrep htop fzf \
libusb-1.0-0-dev \
libeigen3-dev \
@@ -123,7 +124,7 @@ RUN sh -c "$(wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools
sed -i 's/ZSH_THEME=\"[a-z0-9\-]*\"/ZSH_THEME="af-magic"/g' ~/.zshrc && \
echo 'source ~/env_setup.zsh' >> ~/.zshrc && \
echo 'export PATH="${PATH}:/opt/nvim-linux-x86_64/bin"' >> ~/.zshrc && \
- echo 'export PATH=${PATH}:/workspaces/RMCS/.script' >> ~/.zshrc
+ echo 'export PATH="${PATH}:${RMCS_PATH}/.script"' >> ~/.zshrc
# Copy environment setup scripts
COPY --chown=1000:1000 .script/template/env_setup.bash env_setup.bash
diff --git a/docker-compose.yml b/docker-compose.yml
index 384717e7..d59afc5b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,15 +1,18 @@
services:
rmcs-develop:
image: qzhhhi/rmcs-develop:latest
- container_name: rmcs-develop
- user: "developer"
- hostname: "developer"
+ user: "${CONTAINER_USER}"
privileged: true
+ command: >
+ bash -c "
+ sudo chown -R ${CONTAINER_USER}:${CONTAINER_USER} ${CONTAINER_HOME}/.config
+ exec bash
+ "
volumes:
- /dev:/dev:bind
- /tmp/.X11-unix:/tmp/.X11-unix:bind
- /run/user/1000/wayland-0:/run/user/1000/wayland-0
- - ~/.config/nvim/:/home/developer/.config/nvim/:bind
+ - ${HOST_NVIM_DIR}:${CONTAINER_HOME}/.config/nvim/:bind
- .:/workspaces/RMCS:bind
environment:
- DISPLAY=${DISPLAY}
diff --git a/rmcs_ws/src/rmcs_bringup/config/wheelleg-infantry.yaml b/rmcs_ws/src/rmcs_bringup/config/wheelleg-infantry.yaml
new file mode 100644
index 00000000..2d4ba216
--- /dev/null
+++ b/rmcs_ws/src/rmcs_bringup/config/wheelleg-infantry.yaml
@@ -0,0 +1,159 @@
+rmcs_executor:
+ ros__parameters:
+ update_rate: 1000.0
+ components:
+ - rmcs_core::hardware::WheelLegInfantry -> infantry_hardware
+
+ - rmcs_core::broadcaster::ValueBroadcaster -> value_broadcaster
+
+ # - 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::SimpleGimbalController -> gimbal_controller
+ # - rmcs_core::controller::pid::ErrorPidController -> yaw_angle_pid_controller
+ # - rmcs_core::controller::pid::ErrorPidController -> pitch_angle_pid_controller
+
+ # - rmcs_core::controller::shooting::FrictionWheelController -> friction_wheel_controller
+ # - rmcs_core::controller::shooting::HeatController -> heat_controller
+ # - rmcs_core::controller::pid::PidController -> left_friction_velocity_pid_controller
+ # - rmcs_core::controller::pid::PidController -> right_friction_velocity_pid_controller
+
+ - rmcs_core::controller::chassis::WheelLegController -> wheel_leg_controller
+ - rmcs_core::controller::chassis::WheelLegChassisController -> chassis_controller
+ # - rmcs_core::controller::chassis::ChassisPowerController -> chassis_power_controller
+
+
+ # - rmcs_auto_aim::AutoAimInitializer -> auto_aim_initializer
+ # - rmcs_auto_aim::AutoAimController -> auto_aim_controller
+
+infantry_hardware:
+ ros__parameters:
+ usb_pid_top_board: 0x93ac
+ usb_pid_bottom_board: 0xf30c
+ yaw_motor_zero_point: 32285
+ pitch_motor_zero_point: 6321
+ left_front_hip_motor_zero_point: 748
+ left_back_hip_motor_zero_point: 32855
+ right_back_hip_motor_zero_point: 32473
+ right_front_hip_motor_zero_point: 65277
+
+value_broadcaster:
+ ros__parameters:
+ forward_list:
+ - /chassis/yaw/angle
+ - /chassis/yaw/velocity
+ - /chassis/pitch/angle
+ - /chassis/pitch/velocity
+ - /chassis/left_wheel/control_torque
+ - /chassis/right_wheel/control_torque
+
+
+yaw_angle_pid_controller:
+ ros__parameters:
+ measurement: /gimbal/yaw/control_angle_error
+ control: /gimbal/yaw/control_velocity
+ kp: 16.0
+ ki: 0.0
+ kd: 0.0
+
+pitch_angle_pid_controller:
+ ros__parameters:
+ measurement: /gimbal/pitch/control_angle_error
+ control: /gimbal/pitch/control_velocity
+ kp: 10.00
+ ki: 0.0
+ kd: 0.0
+
+friction_wheel_controller:
+ ros__parameters:
+ friction_wheels:
+ - /gimbal/left_friction
+ - /gimbal/right_friction
+ friction_velocities:
+ - 600.0
+ - 600.0
+ friction_soft_start_stop_time: 1.0
+
+heat_controller:
+ ros__parameters:
+ heat_per_shot: 10000
+ reserved_heat: 0
+
+bullet_feeder_controller:
+ ros__parameters:
+ bullets_per_feeder_turn: 10.0
+ shot_frequency: 24.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
+
+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: 3
+ 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.02
+ pitch_error: 0.01
+ shoot_velocity: 21.0
+ predict_sec: 0.050
+ # 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: true
+ raw_img_pub: false # Set false in actual use
+ image_viewer_type: 0
diff --git a/rmcs_ws/src/rmcs_core/librmcs b/rmcs_ws/src/rmcs_core/librmcs
index b82f2eaf..1347b210 160000
--- a/rmcs_ws/src/rmcs_core/librmcs
+++ b/rmcs_ws/src/rmcs_core/librmcs
@@ -1 +1 @@
-Subproject commit b82f2eafd21371a23d046e5f75884fe6c6e49124
+Subproject commit 1347b2109d0c93dbc3cfe5daaa6acd7754987f81
diff --git a/rmcs_ws/src/rmcs_core/plugins.xml b/rmcs_ws/src/rmcs_core/plugins.xml
index ac7d1368..2cb6b2bb 100644
--- a/rmcs_ws/src/rmcs_core/plugins.xml
+++ b/rmcs_ws/src/rmcs_core/plugins.xml
@@ -17,8 +17,14 @@
Test plugin.
+
+ Test plugin.
+
Test plugin.
+
+
+ Test plugin.
Test plugin.
@@ -29,6 +35,9 @@
Steering wheel controller.
+
+ Wheel leg controller.
+
Test plugin.
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 da70a96f..6b5b7539 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
@@ -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;
@@ -201,8 +201,8 @@ class ChassisController
static constexpr double nan = std::numeric_limits::quiet_NaN();
// Maximum control velocities
- static constexpr double translational_velocity_max = 10.0;
- static constexpr double angular_velocity_max = 16.0;
+ static constexpr double translational_velocity_max = 2.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/controller/chassis/wheel_leg/README.md b/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/README.md
new file mode 100644
index 00000000..c301b00d
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/README.md
@@ -0,0 +1,22 @@
+#Wheel-leg Controller
+
+desire state: [s ds phi d_phi theta_l d_theta_l theta_r d_theta_r theta_b d_theta_b]
+
+## reset
+启动时收腿至最短后再启动lqr计算。
+
+## Wheel-leg Mode
+balanceless: 锁死关节,仅控轮子移动
+follow: yaw轴角度、角速度
+spin: yaw轴角速度
+
+### balanceless
+非平衡模式,关节以位置模式固定在0点,仅控制轮毂,不进行lqr计算。
+
+### follow
+
+## Special Situation
+rescue tip over: 关闭lqr,仅关节
+jump: 跳跃
+climb: 爬台阶
+
diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/desire_state_solver.hpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/desire_state_solver.hpp
new file mode 100644
index 00000000..5abba902
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/desire_state_solver.hpp
@@ -0,0 +1,178 @@
+#pragma once
+
+#include "rmcs_msgs/wheel_leg_state.hpp"
+#include
+
+namespace rmcs_core::controller {
+
+class DesireStateSolver {
+public:
+ explicit DesireStateSolver(double desire_leg_length)
+ : initial_leg_length_(desire_leg_length) {
+ reset();
+ }
+
+ void reset() {
+ desire_state_ = reset_state();
+
+ desire_leg_length_ = initial_leg_length_;
+ desire_roll_angle_ = 0.0;
+
+ jump_state_ = JumpState::IDLE;
+ }
+
+ rmcs_msgs::WheelLegState update(Eigen::Vector3d control_velocity) {
+ // desire_leg_length_ += vz * leg_length_sensitivity_;
+
+ if (std::abs(measure_state_.velocity) < 1e-2) {
+ park_mode_ = true;
+ } else {
+ park_mode_ = false;
+ }
+
+ // distance :always 0 during velocity control
+ desire_state_.distance = 0.0;
+ desire_state_.velocity = 0.0;
+
+ // yaw angle:
+ desire_state_.yaw_angle = 0.0; // else measure.yaw
+ desire_state_.yaw_velocity = 0.0;
+
+ // leg tilt angle:
+ desire_state_.left_tilt_angle = 0.0;
+ desire_state_.left_tilt_velocity = 0.0;
+
+ desire_state_.right_tilt_angle = 0.0;
+ desire_state_.right_tilt_velocity = 0.0;
+
+ desire_state_.body_pitch_angle = 0.0;
+ desire_state_.body_pitch_velocity = 0.0;
+
+ return desire_state_;
+ }
+
+ void update_measure_state(rmcs_msgs::WheelLegState& measure_state) {
+ measure_state_ = measure_state;
+ }
+
+ double
+ update_desire_leg_length(const bool is_leg_extended, const double min, const double max) {
+ if (is_leg_extended) {
+ desire_leg_length_ = 0.25;
+ } else {
+ desire_leg_length_ = 0.16;
+ }
+ desire_leg_length_ = std::clamp(desire_leg_length_, min, max);
+
+ return desire_leg_length_;
+ }
+
+ double update_desire_roll_angle() const { return desire_roll_angle_; }
+
+ void update_jump_state(bool jump_active, double leg_length) {
+ switch (jump_state_) {
+ case JumpState::IDLE: {
+ if (jump_active) {
+ jump_state_ = JumpState::EXTEND_LEGS;
+ }
+ break;
+ }
+ case JumpState::EXTEND_LEGS: {
+ desire_leg_length_ = 0.35;
+ if (leg_length >= 0.33) {
+ jump_state_ = JumpState::CONTRACT_LEGS;
+ }
+ break;
+ }
+ case JumpState::CONTRACT_LEGS: {
+ desire_leg_length_ = 0.14;
+ if (leg_length <= 0.15) {
+ jump_state_ = JumpState::LEVITATE;
+ }
+ break;
+ }
+ case JumpState::LEVITATE: {
+ desire_leg_length_ = 0.17;
+ if (leg_length <= 0.18) {
+ jump_state_ = JumpState::LAND;
+ }
+ break;
+ }
+ case JumpState::LAND: {
+ jump_state_ = JumpState::IDLE;
+ break;
+ }
+ }
+ }
+
+ void update_climb_state(bool climb_active, double leg_length) {}
+
+ double desire_leg_length() const { return desire_leg_length_; }
+ double desire_roll_angle() const { return desire_roll_angle_; }
+
+private:
+ enum class JumpState {
+ IDLE = 0,
+ EXTEND_LEGS = 1,
+ CONTRACT_LEGS = 2,
+ LEVITATE = 3,
+ LAND = 4,
+ };
+
+ enum class ClimbState {
+ IDLE = 0,
+ DETECT = 1,
+ CONTRACT_LEGS = 2,
+ EXTEND_LEGS = 3,
+ LAND = 4,
+ };
+
+ void update_desire_state(Eigen::Vector3d control_velocity, double control_angle) {
+ // Update desire state based on control velocity and measure state
+
+ // x-axis translational velocity, z-axis vertical velocity, z-axis angular velocity
+ auto& [vx, vz, wz] = control_velocity;
+
+ // distance :always 0 during velocity control
+ desire_state_.distance = 0.0;
+ desire_state_.velocity = vx;
+
+ desire_state_.yaw_angle = 0.0; //
+ desire_state_.yaw_velocity = wz;
+
+ desire_state_.left_tilt_angle = 0.0;
+ desire_state_.left_tilt_velocity = 0.0;
+ desire_state_.right_tilt_angle = 0.0;
+ desire_state_.right_tilt_velocity = 0.0;
+ desire_state_.body_pitch_angle = 0.0;
+ desire_state_.body_pitch_velocity = 0.0;
+ }
+
+ constexpr static rmcs_msgs::WheelLegState reset_state() {
+ return rmcs_msgs::WheelLegState{nan_, nan_, nan_, nan_, nan_, nan_, nan_, nan_, nan_, nan_};
+ }
+
+ constexpr static double nan_ = std::numeric_limits::quiet_NaN();
+
+ constexpr static double leg_length_sensitivity_ = 0.00001;
+
+ constexpr static double dt_ = 0.001;
+
+ // Maximum control velocities
+ static constexpr double translational_velocity_max_ = 2.0;
+ static constexpr double angular_velocity_max_ = 2.0;
+
+ const double initial_leg_length_;
+
+ bool park_mode_ = false;
+
+ double desire_leg_length_;
+ double desire_roll_angle_;
+
+ JumpState jump_state_ = JumpState::IDLE;
+ // ClimbState climb_state_ = ClimbState::IDLE;
+
+ rmcs_msgs::WheelLegState measure_state_;
+ rmcs_msgs::WheelLegState desire_state_;
+};
+} // namespace rmcs_core::controller
\ No newline at end of file
diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/vmc_solver.hpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/vmc_solver.hpp
new file mode 100644
index 00000000..ab1bfed3
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/vmc_solver.hpp
@@ -0,0 +1,102 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace rmcs_core::controller::chassis {
+class VmcSolver {
+public:
+ VmcSolver(double l1, double l2, double l5)
+ : l1_(l1)
+ , l2_(l2)
+ , l3_(l2)
+ , l4_(l1)
+ , l5_(l5) {
+ reset();
+ }
+
+ void reset() {
+ tilt_angle_ = nan_;
+ leg_length_ = nan_;
+ }
+
+ Eigen::Vector2d update(double phi1, double phi4) {
+ if (std::isnan(phi1) || std::isnan(phi4)) {
+ reset();
+ return Eigen::Vector2d{leg_length_, tilt_angle_};
+ }
+
+ calculate_five_link_solution(phi1, phi4);
+
+ return get_leg_posture();
+ }
+
+ Eigen::Vector2d update_joint_torque(double F, double Tp) {
+ return joint_torque_matrix_ * Eigen::Vector2d{F, Tp};
+ }
+
+ Eigen::Vector2d update_virtual_torque(double T1, double T2) {
+ return joint_torque_matrix_.inverse() * Eigen::Vector2d{T1, T2};
+ }
+
+private:
+ void calculate_five_link_solution(double phi1, double phi4) {
+ // The coordinate system and variable definitions are referenced from the Zhihu article:
+ // https://zhuanlan.zhihu.com/p/613007726
+ auto xb = l1_ * std::cos(phi1), yb = l1_ * std::sin(phi1);
+ auto xd = l5_ + l4_ * std::cos(phi4), yd = l4_ * std::sin(phi4);
+
+ auto lbd = std::sqrt((xd - xb) * (xd - xb) + (yd - yb) * (yd - yb));
+
+ auto a = 2 * l2_ * (xd - xb), b = 2 * l2_ * (yd - yb),
+ c = l2_ * l2_ + lbd * lbd - l3_ * l3_;
+
+ auto phi2 = 2 * std::atan2(b + std::sqrt(a * a + b * b - c * c), (a + c)),
+ phi3 = std::atan2((yb - yd) + l2_ * std::sin(phi2), (xb - xd) + l2_ * std::cos(phi2));
+
+ auto xc = l1_ * std::cos(phi1) + l2_ * std::cos(phi2),
+ yc = l1_ * std::sin(phi1) + l2_ * std::sin(phi2);
+
+ auto phi0 = std::atan2(yc, xc - l5_ / 2.0);
+
+ tilt_angle_ = pi_ / 2.0 - phi0;
+ leg_length_ = std::sqrt((xc - l5_ / 2.0) * (xc - l5_ / 2.0) + yc * yc);
+
+ leg_posture_ = {leg_length_, tilt_angle_};
+
+ auto j11 = l1_ * std::sin(phi0 - phi3) * std::sin(phi1 - phi2) / std::sin(phi3 - phi2),
+ j12 = l4_ * std::sin(phi0 - phi2) * std::sin(phi3 - phi4) / std::sin(phi3 - phi2),
+ j21 = l1_ * std::cos(phi0 - phi3) * std::sin(phi1 - phi2)
+ / (leg_length_ * std::sin(phi3 - phi2)),
+ j22 = l4_ * std::cos(phi0 - phi2) * std::sin(phi3 - phi4)
+ / (leg_length_ * std::sin(phi3 - phi2));
+
+ auto jacobian_matrix = Eigen::Matrix2d{
+ {j11, j12},
+ {j21, j22}
+ };
+ auto rotation_matrix = Eigen::Rotation2Dd(phi0 - pi_ / 2.0);
+ auto transform_matrix = Eigen::Matrix2d{
+ {0, -1 / leg_length_},
+ {1, 0}
+ };
+
+ joint_torque_matrix_ = jacobian_matrix.transpose() * rotation_matrix * transform_matrix;
+ }
+
+ Eigen::Vector2d get_leg_posture() const { return leg_posture_; }
+
+ static constexpr double inf_ = std::numeric_limits::infinity();
+ static constexpr double nan_ = std::numeric_limits::quiet_NaN();
+
+ static constexpr double pi_ = std::numbers::pi;
+
+ const double l1_, l2_, l3_, l4_, l5_;
+
+ double leg_length_, tilt_angle_;
+
+ Eigen::Vector2d leg_posture_;
+ Eigen::Matrix2d joint_torque_matrix_;
+};
+} // namespace rmcs_core::controller::chassis
\ No newline at end of file
diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/wheel_leg_chassis_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/wheel_leg_chassis_controller.cpp
new file mode 100644
index 00000000..e5432c39
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/wheel_leg_chassis_controller.cpp
@@ -0,0 +1,276 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "controller/pid/pid_calculator.hpp"
+
+namespace rmcs_core::controller::chassis {
+
+class WheelLegChassisController
+ : public rmcs_executor::Component
+ , public rclcpp::Node {
+public:
+ WheelLegChassisController()
+ : Node(
+ get_component_name(),
+ rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true))
+ , following_velocity_controller_(7.0, 0.0, 0.0) {
+ following_velocity_controller_.output_max = angular_velocity_max;
+ following_velocity_controller_.output_min = -angular_velocity_max;
+
+ register_input("/remote/joystick/right", joystick_right_);
+ register_input("/remote/joystick/left", joystick_left_);
+ register_input("/remote/switch/right", switch_right_);
+ register_input("/remote/switch/left", switch_left_);
+ register_input("/remote/mouse/velocity", mouse_velocity_);
+ register_input("/remote/mouse", mouse_);
+ register_input("/remote/keyboard", keyboard_);
+ register_input("/remote/rotary_knob", rotary_knob_);
+
+ register_input("/gimbal/yaw/angle", gimbal_yaw_angle_, false);
+ register_input("/gimbal/yaw/control_angle_error", gimbal_yaw_angle_error_, false);
+
+ register_input("/chassis/yaw/angle", chassis_yaw_angle_imu_);
+
+ register_output("/chassis/angle", chassis_angle_, nan);
+ register_output("/chassis/control_angle", chassis_control_angle_, nan);
+
+ register_output("/chassis/control_mode", mode_);
+
+ register_output("/chassis/status/balanceless", is_balanceless_, false);
+ register_output("/chassis/status/rescue_tip_over", is_rescue_tip_over_, false);
+
+ register_output("/chassis/status/leg_extended", is_leg_extended_, false);
+ register_output("/chassis/status/jump", is_jump_active_, false);
+ register_output("/chassis/status/climb", is_climb_active_, false);
+
+ register_output("/chassis/control_velocity", chassis_control_velocity_);
+ }
+
+ void before_updating() override {
+ if (!gimbal_yaw_angle_.ready()) {
+ gimbal_yaw_angle_.make_and_bind_directly(0.0);
+ RCLCPP_WARN(get_logger(), "Failed to fetch \"/gimbal/yaw/angle\". Set to 0.0.");
+ }
+ if (!gimbal_yaw_angle_error_.ready()) {
+ gimbal_yaw_angle_error_.make_and_bind_directly(0.0);
+ RCLCPP_WARN(
+ get_logger(), "Failed to fetch \"/gimbal/yaw/control_angle_error\". Set to 0.0.");
+ }
+ }
+
+ void update() override {
+ using namespace rmcs_msgs;
+
+ auto switch_right = *switch_right_;
+ auto switch_left = *switch_left_;
+ auto keyboard = *keyboard_;
+
+ do {
+ if ((switch_left == Switch::UNKNOWN || switch_right == Switch::UNKNOWN)
+ || (switch_left == Switch::DOWN && switch_right == Switch::DOWN)) {
+ reset_all_controls();
+ break;
+ }
+
+ auto mode = *mode_;
+ if (switch_left != Switch::DOWN) {
+ if (last_switch_right_ == Switch::DOWN && switch_right == Switch::MIDDLE) {
+ if (mode == rmcs_msgs::WheelLegMode::BALANCELESS) {
+ mode = rmcs_msgs::WheelLegMode::FOLLOW;
+ }
+ }
+
+ if ((last_switch_right_ == Switch::MIDDLE && switch_right == Switch::DOWN)
+ || (!last_keyboard_.c && keyboard.c)) {
+ if (mode == rmcs_msgs::WheelLegMode::SPIN) {
+ mode = rmcs_msgs::WheelLegMode::FOLLOW;
+ } else {
+ mode = rmcs_msgs::WheelLegMode::SPIN;
+ spinning_forward_ = !spinning_forward_;
+ }
+ } else if (!last_keyboard_.x && keyboard.x) {
+ mode = mode == rmcs_msgs::WheelLegMode::LAUNCH_RAMP
+ ? rmcs_msgs::WheelLegMode::FOLLOW
+ : rmcs_msgs::WheelLegMode::LAUNCH_RAMP;
+ } else if (!last_keyboard_.b && keyboard.b) {
+ mode = mode == rmcs_msgs::WheelLegMode::BALANCELESS
+ ? rmcs_msgs::WheelLegMode::FOLLOW
+ : rmcs_msgs::WheelLegMode::BALANCELESS;
+ }
+
+ // Change leg length
+ if (!last_keyboard_.z && keyboard.z) {
+ leg_extended_ = !leg_extended_;
+ }
+
+ // Jump
+ jump_active_ = !last_keyboard_.e && keyboard.e;
+
+ // Climb
+ climb_active_ = !last_keyboard_.q && keyboard.q;
+
+ // Rescue tip-over
+ rescue_tip_over_ = !last_keyboard_.g && keyboard.g;
+
+ *is_balanceless_ = mode == rmcs_msgs::WheelLegMode::BALANCELESS;
+ *is_leg_extended_ = leg_extended_;
+ *is_jump_active_ = jump_active_;
+ *is_climb_active_ = climb_active_;
+ *is_rescue_tip_over_ = rescue_tip_over_;
+
+ *mode_ = mode;
+ }
+
+ update_velocity_control();
+ update_chassis_angle_control();
+ } while (false);
+
+ last_switch_right_ = switch_right;
+ last_switch_left_ = switch_left;
+ last_keyboard_ = keyboard;
+ }
+
+ void reset_all_controls() {
+ *mode_ = rmcs_msgs::WheelLegMode::BALANCELESS;
+
+ *chassis_control_velocity_ = {nan, nan, nan};
+ }
+
+ void update_velocity_control() {
+ auto translational_velocity = update_translational_velocity_control();
+ auto angular_velocity = update_angular_velocity_control();
+
+ chassis_control_velocity_->vector << translational_velocity, angular_velocity;
+ }
+
+ Eigen::Vector2d update_translational_velocity_control() {
+ auto keyboard = *keyboard_;
+ Eigen::Vector2d keyboard_move{keyboard.w - keyboard.s, keyboard.a - keyboard.d};
+
+ Eigen::Vector2d translational_velocity =
+ Eigen::Rotation2Dd{*gimbal_yaw_angle_} * (*joystick_right_ + keyboard_move);
+
+ if (translational_velocity.norm() > 1.0)
+ translational_velocity.normalize();
+
+ translational_velocity *= translational_velocity_max;
+
+ return translational_velocity;
+ }
+
+ void update_chassis_angle_control() {
+ double chassis_control_angle = nan;
+
+ *chassis_control_angle_ = chassis_control_angle;
+ }
+
+ double update_angular_velocity_control() {
+ double angular_velocity = 0.0;
+ double chassis_control_angle = nan;
+
+ switch (*mode_) {
+ case rmcs_msgs::WheelLegMode::BALANCELESS:
+ case rmcs_msgs::WheelLegMode::SPIN: {
+ angular_velocity =
+ 0.6 * (spinning_forward_ ? angular_velocity_max : -angular_velocity_max);
+ chassis_control_angle =
+ *chassis_yaw_angle_imu_; // When spinning, yaw angle's output is set to zero.
+ } break;
+ case rmcs_msgs::WheelLegMode::FOLLOW:
+ case rmcs_msgs::WheelLegMode::LAUNCH_RAMP: {
+ double err = calculate_unsigned_chassis_angle_error(chassis_control_angle);
+ // err: [0, 2pi) -> signed
+ // Only one direction can be used for alignment.
+ // TODO: Dynamically determine the split angle based on chassis velocity.
+ constexpr double alignment = 2 * std::numbers::pi;
+ if (err > alignment / 2)
+ err -= alignment;
+
+ angular_velocity = following_velocity_controller_.update(err);
+
+ chassis_control_angle = 0.0;
+ } break;
+ }
+ *chassis_angle_ = 2 * std::numbers::pi - *gimbal_yaw_angle_;
+ *chassis_control_angle_ = chassis_control_angle;
+
+ return angular_velocity;
+ }
+
+ double calculate_unsigned_chassis_angle_error(double& chassis_control_angle) {
+ chassis_control_angle = *gimbal_yaw_angle_error_;
+ if (chassis_control_angle < 0)
+ chassis_control_angle += 2 * std::numbers::pi;
+ // chassis_control_angle: [0, 2pi).
+
+ // err = setpoint - measurement
+ // ^ ^
+ // |gimbal_yaw_angle_error |chassis_angle
+ // ^
+ // |(2pi - gimbal_yaw_angle)
+ double err = chassis_control_angle + *gimbal_yaw_angle_;
+ if (err >= 2 * std::numbers::pi)
+ err -= 2 * std::numbers::pi;
+ // err: [0, 2pi).
+
+ return err;
+ }
+
+private:
+ static constexpr double inf = std::numeric_limits::infinity();
+ static constexpr double nan = std::numeric_limits::quiet_NaN();
+
+ // Maximum control velocities
+ static constexpr double translational_velocity_max = 2.0;
+ static constexpr double angular_velocity_max = 2.0;
+
+ InputInterface joystick_right_;
+ InputInterface joystick_left_;
+ InputInterface switch_right_;
+ InputInterface switch_left_;
+ InputInterface mouse_velocity_;
+ InputInterface mouse_;
+ InputInterface keyboard_;
+ 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();
+
+ InputInterface gimbal_yaw_angle_, gimbal_yaw_angle_error_;
+ InputInterface chassis_yaw_angle_imu_;
+
+ OutputInterface chassis_angle_, chassis_control_angle_;
+
+ OutputInterface mode_;
+
+ OutputInterface is_balanceless_;
+ OutputInterface is_rescue_tip_over_;
+
+ OutputInterface is_leg_extended_;
+ OutputInterface is_jump_active_;
+ OutputInterface is_climb_active_;
+
+ bool spinning_forward_ = true;
+ bool leg_extended_ = false, jump_active_ = false, climb_active_ = false,
+ rescue_tip_over_ = false, launch_ramp_active_ = false;
+
+ pid::PidCalculator following_velocity_controller_;
+
+ OutputInterface chassis_yaw_control_angle_;
+ OutputInterface chassis_control_velocity_;
+};
+
+} // namespace rmcs_core::controller::chassis
+
+#include
+
+PLUGINLIB_EXPORT_CLASS(
+ rmcs_core::controller::chassis::WheelLegChassisController, rmcs_executor::Component)
\ No newline at end of file
diff --git a/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/wheel_leg_controller.cpp b/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/wheel_leg_controller.cpp
new file mode 100644
index 00000000..725328e4
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/controller/chassis/wheel_leg/wheel_leg_controller.cpp
@@ -0,0 +1,773 @@
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "controller/lqr/lqr_calculator.hpp"
+#include "controller/pid/pid_calculator.hpp"
+#include "desire_state_solver.hpp"
+#include "filter/kalman_filter.hpp"
+#include "filter/low_pass_filter.hpp"
+#include "rmcs_executor/component.hpp"
+
+#include "vmc_solver.hpp"
+
+namespace rmcs_core::controller::chassis {
+class WheelLegController
+ : public rmcs_executor::Component
+ , public rclcpp::Node {
+public:
+ WheelLegController()
+ : Node(
+ get_component_name(),
+ rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true))
+ , body_mess_(13.3)
+ , leg_mess_(0.629)
+ , wheel_mess_(0.459)
+ , wheel_radius_(0.06)
+ , wheel_distance_(0.2192)
+ , centroid_position_coefficient_(0.2945)
+ , left_leg_vmc_solver_(0.21, 0.25, 0.0)
+ , right_leg_vmc_solver_(0.21, 0.25, 0.0)
+ , velocity_kalman_filter_(A_, H_, Q_, R_)
+ , chassis_gyro_velocity_filter_(5.0, 1000.0)
+ , desire_state_solver_(0.15)
+ , roll_angle_pid_calculator_(1.0, 0.0, 1.0)
+ , leg_length_pid_calculator_(90.0, 0.0, 0.0)
+ , balanceless_left_wheel_pid_calculator_(3.0, 0.0, 0.0)
+ , balanceless_right_wheel_pid_calculator_(3.0, 0.0, 0.0)
+ , rescue_velocity_pid_calculator_(3.0, 0.0, 0.0) {
+
+ register_input("/chassis/left_front_hip/max_torque", hip_motor_max_control_torque_);
+ register_input("/chassis/left_wheel/max_torque", wheel_motor_max_control_torque_);
+
+ register_input("/chassis/control_velocity", chassis_control_velocity_);
+ register_input("/chassis/control_mode", mode_);
+
+ register_input("/chassis/control_angle", chassis_control_angle_);
+
+ register_input("/chassis/status/balanceless", is_balanceless_);
+ register_input("/chassis/status/rescue_tip_over", is_rescue_tip_over_);
+
+ register_input("/chassis/status/leg_extended", is_leg_extended_);
+ register_input("/chassis/status/jump", is_jump_active_);
+ register_input("/chassis/status/climb", is_climb_active_);
+
+ register_input("/chassis/left_front_hip/angle", left_front_hip_angle_);
+ register_input("/chassis/left_back_hip/angle", left_back_hip_angle_);
+ register_input("/chassis/right_front_hip/angle", right_front_hip_angle_);
+ register_input("/chassis/right_back_hip/angle", right_back_hip_angle_);
+
+ register_input("/chassis/left_front_hip/velocity", left_front_hip_velocity_);
+ register_input("/chassis/left_back_hip/velocity", left_back_hip_velocity_);
+ register_input("/chassis/right_front_hip/velocity", right_front_hip_velocity_);
+ register_input("/chassis/right_back_hip/velocity", right_back_hip_velocity_);
+
+ register_input("/chassis/left_front_hip/torque", left_front_hip_torque_);
+ register_input("/chassis/left_back_hip/torque", left_back_hip_torque_);
+ register_input("/chassis/right_front_hip/torque", right_front_hip_torque_);
+ register_input("/chassis/right_back_hip/torque", right_back_hip_torque_);
+
+ register_input("/chassis/left_wheel/velocity", left_wheel_velocity_);
+ register_input("/chassis/right_wheel/velocity", right_wheel_velocity_);
+
+ register_input("/chassis/x_axis/acceleration", chassis_x_axis_acceleration_imu_);
+ register_input("/chassis/z_axis/acceleration", chassis_z_axis_acceleration_imu_);
+
+ register_input("/chassis/yaw/velocity", chassis_yaw_velocity_imu_);
+ register_input("/chassis/pitch/velocity", chassis_pitch_velocity_imu_);
+ register_input("/chassis/roll/velocity", chassis_roll_velocity_imu_);
+
+ register_input("/chassis/yaw/angle", chassis_yaw_angle_imu_);
+ register_input("/chassis/pitch/angle", chassis_pitch_angle_imu_);
+ register_input("/chassis/roll/angle", chassis_roll_angle_imu_);
+
+ register_output("/chassis/left_front_hip/control_torque", left_front_hip_control_torque_);
+ register_output("/chassis/left_back_hip/control_torque", left_back_hip_control_torque_);
+ register_output("/chassis/right_front_hip/control_torque", right_front_hip_control_torque_);
+ register_output("/chassis/right_back_hip/control_torque", right_back_hip_control_torque_);
+
+ register_output("/chassis/left_front_hip/control_angle", left_front_hip_control_angle_);
+ register_output("/chassis/left_back_hip/control_angle", left_back_hip_control_angle_);
+ register_output("/chassis/right_front_hip/control_angle", right_front_hip_control_angle_);
+ register_output("/chassis/right_back_hip/control_angle", right_back_hip_control_angle_);
+
+ register_output("/chassis/left_wheel/control_torque", left_wheel_control_torque_);
+ register_output("/chassis/right_wheel/control_torque", right_wheel_control_torque_);
+
+ velocity_kalman_filter_.set_process_noise_transition(W_);
+ }
+
+ ~WheelLegController() override { reset_all_controls(); }
+
+ void before_updating() override {
+ RCLCPP_INFO(
+ get_logger(), "Max control torque of hip motor: %.f, wheel motor: %.f",
+ *hip_motor_max_control_torque_, *wheel_motor_max_control_torque_);
+ }
+
+ void update() override {
+ if (std::isnan(chassis_control_velocity_->vector[0])) {
+ reset_all_controls();
+ return;
+ }
+
+ // Observer
+ auto hip_angles = calculate_hips_angles();
+ auto wheel_velocities = calculate_wheel_velocities();
+
+ auto leg_posture = calculate_leg_posture(hip_angles);
+ auto distance = calculate_translational_distance(leg_posture, wheel_velocities);
+
+ // State: [s ds phi d_phi theta_l d_theta_l theta_r d_theta_r theta_b d_theta_b]
+ auto measure_state = calculate_measure_state(distance, leg_posture);
+
+ // Controller
+ auto chassis_control_velocity = calculate_chassis_control_velocity();
+ auto leg_forces = calculate_leg_force(leg_posture, measure_state);
+
+ auto desire_state = calculate_desire_state(chassis_control_velocity, measure_state);
+ auto control_torques = calculate_control_torques(desire_state, measure_state, leg_posture);
+
+ // if (*is_balanceless_) {
+ // update_balanceless_control_torques(chassis_control_velocity, wheel_velocities);
+ // return;
+ // }
+
+ // if (!stand_active_) {
+ // update_hip_stand_control_torques(leg_posture);
+ // return;
+ // }
+
+ // if (*is_rescue_tip_over_) {
+ // update_hip_rescue_tip_over_control_torques();
+ // }
+
+ update_hip_and_wheel_torques(leg_forces, control_torques);
+
+ // *left_front_hip_control_torque_ = 0.0;
+ // *left_back_hip_control_torque_ = 0.0;
+ *right_front_hip_control_torque_ = 0.0;
+ *right_back_hip_control_torque_ = 0.0;
+
+ *left_wheel_control_torque_ = 0.0;
+ *right_wheel_control_torque_ = 0.0;
+ }
+
+private:
+ struct LegPosture {
+ Eigen::Vector2d leg_length;
+ Eigen::Vector2d tilt_angle;
+
+ Eigen::Vector2d diff_leg_length;
+ Eigen::Vector2d diff_tilt_angle;
+
+ Eigen::Vector2d second_order_diff_leg_length;
+ Eigen::Vector2d second_order_diff_tilt_angle;
+ };
+
+ void reset_all_controls() {
+ left_leg_vmc_solver_.reset();
+ right_leg_vmc_solver_.reset();
+
+ desire_state_solver_.reset();
+ velocity_kalman_filter_.reset();
+ chassis_gyro_velocity_filter_.reset();
+
+ *left_front_hip_control_torque_ = nan_;
+ *left_back_hip_control_torque_ = nan_;
+ *right_front_hip_control_torque_ = nan_;
+ *right_back_hip_control_torque_ = nan_;
+
+ *left_wheel_control_torque_ = 0.0;
+ *right_wheel_control_torque_ = 0.0;
+
+ *left_front_hip_control_angle_ = nan_;
+ *left_back_hip_control_angle_ = nan_;
+ *right_front_hip_control_angle_ = nan_;
+ *right_back_hip_control_angle_ = nan_;
+ }
+
+ Eigen::Vector2d calculate_wheel_velocities() {
+ return {
+ *left_wheel_velocity_ * wheel_radius_, //
+ *right_wheel_velocity_ * wheel_radius_};
+ }
+
+ Eigen::Vector4d calculate_hips_angles() {
+ return {
+ *left_front_hip_angle_, //
+ *left_back_hip_angle_, //
+ *right_back_hip_angle_, //
+ *right_front_hip_angle_, //
+ };
+ }
+
+ LegPosture calculate_leg_posture(const Eigen::Vector4d& hip_angles) {
+ const auto& [lf, lb, rb, rf] = hip_angles;
+ LegPosture result;
+
+ const auto& [left_leg_length, left_tilt_angle] = left_leg_vmc_solver_.update(lf, lb);
+ const auto& [right_leg_length, right_tilt_angle] = right_leg_vmc_solver_.update(rf, rb);
+
+ result.leg_length = Eigen::Vector2d{left_leg_length, right_leg_length};
+ result.tilt_angle = Eigen::Vector2d{left_tilt_angle, right_tilt_angle};
+
+ result.tilt_angle.array() += *chassis_pitch_angle_imu_;
+
+ result.diff_leg_length = (result.leg_length - last_leg_length_) / dt_;
+ last_leg_length_ = result.leg_length;
+ result.diff_tilt_angle = (result.tilt_angle - last_tilt_angle_) / dt_;
+ last_tilt_angle_ = result.tilt_angle;
+
+ result.second_order_diff_leg_length = (result.diff_leg_length - last_dot_leg_length_) / dt_;
+ last_dot_leg_length_ = result.diff_leg_length;
+ result.second_order_diff_tilt_angle = (result.diff_tilt_angle - last_dot_tilt_angle_) / dt_;
+ last_dot_tilt_angle_ = result.diff_tilt_angle;
+
+ return result;
+ }
+
+ Eigen::Vector2d calculate_support_force(LegPosture leg_posture) {
+ Eigen::Vector2d result;
+ auto& [left_support_force, right_support_force] = result;
+
+ const auto& [left_leg_force, left_leg_torque] = left_leg_vmc_solver_.update_virtual_torque(
+ *left_front_hip_torque_, *left_back_hip_torque_);
+ const auto& [right_leg_force, right_leg_torque] =
+ right_leg_vmc_solver_.update_virtual_torque(
+ *right_front_hip_torque_, *right_back_hip_torque_);
+
+ auto left_leg_to_wheel_force =
+ left_leg_force * std::cos(leg_posture.tilt_angle.x())
+ + left_leg_torque * std::sin(leg_posture.tilt_angle.x()) / leg_posture.leg_length.x();
+ auto right_leg_to_wheel_force =
+ right_leg_force * std::cos(leg_posture.tilt_angle.y())
+ + right_leg_torque * std::sin(leg_posture.tilt_angle.y()) / leg_posture.leg_length.y();
+
+ // todo: test
+ auto left_wheel_vertical_accel =
+ *chassis_z_axis_acceleration_imu_
+ - leg_posture.second_order_diff_leg_length.x() * std::cos(leg_posture.tilt_angle.x())
+ + 2 * leg_posture.diff_leg_length.x() * leg_posture.diff_tilt_angle.x()
+ * std::sin(leg_posture.tilt_angle.x())
+ + leg_posture.leg_length.x() * leg_posture.second_order_diff_tilt_angle.x()
+ * std::sin(leg_posture.tilt_angle.x())
+ + leg_posture.leg_length.x() * leg_posture.diff_tilt_angle.x()
+ * leg_posture.diff_tilt_angle.x() * std::cos(leg_posture.tilt_angle.x());
+
+ auto right_wheel_vertical_accel =
+ *chassis_z_axis_acceleration_imu_
+ - leg_posture.second_order_diff_leg_length.y() * std::cos(leg_posture.tilt_angle.y())
+ + 2 * leg_posture.diff_leg_length.y() * leg_posture.diff_tilt_angle.y()
+ * std::sin(leg_posture.tilt_angle.y())
+ + leg_posture.leg_length.y() * leg_posture.second_order_diff_tilt_angle.y()
+ * std::sin(leg_posture.tilt_angle.y())
+ + leg_posture.leg_length.y() * leg_posture.diff_tilt_angle.y()
+ * leg_posture.diff_tilt_angle.y() * std::cos(leg_posture.tilt_angle.y());
+
+ left_support_force =
+ left_leg_to_wheel_force + wheel_mess_ * (g_ + left_wheel_vertical_accel);
+ right_support_force =
+ right_leg_to_wheel_force + wheel_mess_ * (g_ + right_wheel_vertical_accel);
+
+ return result;
+ }
+
+ Eigen::Vector2d
+ calculate_translational_distance(LegPosture leg_posture, Eigen::Vector2d wheel_velocities) {
+ Eigen::Vector2d result;
+ auto& [distance, velocity] = result;
+
+ auto wheel_velocity = (wheel_velocities.x() + wheel_velocities.y()) / 2.0;
+
+ auto left_leg_velocity =
+ leg_posture.leg_length.x() * std::cos(leg_posture.tilt_angle.x())
+ * leg_posture.diff_tilt_angle.x()
+ + leg_posture.diff_leg_length.x() * std::sin(leg_posture.tilt_angle.x());
+
+ auto right_leg_velocity =
+ leg_posture.leg_length.y() * std::cos(leg_posture.tilt_angle.y())
+ * leg_posture.diff_tilt_angle.y()
+ + leg_posture.diff_leg_length.y() * std::sin(leg_posture.tilt_angle.y());
+
+ // auto calculate_velocity = wheel_velocity + (left_leg_velocity + right_leg_velocity)
+ // / 2.0;
+ auto calculate_velocity = wheel_velocity;
+
+ // Velocity estimation is referenced from the article:
+ // https://zhuanlan.zhihu.com/p/689921165
+ // auto estimate_velocity = velocity_kalman_filter_.update(
+ // Eigen::Vector2d{calculate_velocity, *chassis_x_axis_acceleration_imu_});
+
+ velocity = calculate_velocity;
+
+ auto is_parked = std::abs(velocity) < 1e-2;
+ // When the vehicle stops the position control is activated.
+ if (!last_is_parked_ && is_parked) {
+ distance = last_distance_ + velocity * dt_;
+ } else {
+ distance = 0.0;
+ }
+ last_is_parked_ = is_parked;
+
+ distance = last_distance_ + velocity * dt_;
+ last_distance_ = distance;
+
+ return Eigen::Vector2d{distance, velocity};
+ }
+
+ rmcs_msgs::WheelLegState
+ calculate_measure_state(Eigen::Vector2d distance, LegPosture leg_posture) {
+ rmcs_msgs::WheelLegState measure_state;
+
+ measure_state.distance = distance.x();
+ measure_state.velocity = distance.y();
+
+ measure_state.yaw_angle = *chassis_yaw_angle_imu_;
+ measure_state.yaw_velocity = *chassis_yaw_velocity_imu_;
+
+ measure_state.left_tilt_angle = leg_posture.tilt_angle.x();
+ measure_state.left_tilt_velocity = leg_posture.diff_tilt_angle.x();
+
+ measure_state.right_tilt_angle = leg_posture.tilt_angle.y();
+ measure_state.right_tilt_velocity = leg_posture.diff_tilt_angle.y();
+
+ measure_state.body_pitch_angle = *chassis_pitch_angle_imu_;
+ measure_state.body_pitch_velocity = *chassis_pitch_velocity_imu_;
+
+ measure_state.distance = 0.0;
+ measure_state.velocity = 0.0;
+
+ // measure_state.yaw_angle = 0.0;
+ // measure_state.yaw_velocity = 0.0;
+
+ // measure_state.left_tilt_angle = 0.0;
+ // measure_state.left_tilt_velocity = 0.0;
+
+ // measure_state.right_tilt_angle = 0.0;
+ // measure_state.right_tilt_velocity = 0.0;
+
+ // measure_state.body_pitch_angle = 0.0;
+ // measure_state.body_pitch_velocity = 0.0;
+
+ return measure_state;
+ }
+
+ rmcs_msgs::WheelLegState detect_chassis_levitate(
+ Eigen::Vector2d support_forces, rmcs_msgs::WheelLegState measure_state) {
+ auto& [left_support_force, right_support_force] = support_forces;
+
+ double levitate_force = 20.0;
+ double normal_force = 50.0;
+
+ auto average_support_force = (left_support_force + right_support_force) / 2.0;
+
+ auto mode = *mode_;
+ auto mode_active = mode != rmcs_msgs::WheelLegMode::BALANCELESS;
+
+ if (mode_active) {
+ if (average_support_force < levitate_force) {
+ levitate_active_ = true;
+ } else if (average_support_force > normal_force) {
+ levitate_active_ = false;
+ }
+
+ if (left_support_force < levitate_force) {
+ left_levitate_active_ = true;
+ } else if (left_support_force > normal_force) {
+ left_levitate_active_ = false;
+ }
+
+ if (right_support_force < levitate_force) {
+ right_levitate_active_ = true;
+ } else if (right_support_force > normal_force) {
+ right_levitate_active_ = false;
+ }
+ } else {
+ levitate_active_ = false;
+ left_levitate_active_ = false;
+ right_levitate_active_ = false;
+ }
+
+ if (levitate_active_) {
+ RCLCPP_INFO(
+ get_logger(), "Chassis is levitating! Average support force: %f",
+ average_support_force);
+ measure_state.distance = 0.0;
+ }
+ return measure_state;
+ }
+
+ // When the vehicle is about to fall, reset the leg length.
+ void detect_chassis_fall() {
+ // Detect pitch and roll angle
+ if (fabs(*chassis_roll_angle_imu_ - desire_roll_angle_) > about_to_fall_roll_angle_) {
+ about_to_fall_ = true;
+ }
+
+ if (levitate_active_) {
+ about_to_fall_ = false;
+ }
+
+ // It will inactive when rotating, climbing, or launching ramp.
+ if (*is_climb_active_ || *mode_ == rmcs_msgs::WheelLegMode::LAUNCH_RAMP
+ || *mode_ == rmcs_msgs::WheelLegMode::SPIN) {
+ about_to_fall_ = false;
+ }
+ }
+
+ Eigen::Vector3d calculate_chassis_control_velocity() {
+ Eigen::Vector3d chassis_control_velocity;
+ chassis_control_velocity = chassis_control_velocity_->vector;
+ return chassis_control_velocity;
+ }
+
+ rmcs_msgs::WheelLegState calculate_desire_state(
+ Eigen::Vector3d control_velocity, rmcs_msgs::WheelLegState measure_state) {
+ rmcs_msgs::WheelLegState desire_state{};
+
+ desire_state_solver_.update_measure_state(measure_state);
+ desire_state = desire_state_solver_.update(std::move(control_velocity));
+
+ desire_leg_length_ =
+ desire_state_solver_.update_desire_leg_length(*is_leg_extended_, 0.13, 0.36);
+ desire_roll_angle_ = desire_state_solver_.update_desire_roll_angle();
+
+ return desire_state;
+ }
+
+ Eigen::Vector2d
+ calculate_leg_force(LegPosture leg_posture, rmcs_msgs::WheelLegState measure_state) {
+ Eigen::Vector2d result;
+
+ auto leg_length = (leg_posture.leg_length.x() + leg_posture.leg_length.y()) / 2.0;
+
+ auto roll_control_force =
+ roll_angle_pid_calculator_.update(desire_roll_angle_ - *chassis_roll_angle_imu_);
+ auto leg_length_control_force =
+ leg_length_pid_calculator_.update(desire_leg_length_ - leg_length);
+
+ auto calculate_compensation_feedforward_force = [this](double coefficient) {
+ return (body_mess_ / 2.0 + centroid_position_coefficient_ * leg_mess_) * coefficient;
+ };
+
+ auto gravity_feedforward_control_force = calculate_compensation_feedforward_force(g_);
+ auto inertial_feedforward_control_force = calculate_compensation_feedforward_force(
+ leg_length / (2 * wheel_distance_) * measure_state.yaw_velocity
+ * measure_state.velocity);
+
+ // F_ψ
+ // F_bl_l = 1 1 1 -1 * F_l
+ // F_bl_r -1 1 1 1 F_g
+ // F_i
+ result = Eigen::Vector2d{
+ roll_control_force + leg_length_control_force + gravity_feedforward_control_force
+ - inertial_feedforward_control_force, // F_bl_l
+ -roll_control_force + leg_length_control_force + gravity_feedforward_control_force
+ + inertial_feedforward_control_force // F_bl_r
+ };
+
+ return result;
+ }
+
+ Eigen::Vector4d calculate_control_torques(
+ rmcs_msgs::WheelLegState desire_state, rmcs_msgs::WheelLegState measure_state,
+ LegPosture leg_posture) {
+ Eigen::Vector4d result{};
+
+ Eigen::Vector error_state = desire_state.vector() - measure_state.vector();
+ auto gain = lqr_calculator_.update(leg_posture.leg_length.x(), leg_posture.leg_length.y());
+
+ if (levitate_active_) {
+ // When levitating, only keep legs vertical,and only remain legs' outputs.
+ // set zero
+ error_state(0) = 0.0; // distance
+ error_state(1) = 0.0; // velocity
+ error_state(2) = 0.0; // yaw angle
+ error_state(3) = 0.0; // yaw velocity
+
+ error_state(8) = 0.0; // body pitch angle
+ error_state(9) = 0.0; // body pitch velocity
+ }
+
+ result = -1.0 * gain * error_state;
+
+ auto output_0 = -gain(0, 0) * error_state(0);
+ auto output_1 = -gain(0, 1) * error_state(1);
+ auto output_2 = -gain(0, 2) * error_state(2);
+ auto output_3 = -gain(0, 3) * error_state(3);
+ auto output_4 = -gain(0, 4) * error_state(4);
+ auto output_5 = -gain(0, 5) * error_state(5);
+ auto output_6 = -gain(0, 6) * error_state(6);
+ auto output_7 = -gain(0, 7) * error_state(7);
+ auto output_8 = -gain(0, 8) * error_state(8);
+ auto output_9 = -gain(0, 9) * error_state(9);
+
+ RCLCPP_INFO(
+ get_logger(), "output: [%f, %f, %f, %f, %f, %f, %f, %f, %f, %f], %f, %f", output_0,
+ output_1, output_2, output_3, output_4, output_5, output_6, output_7, output_8,
+ output_9, result.x(), result.y());
+
+ // RCLCPP_INFO(
+ // get_logger(), "measure: [%f, %f, %f, %f, %f, %f, %f, %f, %f, %f]",
+ // measure_state.distance, measure_state.velocity, measure_state.yaw_angle,
+ // measure_state.yaw_velocity, measure_state.left_tilt_angle,
+ // measure_state.left_tilt_velocity, measure_state.right_tilt_angle,
+ // measure_state.right_tilt_velocity, measure_state.body_pitch_angle,
+ // measure_state.body_pitch_velocity);
+
+ return result;
+ }
+
+ void update_balanceless_control_torques(
+ Eigen::Vector3d chassis_control_velocity, Eigen::Vector2d wheel_velocities) {
+ auto& [chassis_control_velocity_x, chassis_control_velocity_y, chassis_control_velocity_z] =
+ chassis_control_velocity;
+ auto& [left_wheel_velocity, right_wheel_velocity] = wheel_velocities;
+
+ double left_wheel_control_torque = balanceless_left_wheel_pid_calculator_.update(
+ (chassis_control_velocity_x - chassis_control_velocity_y) - left_wheel_velocity);
+ double right_wheel_control_torque = balanceless_right_wheel_pid_calculator_.update(
+ (chassis_control_velocity_x + chassis_control_velocity_y) - right_wheel_velocity);
+
+ *left_wheel_control_torque_ = clamp_wheel_control_torque(left_wheel_control_torque);
+ *right_wheel_control_torque_ = clamp_wheel_control_torque(right_wheel_control_torque);
+ }
+
+ void update_hip_zero_point_control_angles() {
+ *left_front_hip_control_angle_ = 0.0;
+ *left_back_hip_control_angle_ = 0.0;
+ *right_front_hip_control_angle_ = 0.0;
+ *right_back_hip_control_angle_ = 0.0;
+
+ reset_control_torques();
+ }
+
+ void update_hip_rescue_tip_over_control_torques() {
+ double control_velocity = pi_;
+
+ // stand_active_ = false;
+ auto left_front_hip_control_torque =
+ rescue_velocity_pid_calculator_.update(control_velocity - *left_front_hip_velocity_);
+ auto left_back_hip_control_torque =
+ rescue_velocity_pid_calculator_.update(control_velocity - *left_back_hip_velocity_);
+ auto right_front_hip_control_torque =
+ rescue_velocity_pid_calculator_.update(control_velocity - *right_front_hip_velocity_);
+ auto right_back_hip_control_torque =
+ rescue_velocity_pid_calculator_.update(control_velocity - *right_back_hip_velocity_);
+
+ *left_front_hip_control_torque_ = left_front_hip_control_torque;
+ *left_back_hip_control_torque_ = left_back_hip_control_torque;
+ *right_front_hip_control_torque_ = right_front_hip_control_torque;
+ *right_back_hip_control_torque_ = right_back_hip_control_torque;
+
+ reset_control_angles();
+ }
+
+ void update_hip_stand_control_torques(LegPosture leg_posture) {
+ auto leg_length = (leg_posture.leg_length.x() + leg_posture.leg_length.y()) / 2.0;
+
+ auto leg_length_control_force =
+ leg_length_pid_calculator_.update(min_leg_length_ - leg_length);
+
+ if (leg_length < min_leg_length_ + 0.01) {
+ stand_active_ = true;
+ }
+
+ RCLCPP_INFO(
+ get_logger(), "control force: %f, leg length: %f,stand active: %d",
+ leg_length_control_force, leg_length, stand_active_);
+
+ *left_front_hip_control_torque_ = -leg_length_control_force;
+ *left_back_hip_control_torque_ = leg_length_control_force;
+ *right_front_hip_control_torque_ = -leg_length_control_force;
+ *right_back_hip_control_torque_ = leg_length_control_force;
+ }
+
+ void update_hip_and_wheel_torques(Eigen::Vector2d leg_forces, Eigen::Vector4d control_torques) {
+ auto& [left_leg_force, right_leg_force] = leg_forces;
+ auto& [left_wheel_control_torque, right_wheel_control_torque, left_leg_control_torque, right_leg_control_torque] =
+ control_torques;
+
+ auto left_hip_control_torque =
+ left_leg_vmc_solver_.update_joint_torque(left_leg_force, left_leg_control_torque);
+ auto right_hip_control_torque =
+ right_leg_vmc_solver_.update_joint_torque(right_leg_force, right_leg_control_torque);
+
+ // RCLCPP_INFO(
+ // get_logger(), "lf: %f, lb: %f, rf: %f, rb: %f,control_torque: %f",
+ // left_hip_control_torque.x(), left_hip_control_torque.y(),
+ // right_hip_control_torque.x(), right_hip_control_torque.y(), torque);
+
+ if (levitate_active_) {
+ *left_wheel_control_torque_ = 0.0;
+ *right_wheel_control_torque_ = 0.0;
+ }
+
+ *left_wheel_control_torque_ = clamp_wheel_control_torque(left_wheel_control_torque);
+ *right_wheel_control_torque_ = clamp_wheel_control_torque(right_wheel_control_torque);
+
+ *left_front_hip_control_torque_ = clamp_hip_control_torque(-left_hip_control_torque.x());
+ *left_back_hip_control_torque_ = clamp_hip_control_torque(left_hip_control_torque.y());
+ *right_front_hip_control_torque_ = clamp_hip_control_torque(-right_hip_control_torque.x());
+ *right_back_hip_control_torque_ = clamp_hip_control_torque(right_hip_control_torque.y());
+
+ // RCLCPP_INFO(
+ // get_logger(), "lf: %f, lb: %f, rb: %f, rf: %f", *left_front_hip_control_torque_,
+ // *left_back_hip_control_torque_, *right_back_hip_control_torque_,
+ // *right_front_hip_control_torque_);
+
+ reset_control_angles();
+ }
+
+ static double clamp_wheel_control_torque(const double& torque) {
+ return std::clamp(torque, -2.4, 2.4);
+ }
+
+ static double clamp_hip_control_torque(const double& torque) {
+ return std::clamp(torque, -10., 10.);
+ }
+
+ void reset_control_torques() {
+ *left_front_hip_control_torque_ = nan_;
+ *left_back_hip_control_torque_ = nan_;
+ *right_front_hip_control_torque_ = nan_;
+ *right_back_hip_control_torque_ = nan_;
+ }
+
+ void reset_control_angles() {
+ *left_front_hip_control_angle_ = nan_;
+ *left_back_hip_control_angle_ = nan_;
+ *right_front_hip_control_angle_ = nan_;
+ *right_back_hip_control_angle_ = nan_;
+ }
+
+ static constexpr double nan_ = std::numeric_limits::quiet_NaN();
+ static constexpr double inf_ = std::numeric_limits::infinity();
+
+ static constexpr double pi_ = std::numbers::pi;
+
+ static constexpr double dt_ = 1e-3;
+ static constexpr double g_ = 9.80665;
+
+ static constexpr double max_leg_length_ = 0.36;
+ static constexpr double min_leg_length_ = 0.13;
+
+ static constexpr double about_to_fall_roll_angle_ = 0.6;
+
+ static constexpr double sigma_q_ = 1.0;
+ static constexpr double sigma_v_ = 0.75;
+ static constexpr double sigma_a_ = 1.0;
+
+ const Eigen::Matrix2d A_ = (Eigen::Matrix2d() << 1, dt_, 1, 1).finished();
+ const Eigen::Matrix2d H_ = Eigen::Vector2d::Identity().asDiagonal();
+ const Eigen::Matrix2d W_ = Eigen::Vector2d{0.5 * dt_ * dt_, dt_}.asDiagonal();
+ const Eigen::Matrix2d Q_ =
+ Eigen::Vector2d{sigma_q_ * sigma_q_, sigma_q_* sigma_q_}.asDiagonal();
+ const Eigen::Matrix2d R_ =
+ Eigen::Vector2d{sigma_v_ * sigma_v_, sigma_a_* sigma_a_}.asDiagonal();
+
+ const double body_mess_;
+ const double leg_mess_;
+ const double wheel_mess_;
+ const double wheel_radius_;
+ const double wheel_distance_;
+ const double centroid_position_coefficient_;
+
+ InputInterface hip_motor_max_control_torque_;
+ InputInterface wheel_motor_max_control_torque_;
+
+ InputInterface left_front_hip_angle_;
+ InputInterface left_back_hip_angle_;
+ InputInterface right_front_hip_angle_;
+ InputInterface right_back_hip_angle_;
+
+ InputInterface left_front_hip_velocity_;
+ InputInterface left_back_hip_velocity_;
+ InputInterface right_front_hip_velocity_;
+ InputInterface right_back_hip_velocity_;
+
+ InputInterface left_front_hip_torque_;
+ InputInterface left_back_hip_torque_;
+ InputInterface right_front_hip_torque_;
+ InputInterface right_back_hip_torque_;
+
+ InputInterface left_wheel_velocity_;
+ InputInterface right_wheel_velocity_;
+
+ InputInterface chassis_x_axis_acceleration_imu_;
+ InputInterface chassis_z_axis_acceleration_imu_;
+
+ InputInterface chassis_yaw_velocity_imu_;
+ InputInterface chassis_pitch_velocity_imu_;
+ InputInterface chassis_roll_velocity_imu_;
+
+ InputInterface chassis_yaw_angle_imu_;
+ InputInterface chassis_pitch_angle_imu_;
+ InputInterface chassis_roll_angle_imu_;
+
+ InputInterface chassis_control_velocity_;
+ InputInterface mode_;
+
+ InputInterface chassis_control_angle_;
+
+ InputInterface is_balanceless_;
+ InputInterface is_rescue_tip_over_;
+
+ InputInterface is_leg_extended_;
+ InputInterface is_jump_active_;
+ InputInterface is_climb_active_;
+
+ OutputInterface left_front_hip_control_torque_;
+ OutputInterface left_back_hip_control_torque_;
+ OutputInterface right_front_hip_control_torque_;
+ OutputInterface right_back_hip_control_torque_;
+
+ OutputInterface left_front_hip_control_angle_;
+ OutputInterface left_back_hip_control_angle_;
+ OutputInterface right_front_hip_control_angle_;
+ OutputInterface right_back_hip_control_angle_;
+
+ OutputInterface left_wheel_control_torque_;
+ OutputInterface right_wheel_control_torque_;
+
+ double desire_leg_length_, desire_roll_angle_;
+ bool levitate_active_{false}, left_levitate_active_{false}, right_levitate_active_{false};
+
+ bool about_to_fall_{false}, rescue_tip_over_{false};
+
+ bool stand_active_{false};
+ Eigen::Vector2d last_leg_length_, last_tilt_angle_;
+ Eigen::Vector2d last_dot_leg_length_, last_dot_tilt_angle_;
+ double last_distance_;
+ bool last_is_parked_{false};
+
+ Eigen::Vector3d filtered_gyro_velocity_;
+
+ VmcSolver left_leg_vmc_solver_, right_leg_vmc_solver_;
+ filter::KalmanFilter<2, 2> velocity_kalman_filter_;
+ filter::LowPassFilter<3> chassis_gyro_velocity_filter_;
+
+ DesireStateSolver desire_state_solver_;
+
+ pid::PidCalculator roll_angle_pid_calculator_, leg_length_pid_calculator_;
+ pid::PidCalculator balanceless_left_wheel_pid_calculator_,
+ balanceless_right_wheel_pid_calculator_;
+ pid::PidCalculator rescue_velocity_pid_calculator_;
+ lqr::LqrCalculator lqr_calculator_;
+};
+} // namespace rmcs_core::controller::chassis
+
+#include
+
+PLUGINLIB_EXPORT_CLASS(rmcs_core::controller::chassis::WheelLegController, rmcs_executor::Component)
\ No newline at end of file
diff --git a/rmcs_ws/src/rmcs_core/src/controller/lqr/lqr_calculator.hpp b/rmcs_ws/src/rmcs_core/src/controller/lqr/lqr_calculator.hpp
new file mode 100644
index 00000000..4df23ad4
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/controller/lqr/lqr_calculator.hpp
@@ -0,0 +1,526 @@
+#include
+#include
+#include
+
+// Due to the nonlinear nature of the wheeled leg's kinematic model, the Jacobian matrix derived
+// from its linearized kinematic equations becomes highly complex and verbose. Furthermore, since
+// leg length varies, the Jacobian matrix also changes dynamically, making conventional LQR
+// operations impractical. Therefore, calculations are first performed in MATLAB to obtain all gain
+// matrices across the entire leg length range. Subsequently, a bivariate function of the left
+// and right leg lengths is fitted to derive the gain matrix K.
+
+namespace rmcs_core::controller::lqr {
+
+class LqrCalculator {
+public:
+ LqrCalculator() = default;
+
+ auto update(double l_l, double l_r) -> Eigen::Matrix {
+ calculate_fit_gain(l_l, l_r);
+ return gain_;
+ }
+
+private:
+ void calculate_fit_gain(double l_l, double l_r) {
+ double K_result[40];
+
+ double K_result_tmp;
+ double b_K_result_tmp;
+ double c_K_result_tmp;
+ double d_K_result_tmp;
+ double e_K_result_tmp;
+ double f_K_result_tmp;
+ double t2;
+ double t3;
+ double t4;
+ double t5;
+ double t6;
+ double t7;
+
+ // Q = diag([50 1 50 5 200 1 200 1 90000 1]);
+ // R = diag([5 5 1 1]);
+ /* This function was generated by the Symbolic Math Toolbox version 24.2.
+ */
+ /* 2026-03-03 22:47:45 */
+ t2 = l_l * l_l;
+ t3 = std::pow(l_l, 3.0);
+ t5 = l_r * l_r;
+ t6 = std::pow(l_r, 3.0);
+ t4 = t2 * t2;
+ t7 = t5 * t5;
+ K_result_tmp = l_l * l_r;
+ b_K_result_tmp = l_l * t5;
+ c_K_result_tmp = l_l * t6;
+ d_K_result_tmp = l_r * t2;
+ e_K_result_tmp = l_r * t3;
+ f_K_result_tmp = t2 * t5;
+ K_result[0] =
+ (((((l_l * 58.228736048753483 + l_r * 33.103667133299957) - t2 * 247.06523961628389)
+ + t3 * 444.41379470087611)
+ - t4 * 324.04872498056068)
+ + ((((t5 * -27.627137177622231 - t6 * 137.8807600352639) + t7 * 198.21345056752861)
+ - K_result_tmp * 308.73984691592369)
+ + b_K_result_tmp * 267.96175273009129))
+ + ((((c_K_result_tmp * 9.08196319858063 + d_K_result_tmp * 770.58679400356664)
+ - e_K_result_tmp * 538.73756232870119)
+ - f_K_result_tmp * 421.81357833750042)
+ - 2.8104538078645449);
+ K_result[1] =
+ (((((l_l * 62.624920877281482 + l_r * 16.717501327576919) - t2 * 382.36985482885791)
+ + t3 * 987.79798175503208)
+ - t4 * 968.14111559165838)
+ + ((((t5 * -18.413365869447269 - t6 * 128.041787493163) + t7 * 238.80695093998889)
+ - K_result_tmp * 103.7843555941548)
+ + b_K_result_tmp * 236.733047170841))
+ + ((((c_K_result_tmp * -144.55644166273819 + d_K_result_tmp * 33.427333164975558)
+ + e_K_result_tmp * 244.5316281076085)
+ - f_K_result_tmp * 239.45277981777639)
+ - 2.1791443227497149);
+ K_result[2] =
+ (((((l_l * 45.199668912874657 - l_r * 133.37039925261709) - t2 * 313.65146568983653)
+ + t3 * 595.028744608601)
+ - t4 * 347.01549018702309)
+ + (((((t5 * 740.77330333419991 - t6 * 1364.3839191756549) + t7 * 915.878738434753)
+ + K_result_tmp * 390.32339261336381)
+ - b_K_result_tmp * 1563.3084684772871)
+ + c_K_result_tmp * 924.3439106607243))
+ + (((d_K_result_tmp * -176.90117159488489 - e_K_result_tmp * 373.82111774148268)
+ + f_K_result_tmp * 1390.897918209871)
+ + 5.2190528601972819);
+ K_result[3] =
+ ((((((l_l * -95.845777377017981 - l_r * 30.72147189942379) + t2 * 528.88006720250166)
+ - t3 * 1015.512695899405)
+ + t4 * 637.68738136157413)
+ - t5 * 66.986405633576425)
+ + ((((t6 * 315.79585515596318 - t7 * 317.57679216164792)
+ + K_result_tmp * 594.55592177587926)
+ - b_K_result_tmp * 570.44629557727546)
+ + c_K_result_tmp * 225.46417690934081))
+ + (((d_K_result_tmp * -1856.4738304888349 + e_K_result_tmp * 1858.823995736886)
+ + f_K_result_tmp * 669.80668709854081)
+ + 7.3650163595102827);
+ K_result[4] =
+ (((((l_l * 36.995257450124839 + l_r * 12.55026750009255) - t2 * 166.18810963330739)
+ + t3 * 289.4751384119557)
+ - t4 * 201.85967866740819)
+ + ((((t5 * 38.3880830401618 - t6 * 232.21550667564219) + t7 * 248.1973618059354)
+ - K_result_tmp * 215.51446488840591)
+ + b_K_result_tmp * 114.0878835202176))
+ + ((((c_K_result_tmp * 92.578360381467448 + d_K_result_tmp * 605.85647925738556)
+ - e_K_result_tmp * 441.93914615759343)
+ - f_K_result_tmp * 295.32901320048148)
+ - 0.41931959930014751);
+ K_result[5] =
+ (((((l_l * 37.961363816258071 + l_r * 1.4119265703988519) - t2 * 277.03608860455779)
+ + t3 * 775.84381877914984)
+ - t4 * 798.52454084931458)
+ + ((((t5 * 29.366151229234511 - t6 * 197.903846308387) + t7 * 278.9149347533837)
+ - K_result_tmp * 28.053992751778651)
+ + b_K_result_tmp * 114.2151588570368))
+ + ((((c_K_result_tmp * -83.965953117067727 - d_K_result_tmp * 100.92588214903731)
+ + e_K_result_tmp * 308.004300372261)
+ - f_K_result_tmp * 121.23522603912021)
+ + 0.10994337762760779);
+ K_result[6] =
+ ((((((l_l * 37.049884812542693 - l_r * 127.9672518694564) - t2 * 280.79636927197743)
+ + t3 * 524.379350516641)
+ - t4 * 278.28627740861731)
+ + t5 * 676.53633625429188)
+ + ((((t6 * -1196.8605588657031 + t7 * 776.98031728853891)
+ + K_result_tmp * 350.95942296883231)
+ - b_K_result_tmp * 1386.2516759925029)
+ + c_K_result_tmp * 748.64913428876548))
+ + (((d_K_result_tmp * -89.856077579745786 - e_K_result_tmp * 467.34126122417149)
+ + f_K_result_tmp * 1275.5281346346969)
+ + 5.99664580396614);
+ K_result[7] =
+ (((((l_l * -89.670509769918851 - l_r * 35.999895350734519) + t2 * 451.2549383184982)
+ - t3 * 790.71019195217275)
+ + t4 * 437.51663627405679)
+ + ((((t5 * -32.575128085752183 + t6 * 220.565490077677) - t7 * 215.19516337547549)
+ + K_result_tmp * 545.396199463892)
+ - b_K_result_tmp * 501.3084186236652))
+ + ((((c_K_result_tmp * 144.38244592692541 - d_K_result_tmp * 1641.35022798624)
+ + e_K_result_tmp * 1600.5632133735221)
+ + f_K_result_tmp * 606.79421501970444)
+ + 7.9317062154928024);
+ K_result[8] =
+ ((((((l_l * 51.577407150102523 - l_r * 39.3979818534695) - t2 * 315.24055484465413)
+ + t3 * 776.840507806621)
+ - t4 * 758.16765518070565)
+ + t5 * 314.13344756914091)
+ + ((((t6 * -594.63991424554888 + t7 * 270.276717903546)
+ - K_result_tmp * 21.814210226192682)
+ - b_K_result_tmp * 342.088733888716)
+ + c_K_result_tmp * 735.57044414981783))
+ + (((d_K_result_tmp * 159.90362783204969 + e_K_result_tmp * 206.5364740808115)
+ - f_K_result_tmp * 413.15127752314078)
+ - 2.4068995511497668);
+ K_result[9] =
+ (((((l_l * 20.53656644858567 - l_r * 42.864693352418627) - t2 * 290.52665870270249)
+ + t3 * 920.26748845973259)
+ - t4 * 958.10370351267773)
+ + ((((t5 * 162.0390556234299 - t6 * 308.9714037111238) + t7 * 134.48258846522381)
+ + K_result_tmp * 242.7556195917528)
+ - b_K_result_tmp * 217.46356464069859))
+ + ((((c_K_result_tmp * 459.94067595358928 - d_K_result_tmp * 707.64660802859112)
+ + e_K_result_tmp * 941.92423932619306)
+ - f_K_result_tmp * 259.02918572370652)
+ + 2.78567303171074);
+ K_result[10] =
+ (((((l_l * 6.5775853010830216 - l_r * 51.541606325315783) - t2 * 113.0407397213974)
+ + t3 * 212.55103804137309)
+ - t4 * 132.60947514989519)
+ + ((((t5 * -100.70935280893291 + t6 * 965.39604273081454) - t7 * 1132.1522757852181)
+ + K_result_tmp * 433.72380392092509)
+ - b_K_result_tmp * 941.840457189477))
+ + ((((c_K_result_tmp * 342.61790204450239 - d_K_result_tmp * 746.7076972112917)
+ + e_K_result_tmp * 599.50732396263356)
+ + f_K_result_tmp * 775.65595411408913)
+ - 1.04591027895466);
+ K_result[11] =
+ (((((l_l * -1.699054416577543 + l_r * 8.7738985827291334) + t2 * 264.96477569735208)
+ - t3 * 1412.226886309782)
+ + t4 * 1912.3056541742969)
+ + ((((t5 * -159.91699240473679 + t6 * 357.074092707535) - t7 * 135.4729368979817)
+ - K_result_tmp * 132.51931828305641)
+ + b_K_result_tmp * 527.773327732422))
+ + ((((c_K_result_tmp * -995.85853612360154 + d_K_result_tmp * 655.16017084696375)
+ - e_K_result_tmp * 1162.7801501513411)
+ + f_K_result_tmp * 174.073223265881)
+ + 3.4807002393381361);
+ K_result[12] =
+ (((((l_l * 19.2912855880038 - l_r * 15.59006591047687) - t2 * 126.4063075263532)
+ + t3 * 316.0008493924114)
+ - t4 * 305.8852073042824)
+ + ((((t5 * 122.5024065897455 - t6 * 221.77960111450659) + t7 * 97.494722786072813)
+ + K_result_tmp * 1.538105880685477)
+ - b_K_result_tmp * 168.11633504451751))
+ + ((((c_K_result_tmp * 289.7307432078648 + d_K_result_tmp * 62.856368980169449)
+ + e_K_result_tmp * 56.819665366943717)
+ - f_K_result_tmp * 116.4321822117082)
+ - 0.89211028040034157);
+ K_result[13] =
+ (((((l_l * 7.7338960323159478 - l_r * 15.671016038549251) - t2 * 113.5531545626657)
+ + t3 * 369.716857917176)
+ - t4 * 402.223599839497)
+ + ((((t5 * 60.452358518621921 - t6 * 116.11855770874919) + t7 * 40.857521200214073)
+ + K_result_tmp * 93.424925224177514)
+ - b_K_result_tmp * 75.656746031420781))
+ + ((((c_K_result_tmp * 215.49146103404269 - d_K_result_tmp * 297.20004298969769)
+ + e_K_result_tmp * 444.1622228587579)
+ - f_K_result_tmp * 170.3452803115739)
+ + 1.05196229835372);
+ K_result[14] =
+ (((((l_l * 0.72266573134172729 - l_r * 20.918303791295308) - t2 * 55.480524588133029)
+ + t3 * 138.072245511404)
+ - t4 * 101.86482279644611)
+ + ((((t5 * -48.596671037258027 + t6 * 379.32706837522761) - t7 * 395.47803470363169)
+ + K_result_tmp * 193.27642427620231)
+ - b_K_result_tmp * 355.66080710707479))
+ + ((((c_K_result_tmp * 0.62759137639297491 - d_K_result_tmp * 354.79293483631159)
+ + e_K_result_tmp * 213.58411514631521)
+ + f_K_result_tmp * 443.9628188463189)
+ - 0.248606035620003);
+ K_result[15] =
+ (((((l_l * -2.331523503866241 + l_r * 6.6872525523815787) + t2 * 121.02435807687669)
+ - t3 * 569.36243639593363)
+ + t4 * 722.62972804903427)
+ + ((((t5 * -70.181960935253926 + t6 * 130.431896393743) - t7 * 39.907907631632021)
+ - K_result_tmp * 57.960278978475458)
+ + b_K_result_tmp * 260.55692671847851))
+ + ((((c_K_result_tmp * -377.53453789617828 + d_K_result_tmp * 202.8183718739219)
+ - e_K_result_tmp * 317.72759627626817)
+ - f_K_result_tmp * 33.729455672491078)
+ + 1.25525610407707);
+ K_result[16] =
+ ((((((l_l * 148.34175476383379 + l_r * 207.966709996097) - t2 * 698.76655224496278)
+ + t3 * 1695.5704254763209)
+ - t4 * 1411.84685332747)
+ - t5 * 975.40666122237667)
+ + (((((t6 * 1043.498995320381 + t7 * 175.079867115138)
+ - K_result_tmp * 1240.1043098701821)
+ + b_K_result_tmp * 3444.33705956047)
+ - c_K_result_tmp * 3571.28772073013)
+ + d_K_result_tmp * 2394.0408240941761))
+ + ((e_K_result_tmp * -2629.5266782767371 - f_K_result_tmp * 1248.888310453209)
+ - 19.644085951107851);
+ K_result[17] =
+ ((((((l_l * 135.90705046483151 + l_r * 273.90781157732431) - t2 * 405.97709893626148)
+ + t3 * 149.74006691267979)
+ + t4 * 154.9397619975629)
+ - t5 * 1378.4705609299481)
+ + (((((t6 * 3751.0276638740052 - t7 * 3323.697684130791)
+ - K_result_tmp * 1284.6253574316361)
+ - b_K_result_tmp * 220.18713945411631)
+ + c_K_result_tmp * 286.74364972226329)
+ + d_K_result_tmp * 5193.6228202681459))
+ + ((e_K_result_tmp * -4761.1931848640106 - f_K_result_tmp * 1216.918862322118)
+ - 18.896681717238739);
+ K_result[18] =
+ ((((((l_l * 273.23831876652758 - l_r * 518.917376160053) - t2 * 1572.962249519197)
+ + t3 * 3980.2168396595121)
+ - t4 * 3004.6122541564418)
+ + t5 * 4965.73427092819)
+ + (((((t6 * -14741.573790620891 + t7 * 14133.648444324641)
+ - K_result_tmp * 502.98807241679049)
+ - b_K_result_tmp * 441.91513506961172)
+ - c_K_result_tmp * 55.455202164980392)
+ + d_K_result_tmp * 1019.426228953821))
+ + ((e_K_result_tmp * -3485.6317899744381 + f_K_result_tmp * 3758.6001897489359)
+ + 14.75198405409205);
+ K_result[19] =
+ ((((((l_l * -128.19104048905959 - l_r * 262.02437944578412) + t2 * 616.59418773734342)
+ - t3 * 1274.5843627789941)
+ + t4 * 566.2486480392057)
+ + t5 * 974.03462520892379)
+ + (((((t6 * -640.48292692163488 - t7 * 900.26435000822664)
+ + K_result_tmp * 2099.585097699633)
+ - b_K_result_tmp * 5817.7929448079094)
+ + c_K_result_tmp * 5942.5041818988557)
+ - d_K_result_tmp * 4892.6178941754479))
+ + ((e_K_result_tmp * 5937.489393094 + f_K_result_tmp * 2433.1132836876968)
+ + 9.11731354085421);
+ K_result[20] =
+ (((((l_l * -19.90536935447378 - l_r * 40.532760516244842) + t2 * 81.604031621603269)
+ - t3 * 151.37439331904639)
+ + t4 * 144.32877449311289)
+ + ((((t5 * 241.61830850037961 - t6 * 665.29081165091316) + t7 * 633.18882127968516)
+ + K_result_tmp * 127.41876701174731)
+ - b_K_result_tmp * 185.24680601851949))
+ + ((((c_K_result_tmp * 44.095805713565021 - d_K_result_tmp * 346.7031922528239)
+ + e_K_result_tmp * 160.19740081318071)
+ + f_K_result_tmp * 388.309686918287)
+ + 2.4932655339066052);
+ K_result[21] =
+ (((((l_l * -32.214181421295287 - l_r * 23.308786682921031) + t2 * 185.0763194887142)
+ - t3 * 505.95319317668748)
+ + t4 * 533.47570202008626)
+ + (((((t5 * 104.3246217330357 - t6 * 218.4421672747099) + t7 * 233.98704660058269)
+ + K_result_tmp * 132.54057020867751)
+ - b_K_result_tmp * 360.13213865101221)
+ + c_K_result_tmp * 33.58601331476914))
+ + (((d_K_result_tmp * -168.6869130806526 - e_K_result_tmp * 139.39264624224671)
+ + f_K_result_tmp * 607.404956085748)
+ + 2.528939097759423);
+ K_result[22] =
+ ((((((l_l * -73.468680020839116 - l_r * 139.34719164846661) + t2 * 281.736456883201)
+ - t3 * 478.52406016941569)
+ + t4 * 298.45508145958718)
+ + t5 * 692.83625731199515)
+ + ((((t6 * -1431.112907187374 + t7 * 1009.673299590109)
+ + K_result_tmp * 367.46261021059269)
+ - b_K_result_tmp * 638.0149212210415)
+ + c_K_result_tmp * 621.89419781089612))
+ + (((d_K_result_tmp * -799.32112713913023 + e_K_result_tmp * 690.14700424549346)
+ + f_K_result_tmp * 327.28002956982237)
+ + 11.67481120676139);
+ K_result[23] =
+ ((((((l_l * -144.16533642481309 - l_r * 58.879774411056488) + t2 * 759.27555784601373)
+ - t3 * 1816.21341478931)
+ + t4 * 1564.076299085546)
+ + t5 * 166.95024865353)
+ + ((((t6 * -107.70643089018169 - t7 * 51.36552097461248)
+ + K_result_tmp * 336.12313235616818)
+ - b_K_result_tmp * 868.30625683240578)
+ + c_K_result_tmp * 604.29562610538073))
+ + (((d_K_result_tmp * -344.23089952144568 + e_K_result_tmp * 144.84086144542289)
+ + f_K_result_tmp * 477.227902261538)
+ + 10.56958501924821);
+ K_result[24] =
+ ((((((l_l * 265.92356991359048 + l_r * 136.09665232173481) - t2 * 1676.729242505729)
+ + t3 * 4177.3922622201426)
+ - t4 * 3563.777583564342)
+ - t5 * 383.3162869772047)
+ + (((((t6 * 167.0093233847455 + t7 * 704.18911191583243)
+ - K_result_tmp * 1030.4982078401561)
+ + b_K_result_tmp * 1625.876432697112)
+ - c_K_result_tmp * 2654.1201517656409)
+ + d_K_result_tmp * 3139.4410998713938))
+ + ((e_K_result_tmp * -3989.2775805431788 + f_K_result_tmp * 265.8032322914143)
+ - 17.923123361129861);
+ K_result[25] =
+ ((((((l_l * 165.87822406236671 + l_r * 188.8827987290156) - t2 * 1038.4030316983651)
+ + t3 * 2565.381122099755)
+ - t4 * 3031.0179590773441)
+ - t5 * 957.79178550515007)
+ + (((((t6 * 2849.6080286297711 - t7 * 2982.9710578479048)
+ - K_result_tmp * 773.42273491360527)
+ - b_K_result_tmp * 630.88959816365946)
+ + c_K_result_tmp * 2387.1248505329791)
+ + d_K_result_tmp * 3048.1413011036661))
+ + ((e_K_result_tmp * -278.9963681705795 - f_K_result_tmp * 3748.5146705300481)
+ - 20.506023237161351);
+ K_result[26] =
+ ((((((l_l * 282.62473563390739 - l_r * 617.48046578412777) - t2 * 2386.5855580423458)
+ + t3 * 6709.3111107593859)
+ - t4 * 5236.2906273195094)
+ + t5 * 4708.7644185571926)
+ + (((((t6 * -13898.332538807679 + t7 * 14889.466801418081)
+ + K_result_tmp * 721.99825562849458)
+ - b_K_result_tmp * 1316.4094776487709)
+ - c_K_result_tmp * 5262.4974339808687)
+ - d_K_result_tmp * 1651.064775755201))
+ + ((e_K_result_tmp * -4624.4550983701884 + f_K_result_tmp * 11223.540750787581)
+ + 8.1706330027919183);
+ K_result[27] =
+ ((((((l_l * -220.650798687078 - l_r * 175.38156831070481) + t2 * 1360.5068272525559)
+ - t3 * 2632.26488635414)
+ + t4 * 720.0924719059991)
+ + t5 * 536.88397023607752)
+ + ((((t6 * -423.49261769393689 - t7 * 728.0263366591447)
+ + K_result_tmp * 2195.7595651091551)
+ - b_K_result_tmp * 3435.299526884075)
+ + c_K_result_tmp * 5248.2926984051473))
+ + (((d_K_result_tmp * -7535.5864386627954 + e_K_result_tmp * 11599.91573885726)
+ - f_K_result_tmp * 1292.485937994757)
+ + 21.337946464281561);
+ K_result[28] =
+ (((((l_l * -19.63601091357544 - l_r * 38.197419113076229) + t2 * 61.25944368117969)
+ - t3 * 94.764418769428488)
+ + t4 * 106.68241200576929)
+ + ((((t5 * 216.6566111166218 - t6 * 583.75878541510008) + t7 * 603.82942328913771)
+ + K_result_tmp * 158.4677397753108)
+ - b_K_result_tmp * 269.41809070013483))
+ + ((((c_K_result_tmp * -49.145228754568173 - d_K_result_tmp * 343.099189196269)
+ + e_K_result_tmp * 41.455352393830381)
+ + f_K_result_tmp * 601.65635603759)
+ + 2.6580022116202762);
+ K_result[29] =
+ (((((l_l * -34.899831297204813 - l_r * 22.833521448530639) + t2 * 197.58111728289111)
+ - t3 * 507.53181338454459)
+ + t4 * 471.40930444158022)
+ + (((((t5 * 105.5585473773459 - t6 * 224.5158205089468) + t7 * 231.3280436597895)
+ + K_result_tmp * 129.60745144512859)
+ - b_K_result_tmp * 347.18618487867428)
+ + c_K_result_tmp * 114.9359670095277))
+ + (((d_K_result_tmp * -220.982423727273 + e_K_result_tmp * 73.556995823672551)
+ + f_K_result_tmp * 433.04704515253837)
+ + 2.3509210386938451);
+ K_result[30] =
+ ((((((l_l * -70.004947841126011 - l_r * 137.8045609465353) + t2 * 234.10435704022831)
+ - t3 * 298.32598635163492)
+ + t4 * 129.93927394490959)
+ + t5 * 721.32140071126059)
+ + ((((t6 * -1708.441474902465 + t7 * 1484.12177067571)
+ + K_result_tmp * 332.97797958092139)
+ - b_K_result_tmp * 372.81925826393581)
+ + c_K_result_tmp * 5.9600249777896481))
+ + (((d_K_result_tmp * -815.75133504851215 + e_K_result_tmp * 501.17087290251811)
+ + f_K_result_tmp * 665.12823990358788)
+ + 10.863104904686979);
+ K_result[31] =
+ ((((((l_l * -150.24802711608129 - l_r * 59.002410970125922) + t2 * 740.89284784982817)
+ - t3 * 1526.468364312283)
+ + t4 * 1062.146810634383)
+ + t5 * 178.89130062553789)
+ + ((((t6 * -187.92005550723621 - t7 * 4.7484201834860871)
+ + K_result_tmp * 410.61859771781872)
+ - b_K_result_tmp * 856.9640995451922)
+ + c_K_result_tmp * 864.98230598495286))
+ + (((d_K_result_tmp * -774.34580016866073 + e_K_result_tmp * 926.84249789950582)
+ + f_K_result_tmp * 123.5347637461572)
+ + 11.40440724817717);
+ K_result[32] =
+ ((((((l_l * 736.876257179423 + l_r * 24.57871704269186) - t2 * 1712.368318318677)
+ - t3 * 2016.708865159032)
+ + t4 * 5366.706755588727)
+ - t5 * 556.96187658861686)
+ + (((((t6 * 4953.7655203150989 - t7 * 4158.0534193163157)
+ - K_result_tmp * 6350.6691451684956)
+ - b_K_result_tmp * 1296.840720394365)
+ - c_K_result_tmp * 505.08151469443311)
+ + d_K_result_tmp * 24976.367954435271))
+ + ((e_K_result_tmp * -24697.680856733328 - f_K_result_tmp * 1537.158407461053)
+ - 38.221087328987451);
+ K_result[33] =
+ ((((((l_l * -111.7465678538375 + l_r * 763.4600655332946) - t2 * 2344.1945611221909)
+ + t3 * 15367.220037727569)
+ - t4 * 23985.566072400321)
+ - t5 * 334.55505448680282)
+ + (((((t6 * -2388.80358474881 - t7 * 2071.0627607914389)
+ - K_result_tmp * 4574.5811282504856)
+ + b_K_result_tmp * 14976.275505038881)
+ + c_K_result_tmp * 13135.09010058618)
+ - d_K_result_tmp * 6735.1311526055433))
+ + ((e_K_result_tmp * 33325.191621329432 - f_K_result_tmp * 39302.43020772918)
+ - 36.182644324436573);
+ K_result[34] =
+ ((((((l_l * 2420.503318815789 + l_r * 6143.8944379083869) - t2 * 9280.24016315598)
+ + t3 * 14298.01216915547)
+ - t4 * 5802.1046953095693)
+ - t5 * 35386.999984935523)
+ + (((((t6 * 76545.214712403555 - t7 * 51108.20121957302)
+ - K_result_tmp * 13291.407167858129)
+ + b_K_result_tmp * 16546.96365316361)
+ - c_K_result_tmp * 40641.614259948306)
+ + d_K_result_tmp * 36146.3207118627))
+ + ((e_K_result_tmp * -49036.879471330627 + f_K_result_tmp * 22072.777873299241)
+ - 251.3196049645417);
+ K_result[35] =
+ ((((((l_l * 7454.5610907465971 + l_r * 395.83294707917258) - t2 * 51192.718548341429)
+ + t3 * 141603.7524464239)
+ - t4 * 138922.9760037543)
+ + t5 * 4489.5515450181392)
+ + (((((t6 * -23705.802238864639 + t7 * 23523.85335666623)
+ - K_result_tmp * 5230.4462406190878)
+ + b_K_result_tmp * 19054.785103179471)
+ + c_K_result_tmp * 2709.6044186886079)
+ - d_K_result_tmp * 13172.699200378371))
+ + ((e_K_result_tmp * 37692.776031145462 - f_K_result_tmp * 27616.302386032919)
+ - 222.48855189921889);
+ K_result[36] =
+ ((((((l_l * 131.41777137182311 + l_r * 196.94458882445139) - t2 * 516.06737949077467)
+ + t3 * 853.110221233211)
+ - t4 * 661.54984473162676)
+ - t5 * 1073.025086126939)
+ + ((((t6 * 2729.0504665161111 - t7 * 2457.65720047848)
+ - K_result_tmp * 888.28592169532919)
+ + b_K_result_tmp * 1091.394886173809)
+ - c_K_result_tmp * 477.3105776775206))
+ + (((d_K_result_tmp * 2526.5521720645388 - e_K_result_tmp * 1765.359149347313)
+ - f_K_result_tmp * 1816.875247340846)
+ - 14.61403178775773);
+ K_result[37] =
+ ((((((l_l * 180.51178326205991 + l_r * 132.1858833559553) - t2 * 1083.3885780721059)
+ + t3 * 2997.962279988516)
+ - t4 * 3188.066777707395)
+ - t5 * 507.97379577084808)
+ + (((((t6 * 1017.850835039008 - t7 * 1089.332095938563)
+ - K_result_tmp * 748.44306920015936)
+ + b_K_result_tmp * 1605.1230885767461)
+ + c_K_result_tmp * 148.16348316990781)
+ + d_K_result_tmp * 1116.17529547479))
+ + ((e_K_result_tmp * 624.031731806414 - f_K_result_tmp * 3151.285188018489)
+ - 14.01007950341004);
+ K_result[38] =
+ ((((((l_l * 411.43812374517017 + l_r * 588.46790950689967) - t2 * 1693.6615335326451)
+ + t3 * 2973.9705447492761)
+ - t4 * 1775.724544927338)
+ - t5 * 2711.51493725362)
+ + (((((t6 * 5470.16624803537 - t7 * 3612.7035410009039)
+ - K_result_tmp * 1771.438581005081)
+ + b_K_result_tmp * 2401.6127061675152)
+ - c_K_result_tmp * 3173.0482147419671)
+ + d_K_result_tmp * 4388.3635019216472))
+ + ((e_K_result_tmp * -4752.7051042806 + f_K_result_tmp * 17.11838297076914)
+ - 50.250386522225263);
+ K_result[39] =
+ ((((((l_l * 731.44526909753881 + l_r * 191.83506761012259) - t2 * 4150.7971047625942)
+ + t3 * 10424.458049645669)
+ - t4 * 9459.4121383535385)
+ - t5 * 402.47966125676709)
+ + ((((t6 * -256.33154253826632 + t7 * 705.78299551337511)
+ - K_result_tmp * 1064.344092768267)
+ + b_K_result_tmp * 3146.3426354255312)
+ - c_K_result_tmp * 1733.633436063812))
+ + (((d_K_result_tmp * 254.611995259114 + e_K_result_tmp * 1224.48570331033)
+ - f_K_result_tmp * 2305.3502994921878)
+ - 46.54601482144038);
+
+ gain_ = Eigen::Map>(K_result);
+ }
+
+ Eigen::Matrix gain_;
+};
+} // namespace rmcs_core::controller::lqr
diff --git a/rmcs_ws/src/rmcs_core/src/controller/lqr/lqr_controller.hpp b/rmcs_ws/src/rmcs_core/src/controller/lqr/lqr_controller.hpp
new file mode 100644
index 00000000..15f9ea88
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/controller/lqr/lqr_controller.hpp
@@ -0,0 +1,183 @@
+
+#include
+#include
+#include
+// #include
+#include
+#include
+
+// https://github.com/giacomo-b/CppRobotics/blob/e2b5cdad0ea3f982be2c310cf4d9023d347f97a4/include/robotics/linear_control/lqr.hpp
+
+namespace rmcs_core::controller::lqr {
+
+template
+using ColumnVector = Eigen::Matrix;
+
+template
+using SquareMatrix = Eigen::Matrix;
+
+template
+using Matrix = Eigen::Matrix;
+
+double deg2rad(double deg) { return deg * M_PI / 180.0; }
+
+class NormalDistributionRandomGenerator {
+public:
+ template
+ ColumnVector GetColumnVector() {
+ ColumnVector rand_vector;
+
+ for (auto i = 0; i < rand_vector.size(); i++)
+ rand_vector(i) = distribution(generator);
+
+ return rand_vector;
+ }
+
+private:
+ std::default_random_engine generator;
+ std::normal_distribution distribution{0.0, 1.0};
+};
+
+template
+class LqrController {
+ static_assert(StateSize > 0);
+ static_assert(InputSize > 0);
+
+ using VectorNx1 = ColumnVector;
+ using VectorMx1 = ColumnVector;
+
+ using MatrixNxN = SquareMatrix;
+ using MatrixMxM = SquareMatrix;
+ using InputMatrix = Matrix;
+ using GainMatrix = Matrix;
+
+ using State = VectorNx1;
+
+public:
+ /**
+ * @brief Creates a new Linear-Quadratic Regulator
+ * @param A state matrix
+ * @param B control matrix
+ * @param Q state weights matrix
+ * @param R control weights matrix
+ * @todo pass a linear system as input
+ */
+ LqrController(MatrixNxN A, InputMatrix B, MatrixNxN Q, MatrixMxM R)
+ : A(A)
+ , B(B)
+ , Q(Q)
+ , R(R)
+ , K(ComputeGain()) {}
+
+ /**
+ * @brief Solves the LQR problem
+ * @param initial the initial state
+ * @param target the target state
+ * @return a vector containing the state along the whole path
+ * @todo this behavior should be in a user-defined loop, refactor so that only one step is
+ * carried out at a time
+ */
+ std::vector Solve(State initial, State target, double dt) {
+ std::vector path{initial};
+ path.reserve(
+ (unsigned int)std::round(max_time / dt) + 1); // TODO: currently assuming the worst case
+
+ State x = initial - target;
+ VectorMx1 u;
+
+ bool path_found = false;
+ double time = 0;
+ double goal_dist_squared = std::pow(goal_dist, 2);
+
+ while (time < max_time) {
+ time += dt;
+
+ u = Input(x);
+ x = A * x + B * u;
+
+ path.push_back(x + target);
+
+ if (x.squaredNorm() <= goal_dist_squared) {
+ path_found = true;
+ break;
+ }
+ }
+
+ if (!path_found) {
+ std::cerr << "Couldn't find a path\n";
+ return {};
+ }
+
+ return path;
+ }
+
+ /**
+ * @brief Sets the maximum simulation time
+ * @param limit time limit
+ * @todo remove
+ */
+ void SetTimeLimit(double limit) { max_time = limit; }
+
+ /**
+ * @brief Sets the tolerance w.r.t. the target
+ * @param tol tolerance
+ * @todo remove, the user should decide when to exit
+ */
+ void SetFinalPositionTolerance(double tol) { goal_dist = tol; }
+
+ /**
+ * @brief Sets the maximum number of iterations
+ * @param limit maximum number of iterations
+ * @todo remove, the user should decide when to exit
+ */
+ void SetIterationsLimit(int limit) { max_iter = limit; }
+
+ /**
+ * @brief Sets the tolerance for the resolution of the Discrete Algebraic Riccati Equation
+ * @param tol tolerance
+ */
+ void SetDARETolerance(double tol) { eps = tol; }
+
+private:
+ VectorMx1 Input(State x) const { return -K * x; }
+
+ /**
+ * @brief Computes the LQR problem gain matrix
+ * @return
+ */
+ GainMatrix ComputeGain() {
+ MatrixNxN X = SolveDARE();
+ return (B.transpose() * X * B + R).inverse() * (B.transpose() * X * A);
+ }
+
+ /**
+ * @brief Solves a discrete-time algebraic Riccati equation
+ * @return
+ */
+ MatrixNxN SolveDARE() const {
+ MatrixNxN X = Q, Xn = Q;
+ for (auto _ = 0; _ < max_iter; _++) {
+ Xn = A.transpose() * X * A
+ - A.transpose() * X * B * (R + B.transpose() * X * B).inverse() * B.transpose() * X
+ * A
+ + Q;
+ if ((Xn - X).template lpNorm() < eps)
+ break;
+ X = Xn;
+ }
+ return Xn;
+ }
+
+ MatrixNxN A;
+ InputMatrix B;
+ MatrixNxN Q;
+ MatrixMxM R;
+ GainMatrix K;
+
+ double max_time{100.0};
+ double goal_dist{0.1};
+ int max_iter{10};
+ double eps{0.01};
+};
+
+} // namespace rmcs_core::controller::lqr
\ No newline at end of file
diff --git a/rmcs_ws/src/rmcs_core/src/filter/kalman_filter.hpp b/rmcs_ws/src/rmcs_core/src/filter/kalman_filter.hpp
new file mode 100644
index 00000000..148069ea
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/filter/kalman_filter.hpp
@@ -0,0 +1,98 @@
+#pragma once
+
+#include
+
+#include
+
+namespace rmcs_core::filter {
+
+template
+using Matrix = Eigen::Matrix;
+
+template
+class KalmanFilter {
+ using State = Eigen::Vector;
+ using Measure = Eigen::Vector;
+ using Control = Eigen::Vector;
+
+public:
+ explicit KalmanFilter(
+ const Matrix& A, const Matrix& H, const Matrix& Q,
+ const Matrix& R)
+ : measurement_noise_covariance_(R)
+ , state_transition_(A)
+ , process_noise_covariance_(Q)
+ , measurement_transition_(H) {
+ reset();
+
+ set_input_transition();
+ set_error_covariance();
+ set_process_noise_transition();
+ set_measurement_noise_transition();
+ }
+
+ void reset() {
+ prior_estimate_ = State::Zero();
+ posterior_estimate_ = State::Zero();
+
+ kalman_gain_ = Matrix::Zero();
+ }
+
+ void set_initial_state(const State& initial_state) { posterior_estimate_ = initial_state; }
+
+ void set_input_transition(const Matrix& B = Matrix::Zero()) {
+ input_transition_ = B;
+ }
+
+ void set_error_covariance(const Matrix& P = 1000. * Matrix::Identity()) {
+ posterior_error_covariance_ = P;
+ }
+
+ void set_process_noise_transition(const Matrix& W = Matrix::Identity()) {
+ process_noise_transition_ = W;
+ }
+
+ void set_measurement_noise_transition(const Matrix& V = Matrix::Identity()) {
+ measurement_noise_transition_ = V;
+ }
+
+ State update(const Measure& measurement, const Control& control_vector = Control::Zero()) {
+ // Prediction
+ prior_estimate_ =
+ state_transition_ * posterior_estimate_ + input_transition_ * control_vector;
+ prior_error_covariance_ =
+ state_transition_ * posterior_error_covariance_ * state_transition_.transpose()
+ + process_noise_transition_ * process_noise_covariance_
+ * process_noise_transition_.transpose();
+
+ // Correction
+ kalman_gain_ = prior_error_covariance_ * measurement_transition_.transpose()
+ * (measurement_transition_ * prior_error_covariance_
+ * measurement_transition_.transpose()
+ + measurement_noise_transition_ * measurement_noise_covariance_
+ * measurement_noise_transition_.transpose())
+ .inverse();
+ prior_estimate_ += kalman_gain_ * (measurement - measurement_transition_ * prior_estimate_);
+
+ // Update
+ posterior_error_covariance_ =
+ (Matrix::Identity() - kalman_gain_ * measurement_transition_)
+ * prior_error_covariance_;
+ posterior_estimate_ = prior_estimate_;
+
+ return posterior_estimate_;
+ }
+
+private:
+ State prior_estimate_, posterior_estimate_;
+
+ Matrix measurement_noise_transition_, measurement_noise_covariance_;
+ Matrix state_transition_, process_noise_transition_, process_noise_covariance_,
+ prior_error_covariance_, posterior_error_covariance_;
+
+ Matrix kalman_gain_;
+ Matrix input_transition_;
+
+ Matrix measurement_transition_;
+};
+} // namespace rmcs_core::filter
\ No newline at end of file
diff --git a/rmcs_ws/src/rmcs_core/src/hardware/device/dm_motor.hpp b/rmcs_ws/src/rmcs_core/src/hardware/device/dm_motor.hpp
new file mode 100644
index 00000000..df151b66
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/hardware/device/dm_motor.hpp
@@ -0,0 +1,114 @@
+#include
+#include
+
+#include "librmcs/device/dm_motor.hpp"
+
+namespace rmcs_core::hardware::device {
+
+class DmMotor : public librmcs::device::DmMotor {
+public:
+ DmMotor(
+ rmcs_executor::Component& status_component, rmcs_executor::Component& command_component,
+ const std::string& name_prefix) {
+ status_component.register_output(name_prefix + "/angle", angle_, 0.0);
+ status_component.register_output(name_prefix + "/velocity", velocity_, 0.0);
+ status_component.register_output(name_prefix + "/torque", torque_, 0.0);
+ status_component.register_output(name_prefix + "/max_torque", max_torque_, 0.0);
+
+ command_component.register_input(name_prefix + "/control_angle", control_angle_, false);
+ command_component.register_input(name_prefix + "/control_torque", control_torque_, false);
+ command_component.register_input(
+ name_prefix + "/control_velocity", control_velocity_, false);
+ }
+
+ DmMotor(
+ rmcs_executor::Component& status_component, rmcs_executor::Component& command_component,
+ const std::string& name_prefix, const Config& config)
+ : DmMotor(status_component, command_component, name_prefix) {
+ configure(config);
+ }
+
+ void configure(const Config& config) {
+ librmcs::device::DmMotor::configure(config);
+
+ *max_torque_ = max_torque();
+ }
+
+ void update_status() {
+ librmcs::device::DmMotor::update_status();
+ *angle_ = angle();
+ *velocity_ = velocity();
+ *torque_ = torque();
+
+ motor_state_ = error_state();
+ }
+
+ double control_torque() {
+ if (control_torque_.ready()) [[likely]]
+ return *control_torque_;
+ else
+ return std::numeric_limits::quiet_NaN();
+ }
+
+ double control_angle() {
+ if (control_angle_.ready()) [[likely]]
+ return *control_angle_;
+ else
+ return std::numeric_limits::quiet_NaN();
+ }
+
+ constexpr static uint64_t generate_enable_command() {
+ return librmcs::device::DmMotor::generate_enable_command();
+ }
+
+ constexpr static uint64_t generate_clear_error_command() {
+ return librmcs::device::DmMotor::generate_clear_error_command();
+ }
+
+ uint64_t generate_disable_command() {
+ return librmcs::device::DmMotor::generate_disable_command();
+ }
+
+ uint64_t generate_torque_command(double control_torque) {
+ return librmcs::device::DmMotor::generate_torque_command(control_torque);
+ }
+
+ uint64_t generate_angle_command() {
+ if (motor_state_ == 0) {
+ return generate_enable_command();
+ } else if (motor_state_ == 1) {
+ return librmcs::device::DmMotor::generate_angle_command(control_angle());
+ } else {
+ return generate_clear_error_command();
+ }
+ }
+
+ uint64_t generate_command() {
+ if (motor_state_ == 0) {
+ return generate_enable_command();
+ } else if (motor_state_ == 1) {
+ // if (!std::isnan(control_angle())) {
+ // return generate_angle_command();
+ // } else if (!std::isnan(control_torque())) {
+ // return generate_torque_command(control_torque());
+ // }
+
+ return generate_torque_command(control_torque());
+ } else {
+ return generate_clear_error_command();
+ }
+ }
+
+private:
+ rmcs_executor::Component::OutputInterface angle_;
+ rmcs_executor::Component::OutputInterface velocity_;
+ rmcs_executor::Component::OutputInterface torque_;
+ rmcs_executor::Component::OutputInterface max_torque_;
+
+ rmcs_executor::Component::InputInterface control_angle_;
+ rmcs_executor::Component::InputInterface control_velocity_;
+ rmcs_executor::Component::InputInterface control_torque_;
+
+ uint8_t motor_state_;
+};
+} // namespace rmcs_core::hardware::device
\ No newline at end of file
diff --git a/rmcs_ws/src/rmcs_core/src/hardware/wheelleg-infantry.cpp b/rmcs_ws/src/rmcs_core/src/hardware/wheelleg-infantry.cpp
new file mode 100644
index 00000000..86eafda2
--- /dev/null
+++ b/rmcs_ws/src/rmcs_core/src/hardware/wheelleg-infantry.cpp
@@ -0,0 +1,462 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "hardware/device/bmi088.hpp"
+#include "hardware/device/dji_motor.hpp"
+#include "hardware/device/dm_motor.hpp"
+#include "hardware/device/dr16.hpp"
+#include "hardware/device/lk_motor.hpp"
+
+namespace rmcs_core::hardware {
+class WheelLegInfantry
+ : public rmcs_executor::Component
+ , public rclcpp::Node {
+public:
+ WheelLegInfantry()
+ : Node{get_component_name(), rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)}
+ , logger_(get_logger())
+ , infantry_command_(
+ create_partner_component(
+ get_component_name() + "_command", *this)) {
+
+ // top_board_ = std::make_unique(
+ // *this, *infantry_command_,
+ // static_cast(get_parameter("usb_pid_top_board").as_int()));
+ bottom_board_ = std::make_unique(
+ *this, *infantry_command_,
+ static_cast(get_parameter("usb_pid_bottom_board").as_int()));
+
+ gimbal_calibrate_subscription_ = create_subscription(
+ "/gimbal/calibrate", rclcpp::QoS{0}, [this](std_msgs::msg::Int32::UniquePtr&& msg) {
+ gimbal_calibrate_subscription_callback(std::move(msg));
+ });
+ chassis_hips_calibrate_subscription_ = create_subscription(
+ "/chassis/hip/calibrate", rclcpp::QoS{0},
+ [this](std_msgs::msg::Int32::UniquePtr&& msg) {
+ chassis_hip_calibrate_subscription_callback(std::move(msg));
+ });
+ }
+
+ void update() override { bottom_board_->update(); }
+
+ void command_update() { bottom_board_->command_update(); }
+
+private:
+ void gimbal_calibrate_subscription_callback(std_msgs::msg::Int32::UniquePtr) {
+ // RCLCPP_INFO(
+ // logger_, "[gimbal calibration] New yaw offset: %ld",
+ // bottom_board_->gimbal_yaw_motor_.calibrate_zero_point());
+ // RCLCPP_INFO(
+ // logger_, "[gimbal calibration] New pitch offset: %ld",
+ // top_board_->gimbal_pitch_motor_.calibrate_zero_point());
+ }
+
+ void chassis_hip_calibrate_subscription_callback(std_msgs::msg::Int32::UniquePtr) {
+ RCLCPP_INFO(
+ logger_, "[chassis calibration] New left front hip offset: %ld",
+ bottom_board_->chassis_hip_motors[0].calibrate_zero_point());
+ RCLCPP_INFO(
+ logger_, "[chassis calibration] New left back hip offset: %ld",
+ bottom_board_->chassis_hip_motors[1].calibrate_zero_point());
+ RCLCPP_INFO(
+ logger_, "[chassis calibration] New right back hip offset: %ld",
+ bottom_board_->chassis_hip_motors[2].calibrate_zero_point());
+ RCLCPP_INFO(
+ logger_, "[chassis calibration] New right front hip offset: %ld",
+ bottom_board_->chassis_hip_motors[3].calibrate_zero_point());
+ }
+ class WheelLegInfantryCommand : public rmcs_executor::Component {
+ public:
+ explicit WheelLegInfantryCommand(WheelLegInfantry& infantry)
+ : infantry_(infantry) {}
+
+ void update() override { infantry_.command_update(); }
+
+ WheelLegInfantry& infantry_;
+ };
+
+ class TopBoard final : private librmcs::client::CBoard {
+ public:
+ friend class WheelLegInfantry;
+ explicit TopBoard(
+ WheelLegInfantry& infantry, WheelLegInfantryCommand& infantry_command, int usb_pid = -1)
+ : librmcs::client::CBoard(usb_pid)
+ , imu_(1000, 0.2, 0.0)
+ , tf_(infantry.tf_)
+ , gimbal_pitch_motor_(
+ infantry, infantry_command, "/gimbal/pitch",
+ device::LkMotor::Config{device::LkMotor::Type::MG5010E_I10}
+ .set_encoder_zero_point(
+ static_cast(
+ infantry.get_parameter("pitch_motor_zero_point").as_int())))
+ , friction_wheel_motors_(
+ {infantry, infantry_command, "/gimbal/left_friction",
+ device::DjiMotor::Config{device::DjiMotor::Type::M3508}.set_reduction_ratio(
+ 1.0)},
+ {infantry, infantry_command, "/gimbal/right_friction",
+ device::DjiMotor::Config{device::DjiMotor::Type::M3508}
+ .set_reduction_ratio(1.0)
+ .set_reversed()})
+ , 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.
+ // 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};
+ // std::cout << mapping << std::endl;
+
+ return std::make_tuple(x, y, z);
+ });
+
+ infantry.register_output("/gimbal/yaw/velocity", gimbal_yaw_velocity_imu_);
+ infantry.register_output("/gimbal/pitch/velocity", gimbal_pitch_velocity_imu_);
+ }
+
+ ~TopBoard() final {
+ stop_handling_events();
+ event_thread_.join();
+ }
+
+ void update() {
+ imu_.update_status();
+ Eigen::Quaterniond gimbal_imu_pose{imu_.q0(), imu_.q1(), imu_.q2(), imu_.q3()};
+
+ tf_->set_transform(
+ gimbal_imu_pose.conjugate());
+
+ gimbal_pitch_motor_.update_status();
+ for (auto& motor : friction_wheel_motors_)
+ motor.update_status();
+
+ tf_->set_state(
+ gimbal_pitch_motor_.angle());
+
+ *gimbal_yaw_velocity_imu_ = imu_.gz();
+ *gimbal_pitch_velocity_imu_ = imu_.gy();
+ }
+
+ void command_update() {
+ uint16_t control_commands[4]{};
+ control_commands[0] = friction_wheel_motors_[0].generate_command();
+ control_commands[1] = friction_wheel_motors_[1].generate_command();
+
+ transmit_buffer_.add_can1_transmission(
+ 0x200, std::bit_cast(control_commands));
+
+ transmit_buffer_.add_can2_transmission(
+ 0x141, gimbal_pitch_motor_.generate_velocity_command(
+ gimbal_pitch_motor_.control_velocity()));
+
+ 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) {
+ friction_wheel_motors_[0].store_status(can_data);
+ } else if (can_id == 0x202) {
+ friction_wheel_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 == 0x141)
+ gimbal_pitch_motor_.store_status(can_data);
+ }
+
+ 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& tf_;
+
+ OutputInterface gimbal_yaw_velocity_imu_;
+ OutputInterface gimbal_pitch_velocity_imu_;
+
+ device::LkMotor gimbal_pitch_motor_;
+
+ device::DjiMotor friction_wheel_motors_[2];
+
+ librmcs::client::CBoard::TransmitBuffer transmit_buffer_;
+ std::thread event_thread_;
+ };
+ class BottomBoard final : private librmcs::client::CBoard {
+ public:
+ friend class WheelLegInfantry;
+ explicit BottomBoard(
+ WheelLegInfantry& infantry, WheelLegInfantryCommand& infantry_command, int usb_pid = -1)
+ : librmcs::client::CBoard(usb_pid)
+ , dr16_(infantry)
+ , imu_(1000, 0.2, 0.0)
+ , tf_(infantry.tf_)
+ , chassis_wheel_motors_(
+ {infantry, infantry_command, "/chassis/left_wheel",
+ device::DjiMotor::Config{device::DjiMotor::Type::M3508}
+ .set_reduction_ratio(268.0 / 17.0)
+ .enable_multi_turn_angle()},
+ {infantry, infantry_command, "/chassis/right_wheel",
+ device::DjiMotor::Config{device::DjiMotor::Type::M3508}
+ .set_reduction_ratio(268.0 / 17.0)
+ .enable_multi_turn_angle()
+ .set_reversed()})
+ , chassis_hip_motors(
+ {infantry, infantry_command, "/chassis/left_front_hip",
+ device::DmMotor::Config{device::DmMotor::Type::DM8009}
+ .set_encoder_zero_point(
+ static_cast(
+ infantry.get_parameter("left_front_hip_motor_zero_point").as_int()))
+ .set_reversed()},
+ {infantry, infantry_command, "/chassis/left_back_hip",
+ device::DmMotor::Config{device::DmMotor::Type::DM8009}
+ .set_encoder_zero_point(
+ static_cast(
+ infantry.get_parameter("left_back_hip_motor_zero_point").as_int()))
+ .set_reversed()},
+ {infantry, infantry_command, "/chassis/right_back_hip",
+ device::DmMotor::Config{device::DmMotor::Type::DM8009}.set_encoder_zero_point(
+ static_cast(
+ infantry.get_parameter("right_back_hip_motor_zero_point").as_int()))},
+ {infantry, infantry_command, "/chassis/right_front_hip",
+ device::DmMotor::Config{device::DmMotor::Type::DM8009}.set_encoder_zero_point(
+ static_cast(
+ infantry.get_parameter("right_front_hip_motor_zero_point").as_int()))})
+ , gimbal_yaw_motor_(
+ infantry, infantry_command, "/gimbal/yaw",
+ device::LkMotor::Config{device::LkMotor::Type::MG4010E_I10}
+ .set_encoder_zero_point(
+ static_cast(
+ infantry.get_parameter("yaw_motor_zero_point").as_int())))
+ , bullet_feeder_motor_(
+ infantry, infantry_command, "/gimbal/bullet_feeder",
+ device::DjiMotor::Config{device::DjiMotor::Type::M2006}
+ .enable_multi_turn_angle()
+ .set_reversed()
+ .set_reduction_ratio(19 * 2))
+ , 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.
+ // 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};
+ // std::cout << mapping << std::endl;
+
+ return std::make_tuple(x, y, z);
+ });
+
+ infantry.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;
+ };
+
+ infantry.register_output(
+ "/chassis/x_axis/acceleration", chassis_x_axis_acceleration_imu_);
+ infantry.register_output(
+ "/chassis/z_axis/acceleration", chassis_z_axis_acceleration_imu_);
+
+ infantry.register_output("/chassis/yaw/velocity", chassis_yaw_velocity_imu_);
+ infantry.register_output("/chassis/pitch/velocity", chassis_pitch_velocity_imu_);
+ infantry.register_output("/chassis/roll/velocity", chassis_roll_velocity_imu_);
+
+ infantry.register_output("/chassis/yaw/angle", chassis_yaw_angle_imu_);
+ infantry.register_output("/chassis/pitch/angle", chassis_pitch_angle_imu_);
+ infantry.register_output("/chassis/roll/angle", chassis_roll_angle_imu_);
+ }
+
+ ~BottomBoard() {
+ stop_handling_events();
+ event_thread_.join();
+ }
+
+ void update() {
+ dr16_.update_status();
+
+ update_imu();
+
+ for (auto& motor : chassis_wheel_motors_)
+ motor.update_status();
+
+ for (auto& motor : chassis_hip_motors)
+ motor.update_status();
+
+ gimbal_yaw_motor_.update_status();
+ tf_->set_state(
+ gimbal_yaw_motor_.angle());
+
+ bullet_feeder_motor_.update_status();
+ }
+
+ void command_update() {
+ uint16_t control_commands[4]{};
+
+ control_commands[0] = chassis_wheel_motors_[0].generate_command();
+ transmit_buffer_.add_can1_transmission(
+ 0x200, std::bit_cast(control_commands));
+
+ transmit_buffer_.add_can1_transmission(0x01, chassis_hip_motors[0].generate_command());
+ transmit_buffer_.add_can1_transmission(0x02, chassis_hip_motors[1].generate_command());
+
+ control_commands[0] = 0;
+ control_commands[1] = chassis_wheel_motors_[1].generate_command();
+ transmit_buffer_.add_can2_transmission(
+ 0x200, std::bit_cast(control_commands));
+
+ transmit_buffer_.add_can2_transmission(0x03, chassis_hip_motors[2].generate_command());
+ transmit_buffer_.add_can2_transmission(0x04, chassis_hip_motors[3].generate_command());
+
+ transmit_buffer_.trigger_transmission();
+ }
+
+ void update_imu() {
+ imu_.update_status();
+
+ *chassis_yaw_angle_imu_ = std::atan2(
+ 2.0 * (imu_.q0() * imu_.q3() + imu_.q1() * imu_.q2()),
+ 2.0 * (imu_.q0() * imu_.q0() + imu_.q1() * imu_.q1()) - 1.0);
+ *chassis_pitch_angle_imu_ =
+ std::asin(-2.0 * (imu_.q1() * imu_.q3() - imu_.q0() * imu_.q2()));
+ *chassis_roll_angle_imu_ = std::atan2(
+ 2.0 * (imu_.q0() * imu_.q1() + imu_.q2() * imu_.q3()),
+ 2.0 * (imu_.q0() * imu_.q0() + imu_.q3() * imu_.q3()) - 1.0);
+
+ *chassis_yaw_velocity_imu_ = imu_.gz();
+ *chassis_pitch_velocity_imu_ = imu_.gy();
+ *chassis_roll_velocity_imu_ = imu_.gx();
+
+ *chassis_x_axis_acceleration_imu_ = imu_.ax();
+ *chassis_z_axis_acceleration_imu_ = imu_.az();
+ }
+
+ 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_[0].store_status(can_data);
+ } else if (can_id == 0x01) {
+ chassis_hip_motors[0].store_status(can_data);
+ } else if (can_id == 0x02) {
+ chassis_hip_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 == 0x202) {
+ chassis_wheel_motors_[1].store_status(can_data);
+ } else if (can_id == 0x03) {
+ chassis_hip_motors[2].store_status(can_data);
+ } else if (can_id == 0x04) {
+ chassis_hip_motors[3].store_status(can_data);
+ }
+ }
+
+ 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 std::byte* uart_data, uint8_t uart_data_length) override {
+ dr16_.store_status(uart_data, uart_data_length);
+ }
+
+ 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::Dr16 dr16_;
+
+ device::Bmi088 imu_;
+
+ OutputInterface chassis_x_axis_acceleration_imu_;
+ OutputInterface chassis_z_axis_acceleration_imu_;
+
+ OutputInterface chassis_yaw_velocity_imu_;
+ OutputInterface chassis_pitch_velocity_imu_;
+ OutputInterface chassis_roll_velocity_imu_;
+
+ OutputInterface chassis_yaw_angle_imu_;
+ OutputInterface chassis_pitch_angle_imu_;
+ OutputInterface chassis_roll_angle_imu_;
+
+ OutputInterface& tf_;
+
+ device::DjiMotor chassis_wheel_motors_[2];
+
+ device::DmMotor chassis_hip_motors[4];
+
+ device::LkMotor gimbal_yaw_motor_;
+ device::DjiMotor bullet_feeder_motor_;
+
+ librmcs::utility::RingBuffer referee_ring_buffer_receive_{256};
+ OutputInterface referee_serial_;
+
+ librmcs::client::CBoard::TransmitBuffer transmit_buffer_;
+ std::thread event_thread_;
+ };
+
+ rclcpp::Logger logger_;
+
+ OutputInterface tf_;
+
+ std::shared_ptr infantry_command_;
+
+ // std::unique_ptr top_board_;
+ std::unique_ptr bottom_board_;
+
+ rclcpp::Subscription::SharedPtr gimbal_calibrate_subscription_;
+ rclcpp::Subscription::SharedPtr chassis_hips_calibrate_subscription_;
+};
+} // namespace rmcs_core::hardware
+
+#include
+
+PLUGINLIB_EXPORT_CLASS(rmcs_core::hardware::WheelLegInfantry, rmcs_executor::Component)
\ No newline at end of file
diff --git a/rmcs_ws/src/rmcs_executor/src/main.cpp b/rmcs_ws/src/rmcs_executor/src/main.cpp
index 0de17487..99311f24 100644
--- a/rmcs_ws/src/rmcs_executor/src/main.cpp
+++ b/rmcs_ws/src/rmcs_executor/src/main.cpp
@@ -44,7 +44,7 @@ int main(int argc, char** argv) {
std::vector component_descriptions;
if (!executor->get_parameter("components", component_descriptions))
- throw std::runtime_error("para");
+ throw std::runtime_error("Missing parameter 'components' or config is not found");
std::regex regex(R"(\s*(\S+)\s*->\s*(\S+)\s*)");
for (const auto& component_description : component_descriptions) {
diff --git a/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/wheel_leg_mode.hpp b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/wheel_leg_mode.hpp
new file mode 100644
index 00000000..c5b9cfdb
--- /dev/null
+++ b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/wheel_leg_mode.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include
+
+namespace rmcs_msgs {
+enum class WheelLegMode : uint8_t {
+ BALANCELESS = 0,
+ SPIN = 1,
+ FOLLOW = 2,
+ LAUNCH_RAMP = 3,
+};
+}
diff --git a/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/wheel_leg_state.hpp b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/wheel_leg_state.hpp
new file mode 100644
index 00000000..9bf48f89
--- /dev/null
+++ b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/wheel_leg_state.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+#include
+
+namespace rmcs_msgs {
+
+struct WheelLegState {
+ double distance;
+ double velocity;
+ double yaw_angle;
+ double yaw_velocity;
+ double left_tilt_angle;
+ double left_tilt_velocity;
+ double right_tilt_angle;
+ double right_tilt_velocity;
+ double body_pitch_angle;
+ double body_pitch_velocity;
+
+ Eigen::Map> vector() const {
+ return Eigen::Map>(&distance);
+ }
+};
+
+} // namespace rmcs_msgs
\ No newline at end of file