机器人的检查员
一、它是谁?它在哪?它干嘛的?
机器人软件里有一个模块,专门负责在指令真正执行之前做安全检查。就像机场安检------你上飞机之前,安检员会检查你带没带违禁品、机票对不对。这个模块就是机器人的「安检员」。
它位于整个项目的这个地方:
interaction/src/scheduler/checker/
├── checker.h ← 安检员「模板」(爸爸,定义接口)
├── t1_checker.h ← 一种机型的安检员(儿子,具体实现)
├── t1_checker.cpp
├── q1_checker.h ← 另一种机型的安检员(另一个儿子)
└── q1_checker.cpp
二、继承关系------爸爸和儿子
先看爸爸(checker.h),定义了安检员必须会的 5 个技能:
cpp
class Checker {
public:
// 检查动画能不能做(招手、比心、跳舞等)
virtual int32_t CheckAnimation(int32_t animation_id) = 0;
// 检查移动能不能做(前进、后退、转圈等)
virtual bool CheckMove(int32_t direction) = 0;
// 检查跟随能不能开启/关闭
virtual bool CheckFollowTaskCtrl(bool trigger) = 0;
// 检查编排任务(开箱流程等)
virtual bool CheckArrangeTask(const std::string& task_id) = 0;
// 检查触摸有没有效(有人拍你、摸你)
virtual bool CheckTouch(int32_t type) = 0;
};
= 0意思是「纯虚函数」------爸爸只写名字不写内容,具体怎么检查由儿子来实现。
儿子继承爸爸:
cpp
class T1Checker : public Checker { // 儿子继承了爸爸
public:
int32_t CheckAnimation(int32_t animation_id) override; // 重写:我自己的安检规则
bool CheckMove(int32_t direction) override;
bool CheckFollowTaskCtrl(bool trigger) override;
bool CheckArrangeTask(const std::string& task_id) override;
bool CheckTouch(int32_t type) override;
};
为什么要这样设计? 不同机型安检规则不一样。T1 能变形(四足↔双足),Q1 是人形,各自的"能不能做"判断不同。用继承,调用方只需要调 Checker 的接口,不用关心具体是哪款。
三、5 个对外接口一览
| 接口 | 干什么 | 谁调它 |
|---|---|---|
CheckAnimation(id) |
要跳舞/招手/比心,先过安检 | 遥控 / 语音对话 / 开箱流程 |
CheckMove(direction) |
要前进/后退/转圈,先过安检 | 遥控 / 语音对话 |
CheckFollowTaskCtrl(trigger) |
开启/关闭跟随模式,先过安检 | 遥控 |
CheckArrangeTask(task_id) |
开箱流程等编排任务,先过安检 | 开箱流程 |
CheckTouch(type) |
有人摸你(轻拍/滑动),先过安检 | 触摸传感器(硬件触发) |
所有指令,不管来源是遥控、语音、触摸还是内部流程,真正执行前都要先过安检。
四、最核心的接口:CheckAnimation ------ 安检流水线
这是最重要的方法。任何让你跳舞/招手/比心的请求,都走这条流水线。它有 6 道关卡,源码如下:
cpp
int32_t T1Checker::CheckAnimation(int32_t animation_id) {
// ====== 第①关:机器人正在关机吗? ======
// 如果正在关机中,什么动画都别想做,直接拒绝
if (StateManager::GetInstance()->GetSysState().shutdown_state != ShutDownState::kIdle) {
AIMRTE_WARN("System is in shutdown state, ignore animation request, animation_id: {}", animation_id);
return static_cast<int32_t>(AniErrCode::kFormNotSupportCurrAnimation); // 返回 -1
}
// 把 App 传来的数字(比如 7)转成程序能理解的枚举(kDance)
auto animation = static_cast<T1Animation>(animation_id);
// ====== 第②关:当前形态(四足/双足)支持这个动画吗? ======
// 比如"比心"只有双足能做,四足没有手,不支持
if (not CheckAnimationSupport(animation)) {
return static_cast<int32_t>(AniErrCode::kFormNotSupportCurrAnimation); // 返回 -1
}
// ====== 第③关:四足变形条件满足吗? ======
// 四足变双足,必须站着才能变,躺着/趴着不能变形
if (not CheckQuadrupedTransformateCondition(animation)) {
return static_cast<int32_t>(AniErrCode::kTransformateConditionNotMet); // 返回 -3
}
// ====== 第④关:双足模式下,当前 MC 档位对吗? ======
// 双足的 PresetMotion(招手、比心等)必须在 WBC 等特定档位下才能播放
if (not CheckBipedAction()) {
return static_cast<int32_t>(AniErrCode::kBipedActionNotSupportAni); // 返回 -2
}
// ====== 第⑤关:坐/趴/模式切换不受电量限制,直接放行 ======
// 坐下和趴下是安全动作,不管电量多少都让做
if (animation == T1Animation::kSitDown || animation == T1Animation::kLieDown) {
return static_cast<int32_t>(AniErrCode::kSuccess); // 返回 0,直接放行
}
// 模式切换(室内/户外/超能力/搬运)也不受电量限制
if (animation == T1Animation::MODE_CARRY) {
if (StateManager::GetInstance()->GetMotionState().curr_robot_form != RobotForm::kQuadruped) {
AIMRTE_INFO("搬运模式仅四足形态支持,当前形态不满足");
return static_cast<int32_t>(AniErrCode::kFormNotSupportCurrAnimation);
}
return static_cast<int32_t>(AniErrCode::kSuccess);
}
if (animation == T1Animation::MODE_INDOORS || animation == T1Animation::MODE_OUTDOORS
|| animation == T1Animation::MODE_SUPERPOWER || animation == T1Animation::MODE_CARRY) {
return static_cast<int32_t>(AniErrCode::kSuccess);
}
// ====== 第⑥关:电量太低? ======
auto& pmu_state = StateManager::GetInstance()->GetPmuState();
// last_battery_level_audio 含义:
// 0 = 电量 >30%(正常) 1 = 15-30%(偏低)
// 2 = 5-15%(很低) 3 = 1-5%(极低) 4 = 1%(快没了)
bool is_easy_limit = (!robot_config_.IsIgnoreLowBatteryLimit()
&& pmu_state.last_battery_level_audio >= 4)
|| pmu_state.is_charging;
if (is_easy_limit) {
// 电量接近耗尽(等级 >= 4),所有动画全部禁止
AIMRTE_INFO("当前电量状态过低,限制动画执行,动画id: {}, ...", animation_id);
return static_cast<int32_t>(AniErrCode::kFormNotSupportCurrAnimation);
}
bool is_complex_limit = (!robot_config_.IsIgnoreLowBatteryLimit()
&& pmu_state.last_battery_level_audio >= 4)
|| pmu_state.is_charging;
if (is_complex_limit && !IsEasyMotion(animation)) {
// 电量低时,只允许简单动作(招手、比心等),复杂动作(跳舞)拒绝
AIMRTE_INFO("当前电量状态过低,限制复杂动作执行,动画id: {}, ...", animation_id);
return static_cast<int32_t>(AniErrCode::kFormNotSupportCurrAnimation);
}
// 全部通过!
return static_cast<int32_t>(AniErrCode::kSuccess); // 返回 0
}
错误码说明
| 返回值 | 含义 | 谁返回的 |
|---|---|---|
0 (kSuccess) |
通过 ✅ | --- |
-1 (kFormNotSupportCurrAnimation) |
形态不支持 / 关机中 / 电量耗尽 | ①②⑥ |
-2 (kBipedActionNotSupportAni) |
双足当前档位不合法 | ④ |
-3 (kTransformateConditionNotMet) |
四足变形条件不满足 | ③ |
五、第②关详解:CheckAnimationSupport
"这个动画,当前形态(四足/双足)能做吗?"
cpp
bool T1Checker::CheckAnimationSupport(const T1Animation& animation) {
// ====== 四足形态能做的动画(白名单) ======
std::vector<T1Animation> quadruped_support_animations = {
T1Animation::kGreet, // 打招呼
T1Animation::kRightHandshake, // 右手握手
T1Animation::kLeftHandshake, // 左手握手
T1Animation::kSitDown, // 坐下
T1Animation::kLieDown, // 趴下
T1Animation::kStandUp, // 站起
T1Animation::kTransformateRotate,// 旋转变形
T1Animation::kEasyTransformate, // 简易变形
T1Animation::kProud, // 骄傲
T1Animation::kJoy, // 喜悦
T1Animation::kStretch, // 伸懒腰
T1Animation::kDance, // 跳舞
// ... 还有很多
T1Animation::MODE_INDOORS, // 室内模式
T1Animation::MODE_OUTDOORS, // 户外模式
T1Animation::MODE_SUPERPOWER, // 超能力模式
T1Animation::MODE_CARRY, // 搬运模式
};
// ====== 双足形态能做的动画(白名单) ======
std::vector<T1Animation> biped_support_animations = {
T1Animation::kGreet, // 打招呼
T1Animation::kRightHandshake, // 右手握手
T1Animation::kLeftHandshake, // 左手握手
T1Animation::kBothHandsMakeHeart,// 双手比心 ← 只有双足能做!
T1Animation::kDance, // 跳舞
T1Animation::kDance_C, // 跳舞C
T1Animation::kDance_D, // 跳舞D
// ... 还有很多
T1Animation::MODE_INDOORS,
T1Animation::MODE_OUTDOORS,
T1Animation::MODE_SUPERPOWER,
T1Animation::MODE_CARRY,
};
// 获取当前机器人形态(四足还是双足?)
auto curr_form = StateManager::GetInstance()->GetMotionState().curr_robot_form;
if (curr_form == RobotForm::kQuadruped) {
// 当前是四足 → 在四足白名单里找,找到了就通过
return std::find(quadruped_support_animations.begin(),
quadruped_support_animations.end(),
animation) != quadruped_support_animations.end();
} else if (curr_form == RobotForm::kBiped) {
// 当前是双足 → 在双足白名单里找
return std::find(biped_support_animations.begin(),
biped_support_animations.end(),
animation) != biped_support_animations.end();
} else {
// 四足也不是,双足也不是 → 未知形态,不支持
AIMRTE_WARN("不支持形态curr_robot_form: {}", curr_form);
return false;
}
}
举例:四足形态请求"双手比心"(kBothHandsMakeHeart) → 不在四足白名单 → 拒绝(四足没有手!)
六、第③关详解:CheckQuadrupedTransformateCondition
"四足变双足,必须站着才能变,不能躺着变形。"
cpp
bool T1Checker::CheckQuadrupedTransformateCondition(const T1Animation& animation) {
// 如果不是四足,或者不是变形动画(旋转变形、简易变形)→ 不关这关的事,直接过
if (StateManager::GetInstance()->GetMotionState().curr_robot_form != RobotForm::kQuadruped
|| (animation != T1Animation::kTransformateRotate
&& animation != T1Animation::kEasyTransformate)) {
return true; // 双足发起的请求、或者不是变形动画,这关不管
}
// 走到这里的:一定是「四足形态」发起了「变形动画」
// 检查当前四足的姿势是不是允许变形
auto curr_action_id = StateManager::GetInstance()->GetMotionState().curr_action_id;
if (curr_action_id != aimdk_msgs::msg::McAction::QUADRUPED_STAND_DEFAULT // 101 站立
&& curr_action_id != aimdk_msgs::msg::McAction::QUADRUPED_LOCOMOTION_DEFAULT // 102 行走
&& curr_action_id != aimdk_msgs::msg::McAction::QUADRUPED_LOCOMOTION_TERRAIN // 103 越障
&& curr_action_id != aimdk_msgs::msg::McAction::QUADRUPED_LOCOMOTION_RUN // 112 奔跑
&& curr_action_id != aimdk_msgs::msg::McAction::QUADRUPED_GET_DOWN_DEFAULT // 110 趴下
&& curr_action_id != aimdk_msgs::msg::McAction::STORE_DEFAULT // 1001 收纳
&& curr_action_id != aimdk_msgs::msg::McAction::QUADRUPED_SIT_DOWN_DEFAULT) { // 111 坐下
// 以上姿势都不满足 → 拒绝变形
AIMRTE_ERROR("Invalid action id: {}", curr_action_id);
return false;
}
return true; // 姿势合法,允许变形
}
总结:四足变形只能在站立/行走/越障/奔跑/趴下/收纳/坐下这几种姿势下进行。躺着、悬空等状态拒绝变形。
七、第④关详解:CheckBipedAction
"双足模式下,当前 MC 的档位能不能接收 PresetMotion?"
先理解两个概念:
- 档位(Action):MC(运动控制)的工作模式。比如走路模式、WBC 模式、趴下模式。不同档位能做不同的事,就像汽车的不同档位------P 档不能踩油门走,D 档才能。
- PresetMotion:预设动作片段。在 WBC 模式下插入播放一段动画,比如招手、比心、碰拳。它不是切换档位,而是"在当前档位里插播一条广告"。
cpp
bool T1Checker::CheckBipedAction() {
/*
当形态是双足,上肢动作要求是 WBC
双足变形从 WBC 切到双足行走,再变形,这部分需要 MC 实现
检查只检查 WBC
*/
auto curr_action_id = StateManager::GetInstance()->GetMotionState().curr_action_id; // MC 当前档位
auto curr_form = StateManager::GetInstance()->GetMotionState().curr_robot_form; // 当前形态
// 如果是双足,且当前档位不在白名单里 → 拦截
if (curr_form == RobotForm::kBiped
&& (curr_action_id != aimdk_msgs::msg::McAction::QUADRUPED_TO_BIPED // 10 四足变双足中
&& curr_action_id != aimdk_msgs::msg::McAction::QUADRUPED_TO_BIPED_ROTATE // 11 旋转四变双中
&& curr_action_id != aimdk_msgs::msg::McAction::BIPED_LOCOMOTION_DEFAULT // 202 双足走路
&& curr_action_id != aimdk_msgs::msg::McAction::BIPED_LOCOMOTION_TERRAIN // 203 双足越障
&& curr_action_id != aimdk_msgs::msg::McAction::BIPED_LOCOMOTION_WBC // 300 全身控制 ← 最常用
&& curr_action_id != aimdk_msgs::msg::McAction::BIPED_LOCOMOTION_ANIMATION // 301 动画模式
&& curr_action_id != aimdk_msgs::msg::McAction::BIPED_LOCOMOTION_RUN)) { // 207 双足跑步
return false; // 档位不在白名单,拦截!
}
return true; // 通过
}
白名单(允许播放 PresetMotion 的档位):
| Action | 值 | 什么时候 |
|---|---|---|
| QUADRUPED_TO_BIPED | 10 | 正在四足变双足(过渡态) |
| QUADRUPED_TO_BIPED_ROTATE | 11 | 正在旋转四变双(过渡态) |
| BIPED_LOCOMOTION_DEFAULT | 202 | 双足走路模式 |
| BIPED_LOCOMOTION_TERRAIN | 203 | 双足越障模式 |
| BIPED_LOCOMOTION_WBC | 300 | 双足全身控制 ← 最常用 |
| BIPED_LOCOMOTION_ANIMATION | 301 | 双足动画模式 |
| BIPED_LOCOMOTION_RUN | 207 | 双足跑步模式 |
四足不查这一关 ------因为判断条件第一行就是 curr_form == RobotForm::kBiped,四足形态进来直接 false,跳过。
八、第⑥关详解:电池保护
cpp
// IsEasyMotion:判断是不是"简单动作"。简单动作耗电少,低电量也能做
bool T1Checker::IsEasyMotion(const T1Animation& animation) {
// 招手、握手、比心、拍照摆Pose、碰拳、举手、放下 → 都是简单动作
if (animation == T1Animation::kGreet || animation == T1Animation::kRightHandshake
|| animation == T1Animation::kLeftHandshake || animation == T1Animation::kBothHandsMakeHeart
|| animation == T1Animation::kPhotoPose_1 || animation == T1Animation::kPhotoPose_2
|| animation == T1Animation::kPhotoPose_3 || animation == T1Animation::kPhotoPose_4
|| animation == T1Animation::kPhotoThreeShot || animation == T1Animation::kRightHandPunch
|| animation == T1Animation::kLeftHandPunch || animation == T1Animation::kBothHandsCelebrate
|| animation == T1Animation::kLeftHandUp || animation == T1Animation::kRightHandUp
|| animation == T1Animation::kBothHandsUp || animation == T1Animation::kLeftHandDown
|| animation == T1Animation::kRightHandDown || animation == T1Animation::kBothHandsDown) {
return true;
}
return false; // 跳舞、变形等 → 复杂动作,耗电大
}
焊在第⑥关的逻辑里(前面已贴过完整代码):
- 电量等级 >= 4(电量只剩 1%)→ 所有动画全禁
- 电量低但不是极低 → 只允许简单动作(IsEasyMotion 返回 true 的那些),跳舞等复杂动作拒绝
九、其余 4 个检查接口
CheckMove ------ 移动安检
cpp
bool T1Checker::CheckMove(int32_t direction) {
// 关机中不让动
if (StateManager::GetInstance()->GetSysState().shutdown_state != ShutDownState::kIdle) {
AIMRTE_WARN("System is in shutdown state, ignore move command, direction: {}", direction);
return false;
}
// 极低电量不让移动
auto& pmu_state = StateManager::GetInstance()->GetPmuState();
if ((!robot_config_.IsIgnoreLowBatteryLimit() && pmu_state.last_battery_level_audio >= 4)
|| pmu_state.is_charging) {
AIMRTE_WARN("Battery level is critically low, ignore move command, ...");
return false;
}
return true;
}
CheckFollowTaskCtrl ------ 跟随安检
cpp
bool T1Checker::CheckFollowTaskCtrl(bool trigger) {
// 关机中不让跟随
if (StateManager::GetInstance()->GetSysState().shutdown_state != ShutDownState::kIdle) {
AIMRTE_WARN("System is in shutdown state, ignore follow task control");
return false;
}
// 极低电量不让开启跟随
auto& pmu_state = StateManager::GetInstance()->GetPmuState();
if ((!robot_config_.IsIgnoreLowBatteryLimit() && pmu_state.last_battery_level_audio >= 4)
|| pmu_state.is_charging) {
AIMRTE_WARN("Battery level is critically low, ignore follow task control, ...");
return false;
}
return true;
}
CheckArrangeTask ------ 编排任务安检
cpp
bool T1Checker::CheckArrangeTask(const std::string& task_id) {
if (StateManager::GetInstance()->GetSysState().shutdown_state != ShutDownState::kIdle) {
AIMRTE_WARN("System is in shutdown state, ignore arrange task, task_id: {}", task_id);
return false;
}
auto& pmu_state = StateManager::GetInstance()->GetPmuState();
if ((!robot_config_.IsIgnoreLowBatteryLimit() && pmu_state.last_battery_level_audio >= 4)
|| pmu_state.is_charging) {
AIMRTE_WARN("Battery level is critically low, ignore arrange task, ...");
return false;
}
return true;
}
CheckTouch ------ 触摸安检(多一道关)
cpp
bool T1Checker::CheckTouch(int32_t type) {
// 关机中摸它没用
if (StateManager::GetInstance()->GetSysState().shutdown_state != ShutDownState::kIdle) {
AIMRTE_WARN("System is in shutdown state, ignore touch trigger, type: {}", type);
return false;
}
// 被动模式(没上电,躺着不动)或阻尼模式下,触摸无效
auto curr_action_id = StateManager::GetInstance()->GetMotionState().curr_action_id;
if (curr_action_id == aimdk_msgs::msg::McAction::DAMPING_DEFAULT // 阻尼模式(没力)
|| curr_action_id == aimdk_msgs::msg::McAction::PASSIVE_DEFAULT) { // 被动模式(瘫着)
AIMRTE_WARN("Current action is DAMPING_DEFAULT or PASSIVE_DEFAULT, "
"ignore touch trigger, action_id: {}", curr_action_id);
return false; // 机器人躺着不省人事,摸它没反应
}
// 极低电量也拒绝触摸
auto& pmu_state = StateManager::GetInstance()->GetPmuState();
if ((!robot_config_.IsIgnoreLowBatteryLimit() && pmu_state.last_battery_level_audio >= 4)
|| pmu_state.is_charging) {
AIMRTE_WARN("Battery level is critically low, ignore touch trigger, ...");
return false;
}
return true;
}
十、完整调用链路
来源:遥控 / 语音说"跳个舞" / 开箱流程
│
│ PlayAnimation(id=7) RPC 请求
▼
Scheduler::PlayAnimationService [scheduler.cpp:286]
│ log("Received PlayAnimation request with animation_id:7")
│
├─ checker_->CheckAnimation(7) ← 坐安检流水线
│ │
│ ├─ ① 关机中?→ 否
│ ├─ ② 形态支持?→ kDance 双足四足都支持 ✅
│ ├─ ③ 变形条件?→ 不是变形动画,跳过 ✅
│ ├─ ④ CheckBipedAction → curr_action=300(WBC) 在白名单 ✅
│ ├─ ⑤ 坐/趴/模式?→ 否,继续
│ └─ ⑥ 电量?→ 40%,通过!✅
│ return 0(成功)
│
├─ Post 到工作线程
│ │
│ ▼
│ Dispatcher::DispatchAnimation(7) [dispatcher.cpp:473]
│ ├─ CreateTaskAnimation(7) → 生成技能清单(Action + Audio + Motion)
│ └─ WorkerManager::ExecTask → 工人按清单执行
│
└─ res.success = true (回复调用方:收到!)
十一、在机器人行业中的作用
这种「安检员」模式,在机器人行业里叫 前置条件检查(Precondition Check) 或 看门狗(Gatekeeper)。几乎所有真正跑在机器人上的系统都有类似设计。
为什么重要?
机器人不像手机 App------App 崩了大不了闪退,机器人崩了会物理伤害:
- 没电的时候跳舞 → 动作做一半趴下 → 可能摔坏关节
- 四足形态做双足动作 → MC 收到不认识的指令 → 可能失控
- 关机中发动画 → 电机突然上电又断电 → 损坏驱动器
- 躺着变形 → 关节卡死 → 维修费几万块
安检员就是挡在这些灾难前面的一道墙。 任何指令,不管什么来源,必须先证明"此刻做这件事是安全的",才能发给底层执行。
行业同类设计
| 领域 | 类似机制 | 说明 |
|---|---|---|
| 自动驾驶 | Safety Monitor | 变道/加速前检查周围是否有障碍物 |
| 工业机械臂 | Interlock 互锁 | 防护门未关好,机械臂不能启动 |
| 无人机 | Pre-arm Check | GPS 信号不够、电量不足,拒绝起飞 |
| 波士顿动力 Atlas | State Machine Guard | 每个动作只在特定状态下允许执行 |
| ROS2 导航栈 | Behavior Tree Condition | 导航目标不可达,拒绝规划路径 |
三层安全架构
高层决策(AI / Agent):
"用户说了'跳个舞',那就跳吧"
↓ 指令
安检员: ← 我们讲的就是这个
"等等!你是四足还是双足?电量够不够?关机了吗?"
→ 不安全的直接拒绝,不会传到底层
↓ 通过安检
底层执行(MC / HAL):
"收到合法指令,开始控制电机"
决策层只管"用户想做什么",安检员判断"现在能不能做",执行层只管"怎么做到"。三层各司其职,任何一层出问题都不会直接造成物理伤害。
十二、总结
核心思想很简单:别让机器人在错误的状态下做错误的事。
- 关机了不让做动画 → 做了也没用,MC 都关了
- 四足不让比心 → 四足没有手
- 趴着不让双足招手 → 趴着的档位不认 PresetMotion
- 快没电了不让跳舞 → 保护电池,省点电
- 变形必须站着 → 躺着变形会摔