Skip to content

Hands On Controller Binding

konodoki edited this page Jul 20, 2026 · 11 revisions

手把手 3:从零绑定按键并接入状态机

本课接上一课的 SinWaveState。现在状态类已经能运行,我们把它接到键盘、手柄和状态机事件。

目标:

按键盘 0
  -> remote_controller 输出 MotionCommands.btn_10 = 5 一帧
  -> BxiExample 把 btn_10=5 转成 sin_wave event
  -> normal 状态切到 sin_wave

1. 先把 initial_state 改回正常值

上一课为了测试状态,可能临时写了:

initial_state: sin_wave

现在改回:

initial_state: zero_torque

从现在开始,我们通过遥控器事件切状态。

2. 声明状态机 event

打开:

src/bxi_example_py_elf3/config/elf3_state_machine.yaml

remote_events: 下添加:

  sin_wave:
    slot: btn_10
    value: 5

这一步只定义事件来源,不定义状态切换。

含义:

如果 MotionCommands.btn_10 跳变到 5
  -> 产生 sin_wave event

3. 让 normal 能切到 sin_wave

还是在 elf3_state_machine.yaml

找到:

states:
  normal:
    behavior: NormalState
    transitions:
      on_event:

添加:

        sin_wave:
          to: sin_wave
          transition: soft_switch

现在状态图有:

normal --sin_wave--> sin_wave

4. 让 sin_wave 能切回 normal

确认 states.sin_wave 中有:

  sin_wave:
    behavior: SinWaveState
    transitions:
      on_event:
        normal:
          to: normal
          transition: soft_switch
        zero_torque: zero_torque

否则进入 sin_wave 后遥控器按 normal 也回不去。

5. 在键盘 sources 中加按键

打开:

src/remote_controller/config/xbox_default.yaml

找到:

sources:
  keyboard:
    signals:

添加:

      keyboard.sin_wave: {from: keyboard.key, key: "0"}

读法:

keyboard.sin_wave 这个业务 source
  由 keyboard.key 的 0 键产生

6. 把 source 变成 control

找到 controls:,添加:

  keyboard.sin_wave: {type: bool, source: keyboard.sin_wave}

现在 YAML 层有:

keyboard key 0
  -> source keyboard.sin_wave
  -> control keyboard.sin_wave

7. 输出到 MotionCommands

找到:

outputs:
  edge:

添加:

    - output: btn_10=5
      when: [keyboard.sin_wave]

为什么用 edge

  • 状态切换只需要触发一次。
  • edge 会在条件从 false 变 true 时输出一帧。
  • 下一帧自动回 0。

不要用 level 做这种一次性状态切换,除非你明确想按住期间一直保持 btn_10=5

8. 第一次验证

编译:

colcon build --symlink-install --packages-select remote_controller bxi_example_py_elf3

加载环境:

source install/setup.bash

启动键盘遥控器:

ros2 launch remote_controller remote_controller_keyboard.launch.py

观察 /motion_commands

ros2 topic echo /motion_commands

按键盘 0

你应该看到某一帧:

btn_10: 5

然后回到:

btn_10: 0

如果看不到,先别启动机器人,先查遥控器 YAML。

9. 第二次验证:看状态机 event

启动 example:

ros2 launch bxi_example_py_elf3 example_demo_hw.launch.py

观察:

ros2 topic echo /simulation/state_machine_info

按键盘 0

你应该看到:

{
  "events": ["sin_wave"],
  "current": {
    "name": "sin_wave"
  }
}

如果 /motion_commandsbtn_10=5,但状态机没有 event,检查:

  • remote_events.sin_wave.slot 是否是 btn_10
  • remote_events.sin_wave.value 是否是 5
  • 当前 reset 阶段是否还没结束,sync_only 会吞掉启动前事件。

10. 同时支持手柄组合键

现在添加手柄组合键:

右扳机 + X

outputs.edge 改成:

    - output: btn_10=5
      when:
        any:
          - [trigger.right, button.west]
          - [keyboard.sin_wave]

