HRM/Secondary Role - Yet another way. With examples!

Well, now all of my changes are in the HRM branch. I don’t know if there is a way to get a build of it from Github to test it out.

I have mentioned somewhere that I was not too happy with the timeout behavior.

I have a radical idea which begins with adding a “No Action” time-out action. I want to be able to completely abandon pressing a key.

Then I want to change timeout behavior in radical ways which I suspect will have to stay in my own fork forever. :stuck_out_tongue:

2 Likes

Well, now all of my changes are in the HRM branch.

Cool!

Would you please provide recommended configuration?

I don’t know if there is a way to get a build of it from Github to test it out.

CI builds an artifact for every open PR. See Hrm · UltimateHackingKeyboard/firmware@cbf4daf · GitHub

I have a radical idea which begins with adding a “No Action” time-out action. I want to be able to completely abandon pressing a key.

Sounds fine, at least so far :blush:.

Then I want to change timeout behavior in radical ways which I suspect will have to stay in my own fork forever. :stuck_out_tongue:

That may be true, but please elaborate anyways :innocent:.

1 Like

Awesome, thanks!

I didn’t want to type it all out on my anemic phone keyboard/screen, so here goes from my PC:

Before getting into how I want everything to be different (again), let me just say that with the same-half primary, minimum hold time and doubletap postponed until timeout, UHK is in a great place for HRMs! I doubt anyone would find it lackluster.

I just have very specific desires.

The concept is:

I want activations to happen on actions, not on inaction.

I want secondary roles to only activate for key combos, not on their own.

Side note:

It is possible that the solution that I propose and intend to implement should not be a modification of the current timeout behavior, but a new concept. It could also be that it would be the behavior of the timeout NoOp action, with Primary or Secondary timeout behaving as they do now.

The motivation:

I find that i often press one or more mod keys, then think about what I want to do, then change my mind and release them again. If that was Alt, now I have a menu popping up, if it was Gui, i have the Start Menu or Rofi, and so on. This happens a lot more after I switched to HRMs when I don’t change my mind about pressing the modifiers, but I remember that the primary key of the combo is on one of the fingers which are holding mods. I didn’t change my mind, I just needed to use the other hand. Maybe I pressed a mod too many and release one of them. The others now trigger primary from same half. I want those cases to not trigger mod presses, but I also want to be able to hold for a while and make up my mind, then use them as mod keys even though I passed timeout.

What I intend to implement:

Reaching timeout will remove the primary behavior of the key and substitute the timeout behavior on release. The key will no longer be able to trigger primary, only secondary if something brings it into that state, or the timeout action if nothing triggers it as secondary before release. This means that with Primary from same half, same half will no longer trigger resolution, but will be ignored for the purpose of resolving the currently evaluated key, if the current key hold has passed timeout. Those same-half presses will still be evaluated on their own on queue unroll.

Double tap will still trigger primary after timeout as an exception. That is the purpose of double tap, and this is still consistent with action triggered resolution even if the resolution is delayed from the action to ascertain the intention of the action.

How I imagine it will play out:

In the following, all mod keys are alt role keys. If I mention pressing Ctrl, that is an alt role key with secondary role Ctrl. Timeout action is NoOp, and primary from same half is active.

While typing, everything behaves the same as does is now.

When I hold a key, it is always with the intention of performing a combo. If I wanted to hold primary, I would double tap. I very rarely want to press mod keys just for themselves, so this will not be part of “standard actions”. It will still be possible though, more on that later.

If I hold one key past the timeout and release, simple “press-release” primary behavior is overridden by timeout NoOp.

If I hold two mods through timeout and release them, they will ignore each other for resolution, and their “press-release” primary behavior is overridden by timeout NoOp.

If I hold two or more mods through timeout, then release one, that release will not trigger the other key(s) as primary, and will itself resolve as timeout NoOp. The others will still be valid for activating a combo with the other half of the keyboard. I can even still add keys to the combo as well.

