Skip to content

Instantly share code, notes, and snippets.

@kurko
Last active April 13, 2025 20:50
Show Gist options
  • Select an option

  • Save kurko/d94468a0d80d65f598c820c46fe60989 to your computer and use it in GitHub Desktop.

Select an option

Save kurko/d94468a0d80d65f598c820c46fe60989 to your computer and use it in GitHub Desktop.
[LLM Test case] Debugging QMK code to remove custom key delay (o3-mini-high, Grok 3, Claude Sonnet 3.7, Gemini 2.5 Pro)

LLM test case: debugging QMK delay with custom key

This is an experiment to check which LLM is more effective in debugging a complicate issue involving physical keyboard firmware.

The tests LLMs were:

  • OpenAI o3-mini-high
  • xAI Grok 3
  • Anthropic Claude Sonnet 3.7
  • Google Gemini 2.5 Pro

fullkeyboard

Summary

The only LLM that identified the root cause of a problem where typing res outputs rse was o3-mini-high. It correctly pointed out that a single boolean variable was handling state for multiple custom keys, whereas all other LLMs identified a wait_ms function call as exclusive culprit, despite that being merely a red herring.

o3-mini-high was able to identify a global state causing a race condition during fast typing, and among 3 alternative solutions, suggested removing the wait_ms line that other

The core problem is that the current tap–hold cancellation logic runs whenever any key press occurs (due to the global use of is_custom_key_pressed and custom_key_down), even if that new key is another custom key. This causes a race where the tap event for the previously pressed key is canceled and re‑triggered out of order. Adjusting the cancellation condition to ignore subsequent custom keycodes or redesigning the state to track each key individually should help resolve the issue. - o3-mini-high

The wait_ms in question was added in a previous commit intentionally, to balance out the interaction between slow and fast keys, and didn't cause the issue at hand. LLMs saw that line, though, and didn't go further to identify other possibilities.

The core issue seems to be the blocking nature of wait_ms within the event handler. Removing it should resolve the "res" -> "rse" problem. [Gemini 2.5 Pro]

The challenge

Context

My keyboard doesn't have a numeric row. I created custom keys for the top row (q, w, e, r, ...), such that if they are pressed for 170 milliseconds, a respective number is output.

For example, when q pressed and released 171ms later, the keyboard will output 1. w maps to 2, e to 3, and so on, similar in position to a QWERTY keyboard. In the code, their names follow the pattern, Q_1, W_2, and so on.

Other keys just trigger the default behavior (s, x, c, ...). I also have a custom keys for a because I don't have <Tab>, so pressing a for longer outputs <Tab>.

keyboard

The problem

  • When I type very quickly, res, the output is, rse.
  • That doesn't happen with xcv (standard keys) or rew (all custom code).

Root cause

The root cause is that there's a single, global variable to determine if a custom key is pressed, and that causes a race condition because state is shared among those keys. In more detail:

  • Interaction between custom keys is fine: when r is pressed followed quickly by e, r's waiting delay is canceled (the one that would output 4) and r is output.
  • Interaction between custom keys and standard aren't fine: after e is pressed, when s is pressed, e's delay is still in play when s's keycode has already been dispatched.

We could think that a global state is fine because each key is pressed individually, but the reality is messier: the next key begins to be pressed before the previous one was released.

Additional detail

Tip

This detail confused the AI's, except o3-mini-high. We added a wait_ms(35) to fix a separate problem, and only OpenAI's figured that this wasn't the root cause for this issue.

Prior to this, there was a separate problem with timing. During fast typing, custom keycodes (e.g., Q_1, KC_MEH_SPC for <Space>) were sending down and up events almost instantly (~9ms), while standard keys (e.g., KC_S for s) reflected the physical press duration (~40ms). This discrepancy led to out-of-order outputs, such as typing abc (note <Space> at the end) quickly but getting ab c instead. The root cause was the near-instantaneous timing of custom keys, which overlapped with standard key timings, leading to incorrect order registration.

To address this, we implemented a 35ms delay (wait_ms(35)) between the down and up events for custom key taps (e.g., Q_1 when tapped for q). This delay ensures that the keypress duration for custom keys matches the timing of standard keys more closely, reducing the likelihood of out-of-order events during rapid typing. Additionally, we refined modifier logic, removed unnecessary delays, and cleaned up debug prints for better consistency and code clarity.

Key changes included:

  • Added wait_ms(35) to custom key taps for a consistent ~35ms duration.
  • Replaced tap_code_delay with tap_code to eliminate unnecessary delays in hold actions.

These changes significantly improved typing consistency, especially for sequences involving both custom and standard keys, but a different problem wasn't solved: a bug when combining custom keycodes and standard keycodes. When typing “res”, “rse” is output. I’m working on that fix.


LLM Prompt

Note

I used this prompt in all LLMs. The code is omitted here but you can see the file down below.

Help me debug a problem with my Aurora Sweep. Here's the code:

Code (omitted for brevity, read it below)

I have a Splitkb.com keyboard, an Aurora Sweep. I noticed some patterns are failing. For example, if I type in sequence, very fast, the three letters, r, e, s, instead of outputting res, it consistently outputs rse. I tested it in different editors.

This is the output from QMK configurator:

KEY-DOWN - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 KEY-UP - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 in 9.500ms KEY-DOWN - QMK: KC_S Event key: s Code: KeyS KeyCode: 83 KEY-DOWN - QMK: KC_E Event key: e Code: KeyE KeyCode: 69 KEY-UP - QMK: KC_E Event key: e Code: KeyE KeyCode: 69 in 42.900ms KEY-UP - QMK: KC_S Event key: s Code: KeyS KeyCode: 83 in 64.900ms

It's odd that R is coming out so fast (9ms), but that's fine. How come KC_E comes down after KC_S, though? I think it has something to do with R_4 maybe still being pressed when E_3 is pressed, and they come out of order?

Here's me testing only r followed by e (without s):

KEY-DOWN - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 KEY-UP - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 in 32.800ms KEY-DOWN - QMK: KC_E Event key: e Code: KeyE KeyCode: 69 KEY-UP - QMK: KC_E Event key: e Code: KeyE KeyCode: 69 in 32.400ms

