Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE src/hid.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_VKEY app PRIVATE src/behaviors/behavior_vkey.c)
target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_HOLD_TAP app PRIVATE src/behaviors/behavior_hold_tap.c)
target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_STICKY_KEY app PRIVATE src/behaviors/behavior_sticky_key.c)
target_sources(app PRIVATE src/behaviors/behavior_caps_word.c)
Expand Down
32 changes: 32 additions & 0 deletions app/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,38 @@ config ZMK_HID_SEPARATE_MOD_RELEASE_REPORT
Send a separate release event for the modifiers, to make sure the release
of the modifier doesn't get recognized before the actual key's release event.

menuconfig ZMK_HID_VKEY
bool "Vendor-defined HID selector key (vkey)"
default n
help
Add an independent vendor-defined HID top-level collection that reports a
single 1-byte selector (the "vkey" id, 1-255; 0 = released), plus a
&vkey <id> behavior that emits it. Lets a keymap send original key codes
that never collide with the standard keyboard/consumer/mouse usages; a
host-side agent reads the vendor report and maps the id to an action.
Disabled by default: when off, the report descriptor, report state, send
path and behavior are all compiled out and the firmware is byte-identical
to a build without this option.

if ZMK_HID_VKEY

config ZMK_HID_VKEY_USAGE_PAGE
hex "Vendor usage page for the vkey collection"
default 0xFF31
range 0xFF00 0xFFFF
help
16-bit vendor-defined usage page (0xFF00-0xFFFF) the vkey collection lives on.

config ZMK_HID_VKEY_REPORT_ID
int "HID report ID for the vkey selector report"
default 32
range 4 255
help
Report ID for the vkey input report. Must not collide with ZMK's standard
report IDs (1 = keyboard, 2 = consumer, 3 = mouse). Default 32 (0x20).

endif # ZMK_HID_VKEY

menu "Output Types"

config ZMK_USB
Expand Down
5 changes: 5 additions & 0 deletions app/Kconfig.behaviors
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ config ZMK_BEHAVIOR_KEY_TOGGLE
default y
depends on DT_HAS_ZMK_BEHAVIOR_KEY_TOGGLE_ENABLED

config ZMK_BEHAVIOR_VKEY
bool
default y
depends on DT_HAS_ZMK_BEHAVIOR_VKEY_ENABLED && ZMK_HID_VKEY

config ZMK_BEHAVIOR_MOUSE_KEY_PRESS
bool
default y
Expand Down
8 changes: 8 additions & 0 deletions app/dts/bindings/behaviors/zmk,behavior-vkey.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright (c) 2024 The ZMK Contributors
# SPDX-License-Identifier: MIT

description: Vendor-defined "original key" (vkey) behavior — emits Report ID 0x20 selector

compatible: "zmk,behavior-vkey"

include: one_param.yaml
7 changes: 7 additions & 0 deletions app/include/zmk/endpoints.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ bool zmk_endpoint_is_connected(void);
*/
int zmk_endpoint_send_report(uint16_t usage_page);

#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
/**
* Sends the vendor-defined selector key (vkey) report to the selected endpoint.
*/
int zmk_endpoint_send_vkey_report(void);
#endif // IS_ENABLED(CONFIG_ZMK_HID_VKEY)

