diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index bce21700..0c43b913 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -39,6 +39,7 @@ target_sources(app PRIVATE src/events/sensor_event.c) target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/events/ble_active_profile_changed.c) target_sources_ifdef(CONFIG_USB app PRIVATE src/events/usb_conn_state_changed.c) if (NOT CONFIG_ZMK_SPLIT_BLE_ROLE_PERIPHERAL) + target_sources(app PRIVATE src/behaviors/behavior_endpoints.c) target_sources(app PRIVATE src/behaviors/behavior_key_press.c) target_sources(app PRIVATE src/behaviors/behavior_reset.c) target_sources(app PRIVATE src/behaviors/behavior_hold_tap.c) diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index 36c918cf..4a507619 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -10,3 +10,4 @@ #include #include #include +#include diff --git a/app/dts/behaviors/endpoints.dtsi b/app/dts/behaviors/endpoints.dtsi new file mode 100644 index 00000000..f6cb64c1 --- /dev/null +++ b/app/dts/behaviors/endpoints.dtsi @@ -0,0 +1,9 @@ +/ { + behaviors { + end: behavior_endpoints { + compatible = "zmk,behavior-endpoints"; + label = "ENDPOINTS"; + #binding-cells = <1>; + }; + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-endpoints.yaml b/app/dts/bindings/behaviors/zmk,behavior-endpoints.yaml new file mode 100644 index 00000000..fdd76bbd --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-endpoints.yaml @@ -0,0 +1,10 @@ +# +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT +# + +description: Endpoints Behavior + +compatible: "zmk,behavior-endpoints" + +include: one_param.yaml diff --git a/app/include/dt-bindings/zmk/endpoints.h b/app/include/dt-bindings/zmk/endpoints.h new file mode 100644 index 00000000..3bba972a --- /dev/null +++ b/app/include/dt-bindings/zmk/endpoints.h @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define ENDPOINT_TOGGLE_CMD 0 +#define ENDPOINT_USB_CMD 1 +#define ENDPOINT_BLE_CMD 2 + +#define END_TOG ENDPOINT_TOGGLE_CMD +#define END_USB ENDPOINT_USB_CMD +#define END_BLE ENDPOINT_BLE_CMD \ No newline at end of file diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h index aad6265b..aad688e7 100644 --- a/app/include/zmk/endpoints.h +++ b/app/include/zmk/endpoints.h @@ -9,4 +9,12 @@ #include #include +enum zmk_endpoint { + ZMK_ENDPOINT_USB, + ZMK_ENDPOINT_BLE, +}; + +int zmk_endpoints_select(enum zmk_endpoint endpoint); +int zmk_endpoints_toggle(); + int zmk_endpoints_send_report(u8_t usage_report); diff --git a/app/src/behaviors/behavior_endpoints.c b/app/src/behaviors/behavior_endpoints.c new file mode 100644 index 00000000..dd56fc11 --- /dev/null +++ b/app/src/behaviors/behavior_endpoints.c @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_endpoints + +#include +#include +#include + +#include + +#include +#include + +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + switch (binding->param1) { + case ENDPOINT_TOGGLE_CMD: + return zmk_endpoints_toggle(); + case ENDPOINT_USB_CMD: + return zmk_endpoints_select(ZMK_ENDPOINT_USB); + case ENDPOINT_BLE_CMD: + return zmk_endpoints_select(ZMK_ENDPOINT_BLE); + default: + LOG_ERR("Unknown endpoints command: %d", binding->param1); + } + + return -ENOTSUP; +} + +static int behavior_ep_init(struct device *dev) { return 0; } + +static const struct behavior_driver_api behavior_endpoints_driver_api = { + .binding_pressed = on_keymap_binding_pressed, +}; + +DEVICE_AND_API_INIT(behavior_end, DT_INST_LABEL(0), behavior_ep_init, NULL, NULL, APPLICATION, + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_endpoints_driver_api); diff --git a/app/src/endpoints.c b/app/src/endpoints.c index 56204a47..0c795890 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -4,6 +4,9 @@ * SPDX-License-Identifier: MIT */ +#include +#include + #include #include #include @@ -16,21 +19,45 @@ #include LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); -enum endpoint { - ENDPOINT_USB, - ENDPOINT_BLE, -}; +#define DEFAULT_ENDPOINT \ + COND_CODE_1(IS_ENABLED(CONFIG_ZMK_BLE), (ZMK_ENDPOINT_BLE), (ZMK_ENDPOINT_USB)) -#define DEFAULT_ENDPOINT COND_CODE_1(IS_ENABLED(CONFIG_ZMK_BLE), (ENDPOINT_BLE), (ENDPOINT_USB)) +static enum zmk_endpoint current_endpoint = DEFAULT_ENDPOINT; +static enum zmk_endpoint preferred_endpoint = + ZMK_ENDPOINT_USB; /* Used if multiple endpoints are ready */ -static enum endpoint current_endpoint = DEFAULT_ENDPOINT; +static void update_current_endpoint(); + +int zmk_endpoints_select(enum zmk_endpoint endpoint) { + LOG_DBG("Selected endpoint %d", endpoint); + + if (preferred_endpoint == endpoint) { + return 0; + } + + preferred_endpoint = endpoint; + +#if IS_ENABLED(CONFIG_SETTINGS) + settings_save_one("endpoints/preferred", &preferred_endpoint, sizeof(preferred_endpoint)); +#endif + + update_current_endpoint(); + + return 0; +} + +int zmk_endpoints_toggle() { + enum zmk_endpoint new_endpoint = + (preferred_endpoint == ZMK_ENDPOINT_USB) ? ZMK_ENDPOINT_BLE : ZMK_ENDPOINT_USB; + return zmk_endpoints_select(new_endpoint); +} static int send_keypad_report() { struct zmk_hid_keypad_report *keypad_report = zmk_hid_get_keypad_report(); switch (current_endpoint) { #if IS_ENABLED(CONFIG_ZMK_USB) - case ENDPOINT_USB: { + case ZMK_ENDPOINT_USB: { int err = zmk_usb_hid_send_report((u8_t *)keypad_report, sizeof(*keypad_report)); if (err) { LOG_ERR("FAILED TO SEND OVER USB: %d", err); @@ -40,7 +67,7 @@ static int send_keypad_report() { #endif /* IS_ENABLED(CONFIG_ZMK_USB) */ #if IS_ENABLED(CONFIG_ZMK_BLE) - case ENDPOINT_BLE: { + case ZMK_ENDPOINT_BLE: { int err = zmk_hog_send_keypad_report(&keypad_report->body); if (err) { LOG_ERR("FAILED TO SEND OVER HOG: %d", err); @@ -60,7 +87,7 @@ static int send_consumer_report() { switch (current_endpoint) { #if IS_ENABLED(CONFIG_ZMK_USB) - case ENDPOINT_USB: { + case ZMK_ENDPOINT_USB: { int err = zmk_usb_hid_send_report((u8_t *)consumer_report, sizeof(*consumer_report)); if (err) { LOG_ERR("FAILED TO SEND OVER USB: %d", err); @@ -70,7 +97,7 @@ static int send_consumer_report() { #endif /* IS_ENABLED(CONFIG_ZMK_USB) */ #if IS_ENABLED(CONFIG_ZMK_BLE) - case ENDPOINT_BLE: { + case ZMK_ENDPOINT_BLE: { int err = zmk_hog_send_consumer_report(&consumer_report->body); if (err) { LOG_ERR("FAILED TO SEND OVER HOG: %d", err); @@ -99,6 +126,49 @@ int zmk_endpoints_send_report(u8_t usage_page) { } } +#if IS_ENABLED(CONFIG_SETTINGS) + +static int endpoints_handle_set(const char *name, size_t len, settings_read_cb read_cb, + void *cb_arg) { + LOG_DBG("Setting endpoint value %s", log_strdup(name)); + + if (settings_name_steq(name, "preferred", NULL)) { + if (len != sizeof(enum zmk_endpoint)) { + LOG_ERR("Invalid endpoint size (got %d expected %d)", len, sizeof(enum zmk_endpoint)); + return -EINVAL; + } + + int err = read_cb(cb_arg, &preferred_endpoint, sizeof(enum zmk_endpoint)); + if (err <= 0) { + LOG_ERR("Failed to read preferred endpoint from settings (err %d)", err); + return err; + } + + update_current_endpoint(); + } + + return 0; +} + +struct settings_handler endpoints_handler = {.name = "endpoints", .h_set = endpoints_handle_set}; +#endif /* IS_ENABLED(CONFIG_SETTINGS) */ + +static int zmk_endpoints_init(struct device *_arg) { +#if IS_ENABLED(CONFIG_SETTINGS) + settings_subsys_init(); + + int err = settings_register(&endpoints_handler); + if (err) { + LOG_ERR("Failed to register the endpoints settings handler (err %d)", err); + return err; + } + + settings_load(); +#endif + + return 0; +} + static bool is_usb_ready() { #if IS_ENABLED(CONFIG_ZMK_USB) return zmk_usb_is_hid_ready(); @@ -115,21 +185,20 @@ static bool is_ble_ready() { #endif } -static enum endpoint get_selected_endpoint() { +static enum zmk_endpoint get_selected_endpoint() { if (is_ble_ready()) { if (is_usb_ready()) { - LOG_DBG("Both endpoints are ready."); - // TODO: add user setting to control this - return ENDPOINT_USB; + LOG_DBG("Both endpoints are ready. Using %d", preferred_endpoint); + return preferred_endpoint; } LOG_DBG("Only BLE is ready."); - return ENDPOINT_BLE; + return ZMK_ENDPOINT_BLE; } if (is_usb_ready()) { LOG_DBG("Only USB is ready."); - return ENDPOINT_USB; + return ZMK_ENDPOINT_USB; } LOG_DBG("No endpoints are ready."); @@ -144,8 +213,8 @@ static void disconnect_current_endpoint() { zmk_endpoints_send_report(USAGE_CONSUMER); } -static int endpoint_listener(const struct zmk_event_header *eh) { - enum endpoint new_endpoint = get_selected_endpoint(); +static void update_current_endpoint() { + enum zmk_endpoint new_endpoint = get_selected_endpoint(); if (new_endpoint != current_endpoint) { /* Cancel all current keypresses so keys don't stay held on the old endpoint. */ @@ -154,7 +223,10 @@ static int endpoint_listener(const struct zmk_event_header *eh) { current_endpoint = new_endpoint; LOG_INF("Endpoint changed: %d", current_endpoint); } +} +static int endpoint_listener(const struct zmk_event_header *eh) { + update_current_endpoint(); return 0; } @@ -164,4 +236,6 @@ ZMK_SUBSCRIPTION(endpoint_listener, usb_conn_state_changed); #endif #if IS_ENABLED(CONFIG_ZMK_BLE) ZMK_SUBSCRIPTION(endpoint_listener, ble_active_profile_changed); -#endif \ No newline at end of file +#endif + +SYS_INIT(zmk_endpoints_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/docs/docs/behavior/endpoints.md b/docs/docs/behavior/endpoints.md new file mode 100644 index 00000000..caedf3a6 --- /dev/null +++ b/docs/docs/behavior/endpoints.md @@ -0,0 +1,59 @@ +--- +title: Endpoint Behavior +sidebar_label: Endpoints +--- + +## Summary + +The endpoint behavior allows selecting whether keyboard input is sent to the +USB or bluetooth connection when both are connected. This allows connecting a +keyboard to USB for power but sending input to a different device over bluetooth. + +By default, keyboard input is sent to USB when both endpoints are connected. +Once you select a different endpoint, it will be remembered until you change it again. + +## Endpoints Command Defines + +Endpoints command defines are provided through the [`dt-bindings/zmk/endpoints.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/endpoints.h) +header, which is added at the top of the keymap file: + +``` +#include +``` + +This allows you to reference the actions defined in this header: + +| Define | Action | Alias | +| --------------------- | ---------------------------------------------------- | --------- | +| `ENDPOINT_USB_CMD` | Send keyboard input to USB | `END_USB` | +| `ENDPOINT_BLE_CMD` | Send keyboard input to the current bluetooth profile | `END_BLE` | +| `ENDPOINT_TOGGLE_CMD` | Toggle between USB and BLE | `END_TOG` | + +## Endpoints Behavior + +The endpoints behavior changes the preferred endpoint on press. + +### Behavior Binding + +- Reference: `&end` +- Parameter #1: Command, e.g. `END_BLE` + +### Example: + +1. Behavior binding to prefer sending keyboard input to USB + + ``` + &end END_USB + ``` + +1. Behavior binding to prefer sending keyboard input to the current bluetooth profile + + ``` + &end END_BLE + ``` + +1. Behavior binding to toggle between preferring USB and BLE + + ``` + &end END_TOG + ``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 54f6576b..334a35df 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -22,6 +22,7 @@ module.exports = { "behavior/mod-tap", "behavior/reset", "behavior/bluetooth", + "behavior/endpoints", "behavior/lighting", "behavior/power", ],