From a90822217084f8071ecf77b5193d63931198e4e4 Mon Sep 17 00:00:00 2001 From: Tom Dodson Date: Sun, 21 Jun 2026 10:26:04 -0700 Subject: [PATCH 1/4] refactor(hid): extract report descriptor into reusable macros Restructure the single zmk_hid_report_desc[] initializer into composable macros (ZMK_HID_REPORT_DESC_COMMON_PREFIX/_COMMON_SUFFIX, the per-mode _NKRO/_HKRO_KEYBOARD_ITEMS, and the ZMK_HID_REPORT_DESC() combiner) so the descriptor body can be instantiated more than once. No functional change: for the static NKRO and HKRO configurations the generated report descriptor is byte-for-byte identical to before, verified across indicators / pointing / smooth-scrolling and consumer BASIC/FULL permutations. --- app/include/zmk/hid.h | 245 +++++++++++++++++------------------------- 1 file changed, 99 insertions(+), 146 deletions(-) diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index 962180e706d..5513d2566c6 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -101,159 +101,112 @@ #define HID_USAGE16_SINGLE(a) HID_USAGE16((a & 0xFF), ((a >> 8) & 0xFF)) -static const uint8_t zmk_hid_report_desc[] = { - HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), - HID_USAGE(HID_USAGE_GD_KEYBOARD), - HID_COLLECTION(HID_COLLECTION_APPLICATION), - HID_REPORT_ID(ZMK_HID_REPORT_ID_KEYBOARD), - HID_USAGE_PAGE(HID_USAGE_KEY), - HID_USAGE_MIN8(HID_USAGE_KEY_KEYBOARD_LEFTCONTROL), - HID_USAGE_MAX8(HID_USAGE_KEY_KEYBOARD_RIGHT_GUI), - HID_LOGICAL_MIN8(0x00), - HID_LOGICAL_MAX8(0x01), - - HID_REPORT_SIZE(0x01), - HID_REPORT_COUNT(0x08), - HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), - - HID_USAGE_PAGE(HID_USAGE_KEY), - HID_REPORT_SIZE(0x08), - HID_REPORT_COUNT(0x01), - HID_INPUT(ZMK_HID_MAIN_VAL_CONST | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), - -#if IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) - - HID_USAGE_PAGE(HID_USAGE_LED), - HID_USAGE_MIN8(HID_USAGE_LED_NUM_LOCK), - HID_USAGE_MAX8(HID_USAGE_LED_KANA), - HID_REPORT_SIZE(0x01), - HID_REPORT_COUNT(0x05), - HID_OUTPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), - - HID_USAGE_PAGE(HID_USAGE_LED), - HID_REPORT_SIZE(0x03), - HID_REPORT_COUNT(0x01), - HID_OUTPUT(ZMK_HID_MAIN_VAL_CONST | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), - -#endif // IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) - - HID_USAGE_PAGE(HID_USAGE_KEY), +#define ZMK_HID_REPORT_DESC_COMMON_PREFIX \ + HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), HID_USAGE(HID_USAGE_GD_KEYBOARD), \ + HID_COLLECTION(HID_COLLECTION_APPLICATION), HID_REPORT_ID(ZMK_HID_REPORT_ID_KEYBOARD), \ + HID_USAGE_PAGE(HID_USAGE_KEY), HID_USAGE_MIN8(HID_USAGE_KEY_KEYBOARD_LEFTCONTROL), \ + HID_USAGE_MAX8(HID_USAGE_KEY_KEYBOARD_RIGHT_GUI), HID_LOGICAL_MIN8(0x00), \ + HID_LOGICAL_MAX8(0x01), \ + \ + HID_REPORT_SIZE(0x01), HID_REPORT_COUNT(0x08), \ + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), \ + \ + HID_USAGE_PAGE(HID_USAGE_KEY), HID_REPORT_SIZE(0x08), HID_REPORT_COUNT(0x01), \ + HID_INPUT(ZMK_HID_MAIN_VAL_CONST | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), \ + \ + IF_ENABLED( \ + CONFIG_ZMK_HID_INDICATORS, \ + (HID_USAGE_PAGE(HID_USAGE_LED), HID_USAGE_MIN8(HID_USAGE_LED_NUM_LOCK), \ + HID_USAGE_MAX8(HID_USAGE_LED_KANA), HID_REPORT_SIZE(0x01), HID_REPORT_COUNT(0x05), \ + HID_OUTPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), \ + \ + HID_USAGE_PAGE(HID_USAGE_LED), HID_REPORT_SIZE(0x03), HID_REPORT_COUNT(0x01), \ + HID_OUTPUT(ZMK_HID_MAIN_VAL_CONST | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), )) \ + \ + HID_USAGE_PAGE(HID_USAGE_KEY) + +#define ZMK_HID_REPORT_DESC_COMMON_SUFFIX \ + HID_END_COLLECTION, HID_USAGE_PAGE(HID_USAGE_CONSUMER), \ + HID_USAGE(HID_USAGE_CONSUMER_CONSUMER_CONTROL), \ + HID_COLLECTION(HID_COLLECTION_APPLICATION), HID_REPORT_ID(ZMK_HID_REPORT_ID_CONSUMER), \ + HID_USAGE_PAGE(HID_USAGE_CONSUMER), \ + \ + IF_ENABLED(CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC, \ + (HID_LOGICAL_MIN8(0x00), HID_LOGICAL_MAX16(0xFF, 0x00), HID_USAGE_MIN8(0x00), \ + HID_USAGE_MAX8(0xFF), HID_REPORT_SIZE(0x08), )) \ + IF_ENABLED(CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_FULL, \ + (HID_LOGICAL_MIN8(0x00), HID_LOGICAL_MAX16(0xFF, 0x0F), \ + HID_USAGE_MIN8(0x00), HID_USAGE_MAX16(0xFF, 0x0F), \ + HID_REPORT_SIZE(0x10), )) \ + HID_REPORT_COUNT(CONFIG_ZMK_HID_CONSUMER_REPORT_SIZE), \ + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_ARRAY | ZMK_HID_MAIN_VAL_ABS), \ + HID_END_COLLECTION, \ + \ + IF_ENABLED( \ + CONFIG_ZMK_POINTING, \ + (HID_USAGE_PAGE(HID_USAGE_GD), HID_USAGE(HID_USAGE_GD_MOUSE), \ + HID_COLLECTION(HID_COLLECTION_APPLICATION), HID_REPORT_ID(ZMK_HID_REPORT_ID_MOUSE), \ + HID_USAGE(HID_USAGE_GD_POINTER), HID_COLLECTION(HID_COLLECTION_PHYSICAL), \ + HID_USAGE_PAGE(HID_USAGE_BUTTON), HID_USAGE_MIN8(0x1), \ + HID_USAGE_MAX8(ZMK_HID_MOUSE_NUM_BUTTONS), HID_LOGICAL_MIN8(0x00), \ + HID_LOGICAL_MAX8(0x01), HID_REPORT_SIZE(0x01), HID_REPORT_COUNT(0x5), \ + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), \ + HID_REPORT_SIZE(0x03), HID_REPORT_COUNT(0x01), \ + HID_INPUT(ZMK_HID_MAIN_VAL_CONST | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), \ + HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), HID_USAGE(HID_USAGE_GD_X), \ + HID_USAGE(HID_USAGE_GD_Y), HID_LOGICAL_MIN16(0x00, 0x80), \ + HID_LOGICAL_MAX16(0xFF, 0x7F), HID_REPORT_SIZE(0x10), HID_REPORT_COUNT(0x02), \ + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), \ + HID_COLLECTION(HID_COLLECTION_LOGICAL), \ + IF_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING, \ + (HID_USAGE(HID_USAGE_GD_RESOLUTION_MULTIPLIER), HID_LOGICAL_MIN8(0x00), \ + HID_LOGICAL_MAX8(0x0F), HID_PHYSICAL_MIN8(0x01), HID_PHYSICAL_MAX8(0x10), \ + HID_REPORT_SIZE(0x04), HID_REPORT_COUNT(0x01), HID_PUSH, \ + HID_FEATURE(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | \ + ZMK_HID_MAIN_VAL_ABS), )) HID_USAGE(HID_USAGE_GD_WHEEL), \ + HID_LOGICAL_MIN16(0x00, 0x80), HID_LOGICAL_MAX16(0xFF, 0x7F), \ + HID_PHYSICAL_MIN8(0x00), HID_PHYSICAL_MAX8(0x00), HID_REPORT_SIZE(0x10), \ + HID_REPORT_COUNT(0x01), \ + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), \ + HID_END_COLLECTION, HID_COLLECTION(HID_COLLECTION_LOGICAL), \ + IF_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING, \ + (HID_USAGE(HID_USAGE_GD_RESOLUTION_MULTIPLIER), HID_POP, \ + HID_FEATURE(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | \ + ZMK_HID_MAIN_VAL_ABS), )) HID_USAGE_PAGE(HID_USAGE_CONSUMER), \ + HID_USAGE16_SINGLE(HID_USAGE_CONSUMER_AC_PAN), HID_LOGICAL_MIN16(0x00, 0x80), \ + HID_LOGICAL_MAX16(0xFF, 0x7F), HID_PHYSICAL_MIN8(0x00), HID_PHYSICAL_MAX8(0x00), \ + HID_REPORT_SIZE(0x10), HID_REPORT_COUNT(0x01), \ + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), \ + HID_END_COLLECTION, HID_END_COLLECTION, HID_END_COLLECTION, )) + +#define ZMK_HID_REPORT_DESC_NKRO_KEYBOARD_ITEMS \ + HID_LOGICAL_MIN8(0x00), HID_LOGICAL_MAX8(0x01), HID_USAGE_MIN8(0x00), \ + HID_USAGE_MAX8(ZMK_HID_KEYBOARD_NKRO_MAX_USAGE), HID_REPORT_SIZE(0x01), \ + HID_REPORT_COUNT(ZMK_HID_KEYBOARD_NKRO_MAX_USAGE + 1), \ + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS) + +#define ZMK_HID_REPORT_DESC_HKRO_KEYBOARD_ITEMS \ + HID_LOGICAL_MIN8(0x00), HID_LOGICAL_MAX16(0xFF, 0x00), HID_USAGE_MIN8(0x00), \ + HID_USAGE_MAX8(0xFF), HID_REPORT_SIZE(0x08), \ + HID_REPORT_COUNT(CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE), \ + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_ARRAY | ZMK_HID_MAIN_VAL_ABS) + +#define ZMK_HID_REPORT_DESC(keyboard_items) \ + {ZMK_HID_REPORT_DESC_COMMON_PREFIX, keyboard_items, ZMK_HID_REPORT_DESC_COMMON_SUFFIX} #if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) - HID_LOGICAL_MIN8(0x00), - HID_LOGICAL_MAX8(0x01), - HID_USAGE_MIN8(0x00), - HID_USAGE_MAX8(ZMK_HID_KEYBOARD_NKRO_MAX_USAGE), - HID_REPORT_SIZE(0x01), - HID_REPORT_COUNT(ZMK_HID_KEYBOARD_NKRO_MAX_USAGE + 1), - HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), + +static const uint8_t zmk_hid_report_desc[] = + ZMK_HID_REPORT_DESC(ZMK_HID_REPORT_DESC_NKRO_KEYBOARD_ITEMS); + #elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) - HID_LOGICAL_MIN8(0x00), - HID_LOGICAL_MAX16(0xFF, 0x00), - HID_USAGE_MIN8(0x00), - HID_USAGE_MAX8(0xFF), - HID_REPORT_SIZE(0x08), - HID_REPORT_COUNT(CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE), - HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_ARRAY | ZMK_HID_MAIN_VAL_ABS), -#else -#error "A proper HID report type must be selected" -#endif - HID_END_COLLECTION, - HID_USAGE_PAGE(HID_USAGE_CONSUMER), - HID_USAGE(HID_USAGE_CONSUMER_CONSUMER_CONTROL), - HID_COLLECTION(HID_COLLECTION_APPLICATION), - HID_REPORT_ID(ZMK_HID_REPORT_ID_CONSUMER), - HID_USAGE_PAGE(HID_USAGE_CONSUMER), +static const uint8_t zmk_hid_report_desc[] = + ZMK_HID_REPORT_DESC(ZMK_HID_REPORT_DESC_HKRO_KEYBOARD_ITEMS); -#if IS_ENABLED(CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC) - HID_LOGICAL_MIN8(0x00), - HID_LOGICAL_MAX16(0xFF, 0x00), - HID_USAGE_MIN8(0x00), - HID_USAGE_MAX8(0xFF), - HID_REPORT_SIZE(0x08), -#elif IS_ENABLED(CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_FULL) - HID_LOGICAL_MIN8(0x00), - HID_LOGICAL_MAX16(0xFF, 0x0F), - HID_USAGE_MIN8(0x00), - HID_USAGE_MAX16(0xFF, 0x0F), - HID_REPORT_SIZE(0x10), #else -#error "A proper consumer HID report usage range must be selected" +#error "A proper HID report type must be selected" #endif - HID_REPORT_COUNT(CONFIG_ZMK_HID_CONSUMER_REPORT_SIZE), - HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_ARRAY | ZMK_HID_MAIN_VAL_ABS), - HID_END_COLLECTION, - -#if IS_ENABLED(CONFIG_ZMK_POINTING) - HID_USAGE_PAGE(HID_USAGE_GD), - HID_USAGE(HID_USAGE_GD_MOUSE), - HID_COLLECTION(HID_COLLECTION_APPLICATION), - HID_REPORT_ID(ZMK_HID_REPORT_ID_MOUSE), - HID_USAGE(HID_USAGE_GD_POINTER), - HID_COLLECTION(HID_COLLECTION_PHYSICAL), - HID_USAGE_PAGE(HID_USAGE_BUTTON), - HID_USAGE_MIN8(0x1), - HID_USAGE_MAX8(ZMK_HID_MOUSE_NUM_BUTTONS), - HID_LOGICAL_MIN8(0x00), - HID_LOGICAL_MAX8(0x01), - HID_REPORT_SIZE(0x01), - HID_REPORT_COUNT(0x5), - HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), - // Constant padding for the last 3 bits. - HID_REPORT_SIZE(0x03), - HID_REPORT_COUNT(0x01), - HID_INPUT(ZMK_HID_MAIN_VAL_CONST | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), - // Some OSes ignore pointer devices without X/Y data. - HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), - HID_USAGE(HID_USAGE_GD_X), - HID_USAGE(HID_USAGE_GD_Y), - HID_LOGICAL_MIN16(0x00, 0x80), - HID_LOGICAL_MAX16(0xFF, 0x7F), - HID_REPORT_SIZE(0x10), - HID_REPORT_COUNT(0x02), - HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), - HID_COLLECTION(HID_COLLECTION_LOGICAL), -#if IS_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING) - HID_USAGE(HID_USAGE_GD_RESOLUTION_MULTIPLIER), - HID_LOGICAL_MIN8(0x00), - HID_LOGICAL_MAX8(0x0F), - HID_PHYSICAL_MIN8(0x01), - HID_PHYSICAL_MAX8(0x10), - HID_REPORT_SIZE(0x04), - HID_REPORT_COUNT(0x01), - HID_PUSH, - HID_FEATURE(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), -#endif // IS_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING) - HID_USAGE(HID_USAGE_GD_WHEEL), - HID_LOGICAL_MIN16(0x00, 0x80), - HID_LOGICAL_MAX16(0xFF, 0x7F), - HID_PHYSICAL_MIN8(0x00), - HID_PHYSICAL_MAX8(0x00), - HID_REPORT_SIZE(0x10), - HID_REPORT_COUNT(0x01), - HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), - HID_END_COLLECTION, - HID_COLLECTION(HID_COLLECTION_LOGICAL), -#if IS_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING) - HID_USAGE(HID_USAGE_GD_RESOLUTION_MULTIPLIER), - HID_POP, - HID_FEATURE(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), -#endif // IS_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING) - HID_USAGE_PAGE(HID_USAGE_CONSUMER), - HID_USAGE16_SINGLE(HID_USAGE_CONSUMER_AC_PAN), - HID_LOGICAL_MIN16(0x00, 0x80), - HID_LOGICAL_MAX16(0xFF, 0x7F), - HID_PHYSICAL_MIN8(0x00), - HID_PHYSICAL_MAX8(0x00), - HID_REPORT_SIZE(0x10), - HID_REPORT_COUNT(0x01), - HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), - HID_END_COLLECTION, - HID_END_COLLECTION, - HID_END_COLLECTION, -#endif // IS_ENABLED(CONFIG_ZMK_POINTING) -}; #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) From 1ef651862f569e570ebcae64e2c63c94d29aa04f Mon Sep 17 00:00:00 2001 From: Tom Dodson Date: Mon, 22 Jun 2026 17:59:10 -0700 Subject: [PATCH 2/4] refactor(hid): extract shared NKRO/HKRO key primitives Factor the per-usage press/release/is-pressed/boot-report loops out of the static NKRO and HKRO report-type arms into buffer-parameterized static inline primitives (nkro_*/hkro_*). Each arm becomes a thin binding of those primitives to keyboard_report.body.keys; keys_held bookkeeping stays in the arms so its ordering is unchanged. Pure refactor of existing code, no behavior change. --- app/src/hid.c | 154 +++++++++++++++++++++++++++++++------------------- 1 file changed, 96 insertions(+), 58 deletions(-) diff --git a/app/src/hid.c b/app/src/hid.c index d8700c3f4a8..024135160e1 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -113,43 +113,117 @@ static zmk_hid_boot_report_t *boot_report_rollover(uint8_t modifiers) { #endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ +// --- Shared keyboard-usage primitives --------------------------------------------------------- +// +// NKRO (bitmap body) and HKRO (array body) each need exactly one implementation of +// press / release / is-pressed / boot-report conversion, parameterized by the key buffer they +// operate on. The report-type arms below are then just thin bindings of these primitives to +// keyboard_report.body.keys. keys_held bookkeeping stays in the arms so its increment/decrement +// ordering is unchanged. + #if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) -#define TOGGLE_KEYBOARD(code, val) WRITE_BIT(keyboard_report.body.keys[code / 8], code % 8, val) +static inline int nkro_toggle(uint8_t *keys, zmk_key_t usage, int val) { + if (usage > ZMK_HID_KEYBOARD_NKRO_MAX_USAGE) { + return -EINVAL; + } + WRITE_BIT(keys[usage / 8], usage % 8, val); + return 0; +} -#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) -zmk_hid_boot_report_t *zmk_hid_get_boot_report(void) { - if (keys_held > HID_BOOT_KEY_LEN) { - return boot_report_rollover(keyboard_report.body.modifiers); +static inline bool nkro_check(const uint8_t *keys, zmk_key_t usage) { + if (usage > ZMK_HID_KEYBOARD_NKRO_MAX_USAGE) { + return false; } + return keys[usage / 8] & BIT(usage % 8); +} - boot_report.modifiers = keyboard_report.body.modifiers; +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) +static inline zmk_hid_boot_report_t *nkro_boot_report(const uint8_t *keys, size_t len) { memset(&boot_report.keys, 0, HID_BOOT_KEY_LEN); int ix = 0; - uint8_t base_code = 0; - for (int i = 0; i < sizeof(keyboard_report.body.keys); ++i) { + for (int i = 0; i < len; ++i) { if (ix == keys_held) { break; } - if (!keyboard_report.body.keys[i]) { + if (!keys[i]) { continue; } - base_code = i * 8; + uint8_t base_code = i * 8; for (int j = 0; j < 8; ++j) { - if (keyboard_report.body.keys[i] & BIT(j)) { + if (keys[i] & BIT(j)) { boot_report.keys[ix++] = base_code + j; } } } return &boot_report; } +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + +#endif /* NKRO */ + +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) + +static inline void hkro_toggle(uint8_t *keys, size_t size, uint8_t match, uint8_t val) { + for (int idx = 0; idx < size; idx++) { + if (keys[idx] != match) { + continue; + } + keys[idx] = val; + if (val) { + break; + } + } +} + +static inline bool hkro_check(const uint8_t *keys, size_t size, zmk_key_t usage) { + for (int idx = 0; idx < size; idx++) { + if (keys[idx] == usage) { + return true; + } + } + return false; +} + +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) +static inline zmk_hid_boot_report_t *hkro_boot_report(const uint8_t *keys, size_t size) { + int out = 0; + for (int i = 0; i < size; i++) { + uint8_t key = keys[i]; + if (key) { + boot_report.keys[out++] = key; + if (out == keys_held) { + break; + } + } + } + while (out < HID_BOOT_KEY_LEN) { + boot_report.keys[out++] = 0; + } + return &boot_report; +} +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + +#endif /* HKRO */ + +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) + +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) +zmk_hid_boot_report_t *zmk_hid_get_boot_report(void) { + if (keys_held > HID_BOOT_KEY_LEN) { + return boot_report_rollover(keyboard_report.body.modifiers); + } + + boot_report.modifiers = keyboard_report.body.modifiers; + return nkro_boot_report(keyboard_report.body.keys, sizeof(keyboard_report.body.keys)); +} #endif static inline int select_keyboard_usage(zmk_key_t usage) { - if (usage > ZMK_HID_KEYBOARD_NKRO_MAX_USAGE) { - return -EINVAL; + int ret = nkro_toggle(keyboard_report.body.keys, usage, 1); + if (ret) { + return ret; } - TOGGLE_KEYBOARD(usage, 1); #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) ++keys_held; #endif @@ -157,10 +231,10 @@ static inline int select_keyboard_usage(zmk_key_t usage) { } static inline int deselect_keyboard_usage(zmk_key_t usage) { - if (usage > ZMK_HID_KEYBOARD_NKRO_MAX_USAGE) { - return -EINVAL; + int ret = nkro_toggle(keyboard_report.body.keys, usage, 0); + if (ret) { + return ret; } - TOGGLE_KEYBOARD(usage, 0); #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) --keys_held; #endif @@ -168,25 +242,11 @@ static inline int deselect_keyboard_usage(zmk_key_t usage) { } static inline bool check_keyboard_usage(zmk_key_t usage) { - if (usage > ZMK_HID_KEYBOARD_NKRO_MAX_USAGE) { - return false; - } - return keyboard_report.body.keys[usage / 8] & (1 << (usage % 8)); + return nkro_check(keyboard_report.body.keys, usage); } #elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) -#define TOGGLE_KEYBOARD(match, val) \ - for (int idx = 0; idx < CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE; idx++) { \ - if (keyboard_report.body.keys[idx] != match) { \ - continue; \ - } \ - keyboard_report.body.keys[idx] = val; \ - if (val) { \ - break; \ - } \ - } - #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) zmk_hid_boot_report_t *zmk_hid_get_boot_report(void) { if (keys_held > HID_BOOT_KEY_LEN) { @@ -195,25 +255,8 @@ zmk_hid_boot_report_t *zmk_hid_get_boot_report(void) { #if CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE != HID_BOOT_KEY_LEN // Form a boot report from a report of different size. - boot_report.modifiers = keyboard_report.body.modifiers; - - int out = 0; - for (int i = 0; i < CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE; i++) { - uint8_t key = keyboard_report.body.keys[i]; - if (key) { - boot_report.keys[out++] = key; - if (out == keys_held) { - break; - } - } - } - - while (out < HID_BOOT_KEY_LEN) { - boot_report.keys[out++] = 0; - } - - return &boot_report; + return hkro_boot_report(keyboard_report.body.keys, CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE); #else return &keyboard_report.body; #endif /* CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE != HID_BOOT_KEY_LEN */ @@ -221,7 +264,7 @@ zmk_hid_boot_report_t *zmk_hid_get_boot_report(void) { #endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ static inline int select_keyboard_usage(zmk_key_t usage) { - TOGGLE_KEYBOARD(0U, usage); + hkro_toggle(keyboard_report.body.keys, CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE, 0U, usage); #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) ++keys_held; #endif @@ -229,7 +272,7 @@ static inline int select_keyboard_usage(zmk_key_t usage) { } static inline int deselect_keyboard_usage(zmk_key_t usage) { - TOGGLE_KEYBOARD(usage, 0U); + hkro_toggle(keyboard_report.body.keys, CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE, usage, 0U); #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) --keys_held; #endif @@ -237,12 +280,7 @@ static inline int deselect_keyboard_usage(zmk_key_t usage) { } static inline int check_keyboard_usage(zmk_key_t usage) { - for (int idx = 0; idx < CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE; idx++) { - if (keyboard_report.body.keys[idx] == usage) { - return true; - } - } - return false; + return hkro_check(keyboard_report.body.keys, CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE, usage); } #else From 8b55f5fdb3b6b9a7e4bad776c7ad44098d1b8a6f Mon Sep 17 00:00:00 2001 From: Tom Dodson Date: Mon, 22 Jun 2026 17:59:22 -0700 Subject: [PATCH 3/4] feat(hid): add runtime-switchable HKRO/NKRO dynamic NKRO mode Compile in both the HKRO and NKRO HID report descriptors/report bodies and pick between them at boot from a flash-persisted mode. The &dyn_nkro behavior flips the active mode and warm-reboots (descriptors cannot change after enumeration). The HID Report Type choice selects the first-boot default (HKRO at ZMK's own default). Leverages the shared key primitives and the reusable report-descriptor macros from the preceding refactor commits: the dynamic arm just binds those primitives to body.nkro_keys/body.hkro_keys per the boot-resolved mode. --- app/CMakeLists.txt | 2 + app/Kconfig | 32 ++++- app/Kconfig.defaults | 2 +- app/dts/behaviors.dtsi | 1 + app/dts/behaviors/dynamic_nkro.dtsi | 15 ++ .../behaviors/zmk,behavior-dynamic-nkro.yaml | 13 ++ app/include/dt-bindings/zmk/dynamic_nkro.h | 9 ++ app/include/zmk/hid.h | 53 +++++++- app/include/zmk/hid_dynamic_nkro.h | 27 ++++ app/src/behaviors/behavior_dynamic_nkro.c | 75 ++++++++++ app/src/hid.c | 128 ++++++++++++++++-- app/src/hid_dynamic_nkro.c | 103 ++++++++++++++ app/src/hog.c | 9 +- app/src/usb_hid.c | 6 +- .../events.patterns | 1 + .../keycode_events.snapshot | 2 + .../native_sim.conf | 7 + .../native_sim.keymap | 8 ++ 18 files changed, 465 insertions(+), 28 deletions(-) create mode 100644 app/dts/behaviors/dynamic_nkro.dtsi create mode 100644 app/dts/bindings/behaviors/zmk,behavior-dynamic-nkro.yaml create mode 100644 app/include/dt-bindings/zmk/dynamic_nkro.h create mode 100644 app/include/zmk/hid_dynamic_nkro.h create mode 100644 app/src/behaviors/behavior_dynamic_nkro.c create mode 100644 app/src/hid_dynamic_nkro.c create mode 100644 app/tests/keypress/kp-press-release-dynamic-nkro/events.patterns create mode 100644 app/tests/keypress/kp-press-release-dynamic-nkro/keycode_events.snapshot create mode 100644 app/tests/keypress/kp-press-release-dynamic-nkro/native_sim.conf create mode 100644 app/tests/keypress/kp-press-release-dynamic-nkro/native_sim.keymap diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index c41153cb17c..fba4d7f69ac 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -47,6 +47,8 @@ target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SOFT_OFF app PRIVATE src/behaviors/beha add_subdirectory_ifdef(CONFIG_ZMK_POINTING src/pointing/) if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/hid.c) + target_sources_ifdef(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC app PRIVATE src/hid_dynamic_nkro.c) + target_sources_ifdef(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC app PRIVATE src/behaviors/behavior_dynamic_nkro.c) target_sources(app PRIVATE src/behaviors/behavior_key_press.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_KEY_TOGGLE app PRIVATE src/behaviors/behavior_key_toggle.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_HOLD_TAP app PRIVATE src/behaviors/behavior_hold_tap.c) diff --git a/app/Kconfig b/app/Kconfig index 8c0675e0d68..a298573427c 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -80,20 +80,46 @@ config ZMK_HID_REPORT_TYPE_NKRO endchoice +config ZMK_HID_REPORT_TYPE_DYNAMIC + bool "Allow switching HKRO/NKRO at runtime" + select SETTINGS + select ZMK_SETTINGS + help + Compile in both the HKRO and NKRO HID report descriptors/report bodies and pick + between them at boot. The HID Report Type choice above selects the first-boot + default (HKRO when left at ZMK's own default); the &dyn_nkro behavior flips the + active mode and reboots, so a single firmware build can switch between full NKRO + for daily use and "boot keyboard" compatible HKRO for BIOS/UEFI screens, without + reflashing. The chosen mode is persisted to flash and only takes effect after the + reboot the behavior triggers, since USB/BLE HID report descriptors cannot be + changed after enumeration. + config ZMK_HID_KEYBOARD_NKRO_EXTENDED_REPORT bool "Enable extended NKRO reporting" - depends on ZMK_HID_REPORT_TYPE_NKRO + depends on ZMK_HID_REPORT_TYPE_NKRO || ZMK_HID_REPORT_TYPE_DYNAMIC help Enables higher usage range for NKRO (F13-F24 and INTL1-9). Please note this is not compatible with Android currently and you will get no input +if ZMK_HID_REPORT_TYPE_DYNAMIC + +config ZMK_HID_REPORT_TYPE_DYNAMIC_INIT_PRIORITY + int "Dynamic NKRO settings load init priority" + default 10 + help + APPLICATION init priority used to eagerly load the persisted HID report mode + from settings. Must run before ZMK_BLE_INIT_PRIORITY and + ZMK_USB_HID_INIT_PRIORITY, since both register a fixed HID report + descriptor/size for the rest of the boot session. + +endif # ZMK_HID_REPORT_TYPE_DYNAMIC -if ZMK_HID_REPORT_TYPE_HKRO +if ZMK_HID_REPORT_TYPE_HKRO || ZMK_HID_REPORT_TYPE_DYNAMIC config ZMK_HID_KEYBOARD_REPORT_SIZE int "# Keyboard Keys Reportable" -endif # ZMK_HID_REPORT_TYPE_HKRO +endif # ZMK_HID_REPORT_TYPE_HKRO || ZMK_HID_REPORT_TYPE_DYNAMIC config ZMK_HID_CONSUMER_REPORT_SIZE int "# Consumer Keys Reportable" diff --git a/app/Kconfig.defaults b/app/Kconfig.defaults index 8800b454f40..ac3f06400b9 100644 --- a/app/Kconfig.defaults +++ b/app/Kconfig.defaults @@ -10,7 +10,7 @@ config BT_DEVICE_APPEARANCE default 961 # HID -if ZMK_HID_REPORT_TYPE_HKRO +if ZMK_HID_REPORT_TYPE_HKRO || ZMK_HID_REPORT_TYPE_DYNAMIC config ZMK_HID_KEYBOARD_REPORT_SIZE default 6 diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index 653b085d5c5..f3665364feb 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -28,3 +28,4 @@ #include #include #include +#include diff --git a/app/dts/behaviors/dynamic_nkro.dtsi b/app/dts/behaviors/dynamic_nkro.dtsi new file mode 100644 index 00000000000..f8b7d556bcb --- /dev/null +++ b/app/dts/behaviors/dynamic_nkro.dtsi @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/ { + behaviors { + dyn_nkro: dyn_nkro { + compatible = "zmk,behavior-dynamic-nkro"; + #binding-cells = <1>; + display-name = "Dynamic NKRO"; + }; + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-dynamic-nkro.yaml b/app/dts/bindings/behaviors/zmk,behavior-dynamic-nkro.yaml new file mode 100644 index 00000000000..062973e1481 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-dynamic-nkro.yaml @@ -0,0 +1,13 @@ +# Copyright (c) 2025 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: | + Switches the active USB/BLE HID keyboard report mode between full NKRO and + "boot keyboard" compatible HKRO (see CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC). The + chosen mode is persisted to flash and only takes effect after the reboot + this behavior triggers, since the report descriptor can't change without a + full HID re-enumeration. + +compatible: "zmk,behavior-dynamic-nkro" + +include: one_param.yaml diff --git a/app/include/dt-bindings/zmk/dynamic_nkro.h b/app/include/dt-bindings/zmk/dynamic_nkro.h new file mode 100644 index 00000000000..31b4e5a7f69 --- /dev/null +++ b/app/include/dt-bindings/zmk/dynamic_nkro.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DYN_NKRO_TOG 0 +#define DYN_NKRO_NKRO 1 +#define DYN_NKRO_HKRO 2 diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index 5513d2566c6..7ce5a0750e3 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -15,6 +15,9 @@ #if IS_ENABLED(CONFIG_ZMK_POINTING) #include #endif // IS_ENABLED(CONFIG_ZMK_POINTING) +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) +#include +#endif // IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) #include #include @@ -33,9 +36,13 @@ #error "Unknown consumer report usages configuration" #endif -#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) && !IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) #define ZMK_HID_KEYBOARD_MAX_USAGE ZMK_HID_KEYBOARD_NKRO_MAX_USAGE -#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) +#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) && !IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) +#define ZMK_HID_KEYBOARD_MAX_USAGE 0xFF +#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) +// Widest of the two compiled-in modes; each mode's own press/release/check logic still +// enforces its own (possibly narrower) usage bound at runtime. #define ZMK_HID_KEYBOARD_MAX_USAGE 0xFF #else #error "Unknown keyboard report usages configuration" @@ -194,26 +201,52 @@ #define ZMK_HID_REPORT_DESC(keyboard_items) \ {ZMK_HID_REPORT_DESC_COMMON_PREFIX, keyboard_items, ZMK_HID_REPORT_DESC_COMMON_SUFFIX} -#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) && !IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) static const uint8_t zmk_hid_report_desc[] = ZMK_HID_REPORT_DESC(ZMK_HID_REPORT_DESC_NKRO_KEYBOARD_ITEMS); -#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) +#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) && !IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) static const uint8_t zmk_hid_report_desc[] = ZMK_HID_REPORT_DESC(ZMK_HID_REPORT_DESC_HKRO_KEYBOARD_ITEMS); +#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) + +// Both report descriptor variants are compiled in; zmk_hid_report_desc_get() picks the one +// matching the mode loaded from settings at boot (see hid_dynamic_nkro.c). +static const uint8_t zmk_hid_report_desc_nkro[] = + ZMK_HID_REPORT_DESC(ZMK_HID_REPORT_DESC_NKRO_KEYBOARD_ITEMS); + +static const uint8_t zmk_hid_report_desc_hkro[] = + ZMK_HID_REPORT_DESC(ZMK_HID_REPORT_DESC_HKRO_KEYBOARD_ITEMS); + #else #error "A proper HID report type must be selected" #endif +/** + * Returns the active HID report descriptor (and its length) for the current boot session. + * For the static NKRO/HKRO configurations this is just zmk_hid_report_desc; for the dynamic + * configuration it is whichever of zmk_hid_report_desc_{nkro,hkro} matches the persisted mode. + */ +const uint8_t *zmk_hid_report_desc_get(size_t *len); + +/** + * Size, in bytes, of struct zmk_hid_keyboard_report_body for the active mode. For the static + * NKRO/HKRO configurations this is just sizeof(struct zmk_hid_keyboard_report_body); the dynamic + * configuration's body is a union sized for the larger of the two modes, so callers that send + * the body over the wire need the mode-specific size instead of sizeof(). + */ +size_t zmk_hid_keyboard_report_body_size(void); + #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) #define HID_ERROR_ROLLOVER 0x1 #define HID_BOOT_KEY_LEN 6 #if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) && \ + !IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) && \ CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE == HID_BOOT_KEY_LEN typedef struct zmk_hid_keyboard_report_body zmk_hid_boot_report_t; #else @@ -230,10 +263,18 @@ typedef struct zmk_hid_boot_report zmk_hid_boot_report_t; struct zmk_hid_keyboard_report_body { zmk_mod_flags_t modifiers; uint8_t _reserved; -#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) && !IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) uint8_t keys[DIV_ROUND_UP(ZMK_HID_KEYBOARD_NKRO_MAX_USAGE + 1, 8)]; -#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) +#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) && !IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) uint8_t keys[CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE]; +#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) + // Both layouts are compiled in; only nkro_keys or only hkro_keys is meaningful at a time, + // selected by zmk_hid_dynamic_nkro_get_mode(). zmk_hid_keyboard_report_body_size() reports + // the size of whichever one is active, since they're rarely the same length. + union { + uint8_t nkro_keys[DIV_ROUND_UP(ZMK_HID_KEYBOARD_NKRO_MAX_USAGE + 1, 8)]; + uint8_t hkro_keys[CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE]; + }; #endif } __packed; diff --git a/app/include/zmk/hid_dynamic_nkro.h b/app/include/zmk/hid_dynamic_nkro.h new file mode 100644 index 00000000000..7388f837223 --- /dev/null +++ b/app/include/zmk/hid_dynamic_nkro.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +enum zmk_hid_dynamic_nkro_mode { + ZMK_HID_DYNAMIC_NKRO_MODE_NKRO = 0, + ZMK_HID_DYNAMIC_NKRO_MODE_HKRO = 1, +}; + +/** + * The HID report mode active for the current boot session. Decided once, early in boot, + * before USB/BLE HID are initialized, since the report descriptor/size they register cannot + * change without a full re-enumeration (i.e. a reboot). + */ +enum zmk_hid_dynamic_nkro_mode zmk_hid_dynamic_nkro_get_mode(void); + +/** + * Persist a new mode to flash. Does not take effect until the next reboot; callers that want + * the change to apply are expected to trigger one (see behavior_dynamic_nkro.c). + */ +int zmk_hid_dynamic_nkro_set_mode(enum zmk_hid_dynamic_nkro_mode mode); diff --git a/app/src/behaviors/behavior_dynamic_nkro.c b/app/src/behaviors/behavior_dynamic_nkro.c new file mode 100644 index 00000000000..342dd33541c --- /dev/null +++ b/app/src/behaviors/behavior_dynamic_nkro.c @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_dynamic_nkro + +#include +#include +#include + +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + enum zmk_hid_dynamic_nkro_mode current = zmk_hid_dynamic_nkro_get_mode(); + enum zmk_hid_dynamic_nkro_mode target; + + switch (binding->param1) { + case DYN_NKRO_TOG: + target = current == ZMK_HID_DYNAMIC_NKRO_MODE_NKRO ? ZMK_HID_DYNAMIC_NKRO_MODE_HKRO + : ZMK_HID_DYNAMIC_NKRO_MODE_NKRO; + break; + case DYN_NKRO_NKRO: + target = ZMK_HID_DYNAMIC_NKRO_MODE_NKRO; + break; + case DYN_NKRO_HKRO: + target = ZMK_HID_DYNAMIC_NKRO_MODE_HKRO; + break; + default: + LOG_ERR("Unknown dynamic NKRO command: %d", binding->param1); + return -ENOTSUP; + } + + if (target == current) { + return ZMK_BEHAVIOR_OPAQUE; + } + + int ret = zmk_hid_dynamic_nkro_set_mode(target); + if (ret < 0) { + return ret; + } + + LOG_INF("Dynamic NKRO mode persisted, rebooting to apply it"); + + // The descriptor registered with USB/BLE HID can't change without a full + // re-enumeration, so the new mode only takes effect after this reboot. + sys_reboot(SYS_REBOOT_WARM); + + return ZMK_BEHAVIOR_OPAQUE; +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + return ZMK_BEHAVIOR_OPAQUE; +} + +static const struct behavior_driver_api behavior_dynamic_nkro_driver_api = { + .binding_pressed = on_keymap_binding_pressed, + .binding_released = on_keymap_binding_released, + .locality = BEHAVIOR_LOCALITY_GLOBAL, +}; + +BEHAVIOR_DT_INST_DEFINE(0, NULL, NULL, NULL, NULL, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, + &behavior_dynamic_nkro_driver_api); + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/app/src/hid.c b/app/src/hid.c index 024135160e1..9f3a66a6838 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -5,6 +5,7 @@ */ #include "zmk/keys.h" +#include #include LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); @@ -12,8 +13,15 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include static struct zmk_hid_keyboard_report keyboard_report = { - - .report_id = ZMK_HID_REPORT_ID_KEYBOARD, .body = {.modifiers = 0, ._reserved = 0, .keys = {0}}}; + .report_id = ZMK_HID_REPORT_ID_KEYBOARD, +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) + // .keys doesn't exist for the dynamic union body; naming either member zero-initializes + // the whole union the same way the static storage duration default already would. + .body = {.modifiers = 0, ._reserved = 0, .nkro_keys = {0}}, +#else + .body = {.modifiers = 0, ._reserved = 0, .keys = {0}}, +#endif +}; static struct zmk_hid_consumer_report consumer_report = {.report_id = ZMK_HID_REPORT_ID_CONSUMER, .body = {.keys = {0}}}; @@ -116,12 +124,14 @@ static zmk_hid_boot_report_t *boot_report_rollover(uint8_t modifiers) { // --- Shared keyboard-usage primitives --------------------------------------------------------- // // NKRO (bitmap body) and HKRO (array body) each need exactly one implementation of -// press / release / is-pressed / boot-report conversion, parameterized by the key buffer they -// operate on. The report-type arms below are then just thin bindings of these primitives to -// keyboard_report.body.keys. keys_held bookkeeping stays in the arms so its increment/decrement -// ordering is unchanged. +// press / release / is-pressed / boot-report conversion. Every report-type configuration is just +// a binding of these primitives to a key buffer: the static NKRO/HKRO configs bind the single +// body.keys buffer at compile time; the dynamic config binds body.nkro_keys or body.hkro_keys per +// the boot-resolved mode. Keeping the loops here means each is written (and audited) once, +// regardless of how many modes are compiled in. keys_held bookkeeping stays in the public +// dispatchers below so its increment/decrement ordering is unchanged. -#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) || IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) static inline int nkro_toggle(uint8_t *keys, zmk_key_t usage, int val) { if (usage > ZMK_HID_KEYBOARD_NKRO_MAX_USAGE) { @@ -160,9 +170,9 @@ static inline zmk_hid_boot_report_t *nkro_boot_report(const uint8_t *keys, size_ } #endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ -#endif /* NKRO */ +#endif /* NKRO || DYNAMIC */ -#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) || IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) static inline void hkro_toggle(uint8_t *keys, size_t size, uint8_t match, uint8_t val) { for (int idx = 0; idx < size; idx++) { @@ -204,9 +214,9 @@ static inline zmk_hid_boot_report_t *hkro_boot_report(const uint8_t *keys, size_ } #endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ -#endif /* HKRO */ +#endif /* HKRO || DYNAMIC */ -#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) && !IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) zmk_hid_boot_report_t *zmk_hid_get_boot_report(void) { @@ -245,7 +255,16 @@ static inline bool check_keyboard_usage(zmk_key_t usage) { return nkro_check(keyboard_report.body.keys, usage); } -#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) +const uint8_t *zmk_hid_report_desc_get(size_t *len) { + *len = sizeof(zmk_hid_report_desc); + return zmk_hid_report_desc; +} + +size_t zmk_hid_keyboard_report_body_size(void) { + return sizeof(struct zmk_hid_keyboard_report_body); +} + +#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) && !IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) zmk_hid_boot_report_t *zmk_hid_get_boot_report(void) { @@ -283,6 +302,91 @@ static inline int check_keyboard_usage(zmk_key_t usage) { return hkro_check(keyboard_report.body.keys, CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE, usage); } +const uint8_t *zmk_hid_report_desc_get(size_t *len) { + *len = sizeof(zmk_hid_report_desc); + return zmk_hid_report_desc; +} + +size_t zmk_hid_keyboard_report_body_size(void) { + return sizeof(struct zmk_hid_keyboard_report_body); +} + +#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC) + +static inline bool dynamic_nkro_active(void) { + return zmk_hid_dynamic_nkro_get_mode() == ZMK_HID_DYNAMIC_NKRO_MODE_NKRO; +} + +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) +zmk_hid_boot_report_t *zmk_hid_get_boot_report(void) { + if (keys_held > HID_BOOT_KEY_LEN) { + return boot_report_rollover(keyboard_report.body.modifiers); + } + + boot_report.modifiers = keyboard_report.body.modifiers; + + if (dynamic_nkro_active()) { + return nkro_boot_report(keyboard_report.body.nkro_keys, + sizeof(keyboard_report.body.nkro_keys)); + } + return hkro_boot_report(keyboard_report.body.hkro_keys, CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE); +} +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + +static inline int select_keyboard_usage(zmk_key_t usage) { + if (dynamic_nkro_active()) { + int ret = nkro_toggle(keyboard_report.body.nkro_keys, usage, 1); + if (ret) { + return ret; + } + } else { + hkro_toggle(keyboard_report.body.hkro_keys, CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE, 0U, usage); + } +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + ++keys_held; +#endif + return 0; +} + +static inline int deselect_keyboard_usage(zmk_key_t usage) { + if (dynamic_nkro_active()) { + int ret = nkro_toggle(keyboard_report.body.nkro_keys, usage, 0); + if (ret) { + return ret; + } + } else { + hkro_toggle(keyboard_report.body.hkro_keys, CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE, usage, 0U); + } +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + --keys_held; +#endif + return 0; +} + +static inline bool check_keyboard_usage(zmk_key_t usage) { + if (dynamic_nkro_active()) { + return nkro_check(keyboard_report.body.nkro_keys, usage); + } + return hkro_check(keyboard_report.body.hkro_keys, CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE, usage); +} + +const uint8_t *zmk_hid_report_desc_get(size_t *len) { + if (dynamic_nkro_active()) { + *len = sizeof(zmk_hid_report_desc_nkro); + return zmk_hid_report_desc_nkro; + } + + *len = sizeof(zmk_hid_report_desc_hkro); + return zmk_hid_report_desc_hkro; +} + +size_t zmk_hid_keyboard_report_body_size(void) { + size_t keys_len = dynamic_nkro_active() + ? sizeof(((struct zmk_hid_keyboard_report_body *)NULL)->nkro_keys) + : sizeof(((struct zmk_hid_keyboard_report_body *)NULL)->hkro_keys); + return offsetof(struct zmk_hid_keyboard_report_body, nkro_keys) + keys_len; +} + #else #error "A proper HID report type must be selected" #endif diff --git a/app/src/hid_dynamic_nkro.c b/app/src/hid_dynamic_nkro.c new file mode 100644 index 00000000000..419f03d96d6 --- /dev/null +++ b/app/src/hid_dynamic_nkro.c @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include + +// First-boot default (before any mode is persisted) follows the HID Report Type choice: +// CONFIG_ZMK_HID_REPORT_TYPE_NKRO -> NKRO, otherwise HKRO (ZMK's own choice default). +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) +#define ZMK_HID_DYNAMIC_NKRO_DEFAULT_MODE ZMK_HID_DYNAMIC_NKRO_MODE_NKRO +#else +#define ZMK_HID_DYNAMIC_NKRO_DEFAULT_MODE ZMK_HID_DYNAMIC_NKRO_MODE_HKRO +#endif + +static enum zmk_hid_dynamic_nkro_mode current_mode = ZMK_HID_DYNAMIC_NKRO_DEFAULT_MODE; + +enum zmk_hid_dynamic_nkro_mode zmk_hid_dynamic_nkro_get_mode(void) { return current_mode; } + +int zmk_hid_dynamic_nkro_set_mode(enum zmk_hid_dynamic_nkro_mode mode) { + if (mode == current_mode) { + return 0; + } + + current_mode = mode; + + uint8_t stored = (uint8_t)mode; + int ret = settings_save_one("zmk/hid/mode", &stored, sizeof(stored)); + if (ret < 0) { + LOG_ERR("Failed to persist dynamic NKRO mode (%d)", ret); + } + + return ret; +} + +static int hid_dynamic_nkro_settings_set(const char *name, size_t len, settings_read_cb read_cb, + void *cb_arg) { + const char *next; + + if (settings_name_steq(name, "mode", &next) && !next) { + uint8_t stored; + + if (len != sizeof(stored)) { + return -EINVAL; + } + + int ret = read_cb(cb_arg, &stored, sizeof(stored)); + if (ret <= 0) { + return ret; + } + + if (stored != ZMK_HID_DYNAMIC_NKRO_MODE_NKRO && stored != ZMK_HID_DYNAMIC_NKRO_MODE_HKRO) { + LOG_WRN("Ignoring invalid stored dynamic NKRO mode %d", stored); + return -EINVAL; + } + + current_mode = (enum zmk_hid_dynamic_nkro_mode)stored; + LOG_INF("Loaded dynamic NKRO mode %d from settings", current_mode); + } + + return 0; +} + +static struct settings_handler hid_dynamic_nkro_settings_handler = { + .name = "zmk/hid", + .h_set = hid_dynamic_nkro_settings_set, +}; + +static int hid_dynamic_nkro_init(void) { + settings_subsys_init(); + + int ret = settings_register(&hid_dynamic_nkro_settings_handler); + if (ret < 0) { + LOG_ERR("Failed to register dynamic NKRO settings handler (%d)", ret); + return ret; + } + + /* + * USB HID (prio CONFIG_ZMK_USB_HID_INIT_PRIORITY) and BLE HOG (prio + * CONFIG_ZMK_BLE_INIT_PRIORITY) both register a fixed report descriptor/size for the rest + * of the boot session, so the persisted mode has to be loaded eagerly here rather than + * waiting for the application-level settings_load() in main(). + */ + ret = settings_load_subtree("zmk/hid"); + if (ret < 0) { + LOG_ERR("Failed to load dynamic NKRO settings (%d)", ret); + return ret; + } + + LOG_INF("Dynamic NKRO active mode for this boot: %s", + current_mode == ZMK_HID_DYNAMIC_NKRO_MODE_NKRO ? "NKRO" : "HKRO"); + + return 0; +} + +SYS_INIT(hid_dynamic_nkro_init, APPLICATION, CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC_INIT_PRIORITY); diff --git a/app/src/hog.c b/app/src/hog.c index c7d9b861d4a..8d25f0d41db 100644 --- a/app/src/hog.c +++ b/app/src/hog.c @@ -108,15 +108,16 @@ static ssize_t read_hids_report_ref(struct bt_conn *conn, const struct bt_gatt_a static ssize_t read_hids_report_map(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) { - return bt_gatt_attr_read(conn, attr, buf, len, offset, zmk_hid_report_desc, - sizeof(zmk_hid_report_desc)); + size_t report_desc_len; + const uint8_t *report_desc = zmk_hid_report_desc_get(&report_desc_len); + return bt_gatt_attr_read(conn, attr, buf, len, offset, report_desc, report_desc_len); } static ssize_t read_hids_input_report(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) { struct zmk_hid_keyboard_report_body *report_body = &zmk_hid_get_keyboard_report()->body; return bt_gatt_attr_read(conn, attr, buf, len, offset, report_body, - sizeof(struct zmk_hid_keyboard_report_body)); + zmk_hid_keyboard_report_body_size()); } #if IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) @@ -319,7 +320,7 @@ void send_keyboard_report_callback(struct k_work *work) { struct bt_gatt_notify_params notify_params = { .attr = &hog_svc.attrs[5], .data = &report, - .len = sizeof(report), + .len = zmk_hid_keyboard_report_body_size(), }; int err = bt_gatt_notify_cb(conn, ¬ify_params); diff --git a/app/src/usb_hid.c b/app/src/usb_hid.c index 9adc7db8092..f19e87107af 100644 --- a/app/src/usb_hid.c +++ b/app/src/usb_hid.c @@ -56,7 +56,7 @@ static uint8_t *get_keyboard_report(size_t *len) { } #endif struct zmk_hid_keyboard_report *report = zmk_hid_get_keyboard_report(); - *len = sizeof(*report); + *len = sizeof(report->report_id) + zmk_hid_keyboard_report_body_size(); return (uint8_t *)report; } @@ -242,7 +242,9 @@ static int zmk_usb_hid_init(void) { return -EINVAL; } - usb_hid_register_device(hid_dev, zmk_hid_report_desc, sizeof(zmk_hid_report_desc), &ops); + size_t report_desc_len; + const uint8_t *report_desc = zmk_hid_report_desc_get(&report_desc_len); + usb_hid_register_device(hid_dev, report_desc, report_desc_len, &ops); #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) usb_hid_set_proto_code(hid_dev, HID_BOOT_IFACE_CODE_KEYBOARD); diff --git a/app/tests/keypress/kp-press-release-dynamic-nkro/events.patterns b/app/tests/keypress/kp-press-release-dynamic-nkro/events.patterns new file mode 100644 index 00000000000..b1342af4d97 --- /dev/null +++ b/app/tests/keypress/kp-press-release-dynamic-nkro/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/keypress/kp-press-release-dynamic-nkro/keycode_events.snapshot b/app/tests/keypress/kp-press-release-dynamic-nkro/keycode_events.snapshot new file mode 100644 index 00000000000..259501ba3d9 --- /dev/null +++ b/app/tests/keypress/kp-press-release-dynamic-nkro/keycode_events.snapshot @@ -0,0 +1,2 @@ +pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/keypress/kp-press-release-dynamic-nkro/native_sim.conf b/app/tests/keypress/kp-press-release-dynamic-nkro/native_sim.conf new file mode 100644 index 00000000000..0d6aafc6eb9 --- /dev/null +++ b/app/tests/keypress/kp-press-release-dynamic-nkro/native_sim.conf @@ -0,0 +1,7 @@ +CONFIG_GPIO=n +CONFIG_LOG=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_ZMK_LOG_LEVEL_DBG=y +CONFIG_DEBUG=y +CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000 +CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC=y diff --git a/app/tests/keypress/kp-press-release-dynamic-nkro/native_sim.keymap b/app/tests/keypress/kp-press-release-dynamic-nkro/native_sim.keymap new file mode 100644 index 00000000000..eb8a725ed37 --- /dev/null +++ b/app/tests/keypress/kp-press-release-dynamic-nkro/native_sim.keymap @@ -0,0 +1,8 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; From 1cfc6fb806deb30b8e86634b79bd042b5134e43a Mon Sep 17 00:00:00 2001 From: Tom Dodson Date: Mon, 22 Jun 2026 18:07:33 -0700 Subject: [PATCH 4/4] docs: document dynamic NKRO config and behavior Add a Dynamic NKRO behavior page covering &dyn_nkro and its command defines, and document CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC in the system config reference. --- docs/docs/config/system.md | 8 +++ docs/docs/keymaps/behaviors/dynamic-nkro.md | 69 +++++++++++++++++++++ docs/sidebars.js | 1 + 3 files changed, 78 insertions(+) create mode 100644 docs/docs/keymaps/behaviors/dynamic-nkro.md diff --git a/docs/docs/config/system.md b/docs/docs/config/system.md index 1cf70a82554..d8fbe69a8af 100644 --- a/docs/docs/config/system.md +++ b/docs/docs/config/system.md @@ -66,6 +66,14 @@ If `CONFIG_ZMK_HID_REPORT_TYPE_NKRO` is enabled, it may be configured with the f | ---------------------------------------------- | ---- | -------------------------------------------------------------------- | ------- | | `CONFIG_ZMK_HID_KEYBOARD_NKRO_EXTENDED_REPORT` | bool | Enable less frequently used key usages, at the cost of compatibility | n | +Instead of picking a single report type at build time, you can compile in both and switch between them at runtime: + +| Config | Type | Description | Default | +| ------------------------------------ | ---- | ---------------------------------------------------------------- | ------- | +| `CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC` | bool | Compile in both HKRO and NKRO and switch between them at runtime | n | + +With this enabled, the `HKRO`/`NKRO` choice above only sets the first-boot default; the [`&dyn_nkro`](../keymaps/behaviors/dynamic-nkro.md) behavior switches the active mode afterwards. A HID report descriptor can't change without re-enumerating, so switching persists the new mode to flash and reboots to apply it. This lets a single firmware use full NKRO day to day and fall back to HKRO for BIOS/UEFI screens, without reflashing. + Exactly zero or one of the following options may be set to `y`. The first is used if none are set. | Config | Description | diff --git a/docs/docs/keymaps/behaviors/dynamic-nkro.md b/docs/docs/keymaps/behaviors/dynamic-nkro.md new file mode 100644 index 00000000000..494c790fde0 --- /dev/null +++ b/docs/docs/keymaps/behaviors/dynamic-nkro.md @@ -0,0 +1,69 @@ +--- +title: Dynamic NKRO Behavior +sidebar_label: Dynamic NKRO +--- + +## Summary + +The dynamic NKRO behavior switches the keyboard's HID report type between full +N-key roll over (NKRO) and "boot keyboard" compatible 6-key roll over (HKRO) at +runtime, so a single firmware build can use NKRO day to day and fall back to HKRO +for hosts that only understand boot keyboards, such as some BIOS/UEFI screens. + +This behavior requires [`CONFIG_ZMK_HID_REPORT_TYPE_DYNAMIC`](../../config/system.md#hid) +to be enabled, which compiles in both report types. The `HKRO`/`NKRO` config +choice sets the mode used on the very first boot; after that the selected mode is +remembered. + +:::note[Why it reboots] +A HID report descriptor can't change while the keyboard is enumerated with a host. +Switching modes therefore saves the new mode to flash and reboots to apply it, so +expect a brief disconnect when you use this behavior. +::: + +## Dynamic NKRO Command Defines + +Command defines are provided through the [`dt-bindings/zmk/dynamic_nkro.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/dynamic_nkro.h) +header, which is added at the top of the keymap file: + +```dts +#include +``` + +This allows you to reference the actions defined in this header: + +| Define | Action | +| --------------- | ------------------------ | +| `DYN_NKRO_TOG` | Toggle between NKRO/HKRO | +| `DYN_NKRO_NKRO` | Switch to NKRO | +| `DYN_NKRO_HKRO` | Switch to HKRO | + +## Dynamic NKRO Behavior + +The dynamic NKRO behavior changes the active report mode on press. If the +keyboard is already in the requested mode, nothing happens. + +### Behavior Binding + +- Reference: `&dyn_nkro` +- Parameter #1: Command, e.g. `DYN_NKRO_TOG` + +### Examples + +1. Toggle between NKRO and HKRO + + ```dts + &dyn_nkro DYN_NKRO_TOG + ``` + +1. Switch to full NKRO + + ```dts + &dyn_nkro DYN_NKRO_NKRO + ``` + +1. Switch to HKRO + + ```dts + &dyn_nkro DYN_NKRO_HKRO + ``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 167527f5e85..364e3e4add9 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -82,6 +82,7 @@ module.exports = { "keymaps/behaviors/reset", "keymaps/behaviors/bluetooth", "keymaps/behaviors/outputs", + "keymaps/behaviors/dynamic-nkro", "keymaps/behaviors/underglow", "keymaps/behaviors/backlight", "keymaps/behaviors/power",