The basis for a way to cancel QMK macros in progress

The short version: Use QMK‘s send_string_with_delay() with binary content (as it is not a real string) encoded in a particular manner to piecemeal execute a macro, housekeeping_task_user() to run a state machine for executing the macro (which can check for a key press that should cancel), and timer_read32() to get the tick count (for timing things and throttling to avoid interfering with the rest of QMK). timer_read32() and timer_elapsed() are indirectly documented in the example in the QMK’s documentation’s Software Timers (and the unit can be inferred to be 1 ms (or at least approximately 1 ms)).

Dynamic macros, instead of being generated by macros at compile time

Those C macros (not to be confused with the keyboard macros) don’t really take parameters and it is impossible to use a set of highly factored functions for macros. For example, using a function, key_ShiftAltAction(), to only be called with the single letter, ‘c, for Shift + Alt + C, instead of having to manually expand it out using the C macros:

SEND_STRING(KEY_SHIFT_ALT_ACTION(X_C))

Or in other words, the key code, ‘X_C’, can not be contained in a variable when using the C macros. It must be known at compile time (as a constant).

Using send_string_with_delay()

The parameters are not documented at all, but they can be deduced by looking at how dynamic macros are implemented by Via (function dynamic_keymap_macro_send() in file dynamic_keymap.c):

Place a printf statement at the end of the function, just before dynamic_keymap_macro_send() calls send_string_with_delay():

printf("\nAbout to call send_string_with_delay() in dynamic_keymap_macro_send()... Data: 0: <%d>. 1: <%d>. 2: <%d>. 3: <%d>. 4: <%d>. 5: <%d> \n", data[0], data[1], data[2], data[3], data[4], data[5]);

From the command line on the host system, use ‘qmk console’ to capture the output from the calls of printf(). Or use HID Listen for fewer dependencies (e.g., to make it work on older versions of Ubuntu)—though it does require recompiling from source on (64 bit) Linux (much less scary than it sounds).

This will dump the values of the binary string when a Via macro is executed. Note that not all 10 values may be used (thus the last ones may be undefined (can have arbitrary values at anyone time)).

Executing a Via macro with known content makes it fairly obvious what the protocol is.

Example of using send_string_with_delay()

Here is sample output from the printf statement for two calls of send_string_with_delay(). The values are all decimal (not hexadecimal).

Data: 0: <1>. 1: <2>. 2: <79>. 3: <0>

Data: 0: <1>. 1: <4>. 2: <49>. 3: <55>. 4: <124>. 5: <0>

They all start with the (binary) value 1 (the value of the preprocessor symbol SS_QMK_PREFIX).

Second is an action code:

2: SS_DOWN_CODE
3: SS_UP_CODE
4: SS_DELAY_CODE

For 2, the key press, the 79 is the key code (binary). Note that it is a key code, not an ASCII value. The symbolic keycode is KC_RIGHT (for right arrow key), alias KC_RGHT.

For 4, the delay is an ASCII number, not binary. In this example, for a delay of 17 ms:

49 is ASCII “1”
55 is ASCII “7”

The delay is terminated by 124 (ASCII “|”).

The whole (binary) string is variable length and is null-terminated, like a regular (text) string. But it is not a printable (ASCII) string as it contains values less than 32 (decimal).

Idle time processing (for asynchronous execution of macros)

The function housekeeping_task_user() is called very frequently, but timer_read32(), which is really what is called a tick count on other systems (it probably fundamentally is a tick count, which happens to have been configured under QMK to have a period of exactly or close to 1 millisecond), can be used to only do real work in a fraction of the calls, say every 5 ms. (timer_read32() is declared in /platforms/timer.h and defined in /platforms/chibios/timer.c (note: in sub directory “chibios” for ARM-based keyboards).)

The unit for timer_read32() is probably milliseconds (empirically it is; I measured 1000.7 ticks/sec with a stopwatch (over approximately 4 minutes) and exactly 1000 is probably within the measurement error. It has probably been configured to be as close to 1000 as possibly in the QMK configuration of ChibiOS/RT. It could even nominally be exactly 1000). As the return type of timer_read32() is uint32_t, it overruns after about 50 days, so that is usually not a problem for this particular application. And its granularity is about 1 ms (about the same as the unit; other tick counters on other systems have a granularity of 17 ms (corresponding the PC tick rate of 60 Hz). That is, with a unit of 1 ms and a granularity of 16.6 ms, the count increases by 16 or 17, never just 1.). But note that code should never assume it always increases by 1; it should test for greater than 0. For instance, if the granularity is 1.2 ms, every about fifth time it will increase by 2.

With throttling by timer_read32(), a state machine for executing the macro can run from function housekeeping_task_user(), including implementing delays in macro execution. Those delays should not be implemented as busy waits, but instead immediately return during the macro delay phases, so a key press (to stop the macro) is not missed.

There is also the utility function timer_elapsed32() to compute differences from the (previous) return values of timer_read32().

Empirically, the base calling rate for housekeeping_task_user() on the Keychron V5 is about 1200 Hz (every about 0.8 ms), but every about 17 ms, the interval is much longer, 5 ms. This comes out as an average call rate of about 1000 Hz (about every millisecond). The 17 ms is probably not a coincidence; it corresponds to the basic PC tick counter of 60 Hz (16.666 ms).

Increased responsiveness

Note that for delays in macros it is not necessary to busy wait. While in a waiting phase, there can be a target tick count and a call to housekeeping_task_user() can immediately return if not enough time has elapsed (so that the keyboard can reliably detect the user pressing a key to cancel the macro in progress).

Related: Repeating macros

That is, macro keys, after some (optional) delay, repeat if held down, just like any other key. This does not happen in QMK (as macros in QMK are really a convention, not a real feature (though some C macros are provided to support them)).

The code can keep track of the press/release state of the macro key by intercepting key presses and key releases in process_record_user(). Or is there a QMK function for this so it is not necessary with explicit code for keeping track?

Based on the press/release state of the macro, the macro execution engine can simply just jump to the start state of the macro if the key is still held down (instead of going to the end state if the macro key has been released).

Or in other words, implementing repeating macros is almost trivial. Though for very short macros, there should be an upper limit on the repeat rate (like for normal repeating keys). And some delay before it starts repeating, again like for normal repeating keys.

Leave a Reply

Your email address will not be published. Required fields are marked *

*