Skip to content

feat(hid): add optional vendor-defined selector key (vkey)#3390

Open
akira-toriyama wants to merge 1 commit into
zmkfirmware:mainfrom
akira-toriyama:feat/hid-vkey
Open

feat(hid): add optional vendor-defined selector key (vkey)#3390
akira-toriyama wants to merge 1 commit into
zmkfirmware:mainfrom
akira-toriyama:feat/hid-vkey

Conversation

@akira-toriyama

Copy link
Copy Markdown

What

Add CONFIG_ZMK_HID_VKEY (default n). When enabled, ZMK gains:

  • an independent vendor-defined HID top-level collection that reports a single
    1-byte selector — the "vkey id" (1255; 0 = released) — on a
    configurable vendor usage page (CONFIG_ZMK_HID_VKEY_USAGE_PAGE, default
    0xFF31) and report ID (CONFIG_ZMK_HID_VKEY_REPORT_ID, default 0x20);
  • a &vkey <id> behavior that sets the selector to <id> and sends the report
    on press, and sets it to 0 and sends on release.

When disabled (the default) the report descriptor, report state, send path and
behavior are all compiled out — the firmware is byte-identical to a build
without the option.

Why

Some setups want to send "original" key codes that are guaranteed never to
collide with any standard keyboard/consumer/mouse usage, and have a host-side
agent map them to actions (launch apps, window management, IME toggles, …).
Today that needs either reusing function-row/consumer codes (which collide with
real shortcuts) or an out-of-tree patch. A vendor-defined selector report is the
HID-correct way to carry such codes: the host sees a distinct vendor collection
it can match exclusively, and normal typing is untouched because the codes live
on a vendor page no application reads.

How

  • Descriptor (hid.h): under #if IS_ENABLED(CONFIG_ZMK_HID_VKEY), append
    an independent Application collection (Report ID = CONFIG_ZMK_HID_VKEY_REPORT_ID,
    one 8-bit Data/Var/Abs field, logical 0–255) after the standard collections.
    The 16-bit vendor usage page is written as a raw long item (0x06, lo, hi)
    because HID_USAGE_PAGE() emits a 1-byte item; lo/hi are split from the
    Kconfig value at compile time.
  • Report state (hid.c): struct zmk_hid_vkey_report plus
    zmk_hid_vkey_set/clear/get, all gated.
  • Send path (usb_hid.c, endpoints.c): zmk_usb_hid_send_vkey_report and
    zmk_endpoint_send_vkey_report, mirroring the consumer report path, plus a
    GET_REPORT case for the new report ID. zmk_endpoint_clear_reports also
    clears + resends the selector on endpoint switch, so a held vkey can't get
    stranded on the host.
  • Behavior (behavior_vkey.c + DT binding): a one-parameter behavior that
    runs on the central (where HID state and the USB endpoint live) and directly
    sets/sends the selector — it does not raise a keycode event, since the vendor
    selector isn't a KEY/CONSUMER usage.

BLE HOG send is out of scope for this PR (USB transport only). The vendor
collection still appears in the HOG report map because the descriptor is shared,
so adding a HOG send path later is purely additive.

Testing

  • Built all three targets of a Cyboard Imprint (two split halves + a USB dongle
    central) with CONFIG_ZMK_HID_VKEY=y; clang-format (pre-commit) and the build
    are clean.
  • Confirmed the emitted report descriptor carries the vendor collection with the
    expected bytes (Report ID 0x20, usage page 0xFF31, one 8-bit selector) in
    the dongle .elf.
  • The mechanism has been running out-of-tree on that board: &vkey <id>
    press/release emits a 2-byte report [report_id, id] / [report_id, 0] over
    USB, read on macOS via IOHIDManager and mapped to actions; normal typing is
    unaffected.
  • With CONFIG_ZMK_HID_VKEY=n (the default) every vkey addition sits behind
    #if IS_ENABLED(CONFIG_ZMK_HID_VKEY), so the descriptor and symbols are
    unchanged.

Open questions / feedback welcome

  • Naming: vkey / &vkey / CONFIG_ZMK_HID_VKEY — open to a different name
    (e.g. vendor-key / raw-hid) if preferred.
  • BLE: would maintainers want HOG send in this PR, or is a USB-first landing
    with a follow-up acceptable?
  • Selector width: 1 byte (single id at a time) here; a bitmap form for
    simultaneous ids could be a later, backward-compatible extension.

Add CONFIG_ZMK_HID_VKEY (default n). When enabled, ZMK gains an
independent vendor-defined HID top-level collection that reports a
single 1-byte selector (the "vkey" id, 1-255; 0 = released) on a
configurable vendor usage page (CONFIG_ZMK_HID_VKEY_USAGE_PAGE,
default 0xFF31) and report ID (CONFIG_ZMK_HID_VKEY_REPORT_ID, default
0x20), plus a &vkey <id> behavior that emits it on press and clears it
on release.

When disabled (the default) the report descriptor, report state, send
path and behavior are all compiled out, so the firmware is
byte-identical to a build without the option.

Why
---

Some setups want to send "original" key codes that are guaranteed
never to collide with any standard keyboard/consumer/mouse usage, and
have a host-side agent map them to actions (launch apps, window
management, IME toggles, and so on). A vendor-defined selector report
is the HID-correct way to carry such codes: the host sees a distinct
vendor collection it can match exclusively, and normal typing is
untouched because the codes live on a vendor page no application reads.

How
---

- hid.h: under CONFIG_ZMK_HID_VKEY, append an independent Application
  collection (one 8-bit Data/Var/Abs field, logical 0-255) after the
  standard collections. The 16-bit vendor usage page is written as a
  raw long item (0x06, lo, hi) split from the Kconfig value at compile
  time, since HID_USAGE_PAGE() emits a 1-byte item.
- hid.c: a struct zmk_hid_vkey_report plus zmk_hid_vkey_set/clear/get.
- usb_hid.c / endpoints.c: zmk_usb_hid_send_vkey_report and
  zmk_endpoint_send_vkey_report mirroring the consumer report path,
  plus a GET_REPORT case and a clear+resend on endpoint switch so a
  held vkey cannot get stranded on the host.
- behavior_vkey.c: a one-parameter behavior that runs on the central
  and sets/sends the selector directly.

BLE HOG send is out of scope for this PR (USB transport only); the
vendor collection still appears in the HOG report map because the
descriptor is shared, so adding a HOG send path later is additive.
@akira-toriyama
akira-toriyama requested a review from a team as a code owner June 18, 2026 07:36
akira-toriyama added a commit to akira-toriyama/canon that referenced this pull request Jun 18, 2026
vkey の汎用化 upstream PR を zmkfirmware/zmk#3390 として提出(案A=USB-only、
default-off CONFIG_ZMK_HID_VKEY、build-verify 済み・ZMK CI 同一 clang-format 整形)。
canon 側 docs を実態へ更新:

- docs/vkey-roadmap.md: 「Upstream 収束」表と残タスク #2 を「#3390 提出済み」へ。
- patches/zmk/README.md: vkey の upstream PR 欄を「未提出」→ #3390。
- docs/vkey-upstream-pr-draft.md(新規): 汎用化設計 + PR 本文 + ビルド検証結果 + 移行手順。
- docs/vkey-upstream-pr-draft.patch(新規): #3390 として提出した一般化 diff(バニラ ZMK 適用可)。

方針: マージは難航しうるため patches/zmk/vkey-report.patch は canon に維持(canon は
upstream 非依存)。#3390 が実際に merge された時のみ patch 撤去 → 案1 巻き戻しを実行。

Closes #61

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant