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

Invoke behaviors with different "locality" #547

Merged

Conversation

petejohanson
Copy link
Contributor

With this change, behaviors can now have different "locality":

  • Central (e.g. key press)
  • Global (e.g. RGB to toggle on/off on both sides)
  • Reset (e.g. to reset whatever device the "reset" binding was on, so you can have a reset on both sides, and it does what you expect)

This should makes split power and RGB management work, at least for the basics.

@petejohanson petejohanson added enhancement New feature or request core Core functionality/behavior of ZMK behaviors split labels Dec 28, 2020
@petejohanson petejohanson self-assigned this Dec 28, 2020
@petejohanson petejohanson force-pushed the core/peripheral-behavior-invocation branch 2 times, most recently from 6970b68 to f6e91fd Compare December 29, 2020 03:34
app/include/drivers/behavior.h Outdated Show resolved Hide resolved
app/include/drivers/behavior.h Outdated Show resolved Hide resolved
@@ -6,6 +6,8 @@

#pragma once


Copy link
Collaborator

Choose a reason for hiding this comment

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

Just whitespace changes in this file.

app/src/keymap.c Outdated Show resolved Hide resolved
} else {
subscribe_params.notify = split_central_notify_func;
subscribe_params.value = BT_GATT_CCC_NOTIFY;
subscribe_params.ccc_handle = attr->handle;

split_central_subscribe(conn);

return BT_GATT_ITER_STOP;
memcpy(&uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_RUN_BEHAVIOR_UUID), sizeof(uuid));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it intended that split_central_process_connection and this function share the static uuid variable? It looks like everything here overwrites the UUID passed to bt_gatt_discover while split_central_process_connection does not. If that's not intended, maybe put separate static variables into each function to ensure they can't both access the same variable.

I'm not sure I fully understand what this code is doing yet, but if split_central_process_connection is always supposed to be discovering ZMK_SPLIT_BT_SERVICE_UUID, it looks like it actually is re-discovering the last thing you tried to discover.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code is cribbed from a discovery example from Zephyr. As I read it, they're trying to reduce memory usage by re-using several variables as they discover/iterate the attributes. Let me review these changes again before we move forward, be sure we're still happy with how this is shaping up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@joelspadin Ok, I've cleaned this up a lot, and in the process, I believe found and fixed a split bug that would sometimes cause peripherals not to get subscribed to properly if the central is kept on and the peripheral is reset and reconnected to.

The changes depend on the fix in zmkfirmware/zephyr#3 being merged first, then this can be tested.

Copy link
Member

@Nicell Nicell 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 pretty excited for this change! Particularly I'd like to get someone who has RGB on a split that could test this out, as I just have plain split boards. Could we ask for testers in the Discord?