If I ever want to actually hold one or more mods without a primary key, I have several options:

  • I can have dedicated mod keys for that
  • I can press mods on one half, then use an unbound key on the other half to resolve them to secondary.
  • I can press mods on one half and then hold a mod on the other half through timeout, then release that key on the other half. The first half mods will trigger secondary on the release from the other half, but the key used to trigger them will do nothing, essentially the same as the above case, without needing a separate unbound key.

Like I said, I think this is too specific for general users, and will likely have to stay on my own fork.

Forgot about that in my enthusiasm for the new features. Will post them when I get back to my P next.

1 Like

I use the following settings:

set secondaryRole.defaultStrategy advanced
set secondaryRole.advanced.triggerByRelease 1
set secondaryRole.advanced.timeout 1000
set secondaryRole.advanced.timeoutAction secondary
set secondaryRole.advanced.doubletapToPrimary 1
set secondaryRole.advanced.safetyMargin -100
set secondaryRole.advanced.triggerByMouse 1
set secondaryRole.advanced.minimumHoldTime 150
set secondaryRole.advanced.primaryFromSameHalf 1

I have Shift, Control, Alt and Gui on the home row on both sides, mirrored so the same key is on the same digit on each hand, and AltGr on the Alt digit on another row on both sides. I have Mod Layer hold on secondary roles on the thumbs with primaryFromSameHalfDisabled. I only have two layers, so that’s it for mod keys.

For the new behavior I want to make, another way to sum up my intentions is:

In general, I want primary to activate only if a key is tapped. In general, I want secondary to only activate if a full combo is pressed including a primary key (optionally only from the other half). If it’s not a tap or a full combo including a primary, I don’t want anything to happen. A NoOp key from an abandoned combo is considered a primary in order to facilitate those rare cases where I do want secondaries outside of combos.

2 Likes

Help! i have somehow bricked my keyboard. i am unable to flash to it, either through agent or through the build script. i have no idea how it got to this point. as far as i know, i did nothing differently from what i have been doing until now. agent does not see the keyboard at all, as far as i can see. i have tried the reset button on the outer edge of the right half. it shows up both on lsusb and on xinput list and works as expected otherwise, except that my current secondary role implementation is borked, hence the lack of capitalized letters.

Uhk60 or uhk80?

(Is it connected via usb?)

Have you tried to reboot it?

In case of uhk60, there is the unbricking procedure. In case of uhk80, try the reset button.

more info

i was developing on the hrm branch, flashing with the build script, with agent 8.0.1 open in the background for status buffer display.

on the update which apparently broke things, i got the following output

Updating right firmware from right/build/v1-release/uhk-60-right.hex ...
09:16:57.450 › [UhkHidDevice] Transfer error:  Error: Cannot write to hid device
09:16:57.451 › [DeviceService] Device connection state query error Error: Cannot write to hid device
Firmware updated.
Reenumerating device...

i have it connected directly to the pc now.

uhk60v1, tried the reset button, tried and so far failed the internal reset thing below right ctrl, although that might be mechanical failure, will try again.

trying a different pc now.

other pc can see it in agent. will try a reboot.

Ah, right.

Usb communication tends to get scrambled if two remotes are trying to communicate with the uhk at the same time.

When this happens, Agent restart and maybe uhk powercycling is needed. It is safer to kill Agent when communicating with uhk outside of Agent.

I had tried new Agent and power cycling the keyboard and everything. I think something went wonky on the OS level. Anyway I’m back in business. Thanks for the help!

1 Like

A quick test of mine says this works like a charm :-).

Just merged them into master, and eager to hear feedback from others!

Really thanks for taking over this and seeing it through :heart:.

3 Likes

No problem, it’s my pleasure to type on the result.

Like I said, I am working on an update to the timeout mechanics. I am pretty sure I have the NoOp option working well, but I have made some questionable decisions to get it to work which I would like to question you on.

