-
Notifications
You must be signed in to change notification settings - Fork 12
Custom Input Driver
本页说明如何接入 CRSF、SBUS、串口、UDP、TCP、蓝牙或自定义 HID。当前框架的边界是:
InputDeviceManager 负责选择、抢占、断连和安全切换
InputDriver 负责协议、raw signal、可用性和初始就绪状态
YAML 负责 source -> control -> output 的业务映射
driver 不应知道 normal、back_flip、btn_10=1 等业务名字。
只需改 YAML:换 Linux joystick、改键盘按键、改按钮组合、改曲线或控制映射。
需要新 driver:读取新协议或新传输,例如 CRSF、SBUS、串口、UDP/TCP、蓝牙或自定义 HID。
每个 YAML sources.<name> 都是候选设备。候选设备严格独占,优先级大的设备会抢占低优先级设备。
CRSF 示例:
inputs:
selection:
scan_interval_ms: 100
promote_stable_ms: 500
sources:
crsf_remote:
type: crsf
device: /dev/ttyCRSF
priority: 100
baudrate: 420000
ready_timeout_ms: 1000
loss_timeout_ms: 300
cooldown_ms: 1000
signals:
crsf.vx: {from: crsf.vx}
crsf.vy: {from: crsf.vy}
crsf.yaw: {from: crsf.yaw}
crsf.start: {from: crsf.start}
crsf.stop: {from: crsf.stop}命名建议:
- raw signal 使用协议前缀,例如
crsf.vx、udp.mode。 - 轴统一归一化到
[-1, 1];按钮统一为0.0 / 1.0。 - 通道到
crsf.vx、crsf.start的复杂映射可在 driver C++ 中实现。 -
device是通用设备路径;baudrate、port等标量放在InputDeviceConfig.options。
之后再在 controls 中引用 crsf.vx 等语义 source。业务输出不应出现在 driver 中。
驱动应放在专用目录,不要再写入已删除的 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 # 新增
将 src/drivers/crsf_input_driver.cpp 显式加入 remote_controller_core 的 CMake 源文件列表。
通常直接继承 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 CrsfInputDriver : public InputDriverBase {
public:
using InputDriverBase::InputDriverBase;
~CrsfInputDriver() 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(&CrsfInputDriver::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("crsf.vx", normalize(frame.channel(0)));
set_signal("crsf.vy", normalize(frame.channel(1)));
set_signal("crsf.yaw", normalize(frame.channel(2)));
set_signal("crsf.start", frame.start_pressed ? 1.0 : 0.0);
set_signal("crsf.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()、CrsfFrame 和 normalize() 由协议实现提供。读取循环必须用非阻塞 fd 配合 select() / poll(),每次超时检查 stop_flag_。
对 CRSF,
is_available()不能仅检查/dev/ttyCRSF存在。它应以近期 CRC 正确帧为健康依据。由于设备管理器会在激活前调用该函数,driver 可实现轻量、非阻塞的协议 probe 或缓存探测结果。
将新 driver 编译进包后,在创建 COMPublisher 前注册工厂。例如在 main.cpp 中、rclcpp::init() 之后:
remote_controller::register_input_driver_factory(
"crsf",
[](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::CrsfInputDriver(
config,
mapper,
mapper_lock,
std::move(output_handler),
std::move(log_handler)));
});也可以在 driver_registry.cpp 中集中注册,但必须确保注册发生在 InputDeviceManager 建立候选列表之前。未注册的 type: crsf 不会让节点退出:会记录 warning 并使用下一个可用候选设备。
设备管理器按以下规则工作:
高优先级候选连续可用 promote_stable_ms
-> 停止当前设备
-> 清空旧 signal
-> 发布一次零运动命令
-> 启动新 driver
-> 等待 is_ready()
-> 允许正常控制
is_available() 连续失败 loss_timeout_ms
-> 同样进入安全切换
因此 driver 应做到:
-
is_available()只描述设备/协议健康,且绝不阻塞管理器。 -
is_ready()表示已经获得足够的初始状态,避免切换时把未知按键状态当成按下。 - 断连后不自行保留旧值;管理器会清理该设备配置的所有 raw signal。
- edge 命令在切换期间被抑制;用户必须释放后再次按下。
signals.*.timeout_ms 只是单个字段的过期保护:字段未更新时写入 failsafe。它不负责候选设备断连和切换。
- joystick:不设置;静止没有事件是正常的。
- CRSF:用
is_available()+loss_timeout_ms判断“没有有效完整帧”。 - UDP/TCP:当一个字段可能独立停止更新时,可以保留
timeout_ms作为补充。
1. 新 .cpp 已加入 CMake,colcon build 通过。
2. YAML type 已在启动前注册 factory。
3. 高优先级设备插入后稳定抢占,日志有明确原因。
4. 拔出或停止有效帧后,在 loss_timeout_ms 后发布一次零运动命令。
5. 新设备没有完整初始帧时,ready_timeout_ms 后回退,不接受控制。
6. 切换后按住的 start/stop 不会误触发;释放再按才会触发。
7. /motion_commands 与 YAML controls/outputs 一致。