app/west.yml Outdated Show resolved Hide resolved
@@ -134,24 +145,50 @@ int zmk_keymap_apply_position_state(int layer, uint32_t position, bool pressed,
behavior = device_get_binding(binding->behavior_dev);

if (!behavior) {
LOG_DBG("No behavior assigned to %d on layer %d", position, layer);
LOG_WRN("No behavior assigned to %d on layer %d", position, layer);
Copy link
Contributor

Choose a reason for hiding this comment

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

Did this fall out of testing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Crept in, yeah. Was easier to spot in log output, and felt like a better level.

@petejohanson petejohanson force-pushed the core/peripheral-behavior-invocation branch 3 times, most recently from e7f2ebe to 2253530 Compare January 7, 2021 05:09
@okke-formsma
Copy link
Collaborator

The code looks fine to me. There are some comments by @joelspadin that haven't been addressed yet @petejohanson.

@petejohanson petejohanson force-pushed the core/peripheral-behavior-invocation branch 2 times, most recently from c2070cf to 7af2e6e Compare January 9, 2021 03:45
@Percentnineteen
Copy link

I turned on USB debugging to see if I could add anything to my previous report. I couldn't glean anything interesting out of the reports when I tried to toggle external power but I did see something weird when trying to use &reset or &bootloader on the central side. Doing that, the logging receives an error:
zmk: Source not connected

I added to the log message and saw the value of peripherals[payload_wrapper.source].state is 0 (PERIPHERAL_SLOT_STATE_OPEN). At first I thought this was maybe due to the fact that I was only testing central but even when I connected peripheral, I had no luck.

@petejohanson
Copy link
Contributor Author

It looks like I independently developed the same functionality this week and only discovered this PR when I was going through open issues to reference in a PR. The one thing I see missing is state synchronization once a peripheral connects to the central half (for instances where the peripheral was disconnected while a behavior was invoked.) I have an additional write only characteristic for the RGB state and export/import state methods for the rgb underglow subsystem. Synchronizing the state as opposed to running a behavior would allow the animation steps to remain in sync. I had minimal safety checks in the write callback opting to only check if the length matches the expected size of the state struct. For more safety, I was thinking of including checks for the firmware version from the DIS but I don't have that implemented.

I don't have anything implemented for explicitly synchronizing the external power state since I was relying on #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) and updated rgb_settings_set to have

#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER)
    if (ext_power) {
        if (state.on && !ext_power_state) {
            int rc = ext_power_enable(ext_power);
            if (rc != 0) {
                LOG_ERR("Unable to enable EXT_POWER: %d", rc);
            }
        } else if (!state.on && ext_power_state) {
            int rc = ext_power_disable(ext_power);
            if (rc != 0) {
                LOG_ERR("Unable to disable EXT_POWER: %d", rc);
            }
        }
    }
#endif
    if (!state.on) {
        k_timer_stop(&underglow_tick);
    }

after previously fetching the external power state with ext_power_state = ext_power_get(ext_power); in zmk_rgb_underglow_init. Importing the rgb underglow state would enable/disable external power as needed.

RGB underglow/external power state might not be coupled in some configurations though so perhaps the central should just fetch the state and write to the run behavior characteristic

Sorry you missed this!

Related, but not exactly the same, is state synchronization between halves. I would like to get this in as is, and the continue exploring additional work here while also getting some other work moving forward.

Looking forward to seeing what you came up w/!

@petejohanson
Copy link
Contributor Author

I've just pushed a fix for invoking &reset, &bootloader, etc on the central side. Testing would be appreciated.

Regarding external power.... That seems to be working here on my Kyria, but that also has RGB. I will test on my Corne as well. Can you get logs for the peripheral side if you're still having issues w/ the latest on this branch?

@petejohanson
Copy link
Contributor Author

2. if I checkout this pr, &reset and &bootloader work correctly on the peripheral side but no longer do anything on the central side

Please retest, I pushed a fix for this.

3. also, with this pr, &ext_power EP_TOG, EP_ON and EP_OFF only cause the desired effect on the central side - the peripheral is left unaffected

@Percentnineteen oI've tested this again on my Corne, no underglow enabled, and external power properly enables/disables just fine. Can you please pull and be sure you've got the latest code from this PR? There was a small fix for this I included while ago along w/ a rebase, so be sure the latest sha1/commit after you pull is:

commit d3b47391c9d210466a2e542d91c3cf558d89e4b3 (HEAD -> core/peripheral-behavior-invocation, origin/core/peripheral-behavior-invocation)
Author: Peter Johanson <[email protected]>
Date:   Sun Jan 30 03:38:10 2022 +0000

    fix(split): Add define for local source.
    
    * Add `ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL` and use
      it consinstently to fix bug w/ local `&reset`, `&bootloader`, etc.

@Percentnineteen
Copy link

I tested the update and the &reset and &bootloader work now on the central side. However, I tested &ext_power EP_TOG/ON/OFF and they still don't seem to work. I logged the peripheral and got this:

[00:00:42.379,058] <dbg> zmk.kscan_matrix_read: Sending event at 0,0 state on
[00:00:42.379,119] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5
, pressed: true
[00:00:42.379,150] <dbg> zmk.split_listener:
[00:00:42.397,796] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:42.519,042] <dbg> zmk.kscan_matrix_read: Sending event at 0,0 state off
[00:00:42.519,134] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5
, pressed: false
[00:00:42.519,165] <dbg> zmk.split_listener:
[00:00:42.532,806] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:42.532,897] <dbg> zmk.split_svc_run_behavior: INVOKE THE BEHAVIOR: EXTPOW
ER with params 1 0
[00:00:44.166,046] <dbg> zmk.kscan_matrix_read: Sending event at 0,0 state on
[00:00:44.166,137] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5
, pressed: true
[00:00:44.166,137] <dbg> zmk.split_listener:
[00:00:44.182,800] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:44.262,054] <dbg> zmk.kscan_matrix_read: Sending event at 0,0 state off
[00:00:44.262,145] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5
, pressed: false
[00:00:44.262,176] <dbg> zmk.split_listener:
[00:00:44.272,796] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:44.272,888] <dbg> zmk.split_svc_run_behavior: INVOKE THE BEHAVIOR: EXTPOW
ER with params 0 0