含义:

满足任意一组:
  1. trigger.right 和 button.west 同时按下
  2. keyboard.sin_wave 按下

11. 把组合键提升成 command

如果这个组合键未来会复用,可以先定义派生 control。

controls 添加:

  command.sin_wave:
    type: bool
    expr:
      any:
        - [trigger.right, button.west]
        - [keyboard.sin_wave]

然后 outputs 简化:

    - output: btn_10=5
      when: [command.sin_wave]

什么时候应该提升成 command.*

  • 同一个组合键多个地方复用。
  • 组合键很复杂。
  • 想让 outputs 层更干净。
  • 想统一命名某个业务命令。

什么时候不用:

  • 只用一次。
  • 默认配置想保持短。

12. 加一个安全确认键

假设 sin_wave 是危险动作,需要:

左扳机 + 右扳机 + X

配置:

  command.sin_wave:
    type: bool
    expr:
      any:
        - [trigger.left, trigger.right, button.west]
        - [keyboard.sin_wave]

如果键盘也要双确认,可以加一个 keyboard.arm

sources:
  keyboard:
    signals:
      keyboard.arm: {from: keyboard.key, key: "z"}
      keyboard.sin_wave: {from: keyboard.key, key: "0"}

controls:
  keyboard.arm: {type: bool, source: keyboard.arm}
  keyboard.sin_wave: {type: bool, source: keyboard.sin_wave}

  command.sin_wave:
    type: bool
    expr:
      any:
        - [trigger.left, trigger.right, button.west]
        - [keyboard.arm, keyboard.sin_wave]

13. 用 enum 模式开关控制入口

CRSF 的 CH15(默认映射为十字键左右)是三档轴,可作为模式开关:

sources:
  crsf:
    type: crsf
    signals:
      crsf.dpad_x: {from: crsf.channel.15}

controls:
  switch.mode:
    type: enum
    source: crsf.dpad_x
    default: middle
    positions:
      low: [-1.0, -0.35]
      middle: [-0.34, 0.34]
      high: [0.35, 1.0]

要求模式在 high 才能触发:

  command.sin_wave:
    type: bool
    expr:
      any:
        - [switch.mode=high, trigger.right, button.west]
        - [keyboard.arm, keyboard.sin_wave]

这一步展示了框架上限:

不同遥控器输入
  -> 都抽象成 controls
  -> command 层统一组合
  -> outputs 层只管输出 btn/event

14. 本课最终配置片段

遥控器:

sources:
  keyboard:
    type: keyboard
    signals:
      keyboard.arm: {from: keyboard.key, key: "z"}
      keyboard.sin_wave: {from: keyboard.key, key: "0"}

controls:
  keyboard.arm: {type: bool, source: keyboard.arm}
  keyboard.sin_wave: {type: bool, source: keyboard.sin_wave}

  command.sin_wave:
    type: bool
    expr:
      any:
        - [trigger.right, button.west]
        - [keyboard.arm, keyboard.sin_wave]

outputs:
  edge:
    - output: btn_10=5
      when: [command.sin_wave]

状态机:

remote_events:
  sin_wave:
    slot: btn_10
    value: 5

states:
  normal:
    transitions:
      on_event:
        sin_wave:
          to: sin_wave
          transition: soft_switch

  sin_wave:
    behavior: SinWaveState
    transitions:
      on_event:
        normal:
          to: normal
          transition: soft_switch

15. 本课检查清单

1. 按键盘 0 时 /motion_commands 出现 btn_10=5。
2. 下一帧 btn_10 回到 0。
3. /simulation/state_machine_info 的 events 出现 sin_wave。
4. normal 状态能切到 sin_wave。
5. sin_wave 能通过 normal event 回到 normal。
6. 手柄组合键也能触发。
7. command.sin_wave 没有引用不存在的 control。

下一课:手把手 4:从零写一个自定义过渡行为

Clone this wiki locally