Skip to content
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

Add event trace_id to fix bug in sticky keys and simplify hold-taps #670

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

okke-formsma
Copy link
Collaborator

@okke-formsma okke-formsma commented Feb 7, 2021

The 'event trace_id' is a unique identifier that identifies the keypress-keyrelease combination. It can be used to identify which position-state-changed events, behavior calls, and keycode-state-changed events were the result of which keypress (and which release).

The main benefactor of this functionality is sticky keys, where just a position & layer ID combination is not enough to correctly identify a behavior invocation.

Additionally, this is useful for hold-taps, where keycode events generated by the behavior itself must be ignored. So far this was solved with a clunky workaround.

This fixes #508

@okke-formsma okke-formsma force-pushed the event-trace branch 2 times, most recently from 013d793 to 774f95b Compare February 7, 2021 20:13
Copy link
Collaborator

@joelspadin joelspadin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to bed now. Will take a look at the rest of this later.

uint32_t zmk_event_trace_ids[ZMK_KEYMAP_LEN] = {0};

uint32_t zmk_event_trace_id(uint32_t position, bool generate) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add "get" somewhere in the name? Having a verb somewhere in the name of a function usually makes it easier to understand what it's doing (unless you mean for "trace" to be the verb, but I'm assuming not given that it's used in variable names too).

Also, I'm generally not a fan of boolean parameters since in code like

zmk_event_trace_id(position, TRUE);

you have no idea what the 2nd parameter does without looking at the definition of the function. That said, it is convenient for the couple places this is being called, though if it starts getting used elsewhere you may run into the same readability problem.

Consider something like this, which is more verbose but tells you exactly what it's doing:

uint32_t zmk_event_trace_id_get(uint32_t position) {
  return zmk_event_trace_ids[position];
}
uint32_t zmk_event_trace_id_create(uint32_t position) {
  zmk_last_event_trace_id++;
  zmk_event_trace_ids[position] = zmk_last_event_trace_id;
  return zmk_event_trace_id_get(position);
}

...
event = {
  .trace_id = pressed ? zmk_event_trace_id_create(position) : zmk_event_trace_id_get(position),
};

Alternatively:

uint32_t zmk_event_trace_id_get(uint32_t position) {
  return zmk_event_trace_ids[position];
}
void zmk_event_trace_id_update(uint32_t position) {
  zmk_last_event_trace_id++;
  zmk_event_trace_ids[position] = zmk_last_event_trace_id;
}

...
if (pressed) {
  zmk_event_trace_id_update(position);
}
event = {
  .trace_id = zmk_event_trace_id_get(position),
};

Copy link
Collaborator Author

@okke-formsma okke-formsma Feb 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your extensive reply! I've added a commit that implements the create/get version. I like how the intent is clearer when calling with a hardcoded value.

On the other hand this solution exposes an implementation detail of trace_ids to the caller, by making the caller responsible for deciding if a new trace_id should be created or an existing one retrieved. Having this decision within the trace_id method puts the responsibility in place in my opinion. (Maybe 'press' would be a better name for the parameter than 'generate', so the intent is clearer).

The update/get variety may be the best middle ground between the two.

I see arguments for each option, I'm not quite sure what the best one is yet.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand this solution exposes an implementation detail of trace_ids to the caller, by making the caller responsible for deciding if a new trace_id should be created or an existing one retrieved.

I'd argue that the caller is still responsible for deciding that in the version that takes a boolean parameter. You just one have one function that does one of two things based on a parameter instead of two functions that each do one thing.

If you want to hide away the fact that new trace IDs get generated on key press, then renaming the parameter to something like is_press (like you mentioned) would be better, though it would still be difficult to understand if any later code ends up needing to pass just TRUE or FALSE for that parameter. You could also rename the "update" function to something like zmk_event_trace_id_notify_pressed(position) (though then it's not super clear if you're notifying the trace ID system of a press, as intended, or if you want to send a notification). Naming things is hard.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've decided to go with zmk_get_event_trace_id(uint32_t position, bool pressed), as I think it's the clearest code for now. I don't expect other call sites that will use literal booleans, and if that will be the case we can always refactor this method.

With the trace_id it's possible for behaviors to keep track of which events were caused by which trigger.
The trace_id can easily be used to figure out if a key was pressed
before the hold-tap was pressed without searching through all events
that were captured.
Copy link
Collaborator

@joelspadin joelspadin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with the problem being solved here, so I'll let Pete give the final approval, but this looks reasonable to me.

if (!ev->state && find_captured_keydown_event(ev->position) == NULL) {
// no keydown event has been captured, let it bubble.
// we'll catch modifiers later in modifier_state_changed_listener
if (ev->trace_id < undecided_hold_tap->trace_id) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that after 4 billion key presses, the trace ID will eventually wrap and this will temporarily break. That's also 13 years straight of hitting ten keys a second, so it's probably not something to worry about, but here's a solution that always works, even if it wraps (so long as you don't manage to press 2 billion keys between the times the two trace IDs are generated):

if ((int32_t)(ev->trace_id - undecided_hold_tap->trace_id) < 0) {

https://godbolt.org/z/cjs41G

All that said, it looks like you picked max uint32 as a special value below, so you'd need other changes for things to work if the number wraps. Probably not worth the effort.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I made a similar calculation and decided that if someone hits this bug they're due for some undefined behavior.

This fixes a bug with overlapping sticky keys when the same key
positions on multiple layers contain different sticky keys.

This also fixes the case when a MT has a sticky key for hold and a
different sticky key for the tap behavior.

Fixes zmkfirmware#508
@okke-formsma okke-formsma added behaviors bug Something isn't working core Core functionality/behavior of ZMK labels Feb 28, 2021
@dxmh dxmh mentioned this pull request Jun 10, 2021
@okke-formsma
Copy link
Collaborator Author

@petejohanson could you have a look at this?

Copy link
Contributor

@petejohanson petejohanson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to consider this more, but I think the trace ID isn't really part of every payload, but part of some "event context" instead, and should be pervasive, and surfaced in the general event manager API, not hidden in some event payloads.

Thoughts?

@okke-formsma
Copy link
Collaborator Author

I need to consider this more, but I think the trace ID isn't really part of every payload, but part of some "event context" instead, and should be pervasive, and surfaced in the general event manager API, not hidden in some event payloads.

I'm not sure if it's applicable to many events outside the position, (future) behavior and keypress events. If we'd put the trace ID on the zmk_event_t, that would complicate construction for all events without clear benefit. Also, pointers to zmk_event_t would need to be kept around instead of pointers to the events themselves (e.g. zmk_position_state_changed), adding a bit of complexity too. So, at least for now, I'd opt to keep the scope limited.

@@ -18,7 +20,7 @@ int zmk_keymap_layer_toggle(uint8_t layer);
int zmk_keymap_layer_to(uint8_t layer);
const char *zmk_keymap_layer_label(uint8_t layer);

int zmk_keymap_position_state_changed(uint32_t position, bool pressed, int64_t timestamp);
int zmk_keymap_position_state_changed(const struct zmk_position_state_changed *pos_ev);
Copy link
Collaborator Author

@okke-formsma okke-formsma Jun 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example of a function signature that would become more complex if we'd keep a zmk_event_t around instead of zmk_position_state_changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
behaviors bug Something isn't working core Core functionality/behavior of ZMK
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sticky keys: fix multiple sticky keys on the same key position active
3 participants