I issued EXT_TOG twice. The central side turned off immediately after the first command executed while peripheral stayed on. I assume peripheral should shut off immediately also. Central does not turn on when turning the power back on but I believe that is due to a different issue with reinitializing the OLED (VCC goes high, the display just doesn't come back).

I believe I have the latest from this PR. The top commit when I do "git log" is:

Author: Peter Johanson <[email protected]>
Date:   Sun Jan 30 03:38:10 2022 +0000

    fix(split): Add define for local source.

    * Add `ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL` and use
      it consinstently to fix bug w/ local `&reset`, `&bootloader`, etc.

@petejohanson
Copy link
Contributor Author

@Percentnineteen Hmm.... I suspect maybe only the release, not the press, is getting sent. Can you pull again ans flash the right side and get the logs one more time?

Thanks for all the testing!

@Percentnineteen
Copy link

No problem. I pulled and got this in the logs:

[00:00:28.903,137] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5, pressed: true
[00:00:28.903,137] <dbg> zmk.split_listener:
[00:00:28.914,764] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:28.986,053] <dbg> zmk.kscan_matrix_read: Sending event at 0,0 state off
[00:00:28.986,145] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5, pressed: false
[00:00:28.986,175] <dbg> zmk.split_listener:
[00:00:28.997,253] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:28.997,344] <dbg> zmk.split_svc_run_behavior: EXTPOWER with params 1 0: pressed? 0
[00:00:38.375,061] <dbg> zmk.kscan_matrix_read: Sending event at 0,0 state on
[00:00:38.375,122] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5, pressed: true
[00:00:38.375,152] <dbg> zmk.split_listener:
[00:00:38.387,268] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:38.490,081] <dbg> zmk.kscan_matrix_read: Sending event at 0,0 state off
[00:00:38.490,142] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5, pressed: false
[00:00:38.490,173] <dbg> zmk.split_listener:
[00:00:38.507,232] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:38.507,263] <dbg> zmk.split_svc_run_behavior: EXTPOWER with params 0 0: pressed? 0

@petejohanson
Copy link
Contributor Author

No problem. I pulled and got this in the logs:

[00:00:28.903,137] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5, pressed: true
[00:00:28.903,137] <dbg> zmk.split_listener:
[00:00:28.914,764] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:28.986,053] <dbg> zmk.kscan_matrix_read: Sending event at 0,0 state off
[00:00:28.986,145] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5, pressed: false
[00:00:28.986,175] <dbg> zmk.split_listener:
[00:00:28.997,253] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:28.997,344] <dbg> zmk.split_svc_run_behavior: EXTPOWER with params 1 0: pressed? 0
[00:00:38.375,061] <dbg> zmk.kscan_matrix_read: Sending event at 0,0 state on
[00:00:38.375,122] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5, pressed: true
[00:00:38.375,152] <dbg> zmk.split_listener:
[00:00:38.387,268] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:38.490,081] <dbg> zmk.kscan_matrix_read: Sending event at 0,0 state off
[00:00:38.490,142] <dbg> zmk.zmk_kscan_process_msgq: Row: 0, col: 0, position: 5, pressed: false
[00:00:38.490,173] <dbg> zmk.split_listener:
[00:00:38.507,232] <dbg> zmk.split_svc_run_behavior: offset 0 len 19
[00:00:38.507,263] <dbg> zmk.split_svc_run_behavior: EXTPOWER with params 0 0: pressed? 0

Yeah, only seeing the releases, not the presses, which is the issue.

Can you link to your keymap? Is this invoked from a combo, or hold-tap maybe?

@petejohanson
Copy link
Contributor Author

@Percentnineteen Pushed one more small fix. Can you try one more tie? Should only require flashing the peripheral side.

@Percentnineteen
Copy link

Percentnineteen commented Jan 30, 2022

Trying to link to my zmk-config repo but it's a fork of the miryoku repo and GitHub seemingly won't let me share it...

No combos - I've just got a layer-tap to activate a layer and have a key mapped to &ext_power EP_TOG.

