feat(q5_max): add CMD_BATTERY HID push and query for wireless battery level

Adds a new HID_CMD_BATTERY (0x44) command to the shared qmk-host protocol.
When the keyboard is in wireless transport mode (BT or 2.4G) the firmware
now pushes the current battery percentage to the host:

- Periodic push every 5 minutes via timer_read32()/timer_elapsed32(); fires
  immediately on the first matrix_scan_user() call after boot so qmk-host
  gets a reading as soon as the keyboard connects over USB in wireless mode.
- Redundant sends are suppressed: g_last_sent_bat tracks the last value
  transmitted and the packet is skipped when the level has not changed.
- Query-response path: qmk-host can send CMD_BATTERY with HID_FLAG_QUERY at
  any time; the keyboard replies with the current percentage or
  HID_BATT_UNAVAILABLE (0xFF) when in USB transport mode.
- All battery and transport code is guarded by #ifdef LK_WIRELESS_ENABLE so
  the keymap compiles cleanly on any wired-only build.

Firmware size delta: +160 bytes (well within flash budget).
This commit is contained in:
2026-04-15 03:37:18 -04:00
parent 66c73eef89
commit 0fa528b3dd
2 changed files with 73 additions and 0 deletions
@@ -31,6 +31,7 @@
#define HID_CMD_VOLUME 0x41u // System volume level (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_BRIGHTNESS 0x42u // Screen brightness level (host → keyboard)
#define HID_CMD_ACTIVE_APP 0x43u // Active window/app name (reserved — future) #define HID_CMD_ACTIVE_APP 0x43u // Active window/app name (reserved — future)
#define HID_CMD_BATTERY 0x44u // Battery level (keyboard → host; wireless mode only)
#define HID_CMD_ACK 0x7Eu // Generic acknowledgement #define HID_CMD_ACK 0x7Eu // Generic acknowledgement
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -80,6 +81,15 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#define HID_APP_NAME_MAX 28u #define HID_APP_NAME_MAX 28u
// ---------------------------------------------------------------------------
// HID_CMD_BATTERY payload
// [0] Battery percentage 0-100, or HID_BATT_UNAVAILABLE when the keyboard
// is in USB transport mode (battery reading not meaningful / charging).
// Keyboard only sends this packet when get_transport() & TRANSPORT_WIRELESS.
// ---------------------------------------------------------------------------
#define HID_BATT_OFF_LEVEL 0u // Payload byte 0: percentage (0-100)
#define HID_BATT_UNAVAILABLE 0xFFu // Sentinel: not in wireless mode
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Packet size // Packet size
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -20,6 +20,10 @@
#include "raw_hid.h" #include "raw_hid.h"
#include "keychron_raw_hid.h" #include "keychron_raw_hid.h"
#include "hid_protocol.h" #include "hid_protocol.h"
#ifdef LK_WIRELESS_ENABLE
# include "battery.h"
# include "transport.h"
#endif
// Tap Dance declarations // Tap Dance declarations
enum { enum {
@@ -77,6 +81,34 @@ static uint8_t g_last_sent_layer = 0xFF;
static uint8_t g_hid_volume = 0; static uint8_t g_hid_volume = 0;
static uint8_t g_hid_brightness = 0; static uint8_t g_hid_brightness = 0;
// Battery reporting state (wireless mode only).
// Pushes the current percentage every BAT_REPORT_INTERVAL_MS ms.
// Suppresses redundant sends when the level hasn't changed.
#ifdef LK_WIRELESS_ENABLE
# define BAT_REPORT_INTERVAL_MS 300000U // 5 minutes
static uint32_t g_bat_timer = 0;
static bool g_bat_sent_once = false;
static uint8_t g_last_sent_bat = HID_BATT_UNAVAILABLE;
#endif
// Send the current battery percentage to the host.
// Only fires when get_transport() & TRANSPORT_WIRELESS; no-op otherwise.
// Suppresses sends when the level matches the last value sent.
#ifdef LK_WIRELESS_ENABLE
static void hid_send_battery(void) {
if (!(get_transport() & TRANSPORT_WIRELESS)) return;
uint8_t level = battery_get_percentage();
if (level == g_last_sent_bat) return;
g_last_sent_bat = level;
uint8_t data[HID_PACKET_SIZE] = {0};
data[HID_OFF_CMD] = HID_CMD_BATTERY;
data[HID_OFF_SRC] = HID_DEV_Q5MAX;
data[HID_OFF_FLAGS] = 0;
data[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = level;
raw_hid_send(data, HID_PACKET_SIZE);
}
#endif
// Send the current layer state to the host / bridge application. // Send the current layer state to the host / bridge application.
static void hid_send_layer_sync(uint8_t layer, uint8_t locked_mask) { static void hid_send_layer_sync(uint8_t layer, uint8_t locked_mask) {
uint8_t data[HID_PACKET_SIZE] = {0}; uint8_t data[HID_PACKET_SIZE] = {0};
@@ -151,6 +183,26 @@ bool kc_raw_hid_rx_kb(uint8_t *data, uint8_t length) {
break; break;
} }
case HID_CMD_BATTERY: {
// Host is querying the current battery level.
// Reply with the current percentage when in wireless mode, or
// HID_BATT_UNAVAILABLE when wired (USB transport / not meaningful).
uint8_t resp[HID_PACKET_SIZE] = {0};
resp[HID_OFF_CMD] = HID_CMD_BATTERY;
resp[HID_OFF_SRC] = HID_DEV_Q5MAX;
resp[HID_OFF_FLAGS] = HID_FLAG_RESPONSE;
#ifdef LK_WIRELESS_ENABLE
resp[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] =
(get_transport() & TRANSPORT_WIRELESS)
? battery_get_percentage()
: HID_BATT_UNAVAILABLE;
#else
resp[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = HID_BATT_UNAVAILABLE;
#endif
raw_hid_send(resp, HID_PACKET_SIZE);
break;
}
default: default:
break; break;
} }
@@ -423,6 +475,17 @@ void matrix_scan_user(void) {
alt_tab_active = false; alt_tab_active = false;
} }
chord_scan(); chord_scan();
#ifdef LK_WIRELESS_ENABLE
// Push battery level to host every BAT_REPORT_INTERVAL_MS when wireless.
// First call fires immediately (g_bat_sent_once == false) so the host gets
// a reading as soon as the keyboard connects over USB in wireless mode.
if (!g_bat_sent_once || timer_elapsed32(g_bat_timer) >= BAT_REPORT_INTERVAL_MS) {
g_bat_sent_once = true;
g_bat_timer = timer_read32();
hid_send_battery();
}
#endif
} }
// RGB Matrix Indicators -------------------------------------------------- // RGB Matrix Indicators --------------------------------------------------