Skip to content

Custom Input Driver

konodoki edited this page Jul 28, 2026 · 7 revisions

自定义遥控器驱动参考

本页说明如何接入 SBUS、串口、UDP、TCP、蓝牙或自定义 HID。CRSF 已是内置 driver, 也可作为新协议 driver 的完整参考。当前框架的边界是:

InputDeviceManager 负责选择、抢占、断连和安全切换
InputDriver          负责协议、raw signal、可用性和初始就绪状态
YAML                 负责 source -> control -> output 的业务映射

driver 不应知道 normalback_flipbtn_10=1 等业务名字。

1. 什么时候需要新 driver

只需改 YAML:换 Linux joystick、改键盘按键、改按钮组合、改曲线或控制映射。

需要新 driver:读取新协议或新传输,例如 SBUS、串口、UDP/TCP、蓝牙或自定义 HID。

2. 先定义设备和 raw signal

每个 YAML sources.<name> 都是候选设备。候选设备严格独占,优先级大的设备会抢占低优先级设备。

内置 CRSF 示例:

inputs:
  selection:
    scan_interval_ms: 100
    promote_stable_ms: 500

sources:
  crsf_remote:
    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.5}   # CH5 / 右摇杆 Y
      crsf.trigger_left: {from: crsf.channel.6}   # CH6 / LT
      crsf.button_group_a: {from: crsf.channel.7} # A/B/X/Y 编码组
      crsf.button_group_b: {from: crsf.channel.8} # LB/RB/Back/Start/D-pad 编码组

命名建议:

  • raw signal 使用协议前缀,例如 crsf.channel.1udp.mode
  • 轴统一归一化到 [-1, 1];按钮统一为 0.0 / 1.0
  • CRSF driver 将完整帧中的 16 路通道分别写入 crsf.channel.1crsf.channel.16;默认 YAML 使用 CH1..CH6 作为连续 Xbox 输入,并把 CH7/CH8 命名为离散编码按键组。CH7 的 200/400/600/800 表示 A/B/X/Y;CH8 的 200/400/600/800 表示 LB/RB/Back/Start,1180/1380/1580/1780 表示 D-pad 上/下/左/右;992 都表示空闲。通道业务含义应留在 YAML controls 中。
  • device 是通用设备路径;baud_rateport 等标量放在 InputDeviceConfig.options

之后再在 controls 中引用 crsf.left_xcrsf.button_group_a 等 source。业务输出 不应出现在 driver 中。

3. 文件结构

驱动应放在专用目录,不要再写入已删除的 input_driver.cpp

include/remote_controller/drivers/
  input_driver.hpp          # InputDriver / InputDriverBase
  driver_registry.hpp       # factory 注册 API

src/drivers/
  driver_registry.cpp
  input_driver_base.cpp
  joystick_input_driver.cpp
  keyboard_input_driver.cpp
  crsf_input_driver.cpp     # 内置 CRSF driver

内置 CRSF driver 已加入 remote_controller_core 的 CMake 源文件列表。

4. 实现 InputDriverBase

通常直接继承 InputDriverBase。它提供线程安全的 set_signal()、日志和 output 分发。driver 还必须实现:

  • is_available():非阻塞。设备管理器每个扫描周期调用它。
  • is_ready():只有在 driver 已收到足够安全的初始状态后才返回 true
  • start() / stop():启动、停止读取线程。

骨架:

#include "remote_controller/drivers/input_driver.hpp"

#include <atomic>
#include <chrono>
#include <thread>

namespace remote_controller {

class ExampleProtocolInputDriver : public InputDriverBase {
public:
    using InputDriverBase::InputDriverBase;

    ~ExampleProtocolInputDriver() override { stop(); }

    std::string name() const override { return config_.name; }

    bool is_available() override
    {
        // 必须非阻塞。未激活时可做轻量串口/协议 probe;激活后应
        // 根据最近 CRC 正确帧判断健康状态。
        return device_probe_ok() &&
            std::chrono::steady_clock::now() - last_valid_frame_ <
                std::chrono::milliseconds(config_.loss_timeout_ms);
    }

    bool is_ready() const override
    {
        // 一份 CRC 正确的完整通道帧已经写入全部必要 signal。
        return ready_;
    }

    void start() override
    {
        stop();
        stop_flag_ = false;
        ready_ = false;
        thread_ = std::thread(&ExampleProtocolInputDriver::run, this);
    }

