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 keypress that should cancel), and timer_read32() to get the tick count (for timing things and throttling to avoid interfering with the rest of QMK).
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().
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.
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, can be used to only do real work in a fraction of the calls, say every 5 ms.
The unit for timer_read32() is milliseconds. 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 macros in progress).