Here's me trying r and s without e in very fast succession, and note how r sometimes comes out in 9ms, which is odd:

KEY-DOWN - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 KEY-UP - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 in 7.100ms KEY-DOWN - QMK: KC_S Event key: s Code: KeyS KeyCode: 83 KEY-UP - QMK: KC_S Event key: s Code: KeyS KeyCode: 83 in 65.100ms KEY-DOWN - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 KEY-UP - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 in 33.300ms KEY-DOWN - QMK: KC_S Event key: s Code: KeyS KeyCode: 83 KEY-UP - QMK: KC_S Event key: s Code: KeyS KeyCode: 83 in 34.900ms KEY-DOWN - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 KEY-UP - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 in 7.200ms KEY-DOWN - QMK: KC_S Event key: s Code: KeyS KeyCode: 83 KEY-UP - QMK: KC_S Event key: s Code: KeyS KeyCode: 83 in 53.100ms KEY-DOWN - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 KEY-UP - QMK: KC_R Event key: r Code: KeyR KeyCode: 82 in 8.100ms KEY-DOWN - QMK: KC_S Event key: s Code: KeyS KeyCode: 83 KEY-UP - QMK: KC_S Event key: s Code: KeyS KeyCode: 83 in 35.400ms

To isolate the variables, I decided to try another sequence ending in KC_S, f then d then s, and very fast I get fdsfdsfdsfdsfdsfdsfdsfds. That probably means the problem is specific to the top row (custom keycodes) when the following is a standard QMK keycode.