    void stop() override
    {
        stop_flag_ = true;
        close_serial();
        if (thread_.joinable()) {
            thread_.join();
        }
        ready_ = false;
    }

private:
    std::thread thread_;
    std::atomic<bool> ready_{false};
    std::chrono::steady_clock::time_point last_valid_frame_{};

    void run()
    {
        while (!stop_flag_) {
            const CrsfFrame frame = read_next_valid_frame();  // 实现协议帧头、长度与 CRC
            if (!frame.valid) {
                continue;
            }

            // 在同一完整帧内更新所有需要的 signal,再声明 ready。
            set_signal("proto.vx", normalize(frame.channel(0)));
            set_signal("proto.vy", normalize(frame.channel(1)));
            set_signal("proto.yaw", normalize(frame.channel(2)));
            set_signal("proto.start", frame.start_pressed ? 1.0 : 0.0);
            set_signal("proto.stop", frame.stop_pressed ? 1.0 : 0.0);
            last_valid_frame_ = std::chrono::steady_clock::now();
            ready_ = true;
        }
    }
};

}  // namespace remote_controller

骨架中 device_probe_ok()read_next_valid_frame()close_serial()ProtocolFramenormalize() 由协议实现提供。读取循环必须用非阻塞 fd 配合 select() / poll(), 每次超时检查 stop_flag_

对 CRSF,is_available() 不能仅检查 /dev/ttyCRSF 存在。它应以近期 CRC 正确帧为健康依据。由于设备管理器会在激活前调用该函数,driver 可实现轻量、非阻塞的协议 probe 或缓存探测结果。

5. 注册 factory

内置类型 keyboardjoystickgamepadcrsf 已由 driver_registry.cpp 直接创建,不需要 factory 注册。下面只适用于新的非内置 driver:

remote_controller::register_input_driver_factory(
    "my_protocol",
    [](const remote_controller::InputDeviceConfig &config,
       remote_controller::InputMapper &mapper,
       std::mutex &mapper_lock,
       remote_controller::DriverOutputHandler output_handler,
       remote_controller::DriverLogHandler log_handler) {
        return std::unique_ptr<remote_controller::InputDriver>(
            new remote_controller::MyProtocolInputDriver(
                config,
                mapper,
                mapper_lock,
                std::move(output_handler),
                std::move(log_handler)));
    });

也可以在 driver_registry.cpp 中集中注册,但必须确保注册发生在 InputDeviceManager 建立候选列表之前。未注册类型不会让节点退出:会记录 warning 并使用下一个可用候选设备。

6. 断连与切换契约

设备管理器按以下规则工作:

高优先级候选连续可用 promote_stable_ms
  -> 停止当前设备
  -> 清空旧 signal
  -> 发布一次零运动命令
  -> 启动新 driver
  -> 等待 is_ready()
  -> 允许正常控制

普通 driver 的 is_available() 连续失败 loss_timeout_ms
  -> 同样进入安全切换

因此 driver 应做到:

  • is_available() 只描述设备/协议健康,且绝不阻塞管理器。
  • CRSF 的 is_available() 在未激活时执行非阻塞协议 probe;激活后直接以近期 CRC 正确的 RC_CHANNELS_PACKED 帧判断健康,并声明自己已处理 loss_timeout_ms, 以免管理器重复等待。
  • is_ready() 表示已经获得足够的初始状态,避免切换时把未知按键状态当成按下。
  • 断连后不自行保留旧值;管理器会清理该设备配置的所有 raw signal。
  • edge 命令在切换期间被抑制;用户必须释放后再次按下。

7. timeout_ms 何时使用

signals.*.timeout_ms 只是单个字段的过期保护:字段未更新时写入 failsafe。它不负责候选设备断连和切换。

  • joystick:不设置;静止没有事件是正常的。
  • CRSF:用 is_available() + loss_timeout_ms 判断“没有有效完整帧”;不是 signals.*.timeout_ms
  • UDP/TCP:当一个字段可能独立停止更新时,可以保留 timeout_ms 作为补充。

8. 验证清单

1. 新 .cpp 已加入 CMake,colcon build 通过。
2. 自定义 YAML type 已在启动前注册 factory;内置 `crsf` 不需要注册。
3. 高优先级设备插入后稳定抢占,日志有明确原因。
4. 拔出或停止有效帧后,在 loss_timeout_ms 后发布一次零运动命令。
5. 新设备没有完整初始帧时,ready_timeout_ms 后回退,不接受控制。
6. 切换后按住的 start/stop 不会误触发;释放再按才会触发。
7. /motion_commands 与 YAML controls/outputs 一致。

相关文档

Clone this wiki locally