skills/ohos-dev-distributed-mmi-uinput-virtual-device/SKILL.md
Use when testing OpenHarmony input subsystem features (mouse/keyboard/touchpad event flow, device binding, display group isolation) on a real device without physical peripherals. Covers /dev/uinput device creation, event injection, InputManager device discovery, and hot-plug simulation.
npx skillsauth add openharmonyinsight/openharmony-skills ohos-dev-distributed-mmi-uinput-virtual-deviceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
通过 /dev/uinput 创建虚拟 HID 设备,注入精确事件序列,验证输入子系统行为。
BUS_USB(0x03)。BUS_VIRTUAL 会被 MMI 过滤,不触发设备发现回调EV_KEY+EV_REL,否则不被识别为 pointer 设备int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
ioctl(fd, UI_SET_EVBIT, EV_KEY);
ioctl(fd, UI_SET_EVBIT, EV_REL);
ioctl(fd, UI_SET_KEYBIT, BTN_LEFT);
ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT); // 可选
ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE); // 可选
ioctl(fd, UI_SET_RELBIT, REL_X);
ioctl(fd, UI_SET_RELBIT, REL_Y);
ioctl(fd, UI_SET_RELBIT, REL_WHEEL); // 可选
struct uinput_user_dev ud = {};
strncpy(ud.name, "TestMouseA", UINPUT_MAX_NAME_SIZE - 1);
ud.id.bustype = BUS_USB; // 必须
ud.id.vendor = 0x93a;
ud.id.product = 0xAA01; // 每个虚拟设备用不同值
ud.id.version = 1;
write(fd, &ud, sizeof(ud));
ioctl(fd, UI_DEV_CREATE);
int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
ioctl(fd, UI_SET_EVBIT, EV_KEY);
// 不注册 EV_REL —— 确保被识别为键盘而非鼠标
for (int k = KEY_ESC; k <= KEY_RIGHTMETA; k++) {
ioctl(fd, UI_SET_KEYBIT, k);
}
// ... 同上构造 uinput_user_dev, bustype=BUS_USB
每个事件序列必须以 SYN_REPORT 结尾:
// 鼠标相对移动
void InjectMotion(int fd, int dx, int dy) {
struct input_event ev = {};
ev.type = EV_REL; ev.code = REL_X; ev.value = dx;
write(fd, &ev, sizeof(ev));
ev.code = REL_Y; ev.value = dy;
write(fd, &ev, sizeof(ev));
ev.type = EV_SYN; ev.code = SYN_REPORT; ev.value = 0;
write(fd, &ev, sizeof(ev));
}
// 按键/按钮(鼠标按钮和键盘按键共用)
void InjectKey(int fd, int code, int pressed) {
struct input_event ev = {};
ev.type = EV_KEY; ev.code = code; ev.value = pressed;
write(fd, &ev, sizeof(ev));
ev.type = EV_SYN; ev.code = SYN_REPORT; ev.value = 0;
write(fd, &ev, sizeof(ev));
}
// code: BTN_LEFT, BTN_RIGHT (鼠标按钮)
// KEY_A, KEY_LEFTCTRL, KEY_LEFTSHIFT (键盘按键)
// pressed: 1=按下, 0=释放
GetDeviceIds / GetDevice 是异步回调。创建 uinput 设备后需要等待 MMI 枚举:
static int FindDevice(const std::string &name) {
struct FindState {
std::mutex mtx;
std::condition_variable cv;
int foundId = -1;
int pending = 0;
bool done = false;
};
for (int attempt = 0; attempt < 30; ++attempt) {
auto state = std::make_shared<FindState>();
InputManager::GetInstance()->GetDeviceIds([state, name](std::vector<int32_t> &ids) {
{
std::lock_guard<std::mutex> lock(state->mtx);
state->pending = static_cast<int>(ids.size());
if (state->pending == 0) {
state->done = true;
state->cv.notify_one();
return;
}
}
for (auto id : ids) {
InputManager::GetInstance()->GetDevice(id,
[state, id, name](std::shared_ptr<InputDevice> dev) {
bool notify = false;
{
std::lock_guard<std::mutex> lock(state->mtx);
if (dev && dev->GetName() == name && state->foundId < 0) {
state->foundId = id;
}
if (--state->pending == 0) {
state->done = true;
notify = true;
}
}
if (notify) {
state->cv.notify_one();
}
});
}
});
std::unique_lock<std::mutex> lock(state->mtx);
state->cv.wait_for(lock, std::chrono::milliseconds(500), [&]{ return state->done; });
if (state->foundId >= 0) return state->foundId;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
return -1;
}
创建后至少等 3 秒再调 FindDevice,给 libinput 和 MMI 枚举的时间。
ioctl(fd, UI_DEV_DESTROY);
close(fd);
// MMI 的 OnInputDeviceRemoved 会自动清理绑定
// 等 2 秒后 dump 验证 RuntimeBindings 已清理
测试 per-group 隔离时的标准模式:
1. 创建 2 个虚拟设备,绑定到不同 group
2. warm-up:每个设备注入 2-3 个事件触发懒初始化
3. 对设备 A 注入值 X(如 +20,+10 移动),对设备 B 注入值 Y(如 -15,-8)
4. dump → 验证 group0 反映 X,group1 反映 Y,互不影响
warm-up 必要性:绑定后首次事件触发 EnsureGroupState 和 cursorPosMap_ 初始化。跳过 warm-up 会导致 dump 中 group 状态为空。
测试代码写 marker,后台脚本捕获 hidumper:
// 测试代码中
static void DumpPhase(const char *label) {
FILE *f = fopen("/data/local/tmp/phase.txt", "w");
if (f) { fprintf(f, "%s\n", label); fclose(f); }
std::this_thread::sleep_for(std::chrono::seconds(3));
}
#!/system/bin/sh
# run_with_dump.sh — 后台捕获 hidumper
DUMP=/data/local/tmp/dump.txt
PHASE=/data/local/tmp/phase.txt
rm -f "$DUMP" "$PHASE"
$TEST_BIN &
PID=$!
LAST=""
while kill -0 $PID 2>/dev/null; do
if [ -f "$PHASE" ]; then
CUR=$(cat "$PHASE" 2>/dev/null)
if [ "$CUR" != "$LAST" ] && [ -n "$CUR" ]; then
echo "===== DUMP: $CUR =====" >> "$DUMP"
hidumper -s MultimodalInput -a '-G' >> "$DUMP" 2>&1
LAST="$CUR"
fi
fi
sleep 1
done
wait $PID 2>/dev/null
// 必须在测试退出前销毁所有虚拟设备
ioctl(fd, UI_DEV_DESTROY);
close(fd);
不销毁的虚拟设备会在进程退出时自动清理,但 MMI 的设备移除通知可能延迟。
| 错误 | 后果 | 修复 |
|------|------|------|
| bustype = BUS_VIRTUAL | 设备不被 MMI 识别 | 用 BUS_USB |
| 缺少 SYN_REPORT | 事件不被处理 | 每个事件序列末尾加 SYN_REPORT |
| 创建后立即 FindDevice | 找不到设备 | 等 3 秒 |
| 跳过 warm-up 事件 | per-group 状态未初始化 | 绑定后注入 2-3 个移动事件 |
| 鼠标不注册 EV_REL | 不被识别为 pointer 设备 | 同时注册 EV_KEY + EV_REL |
| 多个设备用同一 product ID | 可能混淆设备发现 | 每个设备用不同 product |
testing
--- name: ohos-req-value-decision description: Use after review meeting to record decision and route to next step. Triggers: 评审决策纪要, 评审结论回流, value decision, 评审接纳, 评审不接纳, 评审退回, 下次重新上会. Do NOT use for feature baseline (ohos-req-feature-baseline), review gate checks (ohos-req-review-gate), or IR generation (ohos-req-feature-to-ir). metadata: author: openharmony scope: common stage: requirements capability: value-decision version: 0.3.0 status: draft tags: - sdd - requirements
development
Use when converting an OpenHarmony requirement document, spec, or design proposal into an OpenHarmony review slide deck (需求评审 / 需求变更评审 / 设计评审 PPTX) — produces the fixed OpenHarmony-branded review-deck structure (OH logo on every page) with architecture/flow diagrams and field tables. Triggers on "需求评审PPT", "需求变更评审", "把需求文档转成评审PPT", "spec转评审PPT", "requirement/spec to review deck". NOT for arbitrary or generic slide decks unrelated to OpenHarmony requirement/design review.
testing
Use when performing the Phase 0 Step 0.5 Review Ready Gate on a 04-feature.md, especially when the user says "evaluate gate", "review readiness", "feature ready?", "should we generate IR", or when the ohos-req-intake-orchestration main session needs a structured Ready / Conditional Ready / Not Ready judgment instead of doing the check inline. Reads 01-04, runs seven fixed checks plus a conditional-items check, and returns a machine-readable JSON summary plus a human-readable table that the main session can route on. Do NOT use for feature baseline generation (ohos-req-feature-baseline), value decision recording (ohos-req-value-decision), or IR generation (ohos-req-feature-to-ir).
testing
--- name: ohos-req-requirement-intake description: Use when importing an OHOS requirement into Phase 0.1, especially for 01-requirement.md, requirement intake, background, user value, scenarios, scope, FR/NFR, affected modules, or priority. Triggers: 需求导入, 01-requirement, 需求基线, RR单号. Do NOT use for feasibility analysis (ohos-req-feasibility-analysis), architecture decision (ohos-req-arch-decision), or feature baseline (ohos-req-feature-baseline). metadata: author: openharmony scope: common