forked from zmkfirmware/zmk
-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Small exploration of raw HID over USB + BLE.
- Loading branch information
1 parent
1b7d344
commit be9d819
Showing
4 changed files
with
452 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* Copyright (c) 2020 The ZMK Contributors | ||
* | ||
* SPDX-License-Identifier: MIT | ||
*/ | ||
|
||
#pragma once | ||
|
||
#include <zephyr/usb/usb_device.h> | ||
#include <zephyr/usb/class/usb_hid.h> | ||
|
||
#include <zmk/keys.h> | ||
#include <zmk/hid.h> | ||
#include <dt-bindings/zmk/hid_usage.h> | ||
#include <dt-bindings/zmk/hid_usage_pages.h> | ||
|
||
#ifndef HID_USAGE_PAGE16 | ||
#define HID_USAGE_PAGE16(page, page2) \ | ||
HID_ITEM(HID_ITEM_TAG_USAGE_PAGE, HID_ITEM_TYPE_GLOBAL, 2), page, page2 | ||
#endif | ||
|
||
#ifndef HID_USAGE6 | ||
#define HID_USAGE16(page, page2) HID_ITEM(HID_ITEM_TAG_USAGE, HID_ITEM_TYPE_LOCAL, 2), page, page2 | ||
#endif | ||
|
||
#define KEYMAP_HID_MAX_BYTES 1 | ||
|
||
#define KEYMAP_HID_REPORT_ID 0x42 | ||
|
||
static const uint8_t zmk_keymap_hid_report_desc[] = { | ||
HID_USAGE_PAGE16(0x0B, 0xFF), | ||
HID_USAGE(0x01), | ||
HID_COLLECTION(HID_COLLECTION_APPLICATION), | ||
HID_REPORT_ID(KEYMAP_HID_REPORT_ID), | ||
HID_USAGE_PAGE16(0x0B, 0xFF), | ||
HID_USAGE_MIN8(0x00), | ||
HID_USAGE_MAX16(0xFF, 0x00), | ||
HID_LOGICAL_MIN8(0x00), | ||
HID_LOGICAL_MAX16(0xFF, 0x00), | ||
|
||
HID_REPORT_SIZE(0x08), | ||
HID_REPORT_COUNT(KEYMAP_HID_MAX_BYTES), | ||
/* INPUT (Data,Var,Abs) */ | ||
HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), | ||
|
||
HID_REPORT_ID(KEYMAP_HID_REPORT_ID), | ||
HID_USAGE_PAGE16(0x0B, 0xFF), | ||
HID_USAGE_MIN8(0x00), | ||
HID_USAGE_MAX16(0xFF, 0x00), | ||
HID_LOGICAL_MIN8(0x00), | ||
HID_LOGICAL_MAX16(0xFF, 0x00), | ||
|
||
HID_REPORT_SIZE(0x08), | ||
HID_REPORT_COUNT(KEYMAP_HID_MAX_BYTES), | ||
/* INPUT (Data,Var,Abs) */ | ||
HID_OUTPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), | ||
HID_END_COLLECTION, | ||
}; | ||
|
||
struct zmk_keymap_hid_sample_report_body { | ||
uint8_t data[KEYMAP_HID_MAX_BYTES]; | ||
} __packed; | ||
|
||
struct zmk_keymap_hid_sample_report { | ||
uint8_t report_id; | ||
struct zmk_keymap_hid_sample_report_body body; | ||
} __packed; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
/* | ||
* Copyright (c) 2020 The ZMK Contributors | ||
* | ||
* SPDX-License-Identifier: MIT | ||
*/ | ||
|
||
#include <zephyr/settings/settings.h> | ||
#include <zephyr/init.h> | ||
|
||
#include <zephyr/logging/log.h> | ||
|
||
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); | ||
|
||
#include <zephyr/bluetooth/bluetooth.h> | ||
#include <zephyr/bluetooth/gatt.h> | ||
|
||
#include <zmk/ble.h> | ||
#include <zmk/endpoints_types.h> | ||
#include <zmk/hog.h> | ||
#include <zmk/keymap_hid.h> | ||
|
||
enum { | ||
HIDS_REMOTE_WAKE = BIT(0), | ||
HIDS_NORMALLY_CONNECTABLE = BIT(1), | ||
}; | ||
|
||
struct hids_info { | ||
uint16_t version; /* version number of base USB HID Specification */ | ||
uint8_t code; /* country HID Device hardware is localized for. */ | ||
uint8_t flags; | ||
} __packed; | ||
|
||
struct hids_report { | ||
uint8_t id; /* report id */ | ||
uint8_t type; /* report type */ | ||
} __packed; | ||
|
||
static struct hids_info info = { | ||
.version = 0x0000, | ||
.code = 0x00, | ||
.flags = HIDS_NORMALLY_CONNECTABLE | HIDS_REMOTE_WAKE, | ||
}; | ||
|
||
enum { | ||
HIDS_INPUT = 0x01, | ||
HIDS_OUTPUT = 0x02, | ||
HIDS_FEATURE = 0x03, | ||
}; | ||
|
||
static struct hids_report input = { | ||
.id = KEYMAP_HID_REPORT_ID, | ||
.type = HIDS_INPUT, | ||
}; | ||
|
||
static struct hids_report output = { | ||
.id = KEYMAP_HID_REPORT_ID, | ||
.type = HIDS_OUTPUT, | ||
}; | ||
|
||
static bool host_requests_notification = false; | ||
static uint8_t ctrl_point; | ||
|
||
static ssize_t read_hids_info(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, attr->user_data, | ||
sizeof(struct hids_info)); | ||
} | ||
|
||
static ssize_t read_hids_report_ref(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, attr->user_data, | ||
sizeof(struct hids_report)); | ||
} | ||
|
||
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_keymap_hid_report_desc, | ||
sizeof(zmk_keymap_hid_report_desc)); | ||
} | ||
|
||
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) { | ||
static const struct zmk_keymap_hid_sample_report_body report_body = {.data = {66}}; | ||
return bt_gatt_attr_read(conn, attr, buf, len, offset, &report_body, | ||
sizeof(struct zmk_keymap_hid_sample_report_body)); | ||
} | ||
|
||
static ssize_t write_hids_output_report(struct bt_conn *conn, const struct bt_gatt_attr *attr, | ||
const void *buf, uint16_t len, uint16_t offset, | ||
uint8_t flags) { | ||
if (offset != 0) { | ||
LOG_ERR("Offset is wrong %d", offset); | ||
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); | ||
} | ||
struct zmk_keymap_hid_sample_report_body *report; | ||
if (len == sizeof(struct zmk_keymap_hid_sample_report_body)) { | ||
report = (struct zmk_keymap_hid_sample_report_body *)buf; | ||
} else if (len == sizeof(struct zmk_keymap_hid_sample_report)) { | ||
struct zmk_keymap_hid_sample_report *wrapper = (struct zmk_keymap_hid_sample_report *)buf; | ||
if (wrapper->report_id != KEYMAP_HID_REPORT_ID) { | ||
return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED); | ||
} | ||
|
||
report = &wrapper->body; | ||
} else { | ||
LOG_ERR("Length is wrong %d vs %d", len, sizeof(struct zmk_keymap_hid_sample_report_body)); | ||
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); | ||
} | ||
|
||
int profile = zmk_ble_profile_index(bt_conn_get_dst(conn)); | ||
if (profile < 0) { | ||
LOG_ERR("No profile"); | ||
return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); | ||
} | ||
|
||
LOG_HEXDUMP_DBG(report->data, KEYMAP_HID_MAX_BYTES, "Got output data sent"); | ||
|
||
return len; | ||
} | ||
|
||
static void input_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value) { | ||
host_requests_notification = (value == BT_GATT_CCC_NOTIFY) ? 1 : 0; | ||
} | ||
|
||
static ssize_t write_ctrl_point(struct bt_conn *conn, const struct bt_gatt_attr *attr, | ||
const void *buf, uint16_t len, uint16_t offset, uint8_t flags) { | ||
uint8_t *value = attr->user_data; | ||
|
||
if (offset + len > sizeof(ctrl_point)) { | ||
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); | ||
} | ||
|
||
memcpy(value + offset, buf, len); | ||
|
||
return len; | ||
} | ||
|
||
/* HID Service Declaration */ | ||
BT_GATT_SERVICE_DEFINE( | ||
keymap_hog_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_HIDS), | ||
// BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_PROTOCOL_MODE, BT_GATT_CHRC_WRITE_WITHOUT_RESP, | ||
// BT_GATT_PERM_WRITE, NULL, write_proto_mode, &proto_mode), | ||
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_INFO, BT_GATT_CHRC_READ, BT_GATT_PERM_READ, read_hids_info, | ||
NULL, &info), | ||
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT_MAP, BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT, | ||
read_hids_report_map, NULL, NULL), | ||
|
||
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, | ||
BT_GATT_PERM_READ_ENCRYPT, read_hids_input_report, NULL, NULL), | ||
BT_GATT_CCC(input_ccc_changed, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), | ||
BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, BT_GATT_PERM_READ_ENCRYPT, read_hids_report_ref, | ||
NULL, &input), | ||
|
||
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, | ||
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP, | ||
BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT, NULL, | ||
write_hids_output_report, NULL), | ||
BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, BT_GATT_PERM_READ_ENCRYPT, read_hids_report_ref, | ||
NULL, &output), | ||
|
||
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_CTRL_POINT, BT_GATT_CHRC_WRITE_WITHOUT_RESP, | ||
BT_GATT_PERM_WRITE, NULL, write_ctrl_point, &ctrl_point)); | ||
|
||
static struct bt_conn *destination_connection(void) { | ||
struct bt_conn *conn; | ||
bt_addr_le_t *addr = zmk_ble_active_profile_addr(); | ||
LOG_DBG("Address pointer %p", addr); | ||
if (!bt_addr_le_cmp(addr, BT_ADDR_LE_ANY)) { | ||
LOG_WRN("Not sending, no active address for current profile"); | ||
return NULL; | ||
} else if ((conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, addr)) == NULL) { | ||
LOG_WRN("Not sending, not connected to active profile"); | ||
return NULL; | ||
} | ||
|
||
return conn; | ||
} | ||
|
||
static K_THREAD_STACK_DEFINE(hog_q_stack, CONFIG_ZMK_BLE_THREAD_STACK_SIZE); | ||
|
||
static struct k_work_q hog_work_q; | ||
|
||
K_MSGQ_DEFINE(zmk_hog_keymap_msgq, sizeof(struct zmk_hid_keyboard_report_body), | ||
CONFIG_ZMK_BLE_KEYBOARD_REPORT_QUEUE_SIZE, 4); | ||
|
||
static void send_keymap_report_callback(struct k_work *work) { | ||
struct zmk_keymap_hid_sample_report_body report; | ||
|
||
while (k_msgq_get(&zmk_hog_keymap_msgq, &report, K_NO_WAIT) == 0) { | ||
struct bt_conn *conn = destination_connection(); | ||
if (conn == NULL) { | ||
return; | ||
} | ||
|
||
struct bt_gatt_notify_params notify_params = { | ||
.attr = &keymap_hog_svc.attrs[5], | ||
.data = &report, | ||
.len = sizeof(report), | ||
}; | ||
|
||
int err = bt_gatt_notify_cb(conn, ¬ify_params); | ||
if (err == -EPERM) { | ||
bt_conn_set_security(conn, BT_SECURITY_L2); | ||
} else if (err) { | ||
LOG_DBG("Error notifying %d", err); | ||
} | ||
|
||
bt_conn_unref(conn); | ||
} | ||
} | ||
|
||
static K_WORK_DEFINE(hog_keyboard_work, send_keymap_report_callback); | ||
|
||
int zmk_hog_send_keymap_report(struct zmk_keymap_hid_sample_report_body *report) { | ||
int err = k_msgq_put(&zmk_hog_keymap_msgq, report, K_MSEC(100)); | ||
if (err) { | ||
switch (err) { | ||
case -EAGAIN: { | ||
LOG_WRN("Keyboard message queue full, popping first message and queueing again"); | ||
struct zmk_keymap_hid_sample_report_body discarded_report; | ||
k_msgq_get(&zmk_hog_keymap_msgq, &discarded_report, K_NO_WAIT); | ||
return zmk_hog_send_keymap_report(report); | ||
} | ||
default: | ||
LOG_WRN("Failed to queue keymap report to send (%d)", err); | ||
return err; | ||
} | ||
} | ||
|
||
k_work_submit_to_queue(&hog_work_q, &hog_keyboard_work); | ||
|
||
return 0; | ||
}; | ||
|
||
static void send_work_cb(struct k_work *_work) { | ||
struct zmk_keymap_hid_sample_report_body body = {.data = {42}}; | ||
|
||
zmk_hog_send_keymap_report(&body); | ||
} | ||
|
||
static K_WORK_DEFINE(send_work, send_work_cb); | ||
|
||
static void tick_cb() { k_work_submit(&send_work); } | ||
|
||
static K_TIMER_DEFINE(send_timer, tick_cb, NULL); | ||
|
||
static int zmk_keymap_hog_init(void) { | ||
static const struct k_work_queue_config queue_config = {.name = | ||
"Keymap HID Over GATT Send Work"}; | ||
k_work_queue_start(&hog_work_q, hog_q_stack, K_THREAD_STACK_SIZEOF(hog_q_stack), | ||
CONFIG_ZMK_BLE_THREAD_PRIORITY, &queue_config); | ||
|
||
k_timer_start(&send_timer, K_SECONDS(1), K_SECONDS(1)); | ||
return 0; | ||
} | ||
|
||
SYS_INIT(zmk_keymap_hog_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY); |
Oops, something went wrong.