One is that the boolean secondary on keystate to persist resolution while the queue unwinds naturally does not fit a trinary value of primary/secondary/noop. I have therefore moved the secondary_role_state_t enum to key_states.h because including secondary_role_driver.h in that header is not nice, and has circular dependencies (that header needs the key_state_t for it’s functions). I don’t think that’s a good place for that enum. Alternatives I can think of right now are, in no particular order:

  • Unsightly usage of precompiler statements to limit inclusion from secondary_role_driver.h to key_states.h to what is needed.
  • Forward declaration of key_state_t to allow otherwise unmodified inclusion of secondary_role_driver.h in key_states.h
  • A new, completely different header for that type
  • Declaring the variable as a char and then handling casting where it’s needed, which is only in secondary_role_driver.c

Also, is there a reason that enums are not just declared as char type across the board? I feel like there is a little RAM which could be reclaimed? Or does this compiler automatically choose a fitting type?

I also have questions about the event vector. Specifically, at the time when the NoOp timeout is determined, the EventVector_NativeActionReportsUsed is already set, which it is not normally done for non-bound keys. However, it does not seem to matter that it’s done for unresolved secondary keys as well, and it seems to work just fine for now. Anyway, every cycle sees it unset again. I could move that event vector set to after secondary role has been resolved. Should I do that?

Finally: You put my values in the User Guide? Had I known that, I would have put a different timeout. 1000 is pretty bonkers. It should be something like 300-500 for most users who are not me.

I don’t think that’s a good place for that enum.

It is not aesthethic for sure, but it is simple. I prefer it over more complicated solutions.

Making it a uint8_t secondary : 2; is acceptable too.

is there a reason that enums are not just declared as char type

I always assumed the compiler to choose a fitting type in this codebase, but I don’t remember ever checking it :thinking:. Probably should do so. Thanks for the headups!

Declaring the variable as a char

(We use uint8_t as an idiom for byte.)

I also have questions about the event vector. Specifically, at the time when the NoOp timeout is determined, the EventVector_NativeActionReportsUsed is already set, which it is not normally done for non-bound keys.

You mean the one in usb_report_updater.c’s ApplyKeyAction?

Please leave it as is. (Performance impact is negligible, code complexity impact would be probably huge.)

It leads to one recalculation of reports that is not necessary, that is all. It is no problem unless it triggers a loop.

Finally: You put my values in the User Guide? Had I known that, I would have put a different timeout. 1000 is pretty bonkers. It should be something like 300-500 for most users who are not me.

I should probably have consulted you. Sorry if I offended you by that.

You are welcome to tailor it, or let me know which values I should put there :wink: . (Eventually, we should figure out a better way to get the settings to the users - making them default in Agent, or adding something like a preset dropdown in Agent…)

Nono, no offense, the values are just not what I would recommend others use as “entry level settings”. I will adjust them a little.

I could also just unset it for this particular case.

Well, if I was maintaining a compiler for microprocessors, I think I would lean away from picking smaller types automatically as it might be a nasty surprise for the user if they add an enum value and suddenly run out of RAM, but that’s personal taste I guess.

That didn’t yet make it into firmware 16.0.0, did it?

Yes, it didn’t

No.

You would need to be sure that no one else has set it.

The reader would have to keep in mind scenario-specific unset calls when reasoning about the code.

Now, all “branches” of the algorithm flow tie in one spot - the unset - you would violate that, making the thing much less predictable.

Anyone, every time they are hunting for any bug in the future, would have to scruitinize and verify your reasoning.

1 Like

Also, the additional secondary role state breaks the deterministic else on ifPrimary/ifSecondary. However, an else ifSecondary/ifPrimary would fix that if we can ensure that postponer queue lasts until that second evaluation. I’m unsure of how the scheduling works, but I’m pretty sure that I either don’t have to do anything, else add one cycle of postponing on role resolution.

Do you have any thoughts on that @kareltucek ?