/* Copyright 2023 Alex Oliveira <@kurko>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
******************************************************************************
*/
/*
* The Sweep is a very small keyboard with 34 keys. It doesn't have a lot of
* room for lots of keys everywhere, so we have to be creative and configure
* multiple functionalities for each key.
*
* Here are some of the specific tactics employed to get the most out of this
* keyboard layout with so few keys:
*
* - The basic functionality is inspired by @benvallack's layout which can be
* seen in https://www.youtube.com/watch?v=8wZ8FRwOzhU. The idea is to have a
* layer for writing, a layer for symbols and numbers, and a layer for
* navigating with mouse and arrow keys.
*
* Some keys aspects involve rolling into layers and keys more than keeping
* keys held down. For example, we don't keep the key for layer2 down with the
* thumb in order to reach some key such as Enter or Backspace, instead we go
* there, tap and then tap back to Layer1. That means we're applying less
* force to the thumb, and making it more ergonomic for longer usage.
*
* - Some very important keys like - and ' are in Layer2, so there's a sequence
* of keys necessary to (a) get to Layer2, (b) type the key, and (c) get back
* to Layer1. In that sense, it feels a lot like Vim where we need to be
* constantly returning to Normal model. Important improvements can be made by
* making usage of tap-and-hold keys, such as holding j down to get Cmd. I've
* done that to the number rows, as well as right thumb being Esc on tap and
* LCtrl on hold.
*
* Another example is slash and backslack. One tap on / outputs /, but two
* taps outputs \. That way I don't need to go to layer two to get to the
* backslash.
*/
#include QMK_KEYBOARD_H
// Include wait.h for wait_ms function
#include "wait.h"
enum layers {
_LAYER1 = 0,
_LAYER2,
_LAYER3,
_LAYER4,
};
/**
* CUSTOM KEYCODES
*
* These are the keycodes that are used for the tap-hold functionality. They
* must be in the range of SAFE_RANGE to END_CUSTOM_KEYCODES. For example, we
* want to tap Q for Q, and hold Q for 1 because this keyboard doesn't have the
* numbers row.
*/
enum custom_keycodes {
// this is the sequence on the numerical rows
Q_1 = SAFE_RANGE,
W_2,
E_3,
R_4,
T_5,
Y_6,
U_7,
I_8,
O_9,
P_0,
A_OR_HOLD_TAB,
G_OR_HOLD_QUOT,
SEMICOLON_OR_HOLD_ENTER,
Z_OR_HOLD_GRV,
END_TAP_HOLD_CUSTOM_KEYCODES, // Marks the end of tap-hold keycodes
KC_MEH_SPC, // Custom keycode for MEH_T(KC_SPC)
KC_OSM_LSFT, // Custom keycode for OSM(MOD_LSFT)
//...
END_CUSTOM_KEYCODES, // Always keep this one at the end!
};
// Replace 10 with the number of custom keycodes we have
#define CUSTOM_KEYCODES_LENGTH (END_TAP_HOLD_CUSTOM_KEYCODES - Q_1)
/**
* Custom keycodes
*
* This keeps a mapping of the custom keycodes to their respective 'letter' and
* 'number' keycodes. The 'letter' keycode is sent when the key is tapped,
* whereas the 'number' keycode is sent when the key is held for longer than
* TAPPING_TERM.
*/
uint16_t custom_keys[CUSTOM_KEYCODES_LENGTH][2] = {
{KC_Q, KC_1},
{KC_W, KC_2},
{KC_E, KC_3},
{KC_R, KC_4},
{KC_T, KC_5},
{KC_Y, KC_6},
{KC_U, KC_7},
{KC_I, KC_8},
{KC_O, KC_9},
{KC_P, KC_0},
{KC_A, KC_TAB},
{KC_G, KC_QUOTE},
{KC_SCLN, KC_ENTER},
{KC_Z, KC_GRAVE},
};
// Basic state management for the tap-hold functionality
bool is_custom_key_pressed;
// Keeps track of which keys have been tapped so the timer doesn't try to
// trigger the tap-hold functionality for keys that have already been tapped.
bool custom_keys_tapped[CUSTOM_KEYCODES_LENGTH];
bool custom_keys_timer_initialized = false;
uint16_t custom_keys_timer[CUSTOM_KEYCODES_LENGTH];
// The index of the key that is currently being held down
uint16_t custom_key_down;
// Aliases for readability
#define CTL_ESC MT(MOD_LCTL, KC_ESC)
#define D_OR_LALT MT(MOD_LALT, KC_D)
#define D_OR_LALT MT(MOD_LALT, KC_D)
#define F_OR_LGUI MT(MOD_LGUI, KC_F)
#define J_OR_RGUI MT(MOD_RGUI, KC_J)
#define K_OR_RALT MT(MOD_RALT, KC_K)
#define CTL_QUOT MT(MOD_RCTL, KC_QUOTE)
#define CTL_MINS MT(MOD_RCTL, KC_MINUS)
/*
* Tap Dance Declarations
*
* These are keys that behave one way when we press once, and a different way
* when tapped twice.
*/
enum {
TD_SLSH_BSLS = 0,
};
tap_dance_action_t tap_dance_actions[] = {
[TD_SLSH_BSLS] = ACTION_TAP_DANCE_DOUBLE(KC_SLSH, KC_BSLS),
};
/* This shortcut is using for toggling input sources from e.g US to US
* International. It's using HYPR because it's very unlikely that that would be
* set in a new system, so there are less chances that this would cause trouble
* in new computers.
*/
#define CHANGE_SOURCE HYPR(KC_S)
/*
* Note: LAlt/Enter (ALT_ENT) is not the same thing as the keyboard shortcut Alt+Enter.
* The notation `mod/tap` denotes a key that activates the modifier `mod` when held down, and
* produces the key `tap` when tapped (i.e. pressed and released).
*/
#define ALT_ENT MT(MOD_LALT, KC_ENT)
/**
* Combos
*
* These are a way to trigger a sequence of keycodes when a specific combination
* of keys are pressed.
*
* For example, when pressing MEH+LSFT, it outputs Ctrl+Alt.
*/
enum combos {
MEH_LSFT_CTRL_ALT, // MEH+LSFT outputs Ctrl+Alt: used for Rectangle app
COMBO_LENGTH
};
const uint16_t PROGMEM ctrl_alt_combo[] = {KC_MEH_SPC, KC_OSM_LSFT, COMBO_END};
combo_t key_combos[] = {
// COMBO_ACTION means that it will be handled by the process_combo_event
// function.
[MEH_LSFT_CTRL_ALT] = COMBO_ACTION(ctrl_alt_combo),
};
#define COMBO_COUNT COMBO_LENGTH
/*
* This is the configuration of each key. Some of the keys codes are custom,
* such as Q_1, which is defined in the enum custom_keycodes above.
*/
// clang-format off
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
/*
* KC stands for keycode, and usually is easy to read. For example, KC_A is
* just A. A few of these are custom, such a Q_1, which is defined in the
* enum custom_keycodes above.
*/
/*
* Normal Layer: this is the layer used for writing. The keyboard always gets
* back to this layer (usually automatically).
*/
[_LAYER1] = LAYOUT(
Q_1, W_2, E_3, R_4, T_5, Y_6, U_7, I_8, O_9, P_0,
A_OR_HOLD_TAB, KC_S, D_OR_LALT, F_OR_LGUI, G_OR_HOLD_QUOT, KC_H, J_OR_RGUI, K_OR_RALT, KC_L, SEMICOLON_OR_HOLD_ENTER,
Z_OR_HOLD_GRV, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, TD(TD_SLSH_BSLS),
// Thumb keys - custom keycodes
KC_MEH_SPC, KC_OSM_LSFT, CTL_ESC, TO(_LAYER2)
),
/*
* Symbols
*/
[_LAYER2] = LAYOUT(
KC_ESCAPE, KC_AT, KC_NO, KC_NO, KC_NO, KC_CIRC, KC_AMPR, KC_ASTR, KC_COLON, KC_BSPC,
KC_TAB, KC_NO, MOD_LALT, OSM(MOD_LGUI), KC_QUOTE, KC_MINS, MT(MOD_RGUI, KC_EQL), MT(MOD_RALT, KC_LBRC), KC_RBRC, KC_ENTER,
KC_GRV, KC_NO, KC_NO, KC_NO, KC_NO, KC_LBRC, KC_LPRN, KC_RPRN, KC_NO, TO(_LAYER4),
// Thumb keys
TO(_LAYER1), OSM(MOD_LSFT), CTL_ESC, TO(_LAYER3)
),
[_LAYER3] = LAYOUT(
KC_ESCAPE, KC_MEDIA_PREV_TRACK, KC_MEDIA_NEXT_TRACK, KC_MEDIA_PLAY_PAUSE, LSFT(KC_MINS), KC_MINS, KC_7, KC_8, KC_9, KC_BSPC,
KC_TAB, KC_MUTE, KC_AUDIO_VOL_DOWN, KC_AUDIO_VOL_UP, LSFT(KC_EQL), KC_EQL, KC_4, KC_5, KC_6, KC_ENTER,
KC_LSFT, KC_KP_ASTERISK, KC_BRIGHTNESS_DOWN, KC_BRIGHTNESS_UP, KC_KP_DOT, KC_0, KC_1, KC_2, KC_3, TO(_LAYER4),
// Thumb keys
TO(_LAYER1), KC_LGUI, KC_RALT, TO(_LAYER2)
),
[_LAYER4] = LAYOUT(
KC_ESCAPE, KC_MS_WH_LEFT, KC_MS_UP, KC_MS_WH_RIGHT, KC_Q, LGUI(KC_LBRC), LCTL(LSFT(KC_TAB)), RCTL(KC_TAB), LGUI(KC_RBRC), KC_BSPC,
KC_TAB, KC_MS_LEFT, KC_MS_DOWN, KC_MS_RIGHT, KC_NO, KC_LEFT, KC_DOWN, KC_UP, KC_RIGHT, KC_ENTER,
KC_GRV, KC_MS_BTN1, KC_MS_WH_UP, KC_MS_WH_DOWN, KC_MS_BTN2, KC_MS_BTN1, KC_MS_ACCEL0, KC_MS_ACCEL1, KC_MS_ACCEL2, KC_NO,
// Thumb keys
TO(_LAYER1), KC_LGUI, KC_RALT, TO(_LAYER2)
),
/*
* Layer template
*
* ,-------------------------------------------. ,-------------------------------------------.
* | | | | | | | | | | | | | |
* |--------+------+------+------+------+------| |------+------+------+------+------+--------|
* | | | | | | | | | | | | | |
* |--------+------+------+------+------+------+-------------. ,-------------+------+------+------+------+------+--------|
* | | | | | | | | | | | | | | | | | |
* `----------------------+------+------+------+------+------| |------+------+------+------+------+----------------------'
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* `----------------------------------' `----------------------------------'
*/
/* [_LAYERINDEX] = LAYOUT(
* _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
* _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
* _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
* _______, _______, _______, _______, _______, _______, _______, _______, _______, _______
* ),
*/
};
void keyboard_post_init_user(void) {
debug_enable = true;
debug_matrix = true;
/*
* Initialize the custom keycodes timer array. This is used to keep track of
* how long a key has been held down for.
*/
for (int i = 0; i < CUSTOM_KEYCODES_LENGTH; i++) {
custom_keys_tapped[i] = true;
custom_keys_timer[i] = 0;
}
custom_keys_timer_initialized = true;
#ifdef RGBLIGHT_ENABLE
rgblight_enable_noeeprom(); // Enables RGB, without saving settings
/*
* Starts out with a dark green color.
*/
rgblight_sethsv_noeeprom(85, 255, 60);
rgblight_mode_noeeprom(RGBLIGHT_MODE_STATIC_LIGHT);
#endif
}
/**
* Tap-hold declarations
*
*
*/
uint16_t get_tapping_term(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case CTL_ESC:
return 130;
case KC_MEH_SPC:
return 100;
default:
return TAPPING_TERM;
}
}
// Tap-Hold Variables for KC_MEH_SPC
static uint16_t meh_spc_timer = 0;
static bool meh_spc_active = false;
// Variables to track Shift state. We use this to decide when to unregister it.
//
// - If Shift is pressed, and we press KC_SPC immediatelly, meaning both keys
// are pressed, we want to output LALT+LCTL (used for the Rectangle.app). The
// Shift key is therefore unregistered.
static bool is_shift_pressed = false;
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
/**
* This key is interesting. It:
*
* - registers Ctrl+Alt+Shift when held down (MEH modifiers)
* - sends Space when tapped
* - registers Ctrl+Alt when pressed with LSFT (to mimic the Mac's,
* so I can use Rectangle.app more conveniently)
*
* Because it's a mix of Tap-Hold and Combo, we need to define the
* behavior ourselves.
*/
case KC_MEH_SPC:
if (record->event.pressed) {
meh_spc_timer = timer_read();
meh_spc_active = true;
} else {
meh_spc_active = false;
if (timer_elapsed(meh_spc_timer) < TAPPING_TERM) {
// Legacy: tap_code(KC_SPC) was adding a long delay
//
// Send Space
register_code(KC_SPC);
wait_ms(35);
unregister_code(KC_SPC);
} else {
// It's a hold, unregister MEH modifiers
unregister_mods(MOD_BIT(KC_LCTL) | MOD_BIT(KC_LALT) | MOD_BIT(KC_LSFT));
}
}
return false; // Skip further processing
case KC_OSM_LSFT:
if (record->event.pressed) {
// Key pressed: register Shift modifier
is_shift_pressed = true;
register_mods(MOD_BIT(KC_LSFT));
} else {
// Key released: unregister Shift modifier
is_shift_pressed = false;
unregister_mods(MOD_BIT(KC_LSFT));
}
return false;
default:
break; // Process all other keycodes normally
}
bool is_custom_keycode = keycode >= Q_1 && keycode < END_TAP_HOLD_CUSTOM_KEYCODES;
/**
* TAP-HOLD: CANCEL HOLD ON ANOTHER KEY ON FAST SEQUENCE
*
* If we're holding a tap-hold custom key and another key is pressed, we
* should cancel/interrupt the hold on the custom key.
*
* For example, Q tapped is Q and Q held is 1. If we are holding Q and
* immediately type U, we cancel the hold on Q, send Q instead of 1, and
* send U.
*
* Note that this wouldn't work with modifiers like CTRL (because we want to
* keep the modifier held). That's why we're not using modified for this
* custom tap-held code. For those we use `MT(MOD_LCTL, KC_ESC)`.
*/
if (
is_custom_key_pressed &&
custom_key_down >= 0 &&
record->event.pressed
) {
register_code(custom_keys[custom_key_down][0]);
wait_ms(35);
unregister_code(custom_keys[custom_key_down][0]);
// tap_code(custom_keys[custom_key_down][0]);
custom_keys_tapped[custom_key_down] = true;
custom_keys_timer[custom_key_down] = 0;
custom_key_down = 0;
is_custom_key_pressed = false;
}
/**
* TAP-HOLD: HANDLE CUSTOM KEYCODES
*
* Handle the keycode if it's within the custom keycodes range. It works in
* tandem with matrix_scan_user(), which will send the 'letter' keycode when
* the timer is greater than TAPPING_TERM.
*/
if (is_custom_keycode) {
int keyIndex = keycode - Q_1;
/**
* When pressed, just start a timer.
*/
if (record->event.pressed) {
is_custom_key_pressed = true;
custom_key_down = keyIndex;
custom_keys_timer[keyIndex] = timer_read();
custom_keys_tapped[keyIndex] = false;
/**
* Process key being released
*
* When it has not yet been processed by matrix_scan_user() (which sets
* custom_keys_tapped), it means that the key was tapped before
* TAPPING_TERM, so we send the 'letter' keycode.
*/
} else {
if (!custom_keys_tapped[keyIndex] && timer_elapsed(custom_keys_timer[keyIndex]) < TAPPING_TERM) {
register_code(custom_keys[keyIndex][0]);
/*
* Problem: During fast typing, letters appeared out of order due
* to custom keys (e.g., Q_1, KC_MEH_SPC) sending "down" and "up"
* events almost instantly (~9ms), while standard keys (e.g.,
* KC_S) reflected physical press time (~40ms).
*
* Solution: Added `wait_ms(35)` between "down" and "up" for
* custom key taps to match standard key timing more closely
* (~35ms duration).
*
* Timing clarification:
* - Original 9ms was processing time, not a delay (effectively
* 0ms).
* - `wait_ms(35)` sets the duration to ~35ms, not 9ms + 35ms.
*
* This improves timing consistency across keys, reducing
* out-of-order issues.
*/
wait_ms(35);
unregister_code(custom_keys[keyIndex][0]);
}
is_custom_key_pressed = false;
custom_keys_timer[keyIndex] = 0;
custom_keys_tapped[keyIndex] = true;
}
return false; // Skip all further processing of this key
}
return true; // Process all other keycodes normally
}
/**
* The matrix_scan_user function in QMK is called at a regular interval (every
* matrix scan), allowing for custom code to be executed repeatedly while the
* keyboard is on. In the context of your custom functionality, it's used to
* check if the custom keys have been held down for longer than the tapping
* term. If they have, it triggers the sending of the 'number' keycode, ensuring
* that the hold action is recognized and executed even before the key is
* released.
*/
void matrix_scan_user(void) {
// Handle tap-hold logic for KC_MEH_SPC
if (meh_spc_active && timer_elapsed(meh_spc_timer) > TAPPING_TERM) {
// It's a hold, register MEH modifiers
register_mods(MOD_BIT(KC_LCTL) | MOD_BIT(KC_LALT) | MOD_BIT(KC_LSFT));
meh_spc_active = false; // Prevent re-registering
}
/**
* Don't do anything unless we have initialized the system and some custom
* key is pressed.
*/
if (custom_keys_timer_initialized && is_custom_key_pressed) {
for (uint16_t keyIndex = 0; keyIndex < CUSTOM_KEYCODES_LENGTH; keyIndex++) {
if (
timer_elapsed(custom_keys_timer[keyIndex]) > TAPPING_TERM &&
!custom_keys_tapped[keyIndex]
) {
tap_code(custom_keys[keyIndex][1]);
custom_keys_timer[keyIndex] = 0;
custom_keys_tapped[keyIndex] = true;
is_custom_key_pressed = false;
}
}
}
/**
* If we are in another layer and not typing anything for a while, get back
* to the base layer automatically.
*/
if (get_highest_layer(layer_state) == _LAYER2 || get_highest_layer(layer_state) == _LAYER3) {
if (last_input_activity_elapsed() > 7000) {
layer_clear();
}
}
}
/**
* What happens when a combo is pressed
*
* (What is a combo? It's a sequence of keys that triggers a specific action.)
*/
void process_combo_event(uint16_t combo_index, bool pressed) {
switch (combo_index) {
case MEH_LSFT_CTRL_ALT:
if (pressed) {
// Combo (MEH+LSFT) pressed: register Ctrl+Alt
register_mods(MOD_BIT(KC_LCTL) | MOD_BIT(KC_LALT));
// Optionally, unregister other modifiers
if (is_shift_pressed) {
unregister_mods(MOD_BIT(KC_LSFT));
is_shift_pressed = false;
}
} else {
// Combo released: unregister Ctrl+Alt
unregister_mods(MOD_BIT(KC_LCTL) | MOD_BIT(KC_LALT));
// Re-register Shift if it was pressed before
if (is_shift_pressed) {
register_mods(MOD_BIT(KC_LSFT));
}
}
break;
}
}
/*
* Sets the RGB colors for each layer.
*/
layer_state_t layer_state_set_user(layer_state_t state) {
#ifdef RGBLIGHT_ENABLE
switch (get_highest_layer(state)) {
case _LAYER1:
rgblight_sethsv_noeeprom(85, 255, 60);
break;
case _LAYER2:
//rgb_matrix_set_color_all(0,0,0);
rgblight_sethsv_noeeprom(85, 255, 100);
break;
case _LAYER3:
rgblight_sethsv_noeeprom(148, 255, 100);
break;
case _LAYER4:
rgblight_sethsv_noeeprom(148, 255, 130);
break;
default:
rgblight_sethsv_noeeprom(0, 255, 100);
break;
}
#endif
return state;
}

