Skip to content

Remote Config

konodoki edited this page Aug 2, 2026 · 10 revisions

遥控器输入映射配置

本文只说明 remote_controller 如何把键盘、手柄、CRSF 或其他设备输入映射成统一的 MotionCommands。状态、事件和 route 属于各自 Mod 的 mod.yaml;机器人关节与 IMU 也不经过这份配置。

当前默认配置文件:

src/remote_controller/config/xbox_default.yaml

remote_controller 只在启动时读取一次 YAML,运行期间不会扫描或重新加载配置。修改 src/remote_controller/config/xbox_default.yaml 后,执行:

colcon build --packages-select remote_controller

build 更新 install/ 目录后,重启 remote_controller。YAML 解析失败会使新进程启动失败,并直接报告配置错误。

推荐从上到下按这条线理解:

inputs / sources  ->  curves  ->  controls  ->  outputs  ->  system
设备选择与输入声明       曲线校准     统一控制量      写 MotionCommands   执行系统命令

配置目标是:业务层不要关心具体是 Xbox、键盘、CRSF 还是其他遥控器。业务层只看统一后的 control 和状态机 event。

1. inputs / sources:声明候选设备和输入来源

每个 sources.<group> 都是一个候选输入设备。任意时刻严格只会激活一个设备;sources 同时负责把 driver 产生的 raw source 命名成业务可读 source。

设备选择策略:

inputs:
  selection:
    scan_interval_ms: 100
    promote_stable_ms: 500
  • scan_interval_ms:调用 driver is_available() 的周期,必须大于 0。
  • promote_stable_ms:高优先级设备连续可用多久后才允许抢占,0 表示立即允许。
  • 切换时框架会停止旧驱动、清空旧 signal、发布一次零运动命令、等待新驱动 is_ready(),最后才接受新输入。
  • 切换期间 edge 命令被抑制;必须先松开、再按下 start/stop 等边沿按键才能重新触发。

通用结构:

sources:
  <group_name>:
    type: <driver_type>
    priority: <integer>
    signals:
      <semantic.source.name>:
        from: <raw.source.name>

读法是从左到右:

semantic source 由 raw source 产生

例如:

gamepad.left_y:
  from: js.axis.3

表示:

gamepad.left_y 由 js.axis.3 产生

不要反过来写成 js.axis.3: gamepad.left_y,因为那会让人误解为 raw source 被业务含义反向定义。

1.1 运行时 driver DEBUG

节点参数 --DEBUG 会每秒调用一次每个 input driver 的 debug()。CRSF 会打印最近一帧 CRC 正确的 16 路归一化通道值;joystick 和 keyboard 会打印自己的可用、就绪状态。

ros2 run remote_controller remote_controller --DEBUG --config /path/to/xbox_default.yaml

launch 的自定义参数必须写成 DEBUG:=true

ros2 launch remote_controller remote_controller.launch.py DEBUG:=true

ros2 launch --debug 只影响 ROS 2 的 launch 系统,不会传递给遥控器 driver。

2. joystick / gamepad source

示例:

sources:
  gamepad:
    type: joystick
    device: /dev/input/by-id/usb-...-joystick
    priority: 50
    ready_timeout_ms: 1000
    loss_timeout_ms: 300
    cooldown_ms: 1000
    signals:
      gamepad.left_y:
        from: js.axis.3
      gamepad.left_x:
        from: js.axis.0
      gamepad.yaw:
        from: js.axis.6
      gamepad.a:
        from: js.button.0
      gamepad.b:
        from: js.button.1

字段:

  • type: joysticktype: gamepad:使用 Linux joystick driver。
  • device:设备路径,例如 /dev/input/js0
  • priority:候选设备优先级,值越大越优先。
  • ready_timeout_ms:启动后等待 driver 进入 is_ready() 的最长时间,默认 1000
  • loss_timeout_ms:普通 driver 的 is_available() 连续失败多久才判定断连,默认 300。CRSF 这类按有效帧新鲜度自行判定的 driver 会声明并直接使用该值。
  • cooldown_ms:断连或就绪超时后多久不参与重新选择,默认 1000
  • signals:业务 source 声明。
  • signals.<name>.from:raw source,例如 js.axis.3js.button.0

driver 行为:

  • js.axis.N 会归一化到 [-1, 1]
  • js.button.N 按下为 1.0,松开为 0.0
  • 打开、读取错误或设备拔出会使 driver 变为不可用;设备管理器在 loss_timeout_ms 后做安全切换。

3. keyboard source

示例:

