- Rename dev branch references from q5_dev to dev/q5; add dev/k17 for K17 Max work - Add K17 Max build/flash commands using the correct rgb subvariant path (keychron/k17_max/ansi_encoder/rgb) to distinguish from the white LED variant - Add EEPROM Layout Notes section documenting the Q5 Max address map, VIA_EEPROM_MAGIC_ADDR pinning rules, and EECONFIG_KB_DATA_SIZE #undef requirement - Add Keychron RGB section capturing lessons from the persistence work: when to call eeconfig_init_custom_rgb(), version stamp placement, kc_rgb_save() mode persistence, retail_demo_enable 0xFF bug, and extern array requirements - Add DIP Switch section explaining the frame overlay approach and why direct rgb_matrix_mode() calls must be avoided
6.0 KiB
CLAUDE.md - QMK Development (Keychron Q5 Max / K17 Max)
Guidelines and commands for the customized Keychron firmware project based on the wireless_playground fork.
Project Scope
- Origin: git.rootiest.dev/rootiest/qmk_firmware
- Upstream: github.com/Keychron/qmk_firmware (branch:
wireless_playground) - Primary Keyboard: Keychron Q5 Max (ANSI Encoder)
- Secondary Keyboard: Keychron K17 Max (occasionally)
- Development Branches:
dev/q5— Q5 Max work-in-progress, merges tomaindev/k17— K17 Max work-in-progress, merges tomain
- Feature Goals: Tap-Dance, expanded layers, advanced Chording, Unicode support, and Auto-correct.
Build and Flash Commands
All commands must be run from the root of the repository inside the project
Python virtual environment. The system qmk is not used — activate the venv
first:
source .venv/bin/activate
Every qmk command below assumes the venv is active (or prefix each with
source .venv/bin/activate &&).
Compilation
# Build the Q5 Max ANSI Encoder firmware
qmk compile -kb keychron/q5_max/ansi_encoder -km via
# Build the K17 Max firmware (rgb variant; separate 'white' LED variant exists)
qmk compile -kb keychron/k17_max/ansi_encoder/rgb -km via
Flashing
# Flash the Q5 Max (requires the board to be in bootloader mode)
qmk flash -kb keychron/q5_max/ansi_encoder -km via
# Flash the K17 Max (requires the board to be in bootloader mode)
qmk flash -kb keychron/k17_max/ansi_encoder/rgb -km via
Environment Setup
# Ensure the submodules are up to date (critical for the wireless_playground branch)
git submodule update --init --recursive
# Set the default keyboard/keymap
qmk setup
qmk config user.keyboard=keychron/q5_max/ansi_encoder
qmk config user.keymap=via
Code Style and Patterns
- Keymap Structure: Keep the
keymap.corganized by layers. Use descriptive defines for layer names (e.g.,_BASE,_FN,_CHORD). - Feature Modules: For advanced features like Chording or Tap-Dance, prefer creating separate headers/source files in the keymap folder to keep
keymap.creadable. - Firmware Size: Monitor the compiled
.binsize, as wireless features and large feature sets (like Auto-correct) can quickly fill up flash memory. - Documentation: Comment any complex chording logic or non-standard Tap-Dance implementations to ensure maintainability.
Development Workflow
- Verify the current branch is
dev/q5(Q5 Max) ordev/k17(K17 Max). - Implement features in the relevant keymap directory:
- Q5 Max:
keyboards/keychron/q5_max/ansi_encoder/keymaps/via/ - K17 Max:
keyboards/keychron/k17_max/ansi_encoder/rgb/keymaps/via/
- Q5 Max:
- Test compilation locally before committing.
- Ensure
rules.mkhas the necessary flags enabled (e.g.,TAP_DANCE_ENABLE = yes,UNICODE_ENABLE = yes).
Git Conventions
- Use conventional commits (
feat:,fix:,docs:,chore:, etc.) scoped to the keyboard where relevant (e.g.feat(q5_max):,fix(k17_max):). - Do not include
Co-Authored-By: Claudetrailers in commit messages.
Chained / Stacked PRs
When merging a chain of PRs (e.g. A → main, B → A, C → B), always delete the branch after each merge. Gitea (and GitHub) will automatically retarget any open PRs pointing at the deleted branch to the branch it was merged into. This keeps the chain collapsing cleanly into main without manual retargeting or cleanup PRs.
EEPROM Layout Notes
The Q5 Max uses wear-leveling EEPROM (STM32F401). Key layout facts:
EECONFIG_RGB_MATRIXis at bytes 24–31; byte 0 packsmode[7:2] | enable[1:0].- Keychron custom RGB data (effect list, regions, per-key colours, retail demo flag) lives in
EECONFIG_KB_DATABLOCKimmediately afterEECONFIG_BASE_SIZE(37 bytes). VIA_EEPROM_MAGIC_ADDRis pinned to 544 inansi_encoder/config.h. Do not lower this value — it must stay aboveEECONFIG_BASE_SIZE + EECONFIG_KB_DATA_SIZE. If Keychron EEPROM grows, raise 544 accordingly and clear EEPROM on the board.EECONFIG_KB_DATA_SIZEis computed ineeconfig_kb.hand requires an#undefbefore the#defineto suppress QMK's default-zero value.
Keychron RGB (KEYCHRON_RGB_ENABLE)
Enabled via KEYCHRON_RGB_ENABLE = yes in rules.mk. Key behavioural notes:
eeconfig_init_custom_rgb()loads Keychron RGB state from EEPROM into RAM. It must be called inkeyboard_post_init_kb()and inwireless_enter_connected_kb(); without it the arrays are zero-initialised and Launcher settings are lost on every boot or transport change.eeconfig_reset_custom_rgb()writes defaults to EEPROM and stamps the version. The version stamp (eeprom_update_dword(EECONFIG_KEYBOARD, ...)) belongs here only — not in the load path.kc_rgb_save()must calleeconfig_update_rgb_matrix()to persist the QMK RGB mode alongside the Keychron custom data; otherwisergb_matrix_init()(triggered on every transport change byREINIT_LED_DRIVER = 1) reloads the compile-time defaultRGB_MATRIX_TYPING_HEATMAP.retail_demo_enableis a single byte. A bug in the original Keychron code usedeeprom_read_blockinstead ofeeprom_update_blockineeconfig_reset_custom_rgb(), leaving0xFFon freshly-flashed boards.retail_demo_task()treats any non-zero value as "demo active" and forcesCUSTOM_MIXED_RGBevery scan. The load path now clamps> 1to0and re-writes the byte as a one-time recovery.default_per_key_led[]anddefault_region[]must be defined in board-specific code (e.g.ansi_encoder.c) —keychron_rgb.cdeclares themextern.
DIP Switch (Win/Mac)
The Win-side dip switch uses a frame overlay rather than calling rgb_matrix_mode(). A dip_win_active flag is set on switch change; rgb_matrix_indicators_advanced_user() paints all LEDs white each frame when the flag is set. This avoids writing to EEPROM and preserves the Launcher-configured effect, which would otherwise be overwritten by the direct mode call.