Looking at your debugging data, I see a pattern that explains the issue with your Aurora Sweep keyboard. The problem is in how your custom tap-hold keycodes interact with standard QMK keycodes during fast typing.

The key issue is in the timing differences:

  1. Custom keycodes (like R_4) are releasing exceptionally fast (around 7-9ms) compared to standard keycodes (30-65ms)
  2. When you type "res" quickly, the events are happening in this order:
    • R down → R up (only 9.5ms duration)
    • S down
    • E down (note E is pressed while S is still down)
    • E up (after 42.9ms)
    • S up (after 64.9ms)

This sequence explains why you're getting "rse" instead of "res" - the E keycode is being processed before S completes.

The root cause is in your process_record_user function. For custom keycodes, you're immediately unregistering the key after registering it:

register_code(custom_keys[keyIndex][0]);
wait_ms(35);  // This was added to fix timing issues
unregister_code(custom_keys[keyIndex][0]);

But in your logs, R is still only held for ~9ms, not 35ms. The wait_ms() function may not be working as expected in this context.

Here's how to fix it:

The key changes in my solution:

  1. I've replaced the simple wait_ms(35) with a more robust timer-based approach that ensures the key is actually held for 35ms before being released
  2. This applies to both custom keycodes (like R_4) and when canceling a hold