After converting from miryoku's format to default dtsi format looks like this:

    BASE_layer {
      label = "Base";
      bindings = <
      &kp Q             &kp W             &kp E             &kp R             &kp T             &kp Y             &kp U             &kp I             &kp O             &kp P
      &hm LGUI A        &hm LALT S        &hm LCTRL D       &hm LSHFT F       &kp G             &kp H             &hm LSHFT J       &hm LCTRL K       &hm LALT L        &hm LGUI SQT
      &lt BUTTON Z      &hm RALT X        &kp C             &kp V             &kp B             &kp N             &kp M             &kp COMMA         &hm RALT DOT      &lt BUTTON SLASH
                                          &lt MEDIA ESC     &lt NAV SPC       &lt MOUSE TAB     &lt SYM RET       &lt NUM BSPC      &lt FUN DEL
      >;
    };
    MEDIA_layer {
      label = "Media";
      bindings = < 
      &bootloader       &reset            U_NA              U_NA              U_NA          &ext_power EP_TOG   &ext_power EP_OFF   &ext_power EP_ON    &rgb_ug RGB_TOG   &rgb_ug RGB_BRI
      &kp LGUI          &kp LALT          &kp LCTRL         &kp LSHFT         U_NA          &kp C_PREV          &kp C_VOL_DN        &kp C_VOL_UP        &kp C_NEXT        &out OUT_TOG
      U_NA              &kp RALT          U_NA              U_NA              U_NA          &bt BT_SEL 0        &bt BT_SEL 1        &bt BT_SEL 2        &bt BT_SEL 3      &bt BT_CLR
                                          U_NA              U_NA              U_NA          &kp C_STOP          &kp C_PP            &kp C_MUTE
      >;
    };

@Percentnineteen
Copy link

Crap! I didn't see the last fix before my last message. I pulled that last fix:

commit 90816173d06300a6dcd1d1d018389bef2f07836f (HEAD -> locality-pr, petejohanson/core/peripheral-behavior-invocation)
Author: Peter Johanson <[email protected]>
Date:   Sun Jan 30 05:32:12 2022 +0000

    fix(split): Fix an off-by-one error in split svc.

    * Properly check end of behavior device string for null terminator.

After pulling this, it works. Awesome.

Copy link
Member

@Nicell Nicell left a comment

Choose a reason for hiding this comment

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

I mostly reviewed this so that I could familiarize myself with the changes going forward. All the changes make sense to me, and the idea of central, global, and source looks good. Nice touch that global sends to all peripherals, which I presume will make dongle-ing easier in the future. I left a few comments, but I think I answered one myself. Great work!

app/include/dt-bindings/zmk/rgb.h Show resolved Hide resolved

switch (locality) {
case BEHAVIOR_LOCALITY_CENTRAL:
return invoke_locally(&binding, event, pressed);
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason this doesn't need to be wrapped in #if ZMK_BLE_IS_CENTRAL? Is it because if it's pressed on the peripheral, the position gets sent to the central and never gets sent back to be invoked?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's because technically, for a split or non-split, you'd always want to run this locally, and keymaps aren't ever processed anywhere else, so it doesn't need to be conditional in either case.

Copy link
Member

Choose a reason for hiding this comment

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

Just clicked for me that the #if ZMK_BLE_IS_CENTRAL is being used to identify if the device is a split keyboard, not if it's the central half. Makes a lot more sense. Would #if IS_ENABLED(CONFIG_ZMK_SPLIT) be a better check since only central will ever be running it anyways?

app/src/rgb_underglow.c Show resolved Hide resolved
* GATT characteristic allowing passng data + behavior
  label to invoke the behavior on the peripheral side.
* Behaviors have a locality setting to specify where they run.
* Build reset/power/RGB on peripheral.
* Use Zephyr auto CCC discovery instead of doing it ourselves.
* Split service versus characteristic discovery into dedicated
  steps in the flow.
* Fix for not searching properly when connecting to a peripheral
  a second time.
* Convert relative effect cycling to absolute effect selection.
* Remove `/omit-if-no-ref/` from the behavior nodes.
* Track peripherals by indexes slot, with all appropiate peripheral
  state stored in the slot.
* Event sources tracked by peripheral slot index.
* Don'd omit unreferenced reset behaviors, so they are always
  available in split peripherals.
* Add `ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL` and use
  it consinstently to fix bug w/ local `&reset`, `&bootloader`, etc.
* Properly check end of behavior device string for null terminator.
@petejohanson petejohanson force-pushed the core/peripheral-behavior-invocation branch from 9081617 to 2d91ad8 Compare January 31, 2022 03:52
* Add strlcpy from public domain version.
* Leverage strlcpy to detect truncation of behavior dev strs, and log.
* Use `offsetof` for cleaner detection on peripheral side.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
behaviors core Core functionality/behavior of ZMK enhancement New feature or request split
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet