Files
huoyi_deploy/src/invicta_remote/include/invicta_remote/remote_node.h
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

745 lines
34 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 remote_node.h
* @brief 遥控/设备协议节点:UDP + JSON 报文 ↔ ROS 话题
*
* 职责边界(见 proto.md):
* - 报文层:Header 校验、JSON 解析、错误码(0xE0xx)、MsgId 配对
* - 语义层:只做「语法/枚举/类型」校验,以及协议明确规定的状态前提(如步态仅 RL 下有效);
* 运动状态是否真的切过去,由 invicta_deploy 决定,并通过 /motion_state 回报真实状态
* (协议里 ErrorCode=0 的含义是「命令已接收并触发,不代表业务成功」)
* - 状态上报:1002/6 基础 2 Hz、1002/4 运控 10 Hz、1002/5 设备 2 Hz、1002/3 异常 2 Hz。
* 默认对**所有在线客户端**推送(老版 App 不发 1002 查询,只靠推送);`telemetry/require_subscription`
* 设为 true 则退回协议原文的「先查询再推送」。
*/
#pragma once
#include <ros/ros.h>
#include <sensor_msgs/Imu.h>
#include <sensor_msgs/JointState.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <ctime>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "heartbeat.h"
#include "invicta_msgs/JointCommandArray.h"
#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"
#include "udp_server.h"
namespace invicta_remote {
using namespace invicta_msgs; // MotionParam / GaitParam / UsageMode 及名字函数
using json = nlohmann::json;
class RemoteNode {
public:
explicit RemoteNode(const ros::NodeHandle& pnh) : pnh_(pnh), cfg_(invicta_remote::Config()) {
// 参数来自本节点自己的 config/invicta_remote.toml
udp_port_ = cfg_.udp_port;
motion_command_topic_ = cfg_.topic_motion_command;
motion_state_topic_ = cfg_.topic_motion_state;
joint_states_topic_ = cfg_.topic_joint_states;
imu_topic_ = cfg_.topic_imu;
joint_diagnostics_topic_ = cfg_.topic_joint_diagnostics;
heartbeat_timeout_s_ = cfg_.heartbeat_timeout_s;
unsupported_as_success_ = cfg_.unsupported_as_success;
telemetry_enabled_ = cfg_.telemetry_enabled;
require_subscription_ = cfg_.telemetry_require_subscription;
basic_hz_ = cfg_.telemetry_basic_hz;
motion_hz_ = cfg_.telemetry_motion_hz;
device_hz_ = cfg_.telemetry_device_hz;
error_hz_ = cfg_.telemetry_error_hz;
heartbeat_ = std::make_unique<Heartbeat>(heartbeat_timeout_s_);
motion_command_pub_ = nh_.advertise<invicta_msgs::MotionCommand>(motion_command_topic_, 1);
motion_state_sub_ = nh_.subscribe(motion_state_topic_, 10, &RemoteNode::MotionStateCallback, this);
joint_state_sub_ = nh_.subscribe(joint_states_topic_, 10, &RemoteNode::JointStateCallback, this);
imu_sub_ = nh_.subscribe(imu_topic_, 10, &RemoteNode::ImuCallback, this);
diagnostics_sub_ =
nh_.subscribe(joint_diagnostics_topic_, 10, &RemoteNode::JointDiagnosticsCallback, this);
RegisterHandlers();
}
~RemoteNode() { StopTelemetry(); }
void Start() {
udp_server_ = std::make_unique<UdpServer>([this](const Frame& f) { return OnFrame(f); }, udp_port_);
StartTelemetry();
ROS_INFO_STREAM("[invicta_remote] protocol node ready: " << motion_command_topic_ << " <- " << motion_state_topic_);
ROS_INFO("[invicta_remote] implemented: heartbeat / axis command(2,21) / motion state(2,22) / gait(2,23) / "
"usage mode(1101,5 normal only) / key(1101,12)");
ROS_INFO("[invicta_remote] telemetry: 1002/6 %.0f Hz, 1002/4 %.0f Hz, 1002/5 %.0f Hz, 1002/3 %.0f Hz"
"(%s)", basic_hz_, motion_hz_, device_hz_, error_hz_,
require_subscription_ ? "query subscription required" : "push autonomously");
ROS_INFO("[invicta_remote] not implemented: light, sleep, charge, navigation, velocity command (reply 0xE00A)");
}
private:
struct Result {
int code = kSuccess;
std::string message;
static Result Ok() { return Result{}; }
static Result Err(int c, const std::string& m = "") {
Result r;
r.code = c;
r.message = m.empty() ? ErrorMessage(c) : m;
return r;
}
};
using Handler = std::function<Result(json& items, const Frame& frame)>;
// ---------------------------------------------------------------- 帧入口
std::string OnFrame(const Frame& frame) {
heartbeat_->Touch(frame.ip, frame.port);
if (!frame.format_ok) {
return BuildResponse(0, 0, Result::Err(kErrUnsupportedDataFormat));
}
json doc;
try {
doc = json::parse(frame.body);
} catch (const std::exception& e) {
std::cout << "[GAMEPAD] JSON parse failed: " << e.what() << std::endl;
return BuildResponse(0, 0, Result::Err(kErrDataParsingFailed));
}
if (!doc.is_object() || !doc.contains("PatrolDevice") || !doc["PatrolDevice"].is_object()) {
return BuildResponse(0, 0, Result::Err(kErrDataParsingFailed));
}
json& root = doc["PatrolDevice"];
if (!root.contains("Type") || !root.contains("Command") || !root["Type"].is_number_integer() ||
!root["Command"].is_number_integer()) {
return BuildResponse(0, 0, Result::Err(kErrDataParsingFailed));
}
const int type = root["Type"].get<int>();
const int command = root["Command"].get<int>();
json items = root.contains("Items") && root["Items"].is_object() ? root["Items"] : json::object();
LogReceived(type, command, items);
auto it = handlers_.find({type, command});
if (it == handlers_.end()) {
return BuildResponse(type, command, Result::Err(kErrUnsupportedProtocolType));
}
// 状态上报类查询:直接回一帧当前数据(同时记录订阅,见 HandleStatusQuery)
if (type == kTypeStatus) {
return StatusQueryResponse(command, items, frame);
}
Result res = it->second(items, frame);
if (res.code != kSuccess) {
std::cout << "[GAMEPAD] Type=" << type << " Command=" << command << " -> 0x" << std::hex << res.code
<< std::dec << " " << res.message << std::endl;
}
return BuildResponse(type, command, res);
}
std::string BuildResponse(int type, int command, const Result& res) const {
json doc;
doc["PatrolDevice"]["Items"]["ErrorCode"] = res.code;
doc["PatrolDevice"]["Items"]["ErrorMessage"] = res.message;
doc["PatrolDevice"]["Type"] = type;
doc["PatrolDevice"]["Command"] = command;
doc["PatrolDevice"]["Time"] = CurrentDateTime();
return doc.dump();
}
static std::string CurrentDateTime() {
char buf[32];
std::time_t t = std::time(nullptr);
std::tm tm_buf{};
localtime_r(&t, &tm_buf);
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf);
return std::string(buf);
}
void LogReceived(int type, int command, const json& items) {
const auto now = std::chrono::steady_clock::now();
auto key = std::make_pair(type, command);
auto it = last_rx_log_.find(key);
if (it != last_rx_log_.end() && now - it->second < std::chrono::seconds(1)) return;
last_rx_log_[key] = now;
std::cout << "[GAMEPAD] recv Type=" << type << " Command=" << command << " items=" << items.dump()
<< std::endl;
}
// ---------------------------------------------------------------- 指令发布
void PublishMotionCommand() {
invicta_msgs::MotionCommand msg;
msg.header.stamp = ros::Time::now();
msg.target_state = latest_.target_state;
msg.gait = latest_.gait;
msg.usage_mode = latest_.usage_mode;
msg.x = latest_.x;
msg.y = latest_.y;
msg.yaw = latest_.yaw;
motion_command_pub_.publish(msg);
}
// ---------------------------------------------------------------- 状态缓存
void MotionStateCallback(const invicta_msgs::MotionState::ConstPtr& msg) {
std::lock_guard<std::mutex> lock(state_mutex_);
actual_motion_state_ = msg->motion_state;
actual_gait_ = msg->gait;
actual_usage_mode_ = msg->usage_mode;
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;
}
void JointStateCallback(const sensor_msgs::JointState::ConstPtr& msg) {
std::lock_guard<std::mutex> lock(state_mutex_);
joint_names_ = msg->name;
joint_pos_.assign(msg->position.begin(), msg->position.end());
joint_vel_.assign(msg->velocity.begin(), msg->velocity.end());
// 本机 8 关节(每腿 swing + wheel),不是 M20 的 16;上报时由 FillM20Joints 折叠到 16 槽
have_joints_ = !joint_pos_.empty();
}
void JointDiagnosticsCallback(const invicta_msgs::JointDiagnostics::ConstPtr& msg) {
std::lock_guard<std::mutex> lock(state_mutex_);
diag_names_ = msg->name;
diag_temperature_.assign(msg->temperature.begin(), msg->temperature.end());
diag_status_.assign(msg->status.begin(), msg->status.end());
}
void ImuCallback(const sensor_msgs::Imu::ConstPtr& msg) {
const double w = msg->orientation.w, x = msg->orientation.x, y = msg->orientation.y,
z = msg->orientation.z;
const double n = std::sqrt(w * w + x * x + y * y + z * z);
if (n < 1e-6) return;
const double qw = w / n, qx = x / n, qy = y / n, qz = z / n;
std::lock_guard<std::mutex> lock(state_mutex_);
rpy_[0] = std::atan2(2.0 * (qw * qx + qy * qz), 1.0 - 2.0 * (qx * qx + qy * qy));
rpy_[1] = std::asin(std::max(-1.0, std::min(1.0, 2.0 * (qw * qy - qz * qx))));
rpy_[2] = std::atan2(2.0 * (qw * qz + qx * qy), 1.0 - 2.0 * (qy * qy + qz * qz));
omega_z_ = msg->angular_velocity.z;
have_imu_ = true;
}
// ---------------------------------------------------------------- M20 16 关节布局
// 协议 §3.2 固定 16 槽:每腿 [HipX, HipY, Knee, Wheel],腿序 前左/前右/后左/后右。
// 本机每腿只有 swing + wheel:HipY ← 摆臂角,Wheel ← 轮转速。
// HipX(侧摆)与 Knee(膝)在单连杆腿上不存在,恒为 0——这不是缺数据,而是当前机构
// 「无侧摆、腿笔直」的正确取值,客户端按 16 槽解析时姿态不会异常。
// 保留 16 槽而不是裁成 8 槽,是为了让同一套 App 不必区分机型。
static constexpr int kM20Joints = 16;
static const char* M20JointName(int slot) {
static const char* kNames[kM20Joints] = {
"LeftFrontHipX", "LeftFrontHipY", "LeftFrontKnee", "LeftFrontWheel",
"RightFrontHipX", "RightFrontHipY", "RightFrontKnee", "RightFrontWheel",
"LeftBackHipX", "LeftBackHipY", "LeftBackKnee", "LeftBackWheel",
"RightBackHipX", "RightBackHipY", "RightBackKnee", "RightBackWheel"};
return kNames[slot];
}
/// 关节名 → 腿序号(0 前左 fl、1 前右 fr、2 后左 hl、3 后右 hr);不认识返回 -1
static int LegIndexFromJointName(const std::string& n) {
if (n.compare(0, 2, "fl") == 0) return 0;
if (n.compare(0, 2, "fr") == 0) return 1;
if (n.compare(0, 2, "hl") == 0) return 2;
if (n.compare(0, 2, "hr") == 0) return 3;
return -1;
}
/**
* @brief 一组关节名 → M20 的 16 槽
*
* @param names 关节名(/joint_states 或 /joint_diagnostics 的 name)
* @param value_count 该消息实际带的数据条数(无 name 时按它做下标回退)
* @param slot_index 输出:槽 → names 下标,-1 表示本机没有这个自由度
*
* HipX(侧摆)与 Knee(膝)在单连杆腿上不存在,永远 -1;swing → HipY,wheel → Wheel。
* 优先按 name 解析(fl_swing / fl_wheel …),无 name 时退回「每腿两个、先 swing 后 wheel」。
*/
static void BuildSlotToIndex(const std::vector<std::string>& names, size_t value_count,
int slot_index[kM20Joints]) {
for (int i = 0; i < kM20Joints; ++i) slot_index[i] = -1;
const bool by_name = !names.empty() && names.size() == value_count;
const size_t n = by_name ? names.size() : std::min(value_count, size_t(kM20Joints / 2));
for (size_t k = 0; k < n; ++k) {
int leg = -1;
bool is_wheel = false;
if (by_name) {
leg = LegIndexFromJointName(names[k]);
if (leg < 0) continue;
const bool wheel = names[k].find("wheel") != std::string::npos;
const bool swing = names[k].find("swing") != std::string::npos;
if (!wheel && !swing) continue;
is_wheel = wheel;
} else {
leg = int(k / 2);
is_wheel = (k % 2 == 1);
}
slot_index[leg * 4 + (is_wheel ? 3 : 1)] = int(k);
}
}
/// 16 槽取值:协议里 Joint 数组与具名字段的混合量(角度,轮为 rad/s)
void FillM20Joints(double value[kM20Joints]) const {
for (int i = 0; i < kM20Joints; ++i) value[i] = 0.0;
int slot_index[kM20Joints];
BuildSlotToIndex(joint_names_, std::min(joint_pos_.size(), joint_vel_.size()), slot_index);
for (int slot = 0; slot < kM20Joints; ++slot) {
const int k = slot_index[slot];
if (k < 0) continue;
value[slot] = (slot % 4 == 3) ? joint_vel_[size_t(k)] : joint_pos_[size_t(k)];
}
}
// ---------------------------------------------------------------- 状态上报
json BasicDoc(int type, int command) {
std::lock_guard<std::mutex> lock(state_mutex_);
json doc;
json& items = doc["PatrolDevice"]["Items"];
auto& bs = items["BasicStatus"];
bs["MotionState"] = int(actual_motion_state_);
bs["Gait"] = int(actual_gait_ == 0 ? kGaitBasic : actual_gait_);
bs["Charge"] = int(actual_charge_);
bs["HES"] = int(actual_hes_);
bs["ControlUsageMode"] = int(actual_usage_mode_);
bs["Direction"] = 0;
bs["OOA"] = 0;
bs["PowerManagement"] = 0;
bs["Sleep"] = int(actual_sleep_);
bs["Version"] = actual_version_;
doc["PatrolDevice"]["Type"] = type;
doc["PatrolDevice"]["Command"] = command;
doc["PatrolDevice"]["Time"] = CurrentDateTime();
return doc;
}
json MotionDoc(int type, int command) {
std::lock_guard<std::mutex> lock(state_mutex_);
json doc;
json& items = doc["PatrolDevice"]["Items"];
auto& ms = items["MotionStatus"];
ms["Roll"] = rpy_[0];
ms["Pitch"] = rpy_[1];
ms["Yaw"] = rpy_[2];
ms["OmegaZ"] = omega_z_;
ms["LinearX"] = 0.0; // 待硬件/里程计
ms["LinearY"] = 0.0;
ms["Height"] = 0.0; // 待运动学估计
ms["Payload"] = 0.0;
ms["RemainMile"] = 0.0; // 待电池/里程
auto& motor = items["MotorStatus"];
double value[kM20Joints];
FillM20Joints(value);
if (!have_joints_ && std::chrono::steady_clock::now() - start_time_ > std::chrono::seconds(2)) {
ROS_WARN_THROTTLE(30.0, "[invicta_remote] %s not received yet, 1002/4 joints reported as 0",
joint_states_topic_.c_str());
}
motor["Joint"] = std::vector<double>(value, value + kM20Joints);
for (int i = 0; i < kM20Joints; ++i) motor[M20JointName(i)] = value[i];
doc["PatrolDevice"]["Type"] = type;
doc["PatrolDevice"]["Command"] = command;
doc["PatrolDevice"]["Time"] = CurrentDateTime();
return doc;
}
json DeviceDoc(int type, int command) {
std::lock_guard<std::mutex> lock(state_mutex_);
json doc;
json& items = doc["PatrolDevice"]["Items"];
// 电池 / CPU / GPS 还没有数据源,用 0 占位;电机温度来自 /joint_diagnostics
json battery = {{"Voltage", 0.0}, {"BatteryLevel", 0.0}, {"battery_temperature", 0.0},
{"charge", false}, {"serial", ""}};
items["BatteryList"] = json::array({battery, battery});
items["BatteryStatus"] = {{"VoltageLeft", 0.0}, {"VoltageRight", 0.0},
{"BatteryLevelLeft", 0.0}, {"BatteryLevelRight", 0.0},
{"battery_temperatureLeft", 0.0}, {"battery_temperatureRight", 0.0},
{"chargeLeft", false}, {"chargeRight", false}};
// 温度按 M20 布局展开:本机 8 个电机落在对应槽(swing → HipY、wheel → Wheel),
// 其余槽没有对应电机填 0;驱动器温度本协议不返回,恒 0。
int slot_index[kM20Joints];
BuildSlotToIndex(diag_names_, diag_temperature_.size(), slot_index);
json motor_temp = json::array();
json driver_temp = json::array();
for (int slot = 0; slot < kM20Joints; ++slot) {
const int k = slot_index[slot];
motor_temp.push_back((k >= 0 && size_t(k) < diag_temperature_.size()) ? diag_temperature_[size_t(k)] : 0.0);
driver_temp.push_back(0.0);
}
items["DeviceTemperature"] = {{"Motor", motor_temp}, {"Driver", driver_temp}};
auto& temps = items["DeviceTemperature"];
for (int slot = 0; slot < kM20Joints; ++slot) {
const std::string base = M20JointName(slot);
temps[base + "Motor"] = motor_temp[slot];
temps[base + "Driver"] = 0.0;
}
items["Led"] = {{"Fill", {{"Front", 0}, {"Back", 0}}}};
items["GPS"] = {{"Latitude", 0.0}, {"Longitude", 0.0}, {"Speed", 0.0}, {"Course", 0.0},
{"FixQuality", 0}, {"NumSatellites", 0}, {"Altitude", 0.0},
{"HDOP", 0.0}, {"VDOP", 0.0}, {"PDOP", 0.0}, {"VisibleSatellites", 0}};
items["DevEnable"] = {{"FanSpeed", 100}, {"LoadPower", 1}, {"LedHost", 1}, {"LedExt", 1}, {"FP", 1},
{"Lidar", {{"Front", 1}, {"Back", 1}}}, {"GPS", 1},
{"Video", {{"Front", 1}, {"Back", 1}}}};
items["CPU"] = {{"AOS", {{"Temperature", 0.0}, {"FrequencyInt", 0.0}, {"FrequencyApp", 0.0}}},
{"NOS", {{"Temperature", 0.0}, {"FrequencyInt", 0.0}, {"FrequencyApp", 0.0}}},
{"GOS", {{"Temperature", 0.0}, {"FrequencyInt", 0.0}, {"FrequencyApp", 0.0}}}};
doc["PatrolDevice"]["Type"] = type;
doc["PatrolDevice"]["Command"] = command;
doc["PatrolDevice"]["Time"] = CurrentDateTime();
return doc;
}
json ErrorDoc(int type, int command) {
json doc;
json list = json::array();
uint32_t mask = 0;
{
std::lock_guard<std::mutex> 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();
return doc;
}
/// 查询响应:把对应上报的当前值作为响应返回(协议 §3 的「查询触发」)
std::string StatusQueryResponse(int command, const json&, const Frame& frame) {
heartbeat_->SetSubscribed(frame.ip, frame.port, command);
if (!subscribed_logged_.count({frame.ip, command})) {
subscribed_logged_.insert({frame.ip, command});
std::cout << "[GAMEPAD] " << frame.ip << ":" << frame.port << " query telemetry 1002/" << command
<< std::endl;
}
switch (command) {
case kCmdStatusBasic: return BasicDoc(kTypeStatus, command).dump();
case kCmdStatusMotion: return MotionDoc(kTypeStatus, command).dump();
case kCmdStatusDevice: return DeviceDoc(kTypeStatus, command).dump();
case kCmdStatusError: return ErrorDoc(kTypeStatus, command).dump();
default: return BuildResponse(kTypeStatus, command, Result::Err(kErrUnsupportedProtocolType));
}
}
void StartTelemetry() {
if (!telemetry_enabled_) return;
telemetry_running_ = true;
telemetry_thread_ = std::thread(&RemoteNode::TelemetryLoop, this);
}
void StopTelemetry() {
telemetry_running_ = false;
if (telemetry_thread_.joinable()) telemetry_thread_.join();
}
void TelemetryLoop() {
using clock = std::chrono::steady_clock;
const auto t0 = clock::now();
double next_basic = 0, next_motion = 0, next_device = 0, next_error = 0;
auto now = [&]() { return std::chrono::duration<double>(clock::now() - t0).count(); };
while (ros::ok() && telemetry_running_) {
const double t = now();
if (t >= next_basic) {
next_basic = t + (basic_hz_ > 0 ? 1.0 / basic_hz_ : 0.5);
PushToClients(BasicDoc(kTypeStatus, kCmdStatusBasic).dump(), kCmdStatusBasic);
}
if (t >= next_motion) {
next_motion = t + (motion_hz_ > 0 ? 1.0 / motion_hz_ : 0.1);
PushToClients(MotionDoc(kTypeStatus, kCmdStatusMotion).dump(), kCmdStatusMotion);
}
if (t >= next_device) {
next_device = t + (device_hz_ > 0 ? 1.0 / device_hz_ : 0.5);
PushToClients(DeviceDoc(kTypeStatus, kCmdStatusDevice).dump(), kCmdStatusDevice);
}
if (t >= next_error) {
next_error = t + (error_hz_ > 0 ? 1.0 / error_hz_ : 0.5);
PushToClients(ErrorDoc(kTypeStatus, kCmdStatusError).dump(), kCmdStatusError);
}
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
void PushToClients(const std::string& body, int status_command) {
if (!udp_server_ || body.empty()) return;
for (const auto& c : heartbeat_->GetClients()) {
if (require_subscription_ && !IsSubscribed(c, status_command)) continue;
udp_server_->SendTo(c.ip, c.port, body, next_push_id_++);
}
}
static bool IsSubscribed(const ClientInfo& c, int status_command) {
switch (status_command) {
case kCmdStatusBasic: return c.subscribed_basic;
case kCmdStatusMotion: return c.subscribed_motion;
case kCmdStatusDevice: return c.subscribed_device;
case kCmdStatusError: return c.subscribed_error;
default: return false;
}
}
// ---------------------------------------------------------------- 各命令处理
Result HandleHeartbeat(json&, const Frame& frame) {
const auto now = std::chrono::steady_clock::now();
if (now - last_heartbeat_log_ > std::chrono::seconds(2)) {
last_heartbeat_log_ = now;
std::cout << "[GAMEPAD] heartbeat from " << frame.ip << ":" << frame.port << " (telemetry push started)"
<< std::endl;
}
return Result::Ok();
}
/// 2/21 轴指令(常规模式),比例 [-1,1],协议建议 ≥20 Hz
Result HandleAxis(json& items, const Frame& frame) {
if (!items.contains("X") || !items.contains("Y") || !items.contains("Yaw")) {
return Result::Err(kErrMissingFields, "Missing necessary fields: X/Y/Yaw");
}
if (!items["X"].is_number() || !items["Y"].is_number() || !items["Yaw"].is_number()) {
return Result::Err(kErrMismatchedDataType, "Mismatched data type: X/Y/Yaw must be float");
}
const std::string client = frame.ip + ":" + std::to_string(frame.port);
const auto now = std::chrono::steady_clock::now();
if (!axis_client_.empty() && axis_client_ != client &&
now - axis_client_time_ < std::chrono::seconds(2)) {
return Result::Err(kErrMismatchedClient, "Mismatched request client: " + axis_client_ + " busy");
}
axis_client_ = client;
axis_client_time_ = now;
latest_.x = items["X"].get<float>();
latest_.y = items["Y"].get<float>();
latest_.yaw = items["Yaw"].get<float>();
PublishMotionCommand();
return Result::Ok();
}
/// 2/22 运动状态转换
Result HandleMotionState(json& items, const Frame&) {
if (!items.contains("MotionParam"))
return Result::Err(kErrMissingFields, "Missing necessary fields: MotionParam");
if (!items["MotionParam"].is_number_integer()) {
return Result::Err(kErrMismatchedDataType, "Mismatched data type: MotionParam must be int");
}
const int param = items["MotionParam"].get<int>();
if (!IsKnownMotionParam(param)) {
return Result::Err(kErrUnsupportedFunction, "Unsupported MotionParam: " + std::to_string(param));
}
latest_.target_state = uint8_t(param);
PublishMotionCommand();
return Result::Ok();
}
/// 2/23 步态切换:不同步态对应不同 RL 模型,协议规定仅 RL 控制状态下有效
Result HandleGait(json& items, const Frame&) {
if (!items.contains("GaitParam")) return Result::Err(kErrMissingFields, "Missing necessary fields: GaitParam");
if (!items["GaitParam"].is_number_integer()) {
return Result::Err(kErrMismatchedDataType, "Mismatched data type: GaitParam must be int");
}
const int gait = items["GaitParam"].get<int>();
if (!IsKnownGait(uint32_t(gait))) {
return Result::Err(kErrUnsupportedFunction, "Unsupported GaitParam: " + std::to_string(gait));
}
{
std::lock_guard<std::mutex> lock(state_mutex_);
if (have_state_ && actual_motion_state_ != kMotionRlControl) {
return Result::Err(kErrNotAllowedOperation,
std::string("Not allowed operation: gait switch only valid in RL control state (current ") +
MotionParamName(actual_motion_state_) + ")");
}
}
latest_.gait = uint32_t(gait);
PublishMotionCommand();
return Result::Ok();
}
/// 1101/5 使用模式:0 常规 / 1 导航 / 2 辅助
Result HandleUsageMode(json& items, const Frame&) {
if (!items.contains("Mode")) return Result::Err(kErrMissingFields, "Missing necessary fields: Mode");
if (!items["Mode"].is_number_integer()) {
return Result::Err(kErrMismatchedDataType, "Mismatched data type: Mode must be int");
}
const int mode = items["Mode"].get<int>();
if (mode == kUsageNormal) {
latest_.usage_mode = uint8_t(mode);
PublishMotionCommand();
return Result::Ok();
}
return Unsupported("usage mode " + std::to_string(mode) + " (only normal mode supported)");
}
/// 1101/12 按键事件:新版 App(≥ V1.5.11)用按键,这里映射到同一套运动状态
Result HandleKey(json& items, const Frame&) {
static const std::map<std::string, uint8_t> key_to_motion = {{"G20_KEY_L1", kMotionStanding},
{"G20_KEY_L2", kMotionRlControl},
{"G20_KEY_R1", kMotionLieDown},
{"G20_KEY_R2", kMotionJointDamping}};
if (items.contains("KeyList") && items["KeyList"].is_array()) {
for (const auto& key : items["KeyList"]) {
if (!key.is_string()) continue;
const std::string name = key.get<std::string>();
auto it = key_to_motion.find(name);
if (it != key_to_motion.end()) {
latest_.target_state = it->second;
PublishMotionCommand();
} else {
std::cout << "[GAMEPAD] unknown key " << name << std::endl;
}
}
}
return Result::Ok();
}
/// 未实现的功能:统一返回 0xE00A(可用参数改成回 0)
Result Unsupported(const std::string& what) {
if (unsupported_as_success_) {
std::cout << "[GAMEPAD] " << what << " not implemented, reply 0 per unsupported_as_success=true" << std::endl;
return Result::Ok();
}
std::cout << "[GAMEPAD] " << what << " not implemented, reply 0xE00A" << std::endl;
return Result::Err(kErrUnsupportedFunction, "Unsupported function: " + what);
}
// ---------------------------------------------------------------- 注册
void RegisterHandlers() {
handlers_[{kTypeHeartbeat, kCmdHeartbeat}] = [this](json& i, const Frame& f) { return HandleHeartbeat(i, f); };
handlers_[{kTypeMotion, kCmdAxisControl}] = [this](json& i, const Frame& f) { return HandleAxis(i, f); };
handlers_[{kTypeMotion, kCmdMotionState}] = [this](json& i, const Frame& f) { return HandleMotionState(i, f); };
handlers_[{kTypeMotion, kCmdGait}] = [this](json& i, const Frame& f) { return HandleGait(i, f); };
handlers_[{kTypeMotion, kCmdCharge}] = [this](json&, const Frame&) { return Unsupported("autonomous charging 2/24"); };
handlers_[{kTypeMotion, kCmdVelocity}] = [this](json&, const Frame&) {
return Unsupported("velocity command 2/25 (navigation mode)");
};
handlers_[{kTypeDevice, kCmdLight}] = [this](json&, const Frame&) {
return Unsupported("light 1101/2 (no light hardware on this platform)");
};
handlers_[{kTypeDevice, kCmdUsageMode}] = [this](json& i, const Frame& f) { return HandleUsageMode(i, f); };
handlers_[{kTypeDevice, kCmdSleepSet}] = [this](json&, const Frame&) { return Unsupported("sleep set 1101/6"); };
handlers_[{kTypeDevice, kCmdSleepQuery}] = [this](json&, const Frame&) {
return Unsupported("sleep query 1101/7");
};
handlers_[{kTypeDevice, kCmdKey}] = [this](json& i, const Frame& f) { return HandleKey(i, f); };
// 状态上报:存在性用于入口判断,实际响应在 StatusQueryResponse 里生成
for (int cmd : {kCmdStatusBasic, kCmdStatusMotion, kCmdStatusDevice, kCmdStatusError}) {
handlers_[{kTypeStatus, cmd}] = [](json&, const Frame&) { return Result::Ok(); };
}
}
private:
struct handlers_key_hash {
size_t operator()(const std::pair<int, int>& p) const {
return std::hash<long long>()((static_cast<long long>(p.first) << 32) ^ static_cast<unsigned>(p.second));
}
};
struct ip_cmd_hash {
size_t operator()(const std::pair<std::string, int>& p) const {
return std::hash<std::string>()(p.first) * 131u + static_cast<unsigned>(p.second);
}
};
const invicta_remote::RemoteConfig& cfg_;
ros::NodeHandle nh_;
ros::NodeHandle pnh_;
ros::Publisher motion_command_pub_;
ros::Subscriber motion_state_sub_;
ros::Subscriber joint_state_sub_;
ros::Subscriber imu_sub_;
ros::Subscriber diagnostics_sub_;
std::unique_ptr<Heartbeat> heartbeat_;
std::unique_ptr<UdpServer> udp_server_;
std::unordered_map<std::pair<int, int>, Handler, handlers_key_hash> handlers_;
std::unordered_map<std::pair<int, int>, std::chrono::steady_clock::time_point, handlers_key_hash> last_rx_log_;
std::unordered_set<std::pair<std::string, int>, ip_cmd_hash> subscribed_logged_;
// 参数
int udp_port_ = kUdpDefaultPort;
std::string motion_command_topic_ = "motion_command";
std::string motion_state_topic_ = "motion_state";
std::string joint_states_topic_ = "joint_states";
std::string imu_topic_ = "imu/data";
std::string joint_diagnostics_topic_ = "joint_diagnostics";
double heartbeat_timeout_s_ = 3.0;
bool unsupported_as_success_ = false;
bool telemetry_enabled_ = true;
bool require_subscription_ = false;
double basic_hz_ = 2.0, motion_hz_ = 10.0, device_hz_ = 2.0, error_hz_ = 2.0;
std::atomic<bool> telemetry_running_{false};
std::thread telemetry_thread_;
std::atomic<int> next_push_id_{0};
// 最近一次「指令意图」,每条指令帧都会整体重发(deploy 侧对重复请求做去重)
struct Latest {
uint8_t target_state = kMotionIdle;
uint32_t gait = 0;
uint8_t usage_mode = kUsageNormal;
float x = 0.f, y = 0.f, yaw = 0.f;
} latest_;
std::string axis_client_;
std::chrono::steady_clock::time_point axis_client_time_{};
std::chrono::steady_clock::time_point last_heartbeat_log_{};
// 机器人实际状态(订阅 /motion_state、/joint_states、/imu/data)
std::mutex state_mutex_;
uint8_t actual_motion_state_ = kMotionIdle;
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<std::string> joint_names_;
std::vector<double> joint_pos_, joint_vel_;
bool have_joints_ = false;
/// 启动后 2 s 内不报「还没收到关节状态」:节点刚起来时上游本来就还没发
const std::chrono::steady_clock::time_point start_time_ = std::chrono::steady_clock::now();
std::vector<std::string> diag_names_;
std::vector<double> diag_temperature_;
std::vector<uint8_t> diag_status_;
double rpy_[3] = {0., 0., 0.};
double omega_z_ = 0.;
bool have_imu_ = false;
};
} // namespace invicta_remote