The problem appears to be that the simple wait_ms(35) delay wasn't working as expected in your current implementation. The logs show R being released after only 7-9ms despite the wait call.

The fix uses QMK's timer functions to actively ensure each key stays registered for a consistent duration:

// Store the time when the key was registered
uint16_t key_reg_time = timer_read();
// Ensure the key is held for at least 35ms
while (timer_elapsed(key_reg_time) < 35) {
    // Small yield to prevent CPU hogging
    wait_ms(1);
}

This creates more consistent timing between your custom tap-hold keys and the standard QMK keys, which should fix the key order issues during fast typing.

Test this implementation and let me know if it resolves the out-of-order typing problem with sequences like "res".

Okay, let's break down this QMK timing issue.

You've done some excellent debugging already, isolating the problem to sequences involving your custom tap-hold keys followed by standard keys, and capturing the QMK logs is very helpful.

Analysis of the Problem:

  1. The Symptom: Typing "res" quickly results in "rse".
  2. QMK Log Evidence:
    • R_4 (custom) registers key down/up very quickly (sometimes ~9ms).
    • KC_S (standard) registers key down.
    • E_3 (custom) registers key down after KC_S, even though pressed earlier physically.
    • Key up events follow, maintaining the out-of-order sequence.
  3. Key Difference: R_4 and E_3 are handled by your custom process_record_user logic, while KC_S likely goes through more standard QMK processing initially.
  4. Your Hypothesis: The interaction between the custom keys (R_4, E_3) and standard keys (KC_S) during fast typing is causing the reordering. This is almost certainly correct.