#if IS_ENABLED(CONFIG_ZMK_POINTING)
/**
* Sends the HID mouse report to the selected endpoint.
Expand Down
47 changes: 47 additions & 0 deletions app/include/zmk/hid.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@
#define ZMK_HID_REPORT_ID_CONSUMER 0x02
#define ZMK_HID_REPORT_ID_MOUSE 0x03

#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
// Vendor-defined selector key (vkey). The report ID comes from Kconfig and must
// not collide with the standard reports (0x01 keyboard/leds, 0x02 consumer,
// 0x03 mouse). Aliased so the descriptor and report code stay readable.
#define ZMK_HID_REPORT_ID_VKEY CONFIG_ZMK_HID_VKEY_REPORT_ID
#endif

#ifndef HID_ITEM_TAG_PUSH
#define HID_ITEM_TAG_PUSH 0xA
#endif
Expand Down Expand Up @@ -253,6 +260,29 @@ static const uint8_t zmk_hid_report_desc[] = {
HID_END_COLLECTION,
HID_END_COLLECTION,
#endif // IS_ENABLED(CONFIG_ZMK_POINTING)

#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
// Vendor-defined selector key (vkey). Independent top-level collection
// appended after every standard collection and outside the
// CONFIG_ZMK_POINTING guard. Present only when CONFIG_ZMK_HID_VKEY is set,
// so a default build is byte-identical. The vendor usage page is 16-bit, so
// the long (2-byte) global item 0x06,lo,hi is written raw (HID_USAGE_PAGE()
// emits a 1-byte item and would truncate it); lo/hi are split from Kconfig.
0x06,
(CONFIG_ZMK_HID_VKEY_USAGE_PAGE & 0xFF),
(CONFIG_ZMK_HID_VKEY_USAGE_PAGE >> 8), // Usage Page (vendor, 16-bit)
HID_USAGE(0x01), // Usage (0x01, application)
HID_COLLECTION(HID_COLLECTION_APPLICATION),
HID_REPORT_ID(ZMK_HID_REPORT_ID_VKEY), // Report ID (Kconfig)
HID_USAGE(0x02), // Usage (0x02, selector)
HID_LOGICAL_MIN8(0x00), // Logical Minimum (0)
HID_LOGICAL_MAX16(0xFF, 0x00), // Logical Maximum (255); signed, so 2-byte
HID_REPORT_SIZE(0x08), // Report Size (8)
HID_REPORT_COUNT(0x01), // Report Count (1)
HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR |
ZMK_HID_MAIN_VAL_ABS), // Input(Data,Var,Abs)
HID_END_COLLECTION,
#endif // IS_ENABLED(CONFIG_ZMK_HID_VKEY)
};

#if IS_ENABLED(CONFIG_ZMK_USB_BOOT)
Expand Down Expand Up @@ -311,6 +341,17 @@ struct zmk_hid_consumer_report {
struct zmk_hid_consumer_report_body body;
} __packed;

#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
struct zmk_hid_vkey_report_body {
uint8_t id;
} __packed;

struct zmk_hid_vkey_report {
uint8_t report_id;
struct zmk_hid_vkey_report_body body;
} __packed;
#endif // IS_ENABLED(CONFIG_ZMK_HID_VKEY)

#if IS_ENABLED(CONFIG_ZMK_POINTING)
struct zmk_hid_mouse_report_body {
zmk_mouse_button_flags_t buttons;
Expand Down Expand Up @@ -383,6 +424,12 @@ void zmk_hid_mouse_clear(void);
struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report(void);
struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(void);

#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
int zmk_hid_vkey_set(uint8_t id);
void zmk_hid_vkey_clear(void);
struct zmk_hid_vkey_report *zmk_hid_get_vkey_report(void);
#endif // IS_ENABLED(CONFIG_ZMK_HID_VKEY)

#if IS_ENABLED(CONFIG_ZMK_USB_BOOT)
zmk_hid_boot_report_t *zmk_hid_get_boot_report();
#endif
Expand Down
3 changes: 3 additions & 0 deletions app/include/zmk/usb_hid.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

int zmk_usb_hid_send_keyboard_report(void);
int zmk_usb_hid_send_consumer_report(void);
#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
int zmk_usb_hid_send_vkey_report(void);
#endif
#if IS_ENABLED(CONFIG_ZMK_POINTING)
int zmk_usb_hid_send_mouse_report(void);
#endif // IS_ENABLED(CONFIG_ZMK_POINTING)
Expand Down
49 changes: 49 additions & 0 deletions app/src/behaviors/behavior_vkey.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#define DT_DRV_COMPAT zmk_behavior_vkey

#include <zephyr/device.h>
#include <drivers/behavior.h>
#include <zephyr/logging/log.h>

#include <zmk/hid.h>
#include <zmk/endpoints.h>
#include <zmk/behavior.h>

LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);

// Vendor-defined "original key": on press, set the 1-byte selector report to
// the bound id and send it; on release, set 0 and send. Runs on the central
// (no .locality -> BEHAVIOR_LOCALITY_CENTRAL), where the HID state and the USB
// endpoint live, so no split event marshaling is needed.

static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) {
LOG_DBG("position %d vkey id %d", event.position, binding->param1);
zmk_hid_vkey_set((uint8_t)(binding->param1 & 0xFF));
zmk_endpoint_send_vkey_report();
return ZMK_BEHAVIOR_OPAQUE;
}

static int on_keymap_binding_released(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) {
LOG_DBG("position %d vkey release", event.position);
zmk_hid_vkey_clear();
zmk_endpoint_send_vkey_report();
return ZMK_BEHAVIOR_OPAQUE;
}

static const struct behavior_driver_api behavior_vkey_driver_api = {
.binding_pressed = on_keymap_binding_pressed,
.binding_released = on_keymap_binding_released,
};

#define VKEY_INST(n) \
BEHAVIOR_DT_INST_DEFINE(n, NULL, NULL, NULL, NULL, POST_KERNEL, \
CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_vkey_driver_api);

DT_INST_FOREACH_STATUS_OKAY(VKEY_INST)
38 changes: 38 additions & 0 deletions app/src/endpoints.c
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,38 @@ int zmk_endpoint_send_report(uint16_t usage_page) {
return -ENOTSUP;
}

#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
int zmk_endpoint_send_vkey_report(void) {
switch (current_instance.transport) {
case ZMK_TRANSPORT_NONE:
return 0;

case ZMK_TRANSPORT_USB: {
#if IS_ENABLED(CONFIG_ZMK_USB)
int err = zmk_usb_hid_send_vkey_report();
if (err) {
LOG_ERR("FAILED TO SEND VKEY OVER USB: %d", err);
}
return err;
#else
LOG_ERR("USB endpoint is not supported");
return -ENOTSUP;
#endif /* IS_ENABLED(CONFIG_ZMK_USB) */
}

