Files
huoyi_deploy/src/invicta_deploy/interface/robot/hardware/invicta_interface.hpp
T
caowangrenbo 7ba2bba450 feat(safety): 错误位上报 1002/3;IMU 断流判定;移除硬件假 IMU
- 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] 重复维护入待办
2026-10-23 23:32:57 +08:00

265 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file invicta_interface.hpp
* @brief ROS1 数据面:订阅关节状态与 IMU,发布关节指令。关节表、话题来自 invicta_deploy.toml
*
* 本层只做**模型帧**的透传:/joint_states 与 /joint_commands 上都是模型量(rad / rad/s / Nm)。
* 硬件的方向 `dir` 与零点 `offset` 由 invicta_hardware 在 raw↔model 的唯一边界处施加
* (见 invicta_hardware/README.md「标定边界」),deploy 不再参与标定。
* 上层状态机与策略只看到模型约定下的量。
*
* 相比之前:删掉了 auto_calibrate / ±360° 回绕标定(那是 M20 三关节腿的绝对编码器问题,
* 本构型摆臂行程只有 ±80°,无回绕),也删掉了本地 dir/offset 换算(已上移到 hardware)。
*/
#pragma once
#include <ros/ros.h>
#include <sensor_msgs/Imu.h>
#include <sensor_msgs/JointState.h>
#include <limits>
#include <string>
#include <unordered_map>
#include <vector>
#include "basic_function.hpp"
#include "common_types.h"
#include "config/deploy_config.h"
#include "invicta_msgs/JointCommandArray.h"
#include "invicta_msgs/control_word.h"
#include "robot_interface.h"
namespace interface {
class InvictaInterface : public RobotInterface {
public:
InvictaInterface(const std::string& robot_name, int dof_num)
: RobotInterface(robot_name, dof_num), cfg_(invicta::Config()) {
if (dof_num_ != cfg_.dof) {
ROS_WARN("[InvictaInterface] passed dof=%d does not match configured %d, using config value", dof_num_, cfg_.dof);
}
joint_pos_ = VecXf::Zero(dof_num_);
joint_vel_ = VecXf::Zero(dof_num_);
joint_tau_ = VecXf::Zero(dof_num_);
joint_data_id_.resize(size_t(dof_num_), 0);
driver_status_.assign(size_t(dof_num_), 1);
for (int i = 0; i < dof_num_; ++i) name2slot_[cfg_.joint_names[size_t(i)]] = i;
cmd_pub_ = nh_.advertise<invicta_msgs::JointCommandArray>(cfg_.topic_joint_commands,
cfg_.queue_joint_commands);
joint_state_sub_ = nh_.subscribe(cfg_.topic_joint_states, cfg_.queue_state,
&InvictaInterface::JointStateHandler, this);
imu_sub_ = nh_.subscribe(cfg_.topic_imu, cfg_.queue_state, &InvictaInterface::ImuHandler, this);
}
virtual ~InvictaInterface() {}
// ------------------------------------------------------------------ 接口实现
virtual void Start() {
SendStartupSequence();
std::cout << "[InvictaInterface] topics: " << cfg_.topic_joint_states << " / " << cfg_.topic_imu << " -> "
<< cfg_.topic_joint_commands << std::endl;
std::cout << "[InvictaInterface] model frame passthrough: dir/offset applied by invicta_hardware" << std::endl;
}
virtual void Stop() {}
virtual double GetInterfaceTimeStamp() { return ri_ts_; }
virtual VecXf GetJointPosition() { return joint_pos_; }
virtual VecXf GetJointVelocity() { return joint_vel_; }
virtual VecXf GetJointTorque() { return joint_tau_; }
virtual Vec3f GetImuRpy() { return rpy_; }
virtual Vec3f GetImuAcc() { return acc_; }
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<double>::max();
return (ros::Time::now() - imu_valid_arrival_).toSec();
}
/// IMU 是否健康(控制线程用;状态由 RefreshRobotData() 每周期更新)
virtual bool IsImuHealthy() { return imu_healthy_; }
virtual std::vector<uint16_t> GetJointDataID() { return joint_data_id_; }
virtual std::vector<uint16_t> GetDriverStatusWord() { return driver_status_; }
virtual void SetJointCommand(Eigen::Matrix<float, Eigen::Dynamic, 5> input) {
joint_cmd_ = input;
invicta_msgs::JointCommandArray msg;
msg.header.stamp = ros::Time::now();
msg.name = cfg_.joint_names;
msg.position.resize(size_t(dof_num_));
msg.velocity.resize(size_t(dof_num_));
msg.effort.resize(size_t(dof_num_));
msg.kp.resize(size_t(dof_num_));
msg.kd.resize(size_t(dof_num_));
for (int i = 0; i < dof_num_; ++i) {
const auto& j = cfg_.Joint(i);
// 模型帧透传:不做 dir/offset 换算(帽子在 invicta_hardware)
msg.position[i] = input(i, 1);
msg.velocity[i] = input(i, 3);
msg.effort[i] = input(i, 4);
msg.kp[i] = (cfg_.wheel_kp_zero && j.is_wheel) ? 0.f : input(i, 0);
msg.kd[i] = input(i, 2);
}
msg.control_word = invicta_msgs::kMotorControl;
cmd_pub_.publish(msg);
}
/// 每个控制周期由状态机调一次:刷新 IMU 连续性判定(断流判坏 + 恢复去抖,见 RefreshImuHealth)
virtual void RefreshRobotData() { RefreshImuHealth(); }
protected:
// ------------------------------------------------------------------ 启动序列
void PublishRawCommand(uint16_t control_word) {
invicta_msgs::JointCommandArray msg;
msg.header.stamp = ros::Time::now();
msg.name = cfg_.joint_names;
msg.position.assign(size_t(dof_num_), 0.);
msg.velocity.assign(size_t(dof_num_), 0.);
msg.effort.assign(size_t(dof_num_), 0.);
msg.kp.assign(size_t(dof_num_), 0.);
msg.kd.assign(size_t(dof_num_), 0.);
msg.control_word = control_word;
cmd_pub_.publish(msg);
}
/// 使能时序:disable → error reset → enable;此后运行期恒为 motor control
/// ⚠ ROS1 的发布没有 latch:订阅端连接建立前发出去的消息会被直接丢掉。
/// 若 hardware 节点后启动(或本节点重启),一次性时序帧会整段丢失,
/// 所以这里先等订阅者出现再发。
void SendStartupSequence() {
for (int i = 0; i < 200 && cmd_pub_.getNumSubscribers() == 0 && ros::ok(); ++i) {
ros::Duration(0.01).sleep();
}
if (cmd_pub_.getNumSubscribers() == 0) {
std::cout << "[InvictaInterface] warning: joint_commands has no subscribers yet, startup sequence may be lost" << std::endl;
}
const uint16_t seq[] = {invicta_msgs::kDisable, invicta_msgs::kErrorReset, invicta_msgs::kEnable};
for (uint16_t word : seq) {
PublishRawCommand(word);
ros::Duration(0.01).sleep();
}
std::cout << "[InvictaInterface] startup control sequence complete (disable -> error_reset -> enable)" << std::endl;
}
// ------------------------------------------------------------------ 回调
void JointStateHandler(const sensor_msgs::JointState::ConstPtr& msg) {
const size_t n = size_t(dof_num_);
if (msg->position.size() != n || msg->velocity.size() != n || msg->effort.size() != n) {
ROS_WARN_THROTTLE(5.0, "[InvictaInterface] %s length should be %d, got pos=%zu vel=%zu eff=%zu, dropping frame",
cfg_.topic_joint_states.c_str(), dof_num_, msg->position.size(), msg->velocity.size(),
msg->effort.size());
return;
}
const bool by_name = msg->name.size() == n;
int unknown = 0;
for (size_t k = 0; k < n; ++k) {
const int i = by_name ? LookupSlot(msg->name[k]) : int(k);
if (i < 0) {
++unknown;
continue;
}
// 模型帧透传:不做 dir/offset 换算(帽子在 invicta_hardware)
joint_pos_(i) = float(msg->position[k]);
joint_vel_(i) = float(msg->velocity[k]);
joint_tau_(i) = float(msg->effort[k]);
}
if (!by_name && !warned_name_mismatch_) {
warned_name_mismatch_ = true;
ROS_WARN("[InvictaInterface] %s did not provide the agreed %d names, resolving by array index",
cfg_.topic_joint_states.c_str(), dof_num_);
}
if (unknown > 0) {
ROS_WARN_THROTTLE(5.0, "[InvictaInterface] %s has %d unknown joint names, ignored",
cfg_.topic_joint_states.c_str(), unknown);
}
for (int i = 0; i < dof_num_; ++i) joint_data_id_[i] = joint_data_frame_;
++joint_data_frame_;
ri_ts_ = msg->header.stamp.toSec();
}
int LookupSlot(const std::string& name) const {
auto it = name2slot_.find(name);
return it == name2slot_.end() ? -1 : it->second;
}
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",
cfg_.topic_imu.c_str());
}
return;
}
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;
}
q.normalize();
rpy_ = q.toRotationMatrix().eulerAngles(0, 1, 2).cast<float>(); // ZYX,与 RpyToRm 一致
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:
const invicta::DeployConfig& cfg_;
ros::NodeHandle nh_;
std::unordered_map<std::string, int> 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;
std::vector<uint16_t> joint_data_id_;
std::vector<uint16_t> driver_status_;
bool warned_name_mismatch_ = false;
bool warned_imu_no_orientation_ = false;
ros::Publisher cmd_pub_;
ros::Subscriber joint_state_sub_, imu_sub_;
};
}; // namespace interface