sources:
  keyboard:
    type: keyboard
    poll_timeout_us: 20000
    hold_ms: 200
    stop: space
    signals:
      keyboard.vx:
        from: keyboard.axis
        negative: s
        positive: w
      keyboard.vy:
        from: keyboard.axis
        negative: d
        positive: a
      keyboard.yaw:
        from: keyboard.axis
        negative: e
        positive: q
      keyboard.normal:
        from: keyboard.key
        key: "1"
        hold_ms: 200

字段:

  • type: keyboard:使用终端键盘 driver。
  • poll_timeout_us:轮询终端输入的超时时间,单位微秒。
  • hold_ms:键盘 source 触发后保持多久。键盘终端没有真实 release 事件,所以需要这个字段。
  • stop:清零运动轴的按键,常用 space
  • from: keyboard.axis:声明键盘模拟轴。
  • negative:按下后输出 -1.0 的键。
  • positive:按下后输出 1.0 的键。
  • from: keyboard.key:声明普通按键。
  • key:触发普通按键 source 的键。
  • signals.<name>.hold_ms:覆盖单个 source 的保持时间。

特殊键名:

space
tab
esc
escape

普通键写单字符:

keyboard.sin_wave:
  from: keyboard.key
  key: "u"

4. CRSF source

type: crsf 已内置。它使用 POSIX 串口(默认 460800, 8N1, 无流控), 解析 CRC-8/DVB-S2 正确的 RC_CHANNELS_PACKED (0x16) 帧,并输出 16 个 归一化通道。设备拔出或停止收到有效帧后可自动重开 /dev/ttyCRSF

sources:
  crsf:
    type: crsf
    device: /dev/ttyCRSF
    priority: 100
    baud_rate: 460800
    ready_timeout_ms: 1000
    loss_timeout_ms: 300
    cooldown_ms: 1000
    signals:
      crsf.left_x:
        from: crsf.channel.1        # CH1 / 左摇杆 X
      crsf.left_y:
        from: crsf.channel.2        # CH2 / 左摇杆 Y
      crsf.trigger_right:
        from: crsf.channel.3        # CH3 / RT
      crsf.right_x:
        from: crsf.channel.4        # CH4 / 右摇杆 X
      crsf.right_y:
        from: crsf.channel.6        # CH6 / 右摇杆 Y
      crsf.trigger_left:
        from: crsf.channel.7        # CH7 / LT
      crsf.button_group_a:
        from: crsf.channel.8        # CH8 / A/B/X/Y 编码组
      crsf.button_group_b:
        from: crsf.channel.9        # CH9 / LB/RB/Back/Start/D-pad 编码组
  • 默认 xbox_default.yaml 使用 CH1、CH2、CH3、CH4、CH6、CH7、CH8、CH9。CH1/CH2 是左摇杆 X/Y,CH3/CH7 是 RT/LT,CH4/CH6 是右摇杆 X/Y;CH2 在 move.vx 绑定处反向,CH6 使用时也应添加 direction: -1。CH8 是按键组 A: 200/400/600/800 分别代表 A/B/X/Y,992 为空闲;CH9 是按键组 B: 200/400/600/800 分别代表 LB/RB/Back/Start,1180/1380/1580/1780 分别代表 D-pad 上/下/左/右,992 为空闲。
  • CH8/CH9 在默认 YAML 中会先解为 crsf.button_group_acrsf.button_group_b enum,再通过 inputs[].when 派生 A/B/X/Y/LB/RB/Start/Back 的 bool control。D-pad 左、右 分别以 +1-1 映射到 CRSF yaw;上、下仍保留为 enum,可按需绑定输出。
  • crsf.channel.1crsf.channel.16 是 driver 写入的原始通道,默认按旧工程的 174..1811 映射并限制到 [-1, 1]。业务含义只能由 YAML 中的语义 source 和 control 决定。
  • baud_rate:可覆盖默认波特率;兼容旧字段名 baudrate
  • channel_minchannel_max:可选的通道归一化校准值。
  • 未激活时,driver 做非阻塞串口协议 probe;只有近期收到 CRC 正确的通道帧才会 成为可用候选。激活后 loss_timeout_ms 由 CRSF driver 自己按有效帧新鲜度执行, 不会被候选管理器重复计时。

SBUS、UDP/TCP、蓝牙 HID 或其他串口协议仍按同一套 sources -> controls -> outputs 接入,但需要相应 driver。其他标量参数会保存到 InputDeviceConfig.options,由 对应 C++ driver 读取。具体看 自定义遥控器驱动参考

4.1 signal.timeout_ms:字段过期保护,不是设备断连

只有持续流式协议、且字段可能独立停止更新时才设置:

udp.vx:
  from: udp.vx
  timeout_ms: 500
  failsafe: 0.0

