diff --git a/keyboards/keychron/common/keychron_raw_hid.c b/keyboards/keychron/common/keychron_raw_hid.c index e21776f5c8..1bca970ed0 100644 --- a/keyboards/keychron/common/keychron_raw_hid.c +++ b/keyboards/keychron/common/keychron_raw_hid.c @@ -85,6 +85,11 @@ void get_firmware_version(uint8_t *data) { __attribute__((weak)) void kc_rgb_matrix_rx(uint8_t *data, uint8_t length) {} +/* Default no-op implementation; override in keymap.c to handle custom IDs. */ +__attribute__((weak)) bool kc_raw_hid_rx_kb(uint8_t *data, uint8_t length) { + return false; +} + bool kc_raw_hid_rx(uint8_t *data, uint8_t length) { switch (data[0]) { case KC_GET_PROTOCOL_VERSION: @@ -185,7 +190,7 @@ bool kc_raw_hid_rx(uint8_t *data, uint8_t length) { #endif default: - return false; + return kc_raw_hid_rx_kb(data, length); } raw_hid_send(data, length); diff --git a/keyboards/keychron/common/keychron_raw_hid.h b/keyboards/keychron/common/keychron_raw_hid.h index 4419f0f73f..fedbbd39db 100644 --- a/keyboards/keychron/common/keychron_raw_hid.h +++ b/keyboards/keychron/common/keychron_raw_hid.h @@ -63,3 +63,17 @@ enum { REPORT_RATE_GET, REPORT_RATE_SET, }; + +/** + * Keyboard-level Raw HID extension hook. + * + * Called by kc_raw_hid_rx() for any command ID not handled by Keychron's own + * protocol (0xA0-0xAB range). The default weak implementation returns false + * so that unrecognised commands fall through to VIA. + * + * Override this in your keymap to handle custom command IDs without + * conflicting with Keychron's via_command_kb() definition. + * Return true if the packet was fully handled (including calling + * raw_hid_send() if a reply is needed), false to let VIA process it. + */ +bool kc_raw_hid_rx_kb(uint8_t *data, uint8_t length); diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/hid_protocol.h b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/hid_protocol.h new file mode 100644 index 0000000000..c4649ef2b6 --- /dev/null +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/hid_protocol.h @@ -0,0 +1,94 @@ +// 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 + +// --------------------------------------------------------------------------- +// Packet size +// --------------------------------------------------------------------------- +// Matches RAW_EPSIZE (QMK raw HID endpoint size = 32 bytes). Defined here +// so keymap code does not depend on usb_descriptor.h being in scope, which +// it is not when compiled through Keychron's build path. +#define HID_PACKET_SIZE 32u + +// --------------------------------------------------------------------------- +// Convenience: first byte of payload as an absolute packet index +// --------------------------------------------------------------------------- +#define HID_PAYLOAD(offset) ((uint8_t)((HID_OFF_PAYLOAD) + (offset))) diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c index 173888b756..d3f1839c2c 100644 --- a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c @@ -17,6 +17,9 @@ #include QMK_KEYBOARD_H #include "keychron_common.h" #include "chord_unicode.h" +#include "raw_hid.h" +#include "keychron_raw_hid.h" +#include "hid_protocol.h" // Tap Dance declarations enum { @@ -37,6 +40,16 @@ enum custom_keycodes { 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 static bool alt_tab_active = false; static uint16_t alt_tab_timer = 0; @@ -46,20 +59,109 @@ static uint16_t alt_tab_timer = 0; // momentary (TT/MO) keys are released. 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[HID_PACKET_SIZE] = {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, HID_PACKET_SIZE); +} + +// Handle a Raw HID packet for our custom command range (0x40-0x7E). +// Overrides the weak kc_raw_hid_rx_kb() hook in keychron_raw_hid.c, which is +// called by kc_raw_hid_rx() for any command ID not handled by Keychron's own +// protocol. Must call raw_hid_send() directly for any reply. +bool kc_raw_hid_rx_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[HID_PACKET_SIZE] = {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, HID_PACKET_SIZE); + } 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 static bool caps_mod_held = false; static bool caps_mod_ctrl_registered = false; static uint16_t caps_mod_timer = 0; -enum layers { - BASE, - FN1, - FN2, - FN3, - FN4, - KEEB_CTL, -}; - // clang-format off const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { [BASE] = LAYOUT_ansi_101( @@ -134,8 +236,20 @@ combo_t key_combos[] = { }; // 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) { - 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) {