feat(q5_max): implement bi-directional Raw HID protocol
Add hid_protocol.h defining a shared 32-byte packet structure for the qmk-host bridge application (command IDs 0x40-0x7E, clear of VIA's range). Implement via_command_kb() in keymap.c to intercept incoming packets: LAYER_SYNC applies a new active layer, VOLUME and BRIGHTNESS store host-reported values for future RGB indicators, and ACTIVE_APP is stubbed for a later commit. Layer state changes are broadcast to the host via raw_hid_send() from layer_state_set_user(), guarded by g_hid_recv_active to prevent echo loops when the change itself originated from HID.
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
// Copyright 2024 rootiest
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/*
|
||||||
|
* qmk-host Shared HID Protocol
|
||||||
|
* =============================
|
||||||
|
* Bi-directional Raw HID protocol shared between QMK firmware and the
|
||||||
|
* qmk-host Rust application. All values MUST match between both sides.
|
||||||
|
*
|
||||||
|
* Physical layer (QMK defaults, NOT overridden):
|
||||||
|
* Usage Page : 0xFF60
|
||||||
|
* Usage ID : 0x0061
|
||||||
|
* Packet size: RAW_EPSIZE (32 bytes)
|
||||||
|
*
|
||||||
|
* Packet layout:
|
||||||
|
* Byte 0 : Command ID
|
||||||
|
* Byte 1 : Source Device ID
|
||||||
|
* Byte 2 : Flags
|
||||||
|
* Bytes 3-31: Payload (29 bytes)
|
||||||
|
*
|
||||||
|
* Command IDs use the range 0x40-0x7E to avoid any overlap with VIA's
|
||||||
|
* protocol (0x01-0x3F) or the reserved unhandled sentinel (0xFF).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Command IDs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#define HID_CMD_LAYER_SYNC 0x40u // Layer state sync (keyboard ↔ host ↔ keyboard)
|
||||||
|
#define HID_CMD_VOLUME 0x41u // System volume level (host → keyboard)
|
||||||
|
#define HID_CMD_BRIGHTNESS 0x42u // Screen brightness level (host → keyboard)
|
||||||
|
#define HID_CMD_ACTIVE_APP 0x43u // Active window/app name (reserved — future)
|
||||||
|
#define HID_CMD_ACK 0x7Eu // Generic acknowledgement
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Source Device IDs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#define HID_DEV_HOST 0x00u // qmk-host Rust application
|
||||||
|
#define HID_DEV_Q5MAX 0x01u // Keychron Q5 Max (this firmware)
|
||||||
|
#define HID_DEV_NUMPAD 0x02u // Numpad (future second keyboard)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Packet Flags (Byte 2)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#define HID_FLAG_QUERY 0x01u // Request: send back current state, no change
|
||||||
|
#define HID_FLAG_RESPONSE 0x02u // Response: this is a reply to a query
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Byte offsets within the 32-byte packet
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#define HID_OFF_CMD 0u // Command ID
|
||||||
|
#define HID_OFF_SRC 1u // Source Device ID
|
||||||
|
#define HID_OFF_FLAGS 2u // Flags
|
||||||
|
#define HID_OFF_PAYLOAD 3u // Start of payload
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HID_CMD_LAYER_SYNC payload (from byte HID_OFF_PAYLOAD)
|
||||||
|
// [0] Active layer index (0 = BASE … 5 = KEEB_CTL)
|
||||||
|
// [1] Locked layers bitmask (bit N = layer N is locked)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#define HID_LAYER_OFF_ACTIVE 0u
|
||||||
|
#define HID_LAYER_OFF_LOCKED 1u
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HID_CMD_VOLUME payload
|
||||||
|
// [0] Volume level, 0-100 (%)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#define HID_VOLUME_OFF_LEVEL 0u
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HID_CMD_BRIGHTNESS payload
|
||||||
|
// [0] Brightness level, 0-100 (%)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#define HID_BRITE_OFF_LEVEL 0u
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HID_CMD_ACTIVE_APP payload (reserved — no firmware action yet)
|
||||||
|
// [0..27] Null-terminated UTF-8 application name (max 28 bytes incl. NUL)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#define HID_APP_NAME_MAX 28u
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Convenience: first byte of payload as an absolute packet index
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#define HID_PAYLOAD(offset) ((uint8_t)((HID_OFF_PAYLOAD) + (offset)))
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
#include QMK_KEYBOARD_H
|
#include QMK_KEYBOARD_H
|
||||||
#include "keychron_common.h"
|
#include "keychron_common.h"
|
||||||
#include "chord_unicode.h"
|
#include "chord_unicode.h"
|
||||||
|
#include "raw_hid.h"
|
||||||
|
#include "hid_protocol.h"
|
||||||
|
|
||||||
// Tap Dance declarations
|
// Tap Dance declarations
|
||||||
enum {
|
enum {
|
||||||
@@ -37,6 +39,16 @@ enum custom_keycodes {
|
|||||||
CAPS_MOD, // Tap=ESC, hold=Ctrl, Shift=CapsLock, Alt=CapsWord, GUI=Autocorrect
|
CAPS_MOD, // Tap=ESC, hold=Ctrl, Shift=CapsLock, Alt=CapsWord, GUI=Autocorrect
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Declare layers early so the HID functions below can reference KEEB_CTL.
|
||||||
|
enum layers {
|
||||||
|
BASE,
|
||||||
|
FN1,
|
||||||
|
FN2,
|
||||||
|
FN3,
|
||||||
|
FN4,
|
||||||
|
KEEB_CTL,
|
||||||
|
};
|
||||||
|
|
||||||
// Alt-Tab cycling state
|
// Alt-Tab cycling state
|
||||||
static bool alt_tab_active = false;
|
static bool alt_tab_active = false;
|
||||||
static uint16_t alt_tab_timer = 0;
|
static uint16_t alt_tab_timer = 0;
|
||||||
@@ -46,20 +58,107 @@ static uint16_t alt_tab_timer = 0;
|
|||||||
// momentary (TT/MO) keys are released.
|
// momentary (TT/MO) keys are released.
|
||||||
static layer_state_t locked_layers = 0;
|
static layer_state_t locked_layers = 0;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Raw HID state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Anti-loop guard: set to true while applying a layer change that arrived
|
||||||
|
// over HID so that layer_state_set_user() does not echo it back.
|
||||||
|
static bool g_hid_recv_active = false;
|
||||||
|
|
||||||
|
// Last highest-layer value we sent via HID. 0xFF = never sent.
|
||||||
|
// Avoids redundant sends when layer_state_set_user is called multiple times
|
||||||
|
// with the same effective top layer.
|
||||||
|
static uint8_t g_last_sent_layer = 0xFF;
|
||||||
|
|
||||||
|
// Latest host-reported values (stored for future RGB indicator use).
|
||||||
|
static uint8_t g_hid_volume = 0;
|
||||||
|
static uint8_t g_hid_brightness = 0;
|
||||||
|
|
||||||
|
// Send the current layer state to the host / bridge application.
|
||||||
|
static void hid_send_layer_sync(uint8_t layer, uint8_t locked_mask) {
|
||||||
|
uint8_t data[RAW_EPSIZE] = {0};
|
||||||
|
data[HID_OFF_CMD] = HID_CMD_LAYER_SYNC;
|
||||||
|
data[HID_OFF_SRC] = HID_DEV_Q5MAX;
|
||||||
|
data[HID_OFF_FLAGS] = 0;
|
||||||
|
data[HID_PAYLOAD(HID_LAYER_OFF_ACTIVE)] = layer;
|
||||||
|
data[HID_PAYLOAD(HID_LAYER_OFF_LOCKED)] = locked_mask;
|
||||||
|
raw_hid_send(data, RAW_EPSIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a Raw HID packet for our custom command range (0x40-0x7E).
|
||||||
|
// Called from via_command_kb(); must call raw_hid_send() for any reply.
|
||||||
|
bool via_command_kb(uint8_t *data, uint8_t length) {
|
||||||
|
uint8_t cmd = data[HID_OFF_CMD];
|
||||||
|
|
||||||
|
// Only intercept our custom command range; let VIA handle everything else.
|
||||||
|
if (cmd < 0x40u || cmd > 0x7Eu) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t flags = data[HID_OFF_FLAGS];
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case HID_CMD_LAYER_SYNC: {
|
||||||
|
if (flags & HID_FLAG_QUERY) {
|
||||||
|
// Host requests current state — reply without changing anything.
|
||||||
|
uint8_t resp[RAW_EPSIZE] = {0};
|
||||||
|
resp[HID_OFF_CMD] = HID_CMD_LAYER_SYNC;
|
||||||
|
resp[HID_OFF_SRC] = HID_DEV_Q5MAX;
|
||||||
|
resp[HID_OFF_FLAGS] = HID_FLAG_RESPONSE;
|
||||||
|
resp[HID_PAYLOAD(HID_LAYER_OFF_ACTIVE)] = get_highest_layer(layer_state);
|
||||||
|
resp[HID_PAYLOAD(HID_LAYER_OFF_LOCKED)] = (uint8_t)locked_layers;
|
||||||
|
raw_hid_send(resp, RAW_EPSIZE);
|
||||||
|
} else {
|
||||||
|
// Host or peer keyboard is pushing a new active layer.
|
||||||
|
uint8_t new_layer = data[HID_PAYLOAD(HID_LAYER_OFF_ACTIVE)];
|
||||||
|
uint8_t new_locked = data[HID_PAYLOAD(HID_LAYER_OFF_LOCKED)];
|
||||||
|
if (new_layer <= KEEB_CTL) {
|
||||||
|
// Set the guard BEFORE calling layer_move() so that the
|
||||||
|
// resulting layer_state_set_user() call does not echo the
|
||||||
|
// change back to the host, preventing an infinite loop.
|
||||||
|
g_hid_recv_active = true;
|
||||||
|
locked_layers = new_locked;
|
||||||
|
layer_move(new_layer);
|
||||||
|
g_hid_recv_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HID_CMD_VOLUME: {
|
||||||
|
// Store the host-reported volume (0-100).
|
||||||
|
g_hid_volume = data[HID_PAYLOAD(HID_VOLUME_OFF_LEVEL)];
|
||||||
|
// TODO: drive an RGB volume-bar indicator here in a future commit.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HID_CMD_BRIGHTNESS: {
|
||||||
|
// Store the host-reported screen brightness (0-100).
|
||||||
|
g_hid_brightness = data[HID_PAYLOAD(HID_BRITE_OFF_LEVEL)];
|
||||||
|
// TODO: drive an RGB brightness indicator here in a future commit.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HID_CMD_ACTIVE_APP: {
|
||||||
|
// Reserved — no action yet. Payload is a null-terminated UTF-8
|
||||||
|
// application name (up to HID_APP_NAME_MAX bytes).
|
||||||
|
// TODO: implement active-app handling once the host side is ready.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // packet was fully handled by us
|
||||||
|
}
|
||||||
|
|
||||||
// CAPS_MOD state: tap=ESC, hold=Ctrl, Shift+tap=CapsLock, Alt+tap=CapsWord, GUI+tap=Autocorrect
|
// CAPS_MOD state: tap=ESC, hold=Ctrl, Shift+tap=CapsLock, Alt+tap=CapsWord, GUI+tap=Autocorrect
|
||||||
static bool caps_mod_held = false;
|
static bool caps_mod_held = false;
|
||||||
static bool caps_mod_ctrl_registered = false;
|
static bool caps_mod_ctrl_registered = false;
|
||||||
static uint16_t caps_mod_timer = 0;
|
static uint16_t caps_mod_timer = 0;
|
||||||
|
|
||||||
enum layers {
|
|
||||||
BASE,
|
|
||||||
FN1,
|
|
||||||
FN2,
|
|
||||||
FN3,
|
|
||||||
FN4,
|
|
||||||
KEEB_CTL,
|
|
||||||
};
|
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
|
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
|
||||||
[BASE] = LAYOUT_ansi_101(
|
[BASE] = LAYOUT_ansi_101(
|
||||||
@@ -134,8 +233,20 @@ combo_t key_combos[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Re-assert locked layers whenever QMK modifies layer state (e.g. TT release).
|
// Re-assert locked layers whenever QMK modifies layer state (e.g. TT release).
|
||||||
|
// Also notifies the host application of the new active layer via Raw HID,
|
||||||
|
// unless the change was itself triggered by an incoming HID packet (anti-loop).
|
||||||
layer_state_t layer_state_set_user(layer_state_t state) {
|
layer_state_t layer_state_set_user(layer_state_t state) {
|
||||||
return state | locked_layers;
|
state |= locked_layers;
|
||||||
|
|
||||||
|
if (!g_hid_recv_active) {
|
||||||
|
uint8_t top = get_highest_layer(state);
|
||||||
|
if (top != g_last_sent_layer) {
|
||||||
|
g_last_sent_layer = top;
|
||||||
|
hid_send_layer_sync(top, (uint8_t)locked_layers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
void keyboard_post_init_user(void) {
|
void keyboard_post_init_user(void) {
|
||||||
|
|||||||
Reference in New Issue
Block a user