含义是该 raw signal 超过 500 ms 未更新时写入 failsafe。它不会切换设备,也不能替代 driver 的 is_available()

  • Linux joystick:默认不设置。摇杆静止不产生事件是正常行为,设备级断连由 driver 和 loss_timeout_ms 判断。
  • CRSF:应由 driver 根据“最近一次 CRC 正确完整帧”实现 is_available();不要用每通道 timeout_ms 作为主要断连保护。
  • UDP/TCP:可按协议需要保留,用于某一个字段停止更新的额外保护。

5. curves:曲线、死区和校准

curves 负责输入曲线处理。它可以在 control source 上引用。

expo 曲线:

curves:
  stick:
    type: expo
    deadzone: 0.03
    expo: 0.2
    limit: [-1.0, 1.0]
    calibration:
      input: [-1.0, 0.0, 1.0]
      output: [-1.0, 0.0, 1.0]

字段:

  • type: expo:指数曲线。
  • deadzone:死区,小于等于该绝对值时输出 0。
  • expo:指数强度,范围 [0, 1]
  • limit: [min, max]:输出限幅。
  • min / max:等价的限幅字段。
  • calibration.input:输入校准点 [min, center, max]
  • calibration.output:输出校准点 [min, center, max]
  • calibration.clamp:是否限制输入到校准范围内,默认 true。

分段曲线:

curves:
  three_position_curve:
    type: piecewise
    points:
      - [-1.0, -1.0]
      - [-0.2, 0.0]
      - [0.2, 0.0]
      - [1.0, 1.0]

piecewise 适合非线性旋钮、三档开关的预处理。points 必须按 input 从小到大排序。

6. controls:统一控制量

controls 是配置里最重要的一层。后面的 outputs.when 应该判断 control,而不是直接判断 raw source。

6.1 analog control

controls:
  move.vx:
    type: analog
    mix: max_abs
    inputs:
      - source: gamepad.left_y
        direction: -1
        curve: stick
      - source: keyboard.vx
    deadzone: 0.03
    min: -1.0
    max: 1.0
    alpha: 0.03

字段:

  • type: analog:连续模拟量。
  • inputs:唯一的输入规则列表;旧 sourcesourcesexpr 不再支持。
  • mix: max_abs:取绝对值最大的输入。
  • mix: sum:求和;之后由 control 的 min/max 限制范围。
  • mix: first_active:按顺序取第一个非零输入。
  • direction:方向,-1 表示反向。
  • scale:缩放。
  • offset:偏移。
  • curve:引用 curves 中的曲线。
  • deadzone:control 层死区。
  • min / max:输出范围。
  • alpha:低通滤波系数,1.0 表示不滤波。
  • expo:control 层指数曲线。
  • invert:是否反向。

6.2 bool control

按钮:

controls:
  button.south:
    type: bool
    inputs:
      - source: gamepad.a

模拟轴当按钮:

controls:
  trigger.left:
    type: bool
    threshold: 0.85
    hysteresis: 0.05
    inputs:
      - source: gamepad.trigger_left

字段:

  • threshold:大于阈值时认为按下。
  • hysteresis:迟滞,避免阈值附近抖动。
  • invert:反向。

6.3 enum control

三档开关或多档模式推荐用 enum

controls:
  switch.group_b:
    type: enum
    default: idle
    hysteresis: 0.03
    inputs:
      - source: crsf.button_group_b
    positions:
      lb:         [-1.0, -0.846]
      rb:         [-0.845, -0.602]
      back:       [-0.601, -0.358]
      start:      [-0.357, -0.118]
      idle:       [-0.117, 0.114]
      dpad_up:    [0.115, 0.351]
      dpad_down:  [0.352, 0.596]
      dpad_left:  [0.597, 0.841]
      dpad_right: [0.842, 1.0]

字段:

  • default:没有匹配区间时使用的值。
  • positions.<value>: [min, max]:枚举值对应的输入范围。
  • hysteresis:当前枚举值保持范围扩展。

判断 enum:

when: [switch.group_b=dpad_right]

或:

when:
  - equals:
      control: switch.group_b
      value: dpad_right

6.4 条件值派生 control

复杂组合键可以先定义成 command.*

controls:
  command.sin_wave:
    type: bool
    inputs:
      - when:
          any:
            - - trigger.right
              - button.west
              - released: trigger.left
              - released: shoulder.left
              - released: shoulder.right
            - - keyboard.sin_wave
        value: true

再在 outputs 里复用:

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

inputs 还可把 enum 直接映射为模拟量,并使用 priority 抢占:

crsf.yaw:
  type: analog
  mix: sum
  inputs:
    - when: [crsf.button_group_b=dpad_left]
      value: 1.0
      priority: 100
    - when: [crsf.button_group_b=dpad_right]
      value: -1.0
      priority: 100