case ZMK_TRANSPORT_BLE:
// vkey over BLE HOG is not implemented yet (USB transport only for now).
// The vendor collection is still present in the HOG report map because
// the report descriptor is shared; only the send path is missing.
LOG_WRN("vkey over BLE HOG not implemented");
return -ENOTSUP;
}

LOG_ERR("Unhandled endpoint transport %d", current_instance.transport);
return -ENOTSUP;
}
#endif // IS_ENABLED(CONFIG_ZMK_HID_VKEY)

#if IS_ENABLED(CONFIG_ZMK_POINTING)
int zmk_endpoint_send_mouse_report() {
switch (current_instance.transport) {
Expand Down Expand Up @@ -471,12 +503,18 @@ static int zmk_endpoints_init(void) {
void zmk_endpoint_clear_reports(void) {
zmk_hid_keyboard_clear();
zmk_hid_consumer_clear();
#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
zmk_hid_vkey_clear();
#endif
#if IS_ENABLED(CONFIG_ZMK_POINTING)
zmk_hid_mouse_clear();
#endif // IS_ENABLED(CONFIG_ZMK_POINTING)

zmk_endpoint_send_report(HID_USAGE_KEY);
zmk_endpoint_send_report(HID_USAGE_CONSUMER);
#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
zmk_endpoint_send_vkey_report();
#endif
}

static void update_current_endpoint(void) {
Expand Down
16 changes: 16 additions & 0 deletions app/src/hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ static struct zmk_hid_keyboard_report keyboard_report = {

.report_id = ZMK_HID_REPORT_ID_KEYBOARD, .body = {.modifiers = 0, ._reserved = 0, .keys = {0}}};

#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
static struct zmk_hid_vkey_report vkey_report = {.report_id = ZMK_HID_REPORT_ID_VKEY,
.body = {.id = 0}};
#endif

static struct zmk_hid_consumer_report consumer_report = {.report_id = ZMK_HID_REPORT_ID_CONSUMER,
.body = {.keys = {0}}};

Expand Down Expand Up @@ -472,6 +477,17 @@ struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report(void) { return &keyb

struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(void) { return &consumer_report; }

#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
int zmk_hid_vkey_set(uint8_t id) {
vkey_report.body.id = id;
return 0;
}

void zmk_hid_vkey_clear(void) { vkey_report.body.id = 0; }

struct zmk_hid_vkey_report *zmk_hid_get_vkey_report(void) { return &vkey_report; }
#endif // IS_ENABLED(CONFIG_ZMK_HID_VKEY)

#if IS_ENABLED(CONFIG_ZMK_POINTING)

struct zmk_hid_mouse_report *zmk_hid_get_mouse_report(void) { return &mouse_report; }
Expand Down
21 changes: 21 additions & 0 deletions app/src/usb_hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ static int get_report_cb(const struct device *dev, struct usb_setup_packet *setu
*len = sizeof(*report);
break;
}
#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
case ZMK_HID_REPORT_ID_VKEY: {
struct zmk_hid_vkey_report *report = zmk_hid_get_vkey_report();
*data = (uint8_t *)report;
*len = sizeof(*report);
break;
}
#endif // IS_ENABLED(CONFIG_ZMK_HID_VKEY)
default:
LOG_ERR("Invalid report ID %d requested", setup->wValue & HID_GET_REPORT_ID_MASK);
return -EINVAL;
Expand Down Expand Up @@ -222,6 +230,19 @@ int zmk_usb_hid_send_consumer_report(void) {
return zmk_usb_hid_send_report((uint8_t *)report, sizeof(*report));
}

#if IS_ENABLED(CONFIG_ZMK_HID_VKEY)
int zmk_usb_hid_send_vkey_report(void) {
#if IS_ENABLED(CONFIG_ZMK_USB_BOOT)
if (hid_protocol == HID_PROTOCOL_BOOT) {
return -ENOTSUP;
}
#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */

struct zmk_hid_vkey_report *report = zmk_hid_get_vkey_report();
return zmk_usb_hid_send_report((uint8_t *)report, sizeof(*report));
}
#endif // IS_ENABLED(CONFIG_ZMK_HID_VKEY)

#if IS_ENABLED(CONFIG_ZMK_POINTING)
int zmk_usb_hid_send_mouse_report() {
#if IS_ENABLED(CONFIG_ZMK_USB_BOOT)
Expand Down