{"id":4179,"date":"2023-07-31T13:56:16","date_gmt":"2023-07-31T12:56:16","guid":{"rendered":"https:\/\/pmortensen.eu\/world2\/?p=4179"},"modified":"2026-07-25T10:35:41","modified_gmt":"2026-07-25T09:35:41","slug":"the-basis-for-a-way-to-cancel-qmk-macros-in-progress","status":"publish","type":"post","link":"https:\/\/pmortensen.eu\/world2\/2023\/07\/31\/the-basis-for-a-way-to-cancel-qmk-macros-in-progress\/","title":{"rendered":"The basis for a way to cancel QMK macros in progress"},"content":{"rendered":"<p>The short version: Use <a href=\"https:\/\/docs.qmk.fm\/#\/faq_general?id=what-is-qmk\">QMK<\/a>&#8216;s <strong><em>send_string_with_delay()<\/em><\/strong> with binary content (as it is not a real string) encoded in a particular manner to piecemeal execute a macro, <strong><em><a href=\"https:\/\/github.com\/qmk\/qmk_firmware\/blob\/master\/docs\/custom_quantum_functions.md#keyboard-housekeeping\">housekeeping_task_user()<\/a><\/em><\/strong> to run a state machine for executing the macro (which can check for a key press that should cancel), and <strong><em><a href=\"https:\/\/github.com\/qmk\/qmk_firmware\/blob\/master\/platforms\/timer.h#L42\">timer_read32()<\/a><\/em><\/strong> to get the tick count (for timing things and throttling to avoid interfering with the rest of QMK).<\/p>\n<p><strong><em>timer_read()<\/em><\/strong> and <strong><em>timer_elapsed()<\/em><\/strong> are indirectly documented in the example in the QMK&#8217;s documentation&#8217;s <em><a href=\"https:\/\/docs.qmk.fm\/ref_functions#software-timers\">Software Timers<\/a><\/em> (and the unit can be inferred to be 1 ms (or at least approximately 1 ms)), though that is for the 16-bit versions and overflows after about 65 seconds. Probably <strong><em>timer_read32()<\/em><\/strong> and <strong><em>timer_elapsed32()<\/em><\/strong> are to be used instead. While a macro may not take more than 65 seconds to execute, the value from timer_read() will overflow every 65 seconds, and this may not be handled properly.<\/p>\n<h2>Dynamic macros, instead of being generated by macros at compile time<\/h2>\n<p>Those C macros (not to be confused with the keyboard macros) don&#8217;t really take parameters and it is impossible to use a set of highly factored functions for macros. For example, using a function, key_<em>ShiftAltAction()<\/em>, to only be called with the single letter, &#8216;c, for <em>Shift<\/em> + <em>Alt<\/em> + <em>C<\/em>, instead of having to manually expand it out using the C macros:<\/p>\n<p>SEND_STRING(KEY_SHIFT_ALT_ACTION(X_C))<\/p>\n<p>Or in other words, the key code, &#8216;X_C&#8217;, <strong><em>can not be contained in a variable<\/em><\/strong> when using the C macros. It must be known at compile time (as a constant).<\/p>\n<p><!-- <em>send_string_with_delay()<\/em>, with an essential binary protocol. XXXXX. --><\/p>\n<h3 id=\"send_string_with_delay\">Using send_string_with_delay()<\/h3>\n<p>The parameters are not documented at all, but they can be deduced by looking at how dynamic macros are implemented by <a href=\"https:\/\/pmortensen.eu\/world2\/2023\/07\/10\/via-macros-on-mechanical-keyboards-with-qmk-are-less-crippled-than-expected\/\">Via<\/a> (function <a href=\"https:\/\/github.com\/qmk\/qmk_firmware\/blob\/master\/quantum\/dynamic_keymap.c#L257\">dynamic_keymap_macro_send()<\/a> in file <a href=\"https:\/\/github.com\/qmk\/qmk_firmware\/blob\/master\/quantum\/dynamic_keymap.c\">dynamic_keymap.c<\/a>):<\/p>\n<p>Place a <a href=\"https:\/\/pmortensen.eu\/world2\/2023\/09\/11\/qmk-debugging-blues-on-ubuntu\/\">printf<\/a> statement at the end of the function, just before <a href=\"https:\/\/github.com\/qmk\/qmk_firmware\/blob\/d52bafade333d2da76c514f2a152b38b3a446c1c\/quantum\/dynamic_keymap.c#L344C9-L344C31\"><em>dynamic_keymap_macro_send()<\/em> calls <em>send_string_with_delay()<\/em><\/a>:<\/p>\n<pre class=\"brush: perl; gutter: false; title: ; notranslate\" title=\"\">\r\nprintf(&quot;\\nAbout to call send_string_with_delay() in dynamic_keymap_macro_send()... Data: 0: &lt;%d&gt;. 1: &lt;%d&gt;. 2: &lt;%d&gt;. 3: &lt;%d&gt;. 4: &lt;%d&gt;. 5: &lt;%d&gt; \\n&quot;, data[0], data[1], data[2], data[3], data[4], data[5]);\r\n<\/pre>\n<p>From the command line on the host system, use &#8216;qmk console&#8217; to capture the output from the calls of <em>printf()<\/em>. Or use <a href=\"https:\/\/www.pjrc.com\/teensy\/hid_listen.html\">HID Listen<\/a> for fewer dependencies (e.g., to make it work on older versions of Ubuntu)&mdash;though it does require <a href=\"https:\/\/pmortensen.eu\/world2\/2023\/09\/11\/qmk-debugging-blues-on-ubuntu#CompileFromSource\">recompiling from source<\/a> on (64 bit) Linux (<strong><em>much less<\/em><\/strong> scary than it sounds).<\/p>\n<p>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 <em>undefined<\/em> (can have arbitrary values at anyone time)).<\/p>\n<p>Executing a Via macro with known content makes it fairly obvious what the protocol is.<\/p>\n<h3 id=\"ViaMacroBinaryFormat\">Example of using <em>send_string_with_delay()<\/em><\/h3>\n<p>Here is sample output from the printf statement for two calls of <em>send_string_with_delay()<\/em>. The values are all decimal (not hexadecimal).<\/p>\n<p>Data: 0: <1>. 1: <2>. 2: <79>. 3: <0><\/p>\n<p>Data: 0: <1>. 1: <4>. 2: <49>. 3: <55>. 4: <124>. 5: <0><\/p>\n<p>They all start with the (binary) value 1 (the value of the preprocessor symbol SS_QMK_PREFIX).<\/p>\n<p>Second is an action code (key press, key release, and delay, respectively):<\/p>\n<p>2: SS_DOWN_CODE<br \/>\n3: SS_UP_CODE<br \/>\n4: SS_DELAY_CODE<\/p>\n<p>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 <a href=\"https:\/\/en.wikipedia.org\/wiki\/Arrow_keys\">right arrow key<\/a>), alias KC_RGHT.<\/p>\n<p>For 4, the delay is an ASCII number, not binary. In this example, for a delay of 17&nbsp;ms:<\/p>\n<p>  49 is ASCII &#8220;1&#8221;<br \/>\n  55 is ASCII &#8220;7&#8221;<\/p>\n<p>The delay is terminated by 124 (ASCII &#8220;|&#8221;).<\/p>\n<p>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).<\/p>\n<h2 id=\"tickCount\">Idle time processing (for asynchronous execution of macros)<\/h2>\n<p>The function <em>housekeeping_task_user()<\/em> is called very frequently, but <em><a href=\"https:\/\/github.com\/qmk\/qmk_firmware\/blob\/master\/platforms\/timer.h#L42\">timer_read32()<\/a><\/em>, which is really what is called a tick count on other systems (it probably fundamentally <strong><em>is<\/em><\/strong> 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&nbsp;ms. (<em>timer_read32()<\/em> is declared in <em><a href=\"https:\/\/github.com\/qmk\/qmk_firmware\/blob\/master\/platforms\/timer.h#L44\">\/platforms\/timer.h<\/a><\/em> (return type <em>uint32_t<\/em>) and defined in <em><a href=\"https:\/\/github.com\/qmk\/qmk_firmware\/blob\/master\/platforms\/chibios\/timer.c#L80\">\/platforms\/chibios\/timer.c<\/a><\/em> (note: in sub directory &#8220;chibios&#8221; for ARM-based keyboards).)<\/p>\n<p>The unit for <em>timer_read32()<\/em> is probably milliseconds (<strong><em>empirically<\/em><\/strong> 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 <a href=\"https:\/\/en.wikipedia.org\/wiki\/ChibiOS\/RT\">ChibiOS\/RT<\/a>. It could even nominally be exactly 1000). As the return type of <em>timer_read32()<\/em> is <em>uint32_t<\/em>, it overruns after about 50 days, so that is usually not a problem for this particular application. And its granularity is about 1&nbsp;ms (about the same as the unit; other tick counters on other systems have a granularity of 17&nbsp;ms (corresponding the PC tick rate of 60&nbsp;Hz). That is, with a unit of 1&nbsp;ms and a granularity of 16.6&nbsp;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.<\/p>\n<p>With throttling by <em>timer_read32()<\/em>, a state machine for executing the macro can run from function <em>housekeeping_task_user()<\/em>, 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.<\/p>\n<p><!-- We can use a simple timer to schedule things (see 4.). --><\/p>\n<p>There is also the utility function <em>timer_elapsed32()<\/em> to compute differences from the (previous) return values of <em>timer_read32()<\/em>.<\/p>\n<p><!-- Besides using it for timing the macros --><\/p>\n<p>Empirically, the base calling rate for <em>housekeeping_task_user()<\/em> on the <a href=\"https:\/\/pmortensen.eu\/world2\/2023\/06\/19\/keychron-v5-a-reasonably-priced-fully-macro-capable-qmk-based-mechanical-keyboard\/\">Keychron V5<\/a> is about 1200&nbsp;Hz (every about 0.8&nbsp;ms), but every about 17&nbsp;ms, the interval is much longer, 5&nbsp;ms. This comes out as an <strong><em>average<\/em><\/strong> call rate of about 1000&nbsp;Hz (about every millisecond). The 17&nbsp;ms is probably not a coincidence; it corresponds to the basic PC tick counter of 60&nbsp;Hz (16.666&nbsp;ms). <\/p>\n<h3>Increased responsiveness<\/h3>\n<p>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). <\/p>\n<h2>Related: Repeating macros<\/h2>\n<p>That is, macro keys, after some (optional) delay, repeat if held down, just like any other key. This does <strong><em>not<\/em><\/strong> happen in QMK (as macros in QMK are really a convention, not a real feature (though some C macros are provided to support them)).<\/p>\n<p>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?<\/p>\n<p>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).<\/p>\n<p>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.<\/p>\n<p><!-- ========================== E N D ========================== --><\/p>\n<p><!-- \n  Referred to in:\n\n    <https:\/\/www.reddit.com\/r\/Keychron\/comments\/198f98y\/keyrate_speeds_for_delete_and_backspace_keys\/>\n--><\/p>\n<p><!-- This may even prevent missing --><\/p>\n<p><!-- \n\n\n<h2>Appendix A: Spying on send_string_with_delay()<\/h2>\n\n\n\nXXX printf\n--><\/p>\n<p><!-- Internal refs: ID 12622  --><\/p>\n<p><!-- T H E   E N D . . . --><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The short version: Use QMK&#8216;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 &hellip;<\/p>\n<p class=\"read-more\"> <a class=\"more-link\" href=\"https:\/\/pmortensen.eu\/world2\/2023\/07\/31\/the-basis-for-a-way-to-cancel-qmk-macros-in-progress\/\"> <span class=\"screen-reader-text\">The basis for a way to cancel QMK macros in progress<\/span> Read More &raquo;<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[4,28,26,35,16,38],"tags":[],"_links":{"self":[{"href":"https:\/\/pmortensen.eu\/world2\/wp-json\/wp\/v2\/posts\/4179"}],"collection":[{"href":"https:\/\/pmortensen.eu\/world2\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/pmortensen.eu\/world2\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/pmortensen.eu\/world2\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/pmortensen.eu\/world2\/wp-json\/wp\/v2\/comments?post=4179"}],"version-history":[{"count":116,"href":"https:\/\/pmortensen.eu\/world2\/wp-json\/wp\/v2\/posts\/4179\/revisions"}],"predecessor-version":[{"id":6684,"href":"https:\/\/pmortensen.eu\/world2\/wp-json\/wp\/v2\/posts\/4179\/revisions\/6684"}],"wp:attachment":[{"href":"https:\/\/pmortensen.eu\/world2\/wp-json\/wp\/v2\/media?parent=4179"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pmortensen.eu\/world2\/wp-json\/wp\/v2\/categories?post=4179"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pmortensen.eu\/world2\/wp-json\/wp\/v2\/tags?post=4179"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}