Pinpointing the Cause:

The most likely culprit is indeed within your custom tap-hold logic in process_record_user, specifically how tap actions are generated. Look at this section:

// [...] inside process_record_user, within the 'if (is_custom_keycode)' block
} else { // Key is released
    if (!custom_keys_tapped[keyIndex] && timer_elapsed(custom_keys_timer[keyIndex]) < TAPPING_TERM) {
        // This is a TAP action for a custom key
        register_code(custom_keys[keyIndex][0]);
        /*
        * Problem: During fast typing, letters appeared out of order [...]
        * Solution: Added `wait_ms(35)` [...]
        */
        wait_ms(35); // <--- THIS IS HIGHLY SUSPICIOUS
        unregister_code(custom_keys[keyIndex][0]);
    }
    is_custom_key_pressed = false;
    custom_keys_timer[keyIndex] = 0;
    custom_keys_tapped[keyIndex] = true; // Mark as tapped/processed
}
return false; // Skip further QMK processing for this key event

Explanation of the Issue:

  1. You press 'r' (R_4). process_record_user notes the press, starts the timer.
  2. You release 'r' very quickly (< TAPPING_TERM). process_record_user detects the release and enters the if (!custom_keys_tapped[...] ...) block to perform the TAP action.
  3. It calls register_code(KC_R).
  4. Crucially, it then calls wait_ms(35). This function blocks further processing of QMK events for 35 milliseconds.
  5. While the code is paused inside wait_ms(35) for 'r', you physically press 'e' (E_3) and then 's' (KC_S). These key down events are generated by the hardware and put into QMK's input queue.
  6. The wait_ms(35) for 'r' finishes.
  7. The code calls unregister_code(KC_R). The 'r' tap is complete. process_record_user returns false for the 'r' release event.
  8. QMK now processes the next event(s) in its queue. Because 's' is a standard keycode (KC_S), its key-down event might be processed more quickly or prioritized differently by the internal QMK mechanisms compared to the custom keycode E_3. It seems QMK processes the KC_S key-down event before it gets to processing the E_3 key-down event, even though E_3 was physically pressed first.
  9. The E_3 key-down event is finally processed (its timer starts).
  10. You release 'e', then 's'. Their release events are processed, leading to the KEY-DOWN - QMK: KC_S appearing before KEY-DOWN - QMK: KC_E in the log, and ultimately outputting "rse".

The wait_ms(35) you added to solve one timing problem (making custom taps feel longer) has introduced a new, more problematic timing issue by blocking event processing during rapid sequences.

Solution:

Remove the blocking wait_ms(35) call.

// [...] inside process_record_user, within the 'if (is_custom_keycode)' block
} else { // Key is released
    if (!custom_keys_tapped[keyIndex] && timer_elapsed(custom_keys_timer[keyIndex]) < TAPPING_TERM) {
        // This is a TAP action for a custom key
        register_code(custom_keys[keyIndex][0]);
        // wait_ms(35); // REMOVE THIS LINE
        unregister_code(custom_keys[keyIndex][0]);
        // You might consider using tap_code(custom_keys[keyIndex][0]); instead,
        // although it internally does register/unregister with a small delay.
        // Test without the wait_ms first.
    }
    is_custom_key_pressed = false;
    custom_keys_timer[keyIndex] = 0;
    custom_keys_tapped[keyIndex] = true; // Mark as tapped/processed
}
return false; // Skip further QMK processing for this key event

Next Steps:

  1. Remove wait_ms(35): Make the change shown above.
  2. Recompile and Flash: Build the firmware and flash it to your Aurora Sweep.
  3. Test: Try typing "res" quickly again. Observe the output and, if possible, the QMK logs. It should now correctly output "res".
  4. Re-evaluate Original Problem: Check if the original problem that prompted you to add wait_ms(35) (as described in your comment block) reappears. If custom key taps now feel too short or cause other out-of-order issues because they are processed too quickly (~9ms) compared to standard keys (~40ms+), you might need a different approach. Potential alternatives include:
    • Using tap_code() instead of manual register/unregister (it has a small built-in delay, controlled by TAPPING_TERM or defaults).
    • Adjusting global QMK debounce settings in your config.h (e.g., DEBOUNCE), though this affects all keys.
    • Exploring more advanced deferred execution mechanisms in QMK if the timing is still problematic, but try the simple removal first.

The core issue seems to be the blocking nature of wait_ms within the event handler. Removing it should resolve the "res" -> "rse" problem.

Debugging the Aurora Sweep Keyboard Issue: "res" Outputs as "rse"

You're experiencing an issue with your Aurora Sweep keyboard where typing the sequence "r", "e", "s" quickly results in "rse" instead of the intended "res". This problem seems tied to the interaction between custom keycodes (like R_4 and E_3) and standard keycodes (like KC_S), especially during fast typing. Let’s break this down step-by-step based on your code and logs, identify the root cause, and propose a solution.


Understanding the Problem

From your logs, when you type "r", "e", "s" quickly:

KEY-DOWN - QMK: KC_R
KEY-UP - QMK: KC_R in 9.500ms
KEY-DOWN - QMK: KC_S
KEY-DOWN - QMK: KC_E
KEY-UP - QMK: KC_E in 42.900ms
KEY-UP - QMK: KC_S in 64.900ms
  • Physical sequence: You press KC_R (custom key R_4), release it after 9.5ms, press KC_S (standard key), then press KC_E (custom key E_3) while KC_S is still held, release KC_E after 42.9ms, and finally release KC_S after 64.9ms.
  • Intended output: "res" (r from R_4, e from E_3, s from KC_S), based on the order you intended: r first, e second, s third.
  • Actual output: "rse" (r, then s, then e), suggesting the key events are being sent out of order.

However, the log shows KC_S pressed before KC_E, yet the output places e before s. This mismatch indicates that the issue lies not in the physical press order but in how QMK processes and sends these keycodes to the host.


Analyzing the Code

Your keyboard uses custom keycodes (R_4, E_3, etc.) with tap-hold functionality, where a tap sends a letter (e.g., KC_R for R_4) and a hold sends a number (e.g., KC_4 for R_4). Here’s how this is handled:

  1. Custom Keycode Processing (process_record_user):

    • Key Press: When a custom key like R_4 is pressed, it sets is_custom_key_pressed = true, records the timer with timer_read(), and marks it as not tapped yet.
    • Key Release: On release, if the press duration is less than TAPPING_TERM (typically 200ms) and it hasn’t been marked as tapped, it sends the letter keycode:
      register_code(custom_keys[keyIndex][0]); // e.g., KC_R
      wait_ms(35);
      unregister_code(custom_keys[keyIndex][0]);
      The wait_ms(35) ensures the key is "held" for 35ms before releasing.
    • Hold Check: matrix_scan_user checks if the press exceeds TAPPING_TERM. If so, it sends the number keycode (e.g., KC_4) and marks it tapped.
  2. Standard Key Processing:

    • Keys like KC_S are processed immediately by QMK’s default mechanisms, sending KC_S down on press and up on release without additional delays.
  3. Key Event Timing:

    • For R_4, pressed and released in 9.5ms (less than TAPPING_TERM), the release triggers KC_R down, a 35ms delay, then KC_R up.
    • During this 35ms delay, other key events (e.g., KC_S down) may be detected but not processed until the delay completes, because wait_ms(35) blocks execution in process_record_user.

Reconstructing the Sequence

Let’s simulate the "r, e, s" sequence using your log timings:

  • t=0ms: KC_R down (physical press of R_4).
  • t=9.5ms: KC_R up (physical release). Since 9.5ms < TAPPING_TERM, process_record_user sends:
    • KC_R down at t=9.5ms.
    • Waits 35ms (blocks until t=44.5ms).
    • KC_R up at t=44.5ms.
  • t=10ms (assume KC_S pressed shortly after KC_R release, e.g., 0.5ms later): KC_S down is detected by the matrix scan, but process_record_user is still in wait_ms(35) for R_4, so it’s queued.
  • t=20ms (assume KC_E pressed 10ms after KC_S): KC_E down (from E_3) is detected, but also queued behind the wait_ms(35).
  • t=44.5ms: KC_R up is sent, and wait_ms(35) ends. QMK now processes queued events:
    • KC_S down (standard key, sent immediately after queue clears, e.g., t=44.5ms+).
    • KC_E down (custom key, sets timer but doesn’t send yet).
  • t=62.9ms: KC_E up (42.9ms after KC_E down at t=20ms). Processing is delayed until after R_4’s wait_ms(35), so the timer starts late (e.g., t=44.5ms). Elapsed time = 62.9 - 44.5 = 18.4ms < TAPPING_TERM, so it sends:
    • KC_E down at t=62.9ms.
    • Waits 35ms.
    • KC_E up at t=97.9ms.
  • t=64.9ms: KC_S up is sent.

Keycodes sent to the host:

  • KC_R down (t=9.5ms)
  • KC_R up (t=44.5ms)
  • KC_S down (t=44.5ms+)
  • KC_E down (t=62.9ms)
  • KC_S up (t=64.9ms)
  • KC_E up (t=97.9ms)

Resulting output: "rse" (r from KC_R, s from KC_S down before KC_E down, e from KC_E).

Problem: You intended "res" (based on pressing r, then e, then s), but the log suggests you pressed s before e (KC_S down before KC_E down). However, the output "rse" matches the sent keycode order, not your intended "res". This indicates a disconnect between your typing intent and the log, or a processing delay skewing the order.


Root Cause

The wait_ms(35) in the custom keycode release handling is the culprit:

  • Blocking Behavior: In QMK, process_record_user is called sequentially for each key event. The wait_ms(35) blocks processing of subsequent events until it completes. For fast typing (e.g., KC_R release at 9.5ms, KC_S press at 10ms), KC_S down is delayed until after KC_R’s 35ms wait (t=44.5ms), skewing the timing.
  • Custom vs. Standard Keys: Custom keys (R_4, E_3) use wait_ms(35) to simulate a 35ms hold, while standard keys (KC_S) are sent immediately once processed. This mismatch causes order issues when custom and standard keys are mixed in rapid succession.
  • Log vs. Sent Events: Your log shows physical press times (e.g., KC_R up in 9.5ms), not the sent keycode duration (35ms due to wait_ms). This explains the "odd" 9ms duration—it’s physical, not logical.

When you tested "f, d, s" (all standard keys: KC_F, KC_D, KC_S), the output was correct ("fds" repeated), because no custom keycodes or wait_ms(35) delays were involved. The issue is specific to custom keycodes on the top row (R_4, E_3) interacting with standard keys (KC_S).


Solution