每次只有最高 priority 的激活规则组参与 mix;同组 sum 可让正负值抵消。when 支持递归 all/any/not,并支持 raw_range: {source: ..., min: ..., max: ...}。默认 analog alpha1.0; 只有显式设置才平滑。

如果某个组合只用一次,也可以直接写在 outputs.when 里。

7. outputs:写入 MotionCommands

outputs 是最后一层。

outputs:
  conflict_policy: first_wins
  publish_on_change: true

  analog:
    vel_des.x: move.vx
    vel_des.y: move.vy
    yawdot_des: move.yaw

  edge:
    - output: system.start
      when: [system.start]

  level:
    - output: btn_1=1
      when:
        any:
          - - shoulder.right
            - button.west
            - released: shoulder.left
            - released: trigger.left
            - released: trigger.right
          - - keyboard.normal

顶层字段:

  • conflict_policy: first_wins:同一个 btn_N 被多个 binding 写入不同值时,保留先匹配的。
  • conflict_policy: last_wins:后匹配的覆盖先匹配的。
  • conflict_policy: error:冲突时报错。
  • publish_on_change: true:只有 payload 变化时才发布 MotionCommands
  • publish_on_change: false:固定频率发布。

7.1 analog

analog 写连续字段,推荐直接写 MotionCommands 字段路径:

outputs:
  analog:
    vel_des.x: move.vx
    vel_des.y: move.vy
    vel_des.z: move.vz
    yawdot_des: move.yaw
    height_des: body.height

当前支持的简写:

vx     -> vel_des.x
vy     -> vel_des.y
vz     -> vel_des.z
yaw    -> yawdot_des
height -> height_des

高级写法:

outputs:
  analog:
    vel_des.x:
      controls: [move.vx, autonomous.vx]
      mix: first_active
      scale: 1.0
      offset: 0.0
      limit: [-1.0, 1.0]

7.2 level

level 是电平输出。条件满足时持续写值,条件不满足时自动回 0。

outputs:
  level:
    - output: btn_1=1
      when: [keyboard.normal]

适合:

  • 按住期间持续有效的按钮。
  • 旧逻辑里需要保持 btn_N=1 的命令。

当前设计不需要 release_outputslevel 每次刷新都会重新计算所有 binding,组合键不满足时对应 btn_N 自然回 0。

7.3 edge

edge 是上升沿输出。条件从 false 变 true 时输出一帧。

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

适合:

  • 状态切换。
  • toggle / pause。
  • system.startsystem.stop

对于 btn_N=value,下一帧自动回 0。对于 system.*,只执行一次命令。

8. when 条件语法

单个 control:

when: [keyboard.normal]

多个 control 同时满足:

when:
  - shoulder.right
  - button.west
  - released: shoulder.left
  - released: trigger.left
  - released: trigger.right

辅助键组合必须把未参与组合的其他辅助键全部写成 released,否则三键组合也会命中 两键规则。

任意一组满足:

when:
  any:
    - - shoulder.right
      - button.west
      - released: shoulder.left
      - released: trigger.left
      - released: trigger.right
    - - keyboard.normal

显式 pressed:

when:
  - pressed: button.west

显式 released:

when:
  - button.west
  - released: shoulder.left
  - released: shoulder.right
  - released: trigger.left
  - released: trigger.right

枚举等于:

when:
  - switch.mode=high

范围判断:

when:
  - range:
      control: throttle
      min: 0.2
      max: 1.0

9. system:执行系统命令

outputs.edge 可以触发 system.*

outputs:
  edge:
    - output: system.start
      when: [system.start]

然后在 system: 下定义命令:

system:
  start:
    - "<prepare_log_or_environment_command>"
    - "<start_robot_stack_command>"

  stop:
    - "<stop_robot_stack_command>"

互斥锁:

system_mutexes:
  launch:
    acquire: start
    release: stop

清零运动输入:

system_reset_motion_after:
  - stop

10. 添加一个新按键的最小流程

目标:把一个新的键盘或手柄输入映射到一个状态机事件。

  1. sources.keyboard.signals 添加:
keyboard.sin_wave:
  from: keyboard.key
  key: "u"
  1. controls 添加:
keyboard.sin_wave:
  type: bool
  inputs:
    - source: keyboard.sin_wave
  1. outputs.edge 添加一个 btn_N=value 输出:
- output: btn_N=value
  when: [keyboard.sin_wave]
  1. 如果这是状态切换,在对应 Mod 的 mod.yaml 中声明匹配该 btn_N=valueevents,并用 routesactions 消费它。不要把业务事件重新集中到系统 YAML。

相关文档

Clone this wiki locally