A native macOS menu-bar app for the Keychron V1 Max: see the battery level (which stock firmware does not expose over USB), and configure the keyboard — remap keys, edit macros, and control RGB lighting — over the VIA raw-HID protocol. Ships with a small QMK firmware patch that adds a battery read-out command and an Fn+B on-board battery gauge.
- 🔋 Battery in the menu bar — live %/voltage over USB; charging-aware with smoothing; last-known value over the 2.4 GHz dongle with a timestamp.
- ⌨️ Keymap remap — visual V1 Max layout per layer + keycode picker, written live.
- 🅼 Macros — read, edit and write the keyboard's macro slots.
- 🌈 RGB lighting — effect, brightness, speed, hue/saturation; saved to the keyboard.
- 🛠 Firmware patch —
0xA4battery command over USB + an Fn+B on-board colour gauge. - 🌐 7 languages — English · Русский · 中文 · فارسی · Deutsch · Français · Italiano, switchable live in Settings (right-to-left layout for Persian).
⚠️ This ships custom keyboard firmware (a patched QMK build), not stock Keychron firmware. Flashing is opt-in and fully reversible — see Rollback.
- Prebuilt firmware (
firmware/build/…bin) and the app are for the Keychron V1 Max — ANSI, encoder (knob) variant only. That is the single configuration this is built and tested on. Do not flash the prebuilt.binonto any other board. - The patch itself (
firmware/battery-0xA4.patch) only modifies Keychron's shared QMK wireless code (keyboards/keychron/common/…), so it should compile for other Keychron wireless QMK boards (the "…Max" / LK_WIRELESS family — e.g. Q/V/K …Max) if you build it for that specific board. Untested elsewhere, and the app would also need its USB PID (0x0913) and key layout updated for a different model. - The app alone (battery + config) needs no firmware at all over USB except the battery read-out, which relies on the custom
0xA4command — so battery % requires this firmware; keymap/macros/lighting work on stock VIA firmware too. - Rollback: flash
firmware/stock/…binto return to stock. The STM32 ROM bootloader cannot be bricked.
Native Swift (AppKit menu bar + SwiftUI), built with SwiftPM.
- Battery in the menu bar — always visible, no window to open.
- Over the USB cable: live percentage + voltage via the custom
0xA4firmware command. While charging the voltage-based % reads high, so the app flags charging (⚡) and smooths the value; the last on-battery reading is kept as the truest "resting" charge. - Over the 2.4 GHz dongle: the keyboard reports battery only at (re)connect (a hardware
limit of the dongle — see docs/dongle-battery.md); the app listens
on the dongle's
0x008Cchannel and shows the last value with a timestamp.
- Over the USB cable: live percentage + voltage via the custom
- Keymap — visual V1 Max layout per layer + keycode picker → live remap.
- Macros — read/edit/write the keyboard's macro slots.
- Lighting — RGB matrix effect, brightness, speed, hue/saturation; save to the keyboard.
cd app/KeebTool
make app # swift build -c release + assemble KeebTool.app
make run # build and launchRequires macOS 13+ and the Swift toolchain (swift --version). The app is a menu-bar
(LSUIElement) agent; click the battery icon for details and "Open Configurator…".
A small change to Keychron/qmk_firmware (wireless_playground):
0xA4=KC_GET_BATTERYraw-HID command → returns battery percentage + voltage over USB.- Fn+B on-board battery gauge — lights the number row by level with colour: red (<30%), amber (<60%), green otherwise.
- Periodic + on-connect battery push to the wireless module.
| byte | meaning |
|---|---|
data[0] |
0xA4 (echo) |
data[1] |
battery percentage, 0–100 |
data[2] |
battery voltage low byte (mV) |
data[3] |
battery voltage high byte (mV) |
Voltage is little-endian: mV = data[2] | (data[3] << 8).
- USB VID
0x3434; V1 Max ANSI-encoder cable PID0x0913, 2.4 GHz dongle PID0xD030. - raw-HID interface: usage page
0xFF60, usage0x61, 32-byte reports. - Dongle battery channel: usage page
0x008C(byte 0 = percentage). - MCU: STM32 (ChibiOS) with a DFU bootloader → flashed with
dfu-util. - Upstream:
Keychron/qmk_firmware@wireless_playground, targetkeychron/v1_max/ansi_encoder, keymapvia.
The Homebrew qmk formula can be broken and the core arm-none-eabi-gcc ships without newlib;
this recipe works:
# qmk CLI via venv
python3 -m venv ~/.qmk-venv && ~/.qmk-venv/bin/pip install qmk
# ARM toolchain WITH newlib
brew tap osx-cross/arm && brew install osx-cross/arm/arm-gcc-bin@14
brew install dfu-util
# fork + deps
git clone --depth 1 -b wireless_playground https://github.com/Keychron/qmk_firmware.git ~/keychron-qmk
git -C ~/keychron-qmk submodule update --init --recursive
~/.qmk-venv/bin/pip install -r ~/keychron-qmk/requirements.txt
# apply patch + build
git -C ~/keychron-qmk apply /path/to/firmware/battery-0xA4.patch
export PATH="/opt/homebrew/opt/arm-gcc-bin@14/bin:$PATH"
~/.qmk-venv/bin/qmk compile -kb keychron/v1_max/ansi_encoder -km via-
Mode switch → Cable; hold Esc and plug in the USB-C cable → STM32 ROM DFU bootloader.
-
Flash:
dfu-util -a 0 -d 0483:df11 -s 0x08000000:leave -D firmware/build/keychron_v1_max_ansi_encoder_via.bin
A one-time
Transitioning to dfuMANIFEST/ non-zero exit on the leave request is a benign dfu-util quirk — the firmware is written. -
Rollback: flash
firmware/stock/keychron_v1_max_ansi_encoder_via.binthe same way. The STM32 ROM bootloader cannot be bricked.
app/KeebTool/ macOS app (SwiftPM): menu-bar battery + VIA configurator
firmware/
battery-0xA4.patch the firmware change, as a diff vs upstream
build/ keychron_v1_max_..._via.bin patched build (flash this)
stock/ keychron_v1_max_..._via.bin stock firmware (rollback)
docs/ protocol notes + the 2.4 GHz dongle battery write-up
MIT for the macOS app and tooling. The firmware patch is a diff against Keychron's QMK fork and follows upstream GPLv2+. See LICENSE.