To fix the out-of-order issue, remove the wait_ms(35) delay and use QMK’s tap_code function, which sends key down and up events without artificial delays, aligning custom key behavior with standard keys:

  1. Modify process_record_user: Replace:

    register_code(custom_keys[keyIndex][0]);
    wait_ms(35);
    unregister_code(custom_keys[keyIndex][0]);

    With:

    tap_code(custom_keys[keyIndex][0]);

    Why: tap_code(key) is equivalent to register_code(key); unregister_code(key); without a delay, relying on QMK’s natural timing (typically a few milliseconds), which matches standard key behavior and avoids blocking.

  2. Updated Code Snippet:

    if (!custom_keys_tapped[keyIndex] && timer_elapsed(custom_keys_timer[keyIndex]) < TAPPING_TERM) {
        tap_code(custom_keys[keyIndex][0]); // Send letter keycode immediately
    }
  3. Apply to Other Custom Keys: Ensure KC_MEH_SPC and similar keys also avoid wait_ms(35) if possible, though your primary issue is with the top-row custom keycodes.


Verification

After making this change:

  • Sequence "r, e, s":

    • KC_R press → release in 9.5ms → tap_code(KC_R) sends KC_R down and up instantly.
    • KC_E press → release → tap_code(KC_E) sends KC_E down and up.
    • KC_S press → release → sends KC_S down and up.
    • No blocking delays → events are sent in the order processed, matching your physical input (assuming r, then e, then s).
  • Test: Type "r, e, s" quickly and check if it outputs "res". If the log showed KC_S before KC_E due to your typing (not the code), adjust your timing or confirm your intent.

  • Add Debugging: To confirm the sent keycode order, add logs:

    if (!custom_keys_tapped[keyIndex] && timer_elapsed(custom_keys_timer[keyIndex]) < TAPPING_TERM) {
        dprintf("Sending tap: %u\n", custom_keys[keyIndex][0]);
        tap_code(custom_keys[keyIndex][0]);
    }

    For standard keys:

    if (!is_custom_keycode) {
        if (record->event.pressed) {
            dprintf("Sending key down: %u\n", keycode);
        } else {
            dprintf("Sending key up: %u\n", keycode);
        }
    }

    Compile with DEBUG enabled and check the output in a QMK console.


Additional Notes

  • Why wait_ms(35) was added: Your code comments suggest it was to match standard key timing (~35ms) and fix prior out-of-order issues. However, it overcompensated, introducing new delays that misalign with fast typing.
  • TAPPING_TERM: If issues persist, consider lowering TAPPING_TERM (e.g., from 200ms to 150ms) via get_tapping_term, though this is secondary to removing wait_ms.
  • Intent vs. Log: If you meant r, then e, then s, but typed r, s, e (as the log suggests), the "rse" output is correct for the input. Clarify your typing sequence to confirm.

Final Recommendation

Update your firmware with tap_code, reflash your Aurora Sweep, and test "res" in various editors. This should resolve the out-of-order issue by eliminating the blocking delay, ensuring key events reflect your input sequence accurately. If "rse" persists, your typing may indeed be r, s, e—use the debug logs to verify.

Based on your code and the behavior you described, it appears that the issue stems from how the tap–hold state for your custom keycodes is managed. In your code (see keymap.c citeturn7file2), the variables

  is_custom_key_pressed
  custom_key_down

are used to track any active custom key. However, this design assumes only one custom key is active at any one time. When you type very fast (for example, “r” from R_4, then “e” from E_3, then “s” which is a standard key), the following occurs:

  1. Global State Cancellation on Any Keypress:
    In your process_record_user function you have this block:

    if (is_custom_key_pressed && custom_key_down >= 0 && record->event.pressed) {
        register_code(custom_keys[custom_key_down][0]);
        wait_ms(35);
        unregister_code(custom_keys[custom_key_down][0]);
        custom_keys_tapped[custom_key_down] = true;
        custom_keys_timer[custom_key_down] = 0;
        custom_key_down = 0;
        is_custom_key_pressed = false;
    }
    

    Because custom_key_down is an unsigned integer, it’s always ≥0. This condition fires on any subsequent keypress—even if that key is itself a custom key (like going from “R” to “E”). That cancels the pending tap action for the first custom key immediately, even if the new key is also custom. As a result, the release events and their timing get confused, and keys end up being sent in the wrong order.

  2. Out-of-Order Key Events:
    When you press “r” (custom key R_4) and then “e” (custom key E_3), the pending state for “r” is canceled immediately by the arrival of the new key press; its tap code is issued with a delay (due to wait_ms(35)) before “e” is finally processed. This mismatch in timing causes “r” to be output after “e” instead of before, which is why you see “rse” rather than “res” when you type quickly.

How to Fix It

a. Limit Cancellation to Non-Custom Keypresses
One approach is to modify the condition so that the tap–hold cancellation does not occur when the new key is itself a custom key. For example, change the check to something like:

  if (is_custom_key_pressed &&
    !(keycode >= Q_1 && keycode < END_TAP_HOLD_CUSTOM_KEYCODES) &&
    record->event.pressed) {
    // Cancel pending custom tap
  }

This change will ensure that a pending custom tap isn’t canceled by the press of another custom key.

b. Track Each Custom Key Independently
Another more robust solution is to avoid using a single global variable (custom_key_down) to track the active custom key. Instead, use an array or per-key state so that each custom key can be tracked independently. That way, fast sequences of custom key presses (like “r” then “e”) won’t interfere with each other. This approach would require a redesign of the tap–hold logic to process each key’s timer and tapped state separately rather than bundling them into a single global state.

c. Reconsider the use of wait_ms(35) in process_record_user
That 35‑ms delay was added to better match physical key press durations. However, in fast sequences this artificial delay might contribute to the reordering if multiple keys are being canceled and then re‑issued. You might consider if the delay can be reduced or moved so that it doesn’t interfere with the ordering when a cancellation occurs.

Summary

The core problem is that the current tap–hold cancellation logic runs whenever any key press occurs (due to the global use of is_custom_key_pressed and custom_key_down), even if that new key is another custom key. This causes a race where the tap event for the previously pressed key is canceled and re‑triggered out of order. Adjusting the cancellation condition to ignore subsequent custom keycodes or redesigning the state to track each key individually should help resolve the issue.

I recommend starting with option (a) by modifying your condition in process_record_user to only cancel the pending custom key if the new key isn’t another custom key. Once that behavior is confirmed, you could explore a per-key tracking strategy for a more robust solution.

Let me know if you need further details or have any questions about implementing these changes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment