keyboard_handler 源码详细分析

keyboard_handler 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros-tooling/keyboard_handler
版本:0.0.5,许可证 Apache 2.0

keyboard_handler跨平台终端键盘输入库:通过回调订阅按键组合(含 Ctrl/Alt/Shift 修饰键),在后台线程轮询 stdin,供 CLI 工具在运行时响应快捷键。主要下游是 rosbag2play / record 交互控制;本身 不依赖 rclcpp,是纯 C++ 库 + ament 打包。


1. 仓库结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
keyboard_handler/
├── keyboard_handler/
│ ├── include/keyboard_handler/
│ │ ├── keyboard_handler.hpp # 平台别名入口
│ │ ├── keyboard_handler_base.hpp # 公共 API + KeyCode 枚举
│ │ ├── keyboard_handler_unix_impl.hpp
│ │ ├── keyboard_handler_windows_impl.hpp
│ │ └── visibility_control.hpp
│ ├── src/
│ │ ├── keyboard_handler_base.cpp # 回调注册/工具函数
│ │ ├── keyboard_handler_unix_impl.cpp
│ │ ├── keyboard_handler_windows_impl.cpp
│ │ ├── default_unix_key_map.cpp # xterm 转义序列映射
│ │ └── default_windows_key_map.cpp # _getch 码映射
│ ├── test/
│ │ ├── keyboard_handler_unix_tests.cpp
│ │ ├── keyboard_handler_windows_tests.cpp
│ │ ├── fake_player.hpp / fake_recorder.hpp
│ └── CMakeLists.txt / package.xml
├── docs/design/README.md # 设计文档(较完整)
└── README.md

构建产物: 单一共享库 libkeyboard_handler.so(Windows 为 DLL)。


2. 架构总览

公共 API平台实现键位映射表客户端KeyboardHandler\n(平台别名)KeyboardHandlerBaseKeyboardHandlerUnixImpl\ntermios + read 线程KeyboardHandlerWindowsImpl\n_kbhit + _getchdefault_unix_key_map\nxterm 序列default_windows_key_map\nWinKeyCoderosbag2 Player/Recorder其他 CLI 工具
层次 职责
KeyboardHandlerBase 回调注册/删除、KeyCode/KeyModifiers 枚举
Unix/Windows Impl 终端模式切换、后台读键、字节→枚举解析
Key Map 平台原始码 → 统一 KeyCode
keyboard_handler.hpp #ifdef _WIN32 选择实现

3. 公共 API

3.1 平台入口

1
2
3
4
5
6
7
#ifdef _WIN32
#include "keyboard_handler_windows_impl.hpp"
using KeyboardHandler = KeyboardHandlerWindowsImpl;
#else
#include "keyboard_handler_unix_impl.hpp"
using KeyboardHandler = KeyboardHandlerUnixImpl;
#endif

用户只需 #include "keyboard_handler/keyboard_handler.hpp"

3.2 核心方法

方法 说明
add_key_press_callback(callback, key_code, key_modifiers) 注册按键回调,返回 handle
delete_key_press_callback(handle) 按 handle 移除
invalid_handle (0) 注册失败时返回
1
2
3
4
5
6
7
8
callback_handle_t add_key_press_callback(...)
{
if (callback == nullptr || !is_init_succeed_) {
return invalid_handle;
}
callbacks_.emplace(KeyAndModifiers{key_code, key_modifiers}, ...);
return new_handle;
}

同一按键可注册多个回调unordered_multimap + equal_range 派发)。

3.3 KeyCode 与 KeyModifiers

  • KeyCode:100+ 枚举值(字母、数字、F1–F12、方向键、Home/End 等)
  • KeyModifiers:位掩码 SHIFT | ALT | CTRL
  • 自定义运算符:operator| 组合修饰键,operator&& 检测位

工具函数:

  • enum_key_code_to_str() / enum_str_to_key_code()
  • enum_key_modifiers_to_str()

4. Unix 实现(POSIX)

4.1 初始化流程

1
2
3
4
5
6
7
8
9
10
11
if (!isatty_fn(stdin_fd_)) {
std::cerr << "stdin is not a terminal device. Keyboard handling disabled.";
return; // is_init_succeed_ 保持 false
}
tcgetattr → 保存 old_term_settings_
可选安装 SIGINT handler
new_term_settings.c_lflag &= ~(ICANON | ECHO); // 非规范模式、关闭回显
new_term_settings.c_cc[VMIN] = 0;
new_term_settings.c_cc[VTIME] = 1; // 100ms 超时 read
tcsetattr → is_init_succeed_ = true
启动 key_handler_thread_

要点:

  • 必须真实 TTY:stdin 重定向到文件/管道时禁用键盘(不抛异常,便于 gtest)
  • 构造时切非规范模式,析构时恢复 canonical
  • Unix 可选 KeyboardHandler(false) 不安装 SIGINT 处理器(rosbag2 使用此模式,避免与进程信号冲突)

4.2 读键线程

1
2
3
4
5
6
7
8
do {
read_bytes = read_fn(stdin_fd_, buff, BUFF_LEN);
if (read_bytes > 0) {
auto [pressed_key_code, key_modifiers] = parse_input(buff, read_bytes);
lock → equal_range → 调用所有匹配 callback
}
} while (!exit_);
restore_buffer_mode_for_stdin();

4.3 parse_input 解析逻辑

输入特征 解析
2 字节且首字节 0x1B (ESC) ALT + 第二字节
单字节 'A'..'Z' 转小写 + SHIFT
单字节 0–26 CTRL + 对应字母(+96)
其余 key_codes_map_(xterm 序列)

映射表在 default_unix_key_map.cpp,基于 xterm 控制序列(如 \x1b[A = 上箭头)。注释说明不同终端模拟器序列可能不同。

4.4 SIGINT (Ctrl+C) 处理

1
2
3
4
5
void on_signal(int signal_number) {
restore_buffer_mode_for_stdin();
if (old_sigint_handler == SIG_DFL) _exit(...);
else { exit_ = true; 链式调用旧 handler; }
}

Ctrl+C 不会通过 callback 传给客户端(设计限制);仅保证终端模式恢复。


5. Windows 实现

5.1 读键线程

1
2
3
4
5
6
7
8
9
10
11
12
13
while (!exit_) {
if (kbhit_fn()) {
ch = getch_fn();
if (GetAsyncKeyState(VK_MENU)) key_modifiers |= ALT;
if (ch == 0 || ch == 0xE0) { // 功能键/方向键前缀
ch = getch_fn();
win_key_code.second = ch;
}
auto [key, mods] = win_key_code_to_enums(win_key_code);
派发 callbacks
sleep 100ms; // 让出 CPU
}
}
  • 使用 _kbhit + _getch(DOS 传统 API)
  • 功能键需 两次 getch(第一次 0 或 0xE0)
  • ALT 通过 GetAsyncKeyState(VK_MENU) 检测

5.2 win_key_code_to_enums

类似 Unix:处理 CTRL+F1..F12SHIFT+F1..F12、大写字母→SHIFT、0–26→CTRL 等,再查 default_windows_key_map.cpp 中的 {first, second} 对。

Windows 无 SIGINT/termios 处理;构造函数也无 install_signal_handler 参数。


6. 键位映射表

Unix(xterm 序列示例)

1
2
3
static constexpr char CURSOR_UP[]   = {27, 91, 65, '\0'};  // ESC [ A
static constexpr char F1[] = {27, 79, 80, '\0'}; // ESC O P
// ...

SHIFT+F1..F12 序列在注释中列出但未启用(修饰键检测局限)。

Windows(_getch 码示例)

1
2
3
4
{KeyCode::CURSOR_UP,   {0xE0, 72}},
{KeyCode::F1, {0, 59}},
{KeyCode::SPACE, {32, NOT_A_KEY}},
// ...

7. 回调生命周期管理

设计文档给出两种模式:

  1. 显式删除:析构时 delete_key_press_callback(handle)(rosbag2 Recorder 采用)
  2. weak_ptr lambda:客户端先于 handler 销毁时避免悬空指针

测试中的 FakePlayer / FakeRecorder 演示 enable_shared_from_this + weak_ptr 模式。


8. 下游集成:rosbag2

8.1 Player 默认快捷键

默认 KeyCode 功能
空格 SPACE 暂停/继续
CURSOR_RIGHT 播放下一条
CURSOR_UP 提高播放速率
CURSOR_DOWN 降低播放速率

8.2 Recorder

  • 空格:toggle_paused()
  • 析构时删除 callback handle

8.3 Unix 特殊构造

1
2
3
4
5
#ifndef _WIN32
std::make_shared<KeyboardHandler>(false), // 不装 SIGINT handler
#else
std::shared_ptr<KeyboardHandler>(new KeyboardHandler()),
#endif

测试注入 MockKeyboardHandler 模拟按键,无需真实终端。


9. 依赖关系

1
2
3
4
5
keyboard_handler
└── (无 rclcpp / 无 ROS 消息依赖)
仅 ament_cmake 打包
平台:termios/read/signal (Unix)
conio.h/Windows.h (Windows)

纯工具库,ROS 集成体现在 rosbag2 等消费者侧。


10. 测试

测试 平台 方式
keyboard_handler_unix_tests.cpp Unix 注入 mock read/isatty/tcgetattr/tcsetattr
keyboard_handler_windows_tests.cpp Windows 注入 mock _kbhit/_getch/_isatty
gmock + fake_player/recorder 两者 生命周期、多回调、修饰键组合

测试覆盖:按键解析、callback 注册/删除、对象先于 handler 销毁、stdin 非 TTY 安全模式等。


11. 已知局限(设计文档 + 头文件注释)

问题 Unix Windows
CTRL + 0..9
CTRL/ALT/SHIFT + F1..F12 部分
CTRL + SHIFT + key → 仅 CTRL + key
CTRL + ALT + key
ALT + F1..F12
多修饰键同时按下 可能误判 可能误判
Ctrl+C 作 callback ❌(SIGINT 专用)
SIGINT 与外部 handler 冲突 可能
stdin 重定向 禁用(不死锁) 禁用
终端类型差异 xterm 序列可能不匹配

12. 使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "keyboard_handler/keyboard_handler.hpp"

void on_key(KeyboardHandler::KeyCode code,
KeyboardHandler::KeyModifiers mods) {
if (code == KeyboardHandler::KeyCode::SPACE) { /* pause */ }
if (code == KeyboardHandler::KeyCode::A &&
(mods && KeyboardHandler::KeyModifiers::CTRL)) { /* Ctrl+A */ }
}

int main() {
KeyboardHandler handler; // Unix: 改 termios,启后台线程
auto h = handler.add_key_press_callback(
on_key, KeyboardHandler::KeyCode::SPACE);
// ... 主逻辑 ...
handler.delete_key_press_callback(h);
return 0; // 析构恢复终端
}

13. 设计特点

特点 说明
跨平台统一枚举 平台差异隐藏在 Impl + KeyMap
回调多订阅 multimap 支持同一键多个 listener
可测试性 系统调用可注入(DI 构造函数)
gtest 友好 非 TTY 时不抛异常,返回 invalid_handle
线程模型 专用读键线程 + mutex 保护 callback 表
终端恢复 析构/SIGINT 路径恢复 canonical 模式

14. 推荐阅读顺序

  1. docs/design/README.md — 设计目标、局限、生命周期模式
  2. keyboard_handler_base.hpp — KeyCode/KeyModifiers API
  3. keyboard_handler_unix_impl.cpp — termios + parse_input + 线程
  4. keyboard_handler_windows_impl.cpp — kbhit/getch 路径
  5. default_*_key_map.cpp — 平台码表
  6. rosbag2 player.cpp / recorder.cpp — 真实集成
  7. keyboard_handler_unix_tests.cpp — mock 测试模式

15. 小结

keyboard_handler 是 ros-tooling 提供的 轻量跨平台终端键盘库:基类管理回调表,Unix 用 termios 非规范 read,Windows 用 _kbhit/_getch 轮询,通过静态映射表统一为 KeyCode + KeyModifiers。它不绑定 ROS 中间件,但被 rosbag2 play/record 用作运行时交互控制的核心依赖。使用时需注意 修饰键检测局限、Ctrl+C 不进入 callback、以及 stdin 必须连接真实终端 等约束。

如需,我可以把本文写入 ros2doc/ros-tooling/keyboard_handler源码详细分析.md,或继续分析 rosbag2 Player 如何绑定全部快捷键

文章互动

阅读 --

留言

0 条留言

正在加载留言…