-
Notifications
You must be signed in to change notification settings - Fork 12
Framework Internals
本页解释框架运行时到底发生了什么。理解这一页后,写自定义 driver、自定义过渡、复杂 YAML 时会更稳。
入口:
src/remote_controller/src/main.cpp
启动时做这些事:
解析 --driver 和 --config
-> load_remote_config(config_path)
-> 创建 InputMapper
-> 创建 ROS publisher: /motion_commands
-> 创建 10ms timer
-> create_input_driver(driver_type)
-> input_driver.start()
默认 driver 是 joystick。键盘 launch 会传:
--keyboard
等价于 driver type keyboard。
自定义 driver 用:
--driver crsf
配置加载在:
src/remote_controller/src/config.cpp
大致流程:
YAML::LoadFile
-> warn_unknown_root_keys
-> load_sources
-> load_curves
-> load_controls
-> load_outputs
-> load_system_commands
-> load_system_mutexes
-> load system_reset_motion_after
-> validate_config
自检会检查:
- source 名是否重复。
- control 名是否重复。
- control 类型是否合法。
- curve 引用是否存在。
- source 引用是否存在。
- expr 是否只用于 bool control。
- expr 是否成环。
- output 字段是否被支持。
- binding 是否引用未知 control。
- system output 是否引用未知 system action。
- 未使用的 source/control/curve 会输出 warning。
这些诊断会在遥控器节点启动时打印。
核心文件:
src/remote_controller/src/input_mapper.cpp
内部重要变量:
-
signals_:raw source 当前值。 -
controls_:control 当前值,包含 analog、pressed、value。 -
signal_expiry_:键盘 source 的自动过期时间。 -
signal_update_time_:runtime source 最近连接心跳时间。 -
timed_out_sources_:已经进入 failsafe 的 source。 -
binding_active_:每个 edge/level binding 上一帧是否满足。 -
output_slots_:level 输出的btn_N当前值。 -
edge_pulse_slots_:edge 输出的一帧脉冲。 -
height_filtered_:未显式输出height_des时的默认高度滤波。
joystick 轴:
mapper_.set_axis(event.number, event.value);内部转成:
js.axis.<number> = event.value / 32767
joystick 按钮:
mapper_.handle_button(event.number, pressed);内部转成:
js.button.<number> = 1.0 或 0.0
自定义 driver:
mapper_.set_signal("crsf.ch1", value);set_signal() 会:
写 signals_[source]
-> 清除该 source 的 expiry
-> 更新 signal_update_time_
-> 清除 timed_out 标记
-> refresh_bindings()
终端键盘没有 release 事件,所以使用 hold_ms。
按键触发后:
signals_[keyboard.xxx] = 1.0 或 axis 值
signal_expiry_[keyboard.xxx] = now + hold_ms
之后 tick() 发现过期:
signals_[keyboard.xxx] = 0.0
因此 keyboard 是“按一次保持一小段时间”,不是物理按住/松开模型。
遥控器节点每 10ms 调用:
outputs = mapper_.tick();
mapper_.fill_message(message);tick() 做两类事:
- 处理
signal_expiry_,让键盘 source 到期归 0。 - 处理 runtime timeout,让断联 source 进入 failsafe。
runtime timeout 逻辑:
如果 source 设置 timeout_ms
并且 now - signal_update_time_[source] >= timeout_ms
并且该 source 还没 timed_out
-> signals_[source] = failsafe
-> 标记 timed_out
-> refresh_bindings()
timeout_ms 应该表示设备断联,不是值不变。
如果设备连接正常,但摇杆保持不动,driver 仍然要调用:
mapper_.touch_runtime_sources_with_prefix("js.");或:
mapper_.touch_runtime_sources_with_prefix("crsf.");这会更新对应 prefix 的 runtime source 心跳,不改变数值。
设备断开或长期没有有效帧时不要 touch,让 failsafe 生效。
每次刷新时:
refresh_controls()
-> evaluate_control_recursive()
-> evaluate_control()
递归是为了支持派生 control:
command.flip:
type: bool
expr:
any:
- [trigger.left, button.west]如果 expr 依赖成环,加载配置时会报错。
analog control 计算:
读取 sources
-> source direction/scale/offset
-> source curve/deadzone/expo
-> mix
-> control invert
-> control curve/expo
-> control deadzone
-> min/max 映射
-> alpha 低通
bool control 计算:
raw >= threshold +/- hysteresis
enum control 计算:
先尝试用 hysteresis 保持 previous.value
否则按 positions 区间匹配
没有匹配则 default
refresh_bindings()
-> refresh_controls()
-> output_slots_ 清零
-> 遍历所有 bindings
level:
条件满足
-> apply_level_output()
-> 写 output_slots_[slot]
条件不满足
-> 不写
因为每次刷新前 output_slots_ 都清零,所以 level 条件不满足时自然回 0。
edge:
条件满足 且 上一帧不满足
-> apply_edge_pulse_output()
-> 写 edge_pulse_slots_[slot]
edge 的 binding_active_ 会记录上一帧状态,因此只在上升沿触发。
fill_message() 把当前 mapper 状态写到 MotionCommands。
流程:
refresh_controls()
-> 写 analog outputs
-> 对 btn_1..btn_10:
edge_pulse_slots 优先
否则 output_slots
写入 MotionCommands.btn_N
清空 edge_pulse_slots
-> 如果没有写 height_des:
height_des 缓慢回到默认站立高度
edge 脉冲只存在一帧,因为写入后立刻清空。
main.cpp 中:
mapper.fill_message(message)
if publish_on_change and message == last_published_payload:
return
message.header.stamp = now
publish(message)
比较发生在填 header 之前,所以 header 时间戳不会导致每帧都被认为变化。
publish_on_change: true 的语义:
- payload 没变就不发布。
- 减少
/motion_commands占用。 - 方便其他节点临时发布命令。
接收端应该保持上一条速度命令。机器人不应该因为遥控器没有重复发布相同 payload 而停下。
当 binding 输出 system.start:
dispatch_outputs()
-> run_system_action("start")
-> blocking_system_mutex()
-> run_commands()
-> update_system_mutexes()
-> reset_motion_after_system
system_mutexes 用于避免重复启动:
system_mutexes:
launch:
acquire: start
release: stop核心文件:
src/bxi_example_py_elf3/bxi_example_py_elf3/bxi_example_demo.py
启动时:
load_files()
-> 读取 topic_prefix
-> 读取 npz_file_dict / onnx_file_dict
-> 读取 state_machine_config
-> 读取 state_machine_info 参数
加载模型
init_pub_sub()
build_robot_states()
RobotStateMachine(...)
RemoteEventAdapter(...)
创建 timer
定时器周期:
dt = 0.02
主要流程:
前置 reset 流程
-> 读取传感器缓存
-> 取出 pending_remote_events
-> state_machine.update(dt, events)
-> 如果不在 transition:
state_machine.update_current_state(dt)
-> 如果状态产生 motor_target:
send_to_motor(qpos, kp, kd)
-> publish_state_machine_info_if_due(events)
过渡期间不会调用当前状态的 on_update(),而是调用 transition hook。
BxiExample 订阅:
motion_commands
收到消息:
apply_velocity_profile(msg)
events = remote_event_adapter.extract_events(msg)
pending_remote_events.extend(events)
RemoteEventAdapter 对每个 event 记录上一次 slot 值。
完整 event:
back_flip:
slot: btn_10
value: 1触发:
当前 btn_10 == 1
并且 btn_10 对 back_flip 来说发生变化
启动 reset 阶段会 sync_only,只同步上一帧值,不触发事件。
核心文件:
src/bxi_example_py_elf3/bxi_example_py_elf3/state_machine.py
每周期:
如果 active transition 存在:
_update_active_transition(dt)
return True
处理 events
处理 pending delay
如果开始 active transition:
_update_active_transition(dt)
return True
state_elapsed += dt
处理 after rules
如果开始 active transition:
_update_active_transition(0.0)
return True
return False
timer_callback 看到返回 False 才会调用:
state_machine.update_current_state(dt)_handle_events() 中:
event_set = set(events)
按 YAML 中当前状态 rules 顺序遍历
找到第一个 rule.event 在 event_set 中的规则
-> action 或 transition
-> return
因此同一周期多个 event 进入时,当前状态 YAML 里规则顺序决定优先级。
带 delay 的规则:
recover:
to: recover
delay: 0.5触发后进入 pending:
pending.elapsed += dt
pending.elapsed >= delay
-> begin transition
pending 期间如果又触发新的 delayed rule,当前实现会覆盖 _pending。
开始切换:
current.on_exit(ctx)
to_state.on_prepare_enter(ctx, current, profile)
active = ActiveTransition(...)
更新切换:
elapsed += dt
progress = elapsed / profile.duration
exit_progress = elapsed / profile.exit_duration
enter_progress = elapsed / profile.enter_duration
from_state.on_exit_transition(...)
to_state.on_enter_transition(...)
if progress >= 1:
finish
结束切换:
current = to_state
state_elapsed = 0
pending = None
active = None
fired_after_rules.clear()
current.on_enter(ctx)
规则:
toggle_dance_pause:
action: toggle_dance_pause执行顺序:
先查 action_handlers
再调用 current.on_action(ctx, action_name)
都没有处理则报错
状态内 action 返回 True 表示已处理。
初始化时:
_run_graph_checks(initial_state)
_export_graph_from_config(initial_state)
自检内容:
- 转移目标状态是否存在。
- profile 是否存在。
- event 是否在
remote_events声明。 - 从 initial_state 是否可达。
- 状态是否没有任何输出 transition。
- after 自动转移是否成环。
导出:
/tmp/elf3_state_machine.dot
/tmp/elf3_state_machine.mmd
BxiExample 发布:
<topic_prefix>state_machine_info
内容来自:
state_machine.snapshot(include_graph=True)包含:
-
mode:state/transition/pending - 当前状态。
- active transition。
- pending transition。
- 状态图。
- 当前事件。
- 当前速度命令。
想扩展遥控器表达能力:
- 增加新的
ControlConfig.type。 - 增加新的 condition kind。
- 增加批量
set_signals()。 - 增加更多
MotionCommands字段适配。
想扩展状态机:
- 增加 guard 条件。
- 增加 transition priority。
- 增加层级状态机。
- 增加状态组和共享转移。
想扩展机器人过渡:
- 增加更多
enter_behavior/exit_behavior。 - 在
data中定义行为参数。 - 为状态实现专属 transition hook。
扩展时保持分层:
通用状态机能力 -> state_machine.py
机器人电机过渡 -> robot_state_base.py
具体动作行为 -> 状态类
输入协议读取 -> InputDriver
输入业务解释 -> YAML