From 7ba2bba450cf7b392c36f536b1d64744dcb9ddb9 Mon Sep 17 00:00:00 2001 From: CaoWangrenbo Date: Fri, 23 Oct 2026 23:32:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(safety):=20=E9=94=99=E8=AF=AF=E4=BD=8D?= =?UTF-8?q?=E4=B8=8A=E6=8A=A5=201002/3=EF=BC=9BIMU=20=E6=96=AD=E6=B5=81?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=EF=BC=9B=E7=A7=BB=E9=99=A4=E7=A1=AC=E4=BB=B6?= =?UTF-8?q?=E5=81=87=20IMU?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MotionState 增加 error_code,新增 invicta_msgs/error_code.h 共用位定义;SafeController 写入、remote 展开成协议 ErrorList(component 暂为 0) - InvictaInterface 增加 IMU 断流/恢复判定(imu_timeout_s / imu_recover_s),covariance=-1 视为不可用;RL 丢失 IMU 走正常趴下 - invicta_hardware 不再发布硬编码单位四元数 IMU(含错误的 covariance[0]=0) - 更新 README 与 docs/TODO.md:记录已完成项、反馈帧 CANID 1~4 已确认、[joint_limits] 重复维护入待办 --- docs/TODO.md | 71 +++++++------- src/invicta_deploy/README.md | 29 ++++++ src/invicta_deploy/config/invicta_deploy.toml | 4 +- .../include/config/deploy_config.h | 4 + .../include/types/common_types.h | 3 + .../include/utils/motion_state_feedback.hpp | 18 +--- .../include/utils/safe_controller.hpp | 37 ++++---- .../interface/motion_state_reporter.hpp | 2 + .../robot/hardware/invicta_interface.hpp | 45 ++++++++- .../interface/robot/robot_interface.h | 12 +++ .../quadruped_wheel/rl_control_state.hpp | 20 ++++ .../quadruped_wheel/standup_state.hpp | 5 + src/invicta_hardware/README.md | 7 +- .../config/invicta_hardware.toml | 6 +- .../invicta_hardware/hardware_bridge.h | 11 +-- .../invicta_hardware/hardware_config.h | 9 +- .../launch/invicta_hardware.launch | 2 +- .../src/invicta_hardware_node.cpp | 29 +----- .../include/invicta_msgs/error_code.h | 94 +++++++++++++++++++ src/invicta_msgs/msg/MotionState.msg | 1 + .../include/invicta_remote/remote_node.h | 20 +++- 21 files changed, 303 insertions(+), 126 deletions(-) create mode 100644 src/invicta_msgs/include/invicta_msgs/error_code.h diff --git a/docs/TODO.md b/docs/TODO.md index 3233f39..caefc44 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,15 +1,16 @@ # TODO 本文件记录**当前会话中确认但本期不做**的事项。按优先级分组,每项尽量写清「为什么」和「验收方式」, -避免下次重新推导。已完成的部分见各包 README。 +避免下次重新推导。已完成的部分见各包 README 与文末清单。 ## P0:安全链路(真机跑 RL 前必须处理) - [ ] **异常数据接入 deploy 的保护链路** - 硬件已在 `/joint_diagnostics` 提供温度/状态字/电压(10 Hz),但 deploy 侧仍是常量: - `robot_interface.h` 的 `GetMotorTemperture()` 返回 30 ℃、`GetDriverStatusWord()` 返回全 1, - 所以 `SafeController` 的过温检查和驱动报错检查恒不触发。 - 做法:`InvictaInterface` 订阅 `/joint_diagnostics` 并覆盖这两个虚函数。 + 硬件已在 `/joint_diagnostics` 提供温度/状态字/电压(10 Hz),但 deploy 侧仍未接: + `InvictaInterface` 没有订阅该话题,`GetMotorTemperture()` 走基类常量 30 ℃; + `GetDriverStatusWord()` 虽已重写为返回成员 `driver_status_`,但该成员只被初始化成全 1、无人更新, + 所以 `SafeController` 的过温检查与驱动报错检查依然恒不触发。 + 做法:`InvictaInterface` 订阅 `/joint_diagnostics`,按关节名填 `driver_status_`、覆盖 `GetMotorTemperture()`。 注意:协议 Byte9 的 bit0 是「使能中」,而 `IsDriverStatusNormal()` 判断 `status_word != 1` 报错, 失能但健康的电机回 `0x00` 会误判;建议在接口层归一化成「无故障位就填 1」。 验收:人为把某个关节温度改成 101 ℃,deploy 进入 `safe_control_mode=2` 并切 `kJointDamping`。 @@ -18,32 +19,13 @@ 诊断话题 10 Hz,`SafeController` 读缓存值。若 hardware 进程死掉,温度会停在最后一帧的「正常」。 需要在接口层记录诊断时间戳,超时(如 1 s)按异常处理。 验收:杀掉 hardware 节点后 deploy 进入安全状态。 - -- [ ] **异常上报 1002/3 的数据源** - remote 的 `ErrorDoc` 恒为空列表,因为 `MotionState.msg` 没有错误字段。 - 建议给 `MotionState` 加 `uint32 error_code` + `uint16 error_component`(位域与 - `SafeController::robot_error_state_` 对齐,component 用 M20 槽号),remote 再映射成 - `0x8001/0x8002/0x8020/0x8012` 等协议错误码。 - 验收:模拟驱动器故障,App 收到非空 `ErrorList` 且 component 指向正确关节。 - -- [ ] **IMU 数据源缺失** - `/imu/data` 目前发的是单位四元数、`orientation_covariance[0] = 0`(声称有效), - 但协议里没有任何 IMU 字段,硬件桥也没有读 IMU。 - 按本项目自己的约定,拿不到姿态时应把 `orientation_covariance[0]` 置 `-1`; - 改之前要确认 deploy 的 `[rl].require_imu` 会不会因此拒绝进 RL。 + (对照:IMU 的同类问题已解,走 `IsImuHealthy()` 的断流判定——本条可照抄那套「时间戳 + 超时 + 恢复去抖」。) ## P1:标定与配置一致性(上真机前) -- [x] **`dir`/`offset` 的施加方已定:hardware**(2026-10-22) - raw↔model 的换算统一由 `invicta_hardware` 独家承担(`[[legs]].swing/wheel` 的 `dir`/`offset_deg`), - 话题上收发都是模型帧;deploy 的 `InvictaInterface` 退化为纯透传,`invicta_deploy.toml` 不再有 `dir`/`offset_deg`。 - 这样 `[joint_limits]`(模型帧)与被限幅的指令同帧,`/joint_states` 也是模型角。 - 遗留:`[joint_limits]` 仍在两份 TOML 里重复维护(deploy 用一份做状态机自检、hardware 用一份做下发夹紧), - 后续可考虑合并或生成。 - -- [ ] **反馈帧仲裁 ID 需真机确认** - `ReadHardware` 按「反馈使用关节自身 CANID」实现(手册只写了从站 CANID 1~6)。 - 上电后 `candump can0` 核对;若不是 1~4,需要改匹配逻辑。 +- [ ] **`[joint_limits]` 在两份 TOML 里重复维护** + `invicta_deploy.toml` 用一份做状态机自检,`invicta_hardware.toml` 用一份做下发夹紧。 + 两份手改容易漂移,且改限位时要同步改两处。可考虑合并成单一来源(hardware 读 deploy 的,或从一处生成另一处)。 - [ ] **`lie_height = 0` 与位置限位矛盾** `invicta_deploy.toml` 里 `lie_height = 0.00` 反解 `θ = acos(0) = 90° = 1.571 rad`, @@ -58,7 +40,8 @@ 如果当初是估的,建议按实测重新定。 - [ ] **位置指令越界目前静默夹紧** - 力矩和 kp/kd 越界会告警,位置不会。策略动作顶到限位时可能值得提示一次(注意别刷屏)。 + `invicta_hardware_node.cpp` 的 `PackRunBlocks` 里力矩和 kp/kd 越界会告警,位置不会。 + 策略动作顶到限位时可能值得提示一次(`ROS_WARN_THROTTLE`,注意别刷屏)。 ## P2:协议状态上报的剩余占位 @@ -67,6 +50,7 @@ - [ ] `Charge / HES / Sleep`:没有充电状态、硬急停信号与休眠管理。 - [ ] `HipY` 的正负号未与真实 App/M20 对照:如果 App 只是显示数值没问题,若渲染姿态需要核对。 - [ ] 未实现指令:照明灯、休眠设置/查询、自主充电、速度指令(导航模式)、导航任务(Pro)。 +- [ ] 错误上报的 `component` 恒为 0(见文末遗留):要定位到具体关节,需在 deploy 侧把错误位与关节号关联。 ## P3:工程与清理 @@ -74,16 +58,25 @@ deploy 已改为「等订阅者再发」且 hardware 在 `kMotorControl` 未使能时会补发使能,功能上已安全; 但 hardware 循环线程处理 `kErrorReset` 的使能连发会阻塞约 80 ms,期间可能观察不到 `kEnable`。 若要严格逐帧落地,可把连发改成分段非阻塞发送。 -- [ ] `[motor.gains].bench_*` 已被指令里的 kp/kd 取代,确认是否删除。 -- [ ] 构建镜像 `localhost/custom-ros-noetic` 缺 toml++,目前靠挂载外部安装树 + `CMAKE_PREFIX_PATH`。 - 可考虑在镜像里装 toml++,或在仓库内 vendored。 +- [ ] `[motor.gains].bench_*` 已被指令里的 kp/kd 取代(现仅被加载、无消费方),确认是否删除。 - [ ] 真机联调清单:`ip link set canX ... fd on` 权限、`roslaunch` 启动顺序、双路差分驱动方向。 -## 已完成(本会话) +## 已完成 -- [x] SocketCAN FD 收发 + 智身模组协议编解码(使能/失能/运行、12 字节反馈解码) -- [x] 温度读回 → `/joint_diagnostics`(默认 10 Hz,可配置)→ remote 填 1002/5 -- [x] 1002/4 的 8 → 16 槽映射(swing→HipY、wheel→Wheel,HipX/Knee 占位 0) -- [x] 力矩/速度/位置限幅,kp/kd 越界夹紧并告警;限幅与归一化边界分离 + 加载期校验 -- [x] 指令看门狗(100 ms 无指令转阻尼) -- [x] 容器内构建 + 冒烟测试;修复启动时序丢失与订阅回调阻塞 +- [x] **SocketCAN FD 收发 + 智身模组协议编解码**(使能/失能/运行、12 字节反馈解码) +- [x] **温度读回 → `/joint_diagnostics`**(默认 10 Hz,可配置)→ remote 填 1002/5 +- [x] **1002/4 的 8 → 16 槽映射**(swing→HipY、wheel→Wheel,HipX/Knee 占位 0) +- [x] **力矩/速度/位置限幅,kp/kd 越界夹紧并告警**;限幅与归一化边界分离 + 加载期校验 +- [x] **指令看门狗**(100 ms 无指令转阻尼) +- [x] **`dir`/`offset` 的施加方已定:hardware**(2026-10-22) + raw↔model 换算由 `invicta_hardware` 独家承担(`[[legs]].swing/wheel` 的 `dir`/`offset_deg`),话题上收发都是模型帧; + deploy 的 `InvictaInterface` 退化为纯透传,`invicta_deploy.toml` 不再有 `dir`/`offset_deg`。 +- [x] **反馈帧仲裁 ID 已确认为 1~4**(2026-10-23):`ReadHardware` 按关节自身 CANID 匹配,与实测一致,无需改动。 +- [x] **toml++ 改为 vendored**(`third_party/tomlplusplus`),不再依赖镜像或 `CMAKE_PREFIX_PATH`。 +- [x] **容器内构建 + 冒烟测试**(`scripts/smoke_test.sh`);修复启动时序丢失与订阅回调阻塞。 +- [x] **异常上报 1002/3 的数据源**:`MotionState.error_code` + 共用位定义 `invicta_msgs/error_code.h`; + `SafeController` 每周期写 `UserCommand::robot_error_code`,remote 经 `RobotErrorList()` 展开成协议 `ErrorList`。 + 遗留:`component` 恒填 0(未按关节定位),已单列在 P2。 +- [x] **IMU 数据源缺失**:`invicta_hardware` 不再发假 IMU(删掉单位四元数 + `covariance[0]=0`); + deploy 把 `orientation_covariance[0]==-1` 视为不可用,并做断流/恢复判定(`imu_timeout_s`/`imu_recover_s`), + RL 丢失 IMU 走「正常趴下」而非就地垮下。实际数据源 `odin_imu_bridge` 不在本仓库,契约见 `invicta_deploy/README.md`。 diff --git a/src/invicta_deploy/README.md b/src/invicta_deploy/README.md index 83cc505..2ae9411 100644 --- a/src/invicta_deploy/README.md +++ b/src/invicta_deploy/README.md @@ -59,11 +59,40 @@ config/ launch/ 参数与启动文件 | 关节角到达站立位姿(轮关节除外) | `rl/require_pose`、`rl/joint_tolerance` | true / 0.15 rad | | 关节速度足够小 | `rl/velocity_tolerance` | 1.0 rad/s | | IMU 数据有效(无 NaN、加速度在 0.1~3 g) | `rl/require_imu` | true | +| IMU 连续(有新鲜可用帧、且断流后已恢复) | `rl/imu_timeout_s`、`rl/imu_recover_s` | 0.2 s / 1.0 s | | 到达站立位姿后稳定一段时间 | `rl/enter_delay` | 0.5 s | 自检不通过时保持站立,并每秒打印一次原因(`[RL] 暂不进入 RL:...`),`/motion_state` 如实报 1。 另外推理输出若出现 NaN/Inf,会立刻停发指令并落回关节阻尼。 +### IMU 断流:退出 RL 走「正常趴下」,恢复前不再进 RL + +RL 运行中 IMU 断流(超过 `rl/imu_timeout_s` 没有新鲜可用的姿态帧)时,**按正常退出去趴下**: +`RLControlState` 返回 `kLieDown`,由 `LieDownState` 做受控下降(关节空间样条,不依赖 IMU), +而不是就地转阻尼垮下来。恢复需要连续 `rl/imu_recover_s` 的新鲜可用帧, +在此之前 `ReadyForRl()` 拒绝重新进入 RL。 + +注意这**不**通过 `safe_control_mode` 表达:那个标志会让 `LieDownState` 自己也立刻转阻尼 +(`liedown_state.hpp` 里 `safe_control_mode != 0 → kJointDamping`),把受控下降打断成就地垮掉。 +IMU 解算结果含 NaN 仍走 `safe_control_mode`(急停语义),两者刻意分开。 + +### IMU 话题契约 + +姿态/角速度由**独立的 IMU 节点**发布(`invicta_hardware` 不再发布 `/imu/data`), +本节点与 `invicta_remote` 按 `[topics].imu` 订阅。数据源必须满足: + +- `header.stamp` 用 ROS 时间(本节点把它当 `imu_ts_` 转给状态机); +- `frame_id` 本节点**不检查**,但姿态必须是**机体轴**下的姿态。当前数据源 `src/odin_imu_bridge` + 发的是 `base_link_com`(与 `base_link` 同轴、平移到质心),与策略的训练约定一致; +- **姿态必须真给**:`orientation_covariance[0]` 不能是 `-1`(`-1` = 不提供姿态)。给 `-1` 时本节点 + 丢弃该帧并告警,且视为**不可用** → 等同断流(不满足连续性,见上一节)。 + ⚠️ `odin_imu_bridge` 的 `orientation_covariance` 是从里程计的 `pose.covariance` 转出来的, + 如果上游里程计在该位置填 `-1`,本节点就会认为姿态不可用——首次接入时用 + `rostopic echo /imu/data -n1` 确认一下这几个字段; +- 线加速度 `|acc|` 要落在 0.1~3 g,否则 `rl/require_imu` 自检过不去; +- 姿态用于观测里的重力轴夹角(`projected_gravity = R(rpy)⁻¹ ·(0,0,-1)`,见 `invicta_policy_runner.hpp`), + 角速度进观测,线加速度**不进观测**、只用于自检。 + > 无硬件台架(`invicta_hardware` 只发零位状态)时必须关掉位姿与 IMU 自检,否则永远进不了 RL: > `roslaunch invicta_deploy invicta_deploy.launch rl_require_pose:=false rl_require_imu:=false` diff --git a/src/invicta_deploy/config/invicta_deploy.toml b/src/invicta_deploy/config/invicta_deploy.toml index 27e6d32..b1c9664 100644 --- a/src/invicta_deploy/config/invicta_deploy.toml +++ b/src/invicta_deploy/config/invicta_deploy.toml @@ -59,7 +59,9 @@ enter_delay_s = 0.5 # 到站立位姿后再稳定多久 [s] require_pose = true # 是否检查摆臂角已到站立位姿(无硬件台架时可关) joint_tolerance = 0.15 # 摆臂角容差 [rad] velocity_tolerance = 1.0 # 摆臂角速度容差 [rad/s] -require_imu = true # 是否检查 IMU 数据有效 +require_imu = true # 是否检查 IMU 数据有效(含下面的连续性检查) +imu_timeout_s = 0.2 # 多久没有新鲜可用的 IMU 帧判为断流 → 退出 RL 走正常趴下 +imu_recover_s = 1.0 # 断流后需连续这么久有新鲜可用帧才重新判健康(恢复去抖) # ------------------------------------------------------------------ 每腿标定 # 模型约定:摆臂角 θ_model = splay_sign · acos(h/L)。 diff --git a/src/invicta_deploy/include/config/deploy_config.h b/src/invicta_deploy/include/config/deploy_config.h index 7585832..1556ffd 100644 --- a/src/invicta_deploy/include/config/deploy_config.h +++ b/src/invicta_deploy/include/config/deploy_config.h @@ -62,6 +62,8 @@ public: bool rl_require_pose = true; double rl_joint_tolerance = 0.15, rl_velocity_tolerance = 1.0; bool rl_require_imu = true; + double rl_imu_timeout_s = 0.2; ///< 多久没有新鲜可用 IMU 帧判为断流(RL 退出、拒绝再进) + double rl_imu_recover_s = 1.0; ///< 断流后需连续这么久有新鲜可用帧才重新判健康(恢复去抖) // ---- 策略 ---- std::string policy_path; ///< 相对本包目录、或绝对路径 @@ -251,6 +253,8 @@ inline bool DeployConfig::Load(const std::string& path, std::string* error) { detail::Get(rl, "joint_tolerance", rl_joint_tolerance); detail::Get(rl, "velocity_tolerance", rl_velocity_tolerance); detail::GetBool(rl, "require_imu", rl_require_imu); + detail::Get(rl, "imu_timeout_s", rl_imu_timeout_s); + detail::Get(rl, "imu_recover_s", rl_imu_recover_s); if (const toml::table* pol = detail::Section(tbl, "policy")) { detail::GetStr(pol, "path", policy_path); diff --git a/src/invicta_deploy/include/types/common_types.h b/src/invicta_deploy/include/types/common_types.h index 9d14cb1..e5c8480 100644 --- a/src/invicta_deploy/include/types/common_types.h +++ b/src/invicta_deploy/include/types/common_types.h @@ -96,6 +96,9 @@ struct UserCommand { float reserved_scale; uint32_t gait = 0; // 请求/生效的步态(协议 GaitParam),0 = 未请求 uint8_t usage_mode = 0; // 协议使用模式:0 常规 / 1 导航 / 2 辅助 + // 控制器 → 上报通道:SafeController 每周期写入内部错误位掩码, + // MotionStateReporter 读它填 MotionState.error_code(位定义见 invicta_msgs/error_code.h) + uint32_t robot_error_code = 0; }; enum FromType { diff --git a/src/invicta_deploy/include/utils/motion_state_feedback.hpp b/src/invicta_deploy/include/utils/motion_state_feedback.hpp index da95a75..f55194d 100644 --- a/src/invicta_deploy/include/utils/motion_state_feedback.hpp +++ b/src/invicta_deploy/include/utils/motion_state_feedback.hpp @@ -23,14 +23,6 @@ private: Vec3f cmd_vel_; Vec3f max_vel_; - union{ - struct{ - uint32_t joint_pos_limit : 1; - uint32_t posture_limit : 1; - uint32_t reserved : 30; - }error_code_bit_; - uint32_t error_code_; - }; public: MotionStateFeedback(/* args */){ memset(this, 0, sizeof(MotionStateFeedback)); @@ -54,13 +46,9 @@ public: } } - void ClearMotionError(){error_code_ = 0;} - void SetJointPosLimitError(){ - error_code_bit_.joint_pos_limit = 1; - } - void SetPostureLimitError(){ - error_code_bit_.posture_limit = 1; - } + // 错误码不再从这里走出去:内部错误位掩码的定义与展开见 invicta_msgs/error_code.h, + // 由 SafeController 每周期写入 UserCommand::robot_error_code, + // MotionStateReporter 读它填 MotionState.error_code(remote 再映射成协议 ErrorList)。 inline uint8_t GetCurrentState(){return current_state_;} inline uint32_t GetCurrentGait(){return current_gait_;} diff --git a/src/invicta_deploy/include/utils/safe_controller.hpp b/src/invicta_deploy/include/utils/safe_controller.hpp index 9153243..27171ef 100644 --- a/src/invicta_deploy/include/utils/safe_controller.hpp +++ b/src/invicta_deploy/include/utils/safe_controller.hpp @@ -36,9 +36,6 @@ private: std::thread judge_thread_; int run_cnt_ = 0; - double last_imu_ts_, imu_check_time_; - bool imu_check_flag_ = true; - double ri_check_time_, last_ri_ts_; VecXf last_joint_pos_, last_joint_vel_, last_joint_tau_; std::vector joint_data_same_cnt_; @@ -60,27 +57,24 @@ private: std::map error_driver_map_; uint32_t joint_update_state_; + /** + * IMU 检查。两件事分开: + * ① 解算结果含 NaN → 返回 false,调用方置 safe_control_mode(急停语义) + * ② 断流/姿态不可信 → 只置 imu_update_overtime 错误位,**不改 safe_control_mode**: + * RL 退出要走「正常切出→趴下」(RLControlState::GetNextStateName), + * 而趴下状态自己会在 safe_control_mode != 0 时转阻尼(liedown_state), + * 所以这里一旦置 mode 就会把受控下山打断成就地垮下来。 + * 连续性判定(阈值 rl_imu_timeout_s / 恢复 rl_imu_recover_s)由 RobotInterface 提供。 + */ bool IsImuDataNormal() { - double current_imu_ts_ = ri_ptr_->GetImuTimestamp(); Vec3f rpy = ri_ptr_->GetImuRpy(); Vec3f acc = ri_ptr_->GetImuAcc(); Vec3f omg = ri_ptr_->GetImuOmega(); - if (imu_check_flag_) { - last_imu_ts_ = current_imu_ts_; - imu_check_time_ = current_time_; - imu_check_flag_ = false; - } + + robot_error_state_.imu_update_overtime = ri_ptr_->IsImuHealthy() ? 0 : 1; bool res = true; - robot_error_state_.imu_update_overtime = 0; - if (current_imu_ts_ != last_imu_ts_) { - last_imu_ts_ = current_imu_ts_; - imu_check_time_ = current_time_; - } - if (current_time_ - imu_check_time_ == 0.2) { - std::cout << "========== imu lag warning ========>> " << current_time_ - imu_check_time_ << std::endl; - } - + robot_error_state_.imu_num_error = 0; for (int i = 0; i < 3; ++i) { if (std::isnan(rpy(i)) || std::isnan(acc(i)) || std::isnan(omg(i))) { res = false; @@ -199,7 +193,8 @@ private: } if (robot_error_state_.imu_update_overtime) { - std::cout << "IMU update overtime: no IMU data received since timestamp " << last_imu_ts_ << std::endl; + std::cout << "IMU update overtime: no usable IMU frame, age " << ri_ptr_->GetImuAgeSec() << " s" + << std::endl; } if (robot_error_state_.command_heartbeat_overtime) { @@ -317,6 +312,10 @@ public: std::cout << "IMU error!" << std::endl; usr_cmd_->safe_control_mode = 2; } + + // 把内部错误位掩码交给上报通道(MotionStateReporter 读它填 MotionState.error_code) + usr_cmd_->robot_error_code = robot_error_state_.error_code; + if (last_error_code_ != robot_error_state_.error_code) { if (robot_error_state_.error_code != 0) { std::cout << "========== error_code ========>> " << std::hex << robot_error_state_.error_code diff --git a/src/invicta_deploy/interface/motion_state_reporter.hpp b/src/invicta_deploy/interface/motion_state_reporter.hpp index c718f1b..95e40c0 100644 --- a/src/invicta_deploy/interface/motion_state_reporter.hpp +++ b/src/invicta_deploy/interface/motion_state_reporter.hpp @@ -74,6 +74,8 @@ private: msg.charge = 0; // 待硬件:充电状态 msg.hes = 0; // 待硬件:硬急停信号 msg.sleep = 0; // 待实现:休眠管理 + // 内部错误位掩码(SafeController 每周期写入);位定义见 invicta_msgs/error_code.h + msg.error_code = user_cmd_ ? user_cmd_->robot_error_code : 0u; msg.version = version_; pub_.publish(msg); std::this_thread::sleep_until(next); diff --git a/src/invicta_deploy/interface/robot/hardware/invicta_interface.hpp b/src/invicta_deploy/interface/robot/hardware/invicta_interface.hpp index 5b323b9..3c452bb 100644 --- a/src/invicta_deploy/interface/robot/hardware/invicta_interface.hpp +++ b/src/invicta_deploy/interface/robot/hardware/invicta_interface.hpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -72,6 +73,13 @@ public: virtual Vec3f GetImuOmega() { return omega_body_; } virtual VecXf GetContactForce() { return VecXf::Zero(4); } virtual double GetImuTimestamp() { return imu_ts_; } + /// 距最近一帧**可用** IMU 的时长 [s];从未收到过返回极大值(安全线程用,纯读) + virtual double GetImuAgeSec() { + if (!has_imu_valid_) return std::numeric_limits::max(); + return (ros::Time::now() - imu_valid_arrival_).toSec(); + } + /// IMU 是否健康(控制线程用;状态由 RefreshRobotData() 每周期更新) + virtual bool IsImuHealthy() { return imu_healthy_; } virtual std::vector GetJointDataID() { return joint_data_id_; } virtual std::vector GetDriverStatusWord() { return driver_status_; } @@ -98,7 +106,8 @@ public: cmd_pub_.publish(msg); } - virtual void RefreshRobotData() {} + /// 每个控制周期由状态机调一次:刷新 IMU 连续性判定(断流判坏 + 恢复去抖,见 RefreshImuHealth) + virtual void RefreshRobotData() { RefreshImuHealth(); } protected: // ------------------------------------------------------------------ 启动序列 @@ -178,8 +187,12 @@ protected: } void ImuHandler(const sensor_msgs::Imu::ConstPtr& msg) { + imu_arrival_ = ros::Time::now(); + has_imu_ = true; // orientation_covariance[0] == -1 表示「不提供姿态」 if (msg->orientation_covariance[0] == -1.0) { + // 帧在来但没有可用姿态:对 RL 而言等同断流(base_rpy 会停在旧值,不能当有效观测) + has_imu_valid_ = false; if (!warned_imu_no_orientation_) { warned_imu_no_orientation_ = true; ROS_WARN("[InvictaInterface] %s did not provide orientation (covariance=-1), attitude observation invalid", @@ -190,6 +203,7 @@ protected: Eigen::Quaternionf q(float(msg->orientation.w), float(msg->orientation.x), float(msg->orientation.y), float(msg->orientation.z)); if (q.norm() < 1e-6f) { + has_imu_valid_ = false; ROS_WARN_THROTTLE(5.0, "[InvictaInterface] %s quaternion norm near zero, ignoring this frame", cfg_.topic_imu.c_str()); return; } @@ -198,6 +212,29 @@ protected: omega_body_ << msg->angular_velocity.x, msg->angular_velocity.y, msg->angular_velocity.z; acc_ << msg->linear_acceleration.x, msg->linear_acceleration.y, msg->linear_acceleration.z; imu_ts_ = msg->header.stamp.toSec(); + imu_valid_arrival_ = imu_arrival_; + has_imu_valid_ = true; + } + + /** + * IMU 连续性判定(只在控制线程里由 RefreshRobotData() 调一次): + * 判坏:最近一帧可用 IMU 的龄期 > rl_imu_timeout_s(或从未收到过、或帧不提供姿态) + * 恢复:断流之后需要连续 rl_imu_recover_s 的新鲜可用帧才重新判为健康(去抖) + * 之所以要恢复去抖:否则 IMU 抖动一帧就会立刻放行重新进 RL。 + */ + void RefreshImuHealth() { + const ros::Time now = ros::Time::now(); + const bool fresh = has_imu_valid_ && (now - imu_valid_arrival_).toSec() <= cfg_.rl_imu_timeout_s; + if (!fresh) { + imu_good_since_valid_ = false; + imu_healthy_ = false; + return; + } + if (!imu_good_since_valid_) { + imu_good_since_ = now; + imu_good_since_valid_ = true; + } + if ((now - imu_good_since_).toSec() >= cfg_.rl_imu_recover_s) imu_healthy_ = true; } protected: @@ -206,6 +243,12 @@ protected: std::unordered_map name2slot_; double ri_ts_ = 0., imu_ts_ = 0.; + + // ---- IMU 连续性(见 RefreshImuHealth)---- + ros::Time imu_arrival_, imu_valid_arrival_, imu_good_since_; + bool has_imu_ = false; ///< 是否收到过任何 IMU 帧(哪怕不提供姿态) + bool has_imu_valid_ = false; ///< 最近一帧是否提供了可用姿态 + bool imu_good_since_valid_ = false, imu_healthy_ = false; Vec3f omega_body_, rpy_, acc_; VecXf joint_pos_, joint_vel_, joint_tau_; uint16_t joint_data_frame_ = 0; diff --git a/src/invicta_deploy/interface/robot/robot_interface.h b/src/invicta_deploy/interface/robot/robot_interface.h index 1c97f0a..db41db3 100644 --- a/src/invicta_deploy/interface/robot/robot_interface.h +++ b/src/invicta_deploy/interface/robot/robot_interface.h @@ -72,6 +72,18 @@ public: /// IMU 数据时间戳 [s] virtual double GetImuTimestamp() { return 0.; } + /// 距最近一帧**可用** IMU 的时长 [s];从未收到过返回极大值 + virtual double GetImuAgeSec() { return 0.; } + + /** + * @brief IMU 是否健康:最近有新鲜、可用的姿态帧,且断流之后已连续恢复 + * + * 用于两件事:RL 运行中失去 IMU 时走「正常切出→趴下」, + * 以及恢复之前拒绝重新进入 RL(见 rl_control_state / standup_state)。 + * 无 IMU 数据源的实现返回 true(本构型 RL 必须要姿态,台架可关 rl_require_imu)。 + */ + virtual bool IsImuHealthy() { return true; } + /** * @brief 关节电机温度 [℃] * diff --git a/src/invicta_deploy/state_machine/quadruped_wheel/rl_control_state.hpp b/src/invicta_deploy/state_machine/quadruped_wheel/rl_control_state.hpp index cef5fb3..6267353 100644 --- a/src/invicta_deploy/state_machine/quadruped_wheel/rl_control_state.hpp +++ b/src/invicta_deploy/state_machine/quadruped_wheel/rl_control_state.hpp @@ -48,6 +48,7 @@ private: std::thread run_policy_thread_; bool start_flag_ = true; std::atomic policy_output_bad_{false}; ///< 推理输出异常(NaN/Inf) + bool warned_imu_exit_ = false; ///< IMU 丢失退出 RL 的日志只打一次 Eigen::MatrixXf acc_rot = Eigen::MatrixXf::Zero(20, 3); int acc_rot_count = 0; @@ -168,6 +169,7 @@ public: virtual void OnEnter() { policy_output_bad_ = false; + warned_imu_exit_ = false; active_gait_ = -1; // 强制按当前步态重选模型 state_run_cnt_ = -1; start_flag_ = true; @@ -198,6 +200,24 @@ public: virtual StateName GetNextStateName() { if (uc_ptr_->GetUserCommand()->safe_control_mode != 0) return StateName::kJointDamping; + + // IMU 断流 / 姿态不可信:走「正常切出」——交给趴下状态做受控下降。 + // 刻意不用 safe_control_mode 表达,否则 kLieDown 自己会立刻转阻尼(见 liedown_state), + // 变成就地垮下来,正是这里要避免的。恢复之前 RL 也进不来(见 StandUpState::ReadyForRl)。 + if (invicta::Config().rl_require_imu && !ri_ptr_->IsImuHealthy()) { + if (!warned_imu_exit_) { + warned_imu_exit_ = true; + const double age = ri_ptr_->GetImuAgeSec(); + if (age > 1e6) { + std::cout << "[RL] no usable IMU frame yet: leaving RL via normal exit -> lie down" << std::endl; + } else { + std::cout << "[RL] IMU lost (age " << age << " s): leaving RL via normal exit -> lie down" + << std::endl; + } + } + return StateName::kLieDown; + } + if (uc_ptr_->GetUserCommand()->target_mode == uint8_t(RobotMotionState::LieDown)) return StateName::kLieDown; return StateName::kRLControl; diff --git a/src/invicta_deploy/state_machine/quadruped_wheel/standup_state.hpp b/src/invicta_deploy/state_machine/quadruped_wheel/standup_state.hpp index 6c17ed3..b0251df 100644 --- a/src/invicta_deploy/state_machine/quadruped_wheel/standup_state.hpp +++ b/src/invicta_deploy/state_machine/quadruped_wheel/standup_state.hpp @@ -169,6 +169,11 @@ public: LogNotReady("IMU acceleration abnormal"); return false; } + // 连续性:断流或刚恢复还没稳够时间,都不许进 RL(恢复去抖见 InvictaInterface::RefreshImuHealth) + if (!ri_ptr_->IsImuHealthy()) { + LogNotReady("IMU unhealthy: no fresh usable frame, or still recovering"); + return false; + } } return true; } diff --git a/src/invicta_hardware/README.md b/src/invicta_hardware/README.md index b61cd08..14a021f 100644 --- a/src/invicta_hardware/README.md +++ b/src/invicta_hardware/README.md @@ -1,6 +1,6 @@ # invicta_hardware -关节/IMU 数据源与关节指令执行端。已接 **SocketCAN + 智身关节模组 CANFD 协议**(协议见 +关节数据源与关节指令执行端。已接 **SocketCAN + 智身关节模组 CANFD 协议**(协议见 [docs/zhishen_joint_module_canfd.md](../../docs/zhishen_joint_module_canfd.md))。 ## 它负责什么 @@ -8,7 +8,7 @@ 以 `publish_rate_hz`(默认 200 Hz)为一个周期,循环做三件事: 1. `ReadHardware()` — 非阻塞收各路总线上的 12 字节反馈帧,更新关节位置/速度/力矩、温度、状态码、电压 -2. `PublishState()` — 发 `sensor_msgs/JointState` 与 `sensor_msgs/Imu` +2. `PublishState()` — 发 `sensor_msgs/JointState` 3. `WriteHardware()` — 使能后每周期发 MIT 运行帧(p / v / kp / kd / tau) 上游指令断流时(`command_timeout_s`,默认 100 ms)自动转**阻尼帧**(kp=0、速度/力矩 0、只留 `timeout_*_kd`), @@ -21,7 +21,6 @@ | 话题 | 方向 | 消息 | |---|---|---| | `/joint_states` | 发布 | `sensor_msgs/JointState` | -| `/imu/data` | 发布 | `sensor_msgs/Imu` | | `/joint_diagnostics` | 发布 | `invicta_msgs/JointDiagnostics`(默认 10 Hz) | | `/joint_commands` | 订阅 | `invicta_msgs/JointCommandArray` | @@ -61,7 +60,7 @@ - **本节点是 raw↔model 的唯一边界**:话题上收发的都是**模型帧**(模型坐标、rad)。 `[[legs]]` 的 `dir`/`offset_deg` 由本节点施加(`q_model = q_motor·dir + offset`,反向 `q_motor = (q_model − offset)·dir`;速度/力矩只乘 `dir`,不含 offset)。 `invicta_deploy` 侧不再做任何标定,只处理模型量。这样 `[joint_limits]`(模型帧限幅)与被限幅的指令同帧,不会错位;任何看 `/joint_states` 的消费者拿到的也是有物理意义的角度。 -- IMU:姿态四元数(**弧度**)、角速度 `[rad/s]`(机体系)、线加速度 `[m/s²]`(机体系)。⚠️ 目前**还没有 IMU 数据源**,本节点发的是单位四元数,等硬件接上再改。 +- IMU **不由本节点发布**。之前这里发的是硬编码的单位四元数 + 零角速度/零加速度(假数据),还把 `orientation_covariance[0]` 标成 `0`(有效),会让下游把「永远水平静止」当真值——已删除。姿态/角速度由独立的 IMU 节点发布到 `imu/data`(话题名见 deploy/remote 的 `[topics].imu`),契约见 [invicta_deploy/README.md](../invicta_deploy/README.md)。 - 温度取自反馈帧 Byte8:`T = raw − 40`(0~255 ↔ −40~215 ℃);电压 Byte10 直接是伏。 - 状态码 Byte9:bit0 使能,bit1~6 故障(过压/过流/过温/过速/编码器/欠压)。故障位出现时本节点会打 ERROR 日志,并原样放进 `/joint_diagnostics`。 diff --git a/src/invicta_hardware/config/invicta_hardware.toml b/src/invicta_hardware/config/invicta_hardware.toml index 50ecfc9..9640a2d 100644 --- a/src/invicta_hardware/config/invicta_hardware.toml +++ b/src/invicta_hardware/config/invicta_hardware.toml @@ -1,5 +1,5 @@ # ============================================================================= -# invicta_hardware(关节/IMU 数据源 + 指令执行端)自己的配置 +# invicta_hardware(关节数据源 + 指令执行端)自己的配置 # 只放本节点需要的参数:关节表与硬件标定、话题、发布频率、电机与 CAN # 运动学/策略/步态等属于 invicta_deploy,见 invicta_deploy/config/invicta_deploy.toml # ============================================================================= @@ -114,14 +114,12 @@ timeout_wheel_kd = 1.0 # ------------------------------------------------------------------ 本节点的话题与节奏 [topics] joint_states = "joint_states" # 发布 sensor_msgs/JointState -imu = "imu/data" # 发布 sensor_msgs/Imu joint_commands = "joint_commands" # 订阅 invicta_msgs/JointCommandArray joint_diagnostics = "joint_diagnostics" # 发布 invicta_msgs/JointDiagnostics(温度/状态/电压) [hardware] -publish_rate_hz = 200.0 # 关节/IMU 状态发布频率(RL 侧控制环 200 Hz) +publish_rate_hz = 200.0 # 关节状态发布频率(RL 侧控制环 200 Hz) diagnostics_rate_hz = 10.0 # 诊断量频率:CAN 反馈每条指令都有,但话题按低频发 -imu_frame_id = "base_link" command_queue = 1 # 指令只取最新一帧 # true:CAN 打不开就退出(真机默认,避免以为在控车其实没连上) # false:没有 CAN 也启动,只发零位状态与指令缓存(调试 / 无硬件冒烟测试) diff --git a/src/invicta_hardware/include/invicta_hardware/hardware_bridge.h b/src/invicta_hardware/include/invicta_hardware/hardware_bridge.h index 59ab13e..54e405d 100644 --- a/src/invicta_hardware/include/invicta_hardware/hardware_bridge.h +++ b/src/invicta_hardware/include/invicta_hardware/hardware_bridge.h @@ -1,10 +1,10 @@ /** * @file hardware_bridge.h - * @brief 关节/IMU 数据源与关节指令执行端(SocketCAN + 智身模组 CANFD 协议) + * @brief 关节数据源与关节指令执行端(SocketCAN + 智身模组 CANFD 协议) * * 循环(publish_rate_hz,默认 200 Hz): * 1. ReadHardware() —— 非阻塞收各路总线上的 12 字节反馈帧,更新关节状态/温度/状态码 - * 2. PublishState() —— 发 /joint_states 与 /imu/data + * 2. PublishState() —— 发 /joint_states(IMU 不在本节点:由独立节点发布) * 3. WriteHardware() —— 按控制字发使能/失能,使能后每周期发 MIT 运行帧 * 另按 diagnostics_rate_hz(默认 10 Hz)发 /joint_diagnostics。 * @@ -26,7 +26,6 @@ #pragma once #include -#include #include #include @@ -95,10 +94,8 @@ private: // ---- 参数 ---- std::string joint_state_topic_ = "joint_states"; - std::string imu_topic_ = "imu/data"; std::string command_topic_ = "joint_commands"; std::string diagnostics_topic_ = "joint_diagnostics"; - std::string imu_frame_id_ = "base_link"; std::vector joint_names_; double publish_rate_hz_ = 200.0; double diagnostics_rate_hz_ = 10.0; @@ -119,7 +116,6 @@ private: // ---- ROS ---- ros::NodeHandle nh_; ros::Publisher joint_state_pub_; - ros::Publisher imu_pub_; ros::Publisher diagnostics_pub_; ros::Subscriber command_sub_; @@ -140,9 +136,6 @@ private: std::vector temperature_, voltage_; std::vector driver_status_, last_status_; std::vector has_feedback_; ///< 该关节是否收到过反馈(诊断只报有数据的) - double imu_orientation_[4] = {1.0, 0.0, 0.0, 0.0}; ///< w,x,y,z - double imu_angular_velocity_[3] = {0.0, 0.0, 0.0}; - double imu_linear_acceleration_[3] = {0.0, 0.0, 0.0}; std::atomic running_{false}; bool warned_no_command_ = false; diff --git a/src/invicta_hardware/include/invicta_hardware/hardware_config.h b/src/invicta_hardware/include/invicta_hardware/hardware_config.h index eb423ef..7267a43 100644 --- a/src/invicta_hardware/include/invicta_hardware/hardware_config.h +++ b/src/invicta_hardware/include/invicta_hardware/hardware_config.h @@ -75,12 +75,11 @@ public: double bench_joint_kp = 150., bench_joint_kd = 2., bench_wheel_kd = 0.5; // ---- 本节点话题与频率 ---- - std::string topic_joint_states = "joint_states", topic_imu = "imu/data"; + std::string topic_joint_states = "joint_states"; std::string topic_joint_commands = "joint_commands"; std::string topic_joint_diagnostics = "joint_diagnostics"; double publish_rate_hz = 200.0; double diagnostics_rate_hz = 10.0; ///< 温度/状态/电压发布频率(低频诊断,不进控制路径) - std::string imu_frame_id = "base_link"; int command_queue = 1; bool require_can = true; ///< false = 无 CAN 也启动(调试/冒烟测试) @@ -259,14 +258,12 @@ inline bool HardwareConfig::Load(const std::string& path, std::string* error) { const toml::table* topics = detail::Section(tbl, "topics"); detail::GetStr(topics, "joint_states", topic_joint_states); - detail::GetStr(topics, "imu", topic_imu); detail::GetStr(topics, "joint_commands", topic_joint_commands); detail::GetStr(topics, "joint_diagnostics", topic_joint_diagnostics); const toml::table* hw = detail::Section(tbl, "hardware"); detail::Get(hw, "publish_rate_hz", publish_rate_hz); detail::Get(hw, "diagnostics_rate_hz", diagnostics_rate_hz); - detail::GetStr(hw, "imu_frame_id", imu_frame_id); detail::Get(hw, "command_queue", command_queue); detail::Get(hw, "command_timeout_s", command_timeout_s); if (hw) { @@ -283,8 +280,8 @@ inline bool HardwareConfig::Load(const std::string& path, std::string* error) { inline std::string HardwareConfig::Summary() const { char buf[512]; std::string s; - snprintf(buf, sizeof(buf), "[config] %d joints, %zu legs, state publish %.0f Hz, IMU frame=%s\n", dof, legs.size(), - publish_rate_hz, imu_frame_id.c_str()); + snprintf(buf, sizeof(buf), "[config] %d joints, %zu legs, state publish %.0f Hz\n", dof, legs.size(), + publish_rate_hz); s += buf; for (const auto& leg : legs) { snprintf(buf, sizeof(buf), "[config] %s: swing %s(%s#%d) wheel %s(%s#%d)\n", leg.name.c_str(), diff --git a/src/invicta_hardware/launch/invicta_hardware.launch b/src/invicta_hardware/launch/invicta_hardware.launch index b88bf84..beef8e7 100644 --- a/src/invicta_hardware/launch/invicta_hardware.launch +++ b/src/invicta_hardware/launch/invicta_hardware.launch @@ -1,5 +1,5 @@ - + diff --git a/src/invicta_hardware/src/invicta_hardware_node.cpp b/src/invicta_hardware/src/invicta_hardware_node.cpp index c47dc69..fc294e7 100644 --- a/src/invicta_hardware/src/invicta_hardware_node.cpp +++ b/src/invicta_hardware/src/invicta_hardware_node.cpp @@ -1,6 +1,6 @@ /** * @file invicta_hardware_node.cpp - * @brief 关节/IMU 数据源 + 关节指令执行端(SocketCAN,智身模组 CANFD 协议) + * @brief 关节数据源 + 关节指令执行端(SocketCAN,智身模组 CANFD 协议) */ #include #include @@ -53,15 +53,13 @@ HardwareBridge::HardwareBridge(const ros::NodeHandle& pnh) : nh_() { wheel_range_ = ToRange(cfg.motor_wheel); joint_state_pub_ = nh_.advertise(joint_state_topic_, 1); - imu_pub_ = nh_.advertise(imu_topic_, 1); diagnostics_pub_ = nh_.advertise(diagnostics_topic_, 1); command_sub_ = nh_.subscribe(command_topic_, command_queue_, &HardwareBridge::CommandCallback, this); ready_ = OpenBuses(); - ROS_INFO("[invicta_hardware] %s -> %s/%s, subscribing %s @ %.0f Hz; diagnostics %s @ %.0f Hz", joint_state_topic_.c_str(), - imu_topic_.c_str(), imu_frame_id_.c_str(), command_topic_.c_str(), publish_rate_hz_, - diagnostics_topic_.c_str(), diagnostics_rate_hz_); + ROS_INFO("[invicta_hardware] %s -> %s @ %.0f Hz; diagnostics %s @ %.0f Hz", joint_state_topic_.c_str(), + command_topic_.c_str(), publish_rate_hz_, diagnostics_topic_.c_str(), diagnostics_rate_hz_); } bool HardwareBridge::OpenBuses() { @@ -117,10 +115,8 @@ void HardwareBridge::LoadParams(const ros::NodeHandle& pnh) { // 参数来自本节点自己的 config/invicta_hardware.toml const auto& cfg = Config(); joint_state_topic_ = cfg.topic_joint_states; - imu_topic_ = cfg.topic_imu; command_topic_ = cfg.topic_joint_commands; diagnostics_topic_ = cfg.topic_joint_diagnostics; - imu_frame_id_ = cfg.imu_frame_id; publish_rate_hz_ = cfg.publish_rate_hz; diagnostics_rate_hz_ = cfg.diagnostics_rate_hz; command_queue_ = cfg.command_queue; @@ -499,25 +495,6 @@ void HardwareBridge::PublishState() { js.velocity = joint_velocity_; js.effort = joint_effort_; joint_state_pub_.publish(js); - - sensor_msgs::Imu imu; - imu.header.stamp = js.header.stamp; - imu.header.frame_id = imu_frame_id_; - imu.orientation.w = imu_orientation_[0]; - imu.orientation.x = imu_orientation_[1]; - imu.orientation.y = imu_orientation_[2]; - imu.orientation.z = imu_orientation_[3]; - imu.angular_velocity.x = imu_angular_velocity_[0]; - imu.angular_velocity.y = imu_angular_velocity_[1]; - imu.angular_velocity.z = imu_angular_velocity_[2]; - imu.linear_acceleration.x = imu_linear_acceleration_[0]; - imu.linear_acceleration.y = imu_linear_acceleration_[1]; - imu.linear_acceleration.z = imu_linear_acceleration_[2]; - // 首元素置 0 表示「该字段可用」;若某字段确实拿不到,必须置 -1,让使用方知道不能信 - imu.orientation_covariance[0] = 0.; - imu.angular_velocity_covariance[0] = 0.; - imu.linear_acceleration_covariance[0] = 0.; - imu_pub_.publish(imu); } void HardwareBridge::PublishDiagnostics() { diff --git a/src/invicta_msgs/include/invicta_msgs/error_code.h b/src/invicta_msgs/include/invicta_msgs/error_code.h new file mode 100644 index 0000000..992ff36 --- /dev/null +++ b/src/invicta_msgs/include/invicta_msgs/error_code.h @@ -0,0 +1,94 @@ +/** + * @file error_code.h + * @brief 机器人错误位与协议错误码的共用定义,供 invicta_deploy(产生)与 invicta_remote(上报)共用 + * + * 上游对应关系:invicta_deploy 的 `RobotErrorState`(safe_controller.hpp)是一个 32 位位域 + * union,位序**必须**与本文件的 RobotErrorBit 一致(那里是位域声明顺序,这里是显式掩码)。 + * deploy 通过 invicta_msgs/MotionState.msg 的 `error_code` 字段把掩码发出来, + * remote 用 RobotErrorList() 展开成协议 1002/3 的 ErrorList(proto.md §3.4)。 + */ +#pragma once + +#include +#include + +namespace invicta_msgs { + +/// 机器人内部错误位。位序与 invicta_deploy `RobotErrorState` 的位域声明顺序一一对应。 +enum RobotErrorBit : uint32_t { + kRobotErrImuNum = 1u << 0, ///< IMU 数据含 NaN + kRobotErrImuOvertime = 1u << 1, ///< IMU 断流 / 不可信(超过 rl_imu_timeout_s 无新鲜有效帧) + kRobotErrCommandHeartbeatOvertime = 1u << 2, ///< 与上位机心跳超时 + kRobotErrDriver = 1u << 3, ///< 关节驱动器状态字异常 + kRobotErrMotorHeat = 1u << 4, ///< 电机过温预警 + kRobotErrJointNum = 1u << 5, ///< 关节数据含 NaN / 数量不符 + kRobotErrJointUpdate = 1u << 6, ///< 关节数据不更新 + kRobotErrCpuHeat = 1u << 7, ///< CPU 过温 + kRobotErrCpuFreq = 1u << 8, ///< CPU 降频 +}; + +/// 协议错误码(proto.md §3.4,仅列本工程会用到的部分) +enum ProtocolErrorCode : int { + kProtoErrNone = 0, + kProtoErrMotorTempWarn = 0x8001, ///< 电机温度预警 + kProtoErrDriverCommTimeout = 0x8012, ///< 与关节驱动器通讯超时 + kProtoErrEncoderNoValue = 0x8016, ///< 编码器无值 + kProtoErrBodyPosture = 0x8027, ///< 机身姿态错误 + kProtoErrMotionPosture = 0x8029, ///< 运动姿态错误 + kProtoErrCpuTempWarn = 0x8202, ///< CPU 温度过高预警 + kProtoErrCpuFreqProtect = 0x8211, ///< CPU 占用率过高保护 +}; + +/// 协议 1002/3 ErrorList 的一项 +struct RobotErrorItem { + int error_code; ///< 协议 errorCode + int component; ///< bit 位标识部件(关节/电池);本工程暂无按关节定位的信息,填 0 = 整机 + const char* bit_name; ///< 触发的内部错误位名(日志用) +}; + +inline const char* RobotErrorBitName(uint32_t bit) { + switch (bit) { + case kRobotErrImuNum: return "IMU data contains NaN"; + case kRobotErrImuOvertime: return "IMU stale or unusable"; + case kRobotErrCommandHeartbeatOvertime: return "command heartbeat overtime"; + case kRobotErrDriver: return "driver fault"; + case kRobotErrMotorHeat: return "motor overheat warning"; + case kRobotErrJointNum: return "joint data invalid"; + case kRobotErrJointUpdate: return "joint data not updating"; + case kRobotErrCpuHeat: return "CPU overheat warning"; + case kRobotErrCpuFreq: return "CPU frequency warning"; + default: return "unknown"; + } +} + +/// 内部错误位 → 协议 errorCode。协议表里没有对应项时返回 kProtoErrNone(只记日志不上报)。 +inline int ProtocolCodeForRobotError(uint32_t bit) { + switch (bit) { + // IMU 不新鲜/姿态不可信,对上位机表现为机身姿态错误;解算结果含 NaN 则算运动姿态错误 + case kRobotErrImuOvertime: return kProtoErrBodyPosture; + case kRobotErrImuNum: return kProtoErrMotionPosture; + case kRobotErrMotorHeat: return kProtoErrMotorTempWarn; + case kRobotErrDriver: return kProtoErrDriverCommTimeout; + case kRobotErrJointNum: + case kRobotErrJointUpdate: return kProtoErrEncoderNoValue; + case kRobotErrCpuHeat: return kProtoErrCpuTempWarn; + case kRobotErrCpuFreq: return kProtoErrCpuFreqProtect; + // 心跳超时在 proto.md §3.4 的错误码表里没有对应项:只记日志,不编造错误码 + case kRobotErrCommandHeartbeatOvertime: return kProtoErrNone; + default: return kProtoErrNone; + } +} + +/// 把内部错误位掩码展开成协议 ErrorList,跳过没有对应协议码的位。 +inline std::vector RobotErrorList(uint32_t mask) { + std::vector items; + for (uint32_t bit = 1u; bit != 0u; bit <<= 1) { + if ((mask & bit) == 0u) continue; + const int code = ProtocolCodeForRobotError(bit); + if (code == kProtoErrNone) continue; + items.push_back(RobotErrorItem{code, 0, RobotErrorBitName(bit)}); + } + return items; +} + +} // namespace invicta_msgs diff --git a/src/invicta_msgs/msg/MotionState.msg b/src/invicta_msgs/msg/MotionState.msg index cd5b73e..2030ead 100644 --- a/src/invicta_msgs/msg/MotionState.msg +++ b/src/invicta_msgs/msg/MotionState.msg @@ -12,4 +12,5 @@ uint8 usage_mode # 使用模式:0 常规 · 1 导航 · 2 辅助 uint8 charge # 0 空闲 · 1 前往充电桩 · 2 充电中 · 3 退出充电桩 · 4 异常 · 5 在桩未充电 uint8 hes # 硬急停:0 未触发 · 1 已触发 uint8 sleep # 0 未休眠 · 1 已休眠 · 2 进入休眠中 +uint32 error_code # 机器人内部错误位掩码,0 = 正常;位定义见 invicta_msgs/error_code.h string version # 机型标识 diff --git a/src/invicta_remote/include/invicta_remote/remote_node.h b/src/invicta_remote/include/invicta_remote/remote_node.h index 63f327e..3307014 100644 --- a/src/invicta_remote/include/invicta_remote/remote_node.h +++ b/src/invicta_remote/include/invicta_remote/remote_node.h @@ -39,6 +39,7 @@ #include "invicta_msgs/JointDiagnostics.h" #include "invicta_msgs/MotionCommand.h" #include "invicta_msgs/MotionState.h" +#include "invicta_msgs/error_code.h" #include "remote_config.h" #include "json.hpp" #include "protocol_defs.h" @@ -210,6 +211,7 @@ private: actual_charge_ = msg->charge; actual_hes_ = msg->hes; actual_sleep_ = msg->sleep; + actual_error_code_ = msg->error_code; actual_version_ = msg->version; have_state_ = true; } @@ -415,7 +417,21 @@ private: json ErrorDoc(int type, int command) { json doc; - doc["PatrolDevice"]["Items"]["ErrorList"] = json::array(); // 暂无错误源,空列表 + json list = json::array(); + uint32_t mask = 0; + { + std::lock_guard lock(state_mutex_); + mask = actual_error_code_; + } + // 内部错误位 → 协议 errorCode(proto.md §3.4);没有对应协议码的位(如心跳超时)只在上报里省略 + for (const auto& item : RobotErrorList(mask)) { + json e; + e["errorCode"] = item.error_code; + e["component"] = item.component; + list.push_back(e); + } + if (list.empty()) list = json::array(); + doc["PatrolDevice"]["Items"]["ErrorList"] = list; doc["PatrolDevice"]["Type"] = type; doc["PatrolDevice"]["Command"] = command; doc["PatrolDevice"]["Time"] = CurrentDateTime(); @@ -708,6 +724,8 @@ private: uint32_t actual_gait_ = kGaitBasic; uint8_t actual_usage_mode_ = kUsageNormal; uint8_t actual_charge_ = 0, actual_hes_ = 0, actual_sleep_ = 0; + /// 机器人内部错误位掩码(来自 /motion_state);展开成协议 ErrorList 见 invicta_msgs/error_code.h + uint32_t actual_error_code_ = 0; std::string actual_version_ = "INVICTA"; bool have_state_ = false; std::vector joint_names_;