New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Macros #685
Macros #685
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few questions. Thanks!
app/src/behaviors/behavior_macro.c
Outdated
break; | ||
} | ||
behavior_keymap_binding_pressed(&cfg->behaviors[index], event); | ||
k_msleep(cfg->sleep); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This theoretically ties up the system work queue/thread, especially for longer macros.. how well did this work in real life for longer ones? Dropped messages for the HOG queue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tested it on long macros nor in real life.
These macros will not permit any other keyboard interaction until they have finished executing. If that means some queues get full, I suggest people increase their queues as a workaround?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a more robust solution for longer macros would be for to use delayed work items. https://docs.zephyrproject.org/1.9.0/kernel/threads/workqueues.html#submitting-a-work-item
I think the main changes here would be:
k_delayed_work_init
a new item per macro. As shown in the link above, you can attach some state to the work item and give it thebehavior_macro_config*
and current index.- Pull the code inside the loop out into the work item handler function.
- To start the macro, reset its index to 0 and
k_delayed_work_submit
the work item (probably with 0 delay). - Replace
k_msleep
withk_delayed_work_submit
and continue to submit the same item until everything in the macro has been run.
This would allow for other keyboard interaction while a macro is running though, which might or might not be what you want (possibly on a per-macro basis?).
If we wanted to support both short macros that tie up the whole keyboard until they're done (to prevent input from conflicting with the macro) and longer macros that do not, we could add something like an exclusive
property to the macro which selects between loop and sleep (exclusive) and work queue (non exclusive) execution.
app/src/behaviors/behavior_macro.c
Outdated
break; | ||
} | ||
behavior_keymap_binding_pressed(&cfg->behaviors[index], event); | ||
k_msleep(cfg->sleep); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a more robust solution for longer macros would be for to use delayed work items. https://docs.zephyrproject.org/1.9.0/kernel/threads/workqueues.html#submitting-a-work-item
I think the main changes here would be:
k_delayed_work_init
a new item per macro. As shown in the link above, you can attach some state to the work item and give it thebehavior_macro_config*
and current index.- Pull the code inside the loop out into the work item handler function.
- To start the macro, reset its index to 0 and
k_delayed_work_submit
the work item (probably with 0 delay). - Replace
k_msleep
withk_delayed_work_submit
and continue to submit the same item until everything in the macro has been run.
This would allow for other keyboard interaction while a macro is running though, which might or might not be what you want (possibly on a per-macro basis?).
If we wanted to support both short macros that tie up the whole keyboard until they're done (to prevent input from conflicting with the macro) and longer macros that do not, we could add something like an exclusive
property to the macro which selects between loop and sleep (exclusive) and work queue (non exclusive) execution.
|
||
include: zero_param.yaml | ||
|
||
properties: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this system can only press and release keys in order on key up/down or press all the keys in order and hold them until you release the key?
Just going by the macro systems I've seen in other PC software, I'm not sure this will do everything people would want to do with it. (Either that or I don't understand how it works and this needs better documentation.)
The main actions I usually see in macro systems are:
- Press key
- Release key
- Press and release key
- Delay for arbitrary time
It would be nice to be able to write things like
// This macro probably isn't useful, but something similar to it might be.
// Alt+tab forward two windows, then dock the window to the left.
bindings =
<&down LALT>,
<&kp TAB>,
<&kp TAB>,
<&up LALT>,
<&kp LGUI(LEFT)>;
for scenarios where you want to keep one key held while pressing others.
Manually controlling the delays between items could be especially useful in macros for games. For example:
// Automate something I'm too bad at this game to do.
sleep = 0,
bindings =
<&down DOWN>, <&wait 17>,
<&down RIGHT>, <&wait 17>,
<&up DOWN>, <&wait 17>,
<&up RIGHT>, <&down Z>, <&wait 33>,
<&up Z>, <&wait 134>,
<&down X>, <&down C>, <&wait 33>,
<&up X>, <&up C>;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great idea. We can actually implement &down
and &up
and &wait
independently from this PR.
I agree we should remove the delay that now exists between separate keypresses so people can set up their own &waits
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More macro ideas:
- repeat boolean. If it's on, the macro repeats forever while it's held down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you do remove the delay between key presses, note that &kp
will probably still need a delay between press and release. Maybe there should still be a configurable duration for &kp
and then anything else you do with wait commands?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After thinking about this some more, having a special case to put a delay between press and release for just &kp
seems weird. What if instead we added a duration value to each item, and then made macro execution work like this?
- Press item
- Wait item's duration
- Release item
- Move to next item and immediately return to step 1
Then for a delay between items you could just use &none
with the duration you want to delay by.
I need to read up more on how Zephyr handles lists of DT things, so this might not be possible or there might be a better way to do it, but maybe we could add a special binding which the macro code looks for, and instead of pressing/releasing it, it uses its parameter to determine how long the following item should be held? If an item is not preceded by a duration, use something like the sleep
property as a default (and default that property to 0).
default-duration = <10>;
bindings =
<&for 0 &down LALT>,
<&kp TAB>,
<&for 30 &none>.
<&kp TAB>,
<&for 0 &up LALT>,
<&kp LGUI(LEFT)>;
Alternatively (and much more awkwardly), we could have parallel arrays of bindings and durations:
bindings = <&down LALT>, <kp TAB>, <&none>, <&kp TAB>, <&up LALT>, <&kp LGUI(LEFT)>;
duration = <0>, <10>, <30>, <10>, <0>, <10>;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see an issue with making a special case for &press and &release. &wait could be interpreted by the macro code to introduce a delayed work item, it doesn't have to sleep and block the main thread.
Devicetree doesn't allow nesting like &one &two
; you can't give references as arguments. A &for
as you suggested could be created (the syntax would even work), but it would have to be interpreted by the macro to override the next keypress-duration
.
(this is a response to joelspadin)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Devicetree doesn't allow nesting like
&one &two
; you can't give references as arguments.
Macros are parameterless behaviors. It was tested here: https://github.com/BrainWart/zmk-kyria-keymap/blob/simple-macro/config/kyria.keymap
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BrainWart I like the idea. I think we can achieve the same without having to resort to a special keymp syntax just for macros:
gaming-macro? {
macro = <&press DOWN>, <&wait 17>,
<&press RIGHT>, <&wait 17>,
<&release DOWN>, <&wait 17>,
<&release RIGHT>, <&press Z>, <&wait 33>,
<&release Z>, <&wait 134>,
<&press X>, <&press C>, <&wait 33>,
<&release X>, <&release C>;
}
This could work just fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I correctly understand that you mean &press
, &release
, and &wait
would be special things interpreted only by macros and not regular behaviors that you could also stick into a keymap, I think that would work well. You could also C macro your ZMK macros to get something like &kp
:
#define MACRO_KP(key, duration) <&press (key)>, <&wait (duration)>, <&release (key)>
That does mean that you wouldn't be able to do things like put an RGB underglow command in the middle of a macro though, but I'm not sure how useful that would be.
The thing I mainly didn't like about the previous proposal was that it was taking regular keymap behaviors but then needing to layer special macro things on top of every behavior. e.g. &kp
should be pressed, wait for X milliseconds, then released, &press
and &release
should just be pressed (and maybe immediately released?) and continue with no delay, &wait
isn't pressed at all, and I have no idea what should happen if you use a &mt
...
I vote for either:
- Macros use their own set of commands that are independent from keymap behaviors.
- Macros use keymap behaviors, but we always execute each behavior the same way (press, wait, release) and have some means to control how long each one is held.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the strenghts of ZMK is the flexible keymap system using behaviors and devicetree bindings. Whatever we do with macros, we should support any and all behaviors from macros. This means option 1 (macros use their own set of commands) is a no-go in my opinion.
In option 2 you state that 'we always execute each behavior the same way (press, wait, release)'. I think we can make that work with an additional rule: wait is only executed when a behavior defines both the 'press' and 'release' bindings. Consider &press
which is a behavior that only has a "press" binding, and &release
, a behavior that only defines a "release" binding. This allows the user to fully in control timing when using &press
and &release
, and also any new (user-defined) behaviors in the future.
The &wait
must be a little special, when the macro encounters a &wait
it should not call this behavior but only execute a "wait" with the specified delay. I don't see any way to solve this wait within a behavior without blocking the main thread.
to summarize, I think we should:
- leverage the power of the devicetree in macros, which lets us use all existing behaviors
- add behaviors like
&press
and&release
which could work inside and outside of macros - add a behavior like
&wait
which is a no-op in a normal keymap but introduces a wait inside a macro
(please don't reply to this message in this PR but move the conversation to #689)
I'm a big fan of all the ideas for the macros. To keep a bit of velocity I'd like to keep the scope of this PR limited to the current functionality. I propose to track the suggestions in a new ticket and want to invite others to build on this base. :) |
If noone has major issues with this (v1!) PR I'd like to get it merged. Otherwise I'll close this PR and make room for someone else to implement macros. |
app/src/behaviors/behavior_macro.c
Outdated
struct zmk_behavior_binding_event event) { | ||
const struct device *dev = device_get_binding(binding->behavior_dev); | ||
const struct behavior_macro_config *cfg = dev->config; | ||
if (cfg->mode == MODE_KEY_DOWN) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I was first reviewing this, it was difficult for me to understand what it was doing and how it worked. I think that is mainly because of the way it does a completely different thing between hold and up/down modes.
It feels like these are conceptually two different things, and the only code they really share is the DT binding handling. I wonder if it would make more sense to split them into separate behaviors?
- macro: Press and release a bunch of things in sequence when I press a key (with option to trigger on release instead)
- multi(?): Press a bunch of things (optionally staggered with a delay) and hold them until I release the key, then release them in the same order
#689 seems like it would extend/replace the first but not the second.
app/src/behaviors/behavior_macro.c
Outdated
break; | ||
} | ||
behavior_keymap_binding_released(&cfg->behaviors[index], event); | ||
k_msleep(cfg->sleep); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason we need to put delays between releasing the keys here? I can understand why you'd want to put a delay to ensure things are pressed in the same order, but I can't think of any reason the releases would need to be staggered.
break; | ||
} | ||
behavior_keymap_binding_pressed(&cfg->behaviors[index], event); | ||
k_msleep(cfg->sleep); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the event that you set sleep = 0
to press multiple things at the same time, I don't think we should call k_msleep. A sleep of 0 may be treated as a yield (see zephyr/kernel/sched.c z_tick_sleep()
)
The only issue I see here is it feels like you're implementing two different concepts with a single behavior. See #685 (comment) (Would also be nice to avoid calling k_msleep() if the sleep time is 0, since that is not a no-op.) |
I've simplified the macros a whole lot, it's easier to add something in the future than to remove something. Now we won't be able to keep two keys pressed with one key, so if anyone wants that they'll have to think about how to extend macros properly. |
Agreed.
That seems reasonable to me. From what I know of "macros" implemented in other programs and the few requests I've seen on Discord, I feel like the main use case for macros is pressing a sequence of things in order. Will try to review this soon. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll let @petejohanson give the final approval as to whether using sleeps and tying up the main thread is okay, but this looks good to me.
bindings: | ||
type: phandle-array | ||
required: true | ||
sleep: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delay
or delay-ms
(to match the other places where things have a -ms suffix) might be easier to understand. The code is sleeping the thread for that long, but that's an implementation detail.
This makes use of the functionality in the following PR: zmkfirmware/zmk#685
Successfully tested on a Kyria, using macros to output unicode. |
@yannickjmt, I believe this would be due to #241. |
Will this be merged in near future? |
Testing this on a Sweep using combos to call the macros. I found one issue. On keys with mod-taps (home row mods) the combos don't work. I've got lots of other combos on mod-taps that work fine, but the ones with combos output the tapped keys and not the macros. The combos with macros on none mod-taps work as expected. I verified that moving a macro combo that doesn't work on mod-tap keys to a none mod-tap keys works as expected. I had to test with this branch merged w/ main locally because I'm using nice!nano v2 and needed main for that support since it was merged recently. |
I tested a macro for my mail address. This works perfectly over USB, but when I try it over BT it stops at the second character and repeats it a couple times. The longer the macro gets, the longer it repeats the second character. For example: USB: The following warning appears, only when I use BT: |
I wonder if the repeated keys might be a result of macros tying up the main thread instead of allowing it to process things during delays. Are there any important tasks for bluetooth that run on the main thread? |
Some thoughts about the future of this PR are in the macros ticket: #689 (comment) |
Closing this, since we merged an updated version in #1166 |
This builds on #170.
I think "macro" is a better name than "simple macro", refactored the code and updated to the latest ZMK API.