From 8abc449cc29ef35ffd5770e9f7680348b8c708f7 Mon Sep 17 00:00:00 2001 From: zhiayang <500236+zhiayang@users.noreply.github.com> Date: Mon, 25 Sep 2023 21:04:03 -0400 Subject: [PATCH 01/49] feat(drivers): add driver for MAX17048 fuel gauge Add driver for MAX17048 fuel gauge for battery reporting. --- app/module/drivers/sensor/CMakeLists.txt | 3 +- app/module/drivers/sensor/Kconfig | 3 +- .../drivers/sensor/max17048/CMakeLists.txt | 9 + app/module/drivers/sensor/max17048/Kconfig | 23 ++ app/module/drivers/sensor/max17048/max17048.c | 222 ++++++++++++++++++ app/module/drivers/sensor/max17048/max17048.h | 41 ++++ .../dts/bindings/sensor/maxim,max17048.yml | 12 + 7 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 app/module/drivers/sensor/max17048/CMakeLists.txt create mode 100644 app/module/drivers/sensor/max17048/Kconfig create mode 100644 app/module/drivers/sensor/max17048/max17048.c create mode 100644 app/module/drivers/sensor/max17048/max17048.h create mode 100644 app/module/dts/bindings/sensor/maxim,max17048.yml diff --git a/app/module/drivers/sensor/CMakeLists.txt b/app/module/drivers/sensor/CMakeLists.txt index b549320f121..9654600a756 100644 --- a/app/module/drivers/sensor/CMakeLists.txt +++ b/app/module/drivers/sensor/CMakeLists.txt @@ -2,4 +2,5 @@ # SPDX-License-Identifier: MIT add_subdirectory_ifdef(CONFIG_ZMK_BATTERY battery) -add_subdirectory_ifdef(CONFIG_EC11 ec11) \ No newline at end of file +add_subdirectory_ifdef(CONFIG_EC11 ec11) +add_subdirectory_ifdef(CONFIG_MAX17048 max17048) diff --git a/app/module/drivers/sensor/Kconfig b/app/module/drivers/sensor/Kconfig index 6a8ac07ea3e..ad570c58c94 100644 --- a/app/module/drivers/sensor/Kconfig +++ b/app/module/drivers/sensor/Kconfig @@ -5,5 +5,6 @@ if SENSOR rsource "battery/Kconfig" rsource "ec11/Kconfig" +rsource "max17048/Kconfig" -endif # SENSOR \ No newline at end of file +endif # SENSOR diff --git a/app/module/drivers/sensor/max17048/CMakeLists.txt b/app/module/drivers/sensor/max17048/CMakeLists.txt new file mode 100644 index 00000000000..e895fa11fb8 --- /dev/null +++ b/app/module/drivers/sensor/max17048/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2022 The ZMK Contributors +# SPDX-License-Identifier: MIT + +zephyr_include_directories(.) + +zephyr_library() + +zephyr_library_sources_ifdef(CONFIG_MAX17048 max17048.c) +zephyr_library_sources_ifndef(CONFIG_MAX17048 ${ZEPHYR_BASE}/misc/empty_file.c) diff --git a/app/module/drivers/sensor/max17048/Kconfig b/app/module/drivers/sensor/max17048/Kconfig new file mode 100644 index 00000000000..8a7ec16e85d --- /dev/null +++ b/app/module/drivers/sensor/max17048/Kconfig @@ -0,0 +1,23 @@ +# Copyright (c) 2022 The ZMK Contributors +# SPDX-License-Identifier: MIT + +DT_COMPAT_MAXIM_MAX17048 := maxim,max17048 + +menuconfig MAX17048 + bool "MAX17048/9 I2C-based Fuel Gauge" + default $(dt_compat_enabled,$(DT_COMPAT_MAXIM_MAX17048)) + depends on I2C + select ZMK_BATTERY + help + Enable driver for MAX17048/9 I2C-based Fuel Gauge. Supports measuring + battery voltage and state-of-charge. + +if MAX17048 + +config SENSOR_MAX17048_INIT_PRIORITY + int "Init priority" + default 75 + help + Device driver initialization priority. + +endif #MAX17048 diff --git a/app/module/drivers/sensor/max17048/max17048.c b/app/module/drivers/sensor/max17048/max17048.c new file mode 100644 index 00000000000..24cfe093f4a --- /dev/null +++ b/app/module/drivers/sensor/max17048/max17048.c @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT maxim_max17048 + +#include +#include +#include +#include +#include +#include + +#include "max17048.h" + +#define LOG_LEVEL CONFIG_SENSOR_LOG_LEVEL +#include +LOG_MODULE_REGISTER(sensor_max17048); + +static int read_register(const struct device *dev, uint8_t reg, uint16_t *value) { + + if (k_is_in_isr()) { + return -EWOULDBLOCK; + } + + struct max17048_config *config = (struct max17048_config *)dev->config; + + uint8_t data[2] = {0}; + int ret = i2c_burst_read_dt(&config->i2c_bus, reg, &data[0], sizeof(data)); + if (ret != 0) { + LOG_DBG("i2c_write_read FAIL %d\n", ret); + return ret; + } + + // the register values are returned in big endian (MSB first) + *value = sys_get_be16(data); + return 0; +} + +static int write_register(const struct device *dev, uint8_t reg, uint16_t value) { + + if (k_is_in_isr()) { + return -EWOULDBLOCK; + } + + struct max17048_config *config = (struct max17048_config *)dev->config; + + uint8_t data[2] = {0}; + sys_put_be16(value, &data[0]); + + return i2c_burst_write_dt(&config->i2c_bus, reg, &data[0], sizeof(data)); +} + +static int set_rcomp_value(const struct device *dev, uint8_t rcomp_value) { + + struct max17048_drv_data *const drv_data = (struct max17048_drv_data *const)dev->data; + k_sem_take(&drv_data->lock, K_FOREVER); + + uint16_t tmp = 0; + int err = read_register(dev, REG_CONFIG, &tmp); + if (err != 0) { + goto done; + } + + tmp = ((uint16_t)rcomp_value << 8) | (tmp & 0xFF); + err = write_register(dev, REG_CONFIG, tmp); + if (err != 0) { + goto done; + } + + LOG_DBG("set RCOMP to %d", rcomp_value); + +done: + k_sem_give(&drv_data->lock); + return err; +} + +static int set_sleep_enabled(const struct device *dev, bool sleep) { + + struct max17048_drv_data *const drv_data = (struct max17048_drv_data *const)dev->data; + k_sem_take(&drv_data->lock, K_FOREVER); + + uint16_t tmp = 0; + int err = read_register(dev, REG_CONFIG, &tmp); + if (err != 0) { + goto done; + } + + if (sleep) { + tmp |= 0x80; + } else { + tmp &= ~0x0080; + } + + err = write_register(dev, REG_CONFIG, tmp); + if (err != 0) { + goto done; + } + + LOG_DBG("sleep mode %s", sleep ? "enabled" : "disabled"); + +done: + k_sem_give(&drv_data->lock); + return err; +} + +static int max17048_sample_fetch(const struct device *dev, enum sensor_channel chan) { + + struct max17048_drv_data *const drv_data = dev->data; + k_sem_take(&drv_data->lock, K_FOREVER); + + int err = 0; + + if (chan == SENSOR_CHAN_GAUGE_STATE_OF_CHARGE || chan == SENSOR_CHAN_ALL) { + err = read_register(dev, REG_STATE_OF_CHARGE, &drv_data->raw_state_of_charge); + if (err != 0) { + LOG_WRN("failed to read state-of-charge: %d", err); + goto done; + } + LOG_DBG("read soc: %d", drv_data->raw_state_of_charge); + + } else if (chan == SENSOR_CHAN_GAUGE_VOLTAGE || chan == SENSOR_CHAN_ALL) { + err = read_register(dev, REG_VCELL, &drv_data->raw_vcell); + if (err != 0) { + LOG_WRN("failed to read vcell: %d", err); + goto done; + } + LOG_DBG("read vcell: %d", drv_data->raw_vcell); + + } else { + LOG_DBG("unsupported channel %d", chan); + err = -ENOTSUP; + } + +done: + k_sem_give(&drv_data->lock); + return err; +} + +static int max17048_channel_get(const struct device *dev, enum sensor_channel chan, + struct sensor_value *val) { + int err = 0; + + struct max17048_drv_data *const drv_data = dev->data; + k_sem_take(&drv_data->lock, K_FOREVER); + + struct max17048_drv_data *const data = dev->data; + unsigned int tmp = 0; + + switch (chan) { + case SENSOR_CHAN_GAUGE_VOLTAGE: + // 1250 / 16 = 78.125 + tmp = data->raw_vcell * 1250 / 16; + val->val1 = tmp / 1000000; + val->val2 = tmp % 1000000; + break; + + case SENSOR_CHAN_GAUGE_STATE_OF_CHARGE: + val->val1 = (data->raw_state_of_charge >> 8); + val->val2 = (data->raw_state_of_charge & 0xFF) * 1000000 / 256; + break; + + default: + err = -ENOTSUP; + break; + } + + k_sem_give(&drv_data->lock); + return err; +} + +static int max17048_init(const struct device *dev) { + struct max17048_drv_data *drv_data = dev->data; + const struct max17048_config *config = dev->config; + + if (!device_is_ready(config->i2c_bus.bus)) { + LOG_WRN("i2c bus not ready!"); + return -EINVAL; + } + + uint16_t ic_version = 0; + int err = read_register(dev, REG_VERSION, &ic_version); + if (err != 0) { + LOG_WRN("could not get IC version!"); + return err; + } + + // the functions below need the semaphore, so initialise it here + k_sem_init(&drv_data->lock, 1, 1); + + // bring the device out of sleep + set_sleep_enabled(dev, false); + + // set the default rcomp value -- 0x97, as stated in the datasheet + set_rcomp_value(dev, 0x97); + + LOG_INF("device initialised at 0x%x (version %d)", config->i2c_bus.addr, ic_version); + + return 0; +} + +static const struct sensor_driver_api max17048_api_table = {.sample_fetch = max17048_sample_fetch, + .channel_get = max17048_channel_get}; + +#define MAX17048_INIT(inst) \ + static struct max17048_config max17048_##inst##_config = {.i2c_bus = \ + I2C_DT_SPEC_INST_GET(inst)}; \ + \ + static struct max17048_drv_data max17048_##inst##_drvdata = { \ + .raw_state_of_charge = 0, \ + .raw_charge_rate = 0, \ + .raw_vcell = 0, \ + }; \ + \ + /* This has to init after SPI master */ \ + DEVICE_DT_INST_DEFINE(inst, max17048_init, NULL, &max17048_##inst##_drvdata, \ + &max17048_##inst##_config, POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, \ + &max17048_api_table); + +DT_INST_FOREACH_STATUS_OKAY(MAX17048_INIT) diff --git a/app/module/drivers/sensor/max17048/max17048.h b/app/module/drivers/sensor/max17048/max17048.h new file mode 100644 index 00000000000..984f5c70f4e --- /dev/null +++ b/app/module/drivers/sensor/max17048/max17048.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define REG_VCELL 0x02 +#define REG_STATE_OF_CHARGE 0x04 +#define REG_MODE 0x06 +#define REG_VERSION 0x08 +#define REG_HIBERNATE 0x0A +#define REG_CONFIG 0x0C +#define REG_VALERT 0x14 +#define REG_CHARGE_RATE 0x16 +#define REG_VRESET 0x18 +#define REG_STATUS 0x1A + +struct max17048_config { + struct i2c_dt_spec i2c_bus; +}; + +struct max17048_drv_data { + struct k_sem lock; + + uint16_t raw_state_of_charge; + uint16_t raw_charge_rate; + uint16_t raw_vcell; +}; + +#ifdef __cplusplus +} +#endif diff --git a/app/module/dts/bindings/sensor/maxim,max17048.yml b/app/module/dts/bindings/sensor/maxim,max17048.yml new file mode 100644 index 00000000000..786f4b8686d --- /dev/null +++ b/app/module/dts/bindings/sensor/maxim,max17048.yml @@ -0,0 +1,12 @@ +# +# Copyright (c) 2022 The ZMK Contributors +# +# SPDX-License-Identifier: MIT +# + +description: > + This is a representation of the Maxim max17048 I2C Fuel Gauge. + +compatible: "maxim,max17048" + +include: [i2c-device.yaml] From 07c82836e032ee4db64098e3b0aa26295b29b989 Mon Sep 17 00:00:00 2001 From: ClicketySplit <101202583+ClicketySplit@users.noreply.github.com> Date: Thu, 28 Sep 2023 09:54:08 -0600 Subject: [PATCH 02/49] fix(shields): Leeloo-Micro set status disabled for encoders. --- app/boards/shields/leeloo_micro/leeloo_micro.dtsi | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/boards/shields/leeloo_micro/leeloo_micro.dtsi b/app/boards/shields/leeloo_micro/leeloo_micro.dtsi index afe16dfa605..ab68a6153f1 100644 --- a/app/boards/shields/leeloo_micro/leeloo_micro.dtsi +++ b/app/boards/shields/leeloo_micro/leeloo_micro.dtsi @@ -48,7 +48,8 @@ RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(3,4) RC(3,5) RC(2,5) RC(2,6) RC(2,7) label = "LEFT_ENCODER"; a-gpios = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - steps = <120>; + status = "disabled"; + steps = <60>; }; right_encoder: right_encoder { @@ -56,7 +57,8 @@ RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(3,4) RC(3,5) RC(2,5) RC(2,6) RC(2,7) label = "RIGHT_ENCODER"; a-gpios = <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - steps = <120>; + status = "disabled"; + steps = <60>; }; sensors { From 9b3d2cb99fe806db5897063e372a5ae45a58b0ec Mon Sep 17 00:00:00 2001 From: zhiayang <500236+zhiayang@users.noreply.github.com> Date: Thu, 28 Sep 2023 12:19:14 -0400 Subject: [PATCH 03/49] fix(driver): Fix broken compilation for MAX7318 driver --- app/module/drivers/gpio/gpio_max7318.c | 15 +++++++-------- app/module/dts/bindings/gpio/maxim,max7318.yaml | 3 --- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/module/drivers/gpio/gpio_max7318.c b/app/module/drivers/gpio/gpio_max7318.c index 04424b48b09..1842ce7b2d6 100644 --- a/app/module/drivers/gpio/gpio_max7318.c +++ b/app/module/drivers/gpio/gpio_max7318.c @@ -12,16 +12,15 @@ #include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #define LOG_LEVEL CONFIG_GPIO_LOG_LEVEL -#include +#include LOG_MODULE_REGISTER(gpio_max7318); diff --git a/app/module/dts/bindings/gpio/maxim,max7318.yaml b/app/module/dts/bindings/gpio/maxim,max7318.yaml index 94952813edd..6c309768d44 100644 --- a/app/module/dts/bindings/gpio/maxim,max7318.yaml +++ b/app/module/dts/bindings/gpio/maxim,max7318.yaml @@ -12,9 +12,6 @@ compatible: "maxim,max7318" include: [gpio-controller.yaml, i2c-device.yaml] properties: - label: - required: true - "#gpio-cells": const: 2 From c1ebadcd2a1b7cad47c2224a4fb6130cda80b7e3 Mon Sep 17 00:00:00 2001 From: ReFil <31960031+ReFil@users.noreply.github.com> Date: Fri, 29 Sep 2023 21:14:31 +0100 Subject: [PATCH 04/49] feat(hid): Add apple globe keycode * feat(hid): Add apple globe keycode * Update docs/src/data/hid.js Co-authored-by: Cem Aksoylar Co-authored-by: Pete Johanson Co-authored-by: Nick Coutsos --- app/include/dt-bindings/zmk/keys.h | 4 ++++ docs/src/data/groups.js | 1 + docs/src/data/hid.js | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/app/include/dt-bindings/zmk/keys.h b/app/include/dt-bindings/zmk/keys.h index 3e67c402470..364ffa8647a 100644 --- a/app/include/dt-bindings/zmk/keys.h +++ b/app/include/dt-bindings/zmk/keys.h @@ -1439,3 +1439,7 @@ #define C_KEYBOARD_INPUT_ASSIST_CANCEL \ (ZMK_HID_USAGE(HID_USAGE_CONSUMER, HID_USAGE_CONSUMER_KEYBOARD_INPUT_ASSIST_CANCEL)) #define C_KBIA_CANCEL (C_KEYBOARD_INPUT_ASSIST_CANCEL) + +/* Apple Globe key */ +#define C_AC_NEXT_KEYBOARD_LAYOUT_SELECT (ZMK_HID_USAGE(HID_USAGE_CONSUMER, 0x029D)) +#define GLOBE (C_AC_NEXT_KEYBOARD_LAYOUT_SELECT) diff --git a/docs/src/data/groups.js b/docs/src/data/groups.js index 0eb15d27fc5..5a8dc3cfe75 100644 --- a/docs/src/data/groups.js +++ b/docs/src/data/groups.js @@ -49,6 +49,7 @@ export default { "C_AC_DESKTOP_SHOW_ALL_WINDOWS", "C_AC_DESKTOP_SHOW_ALL_APPLICATIONS", "C_VOICE_COMMAND", + "C_AC_NEXT_KEYBOARD_LAYOUT_SELECT", ], applications: [ "C_AL_NEXT_TASK", diff --git a/docs/src/data/hid.js b/docs/src/data/hid.js index 457671728f8..fc61555c8eb 100644 --- a/docs/src/data/hid.js +++ b/docs/src/data/hid.js @@ -7865,4 +7865,25 @@ export default [ }, footnotes: {}, }, + { + names: ["C_AC_NEXT_KEYBOARD_LAYOUT_SELECT", "GLOBE"], + description: "AC Next Keyboard Layout Select (Apple Globe)", + context: "Consumer AC", + clarify: true, + usages: [ + { + application: consumerApplication, + item: usage(consumerPage, 0x29d), + }, + ], + documentation: "https://usb.org/sites/default/files/hut1_2.pdf#page=153", + os: { + windows: null, + linux: null, + android: null, + macos: true, + ios: true, + }, + footnotes: {}, + }, ]; From 933e369d7cacd2680cd8c1355eb7e4a35b4e34f9 Mon Sep 17 00:00:00 2001 From: Andrew Rae Date: Mon, 29 May 2023 22:48:38 -0400 Subject: [PATCH 05/49] feat(core): Adding pre-release for keys that were already pressed. This fixes #1207 and #1076 (and maybe more?). --- app/src/hid_listener.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/hid_listener.c b/app/src/hid_listener.c index 3a11101d54f..796ec19b7a4 100644 --- a/app/src/hid_listener.c +++ b/app/src/hid_listener.c @@ -19,6 +19,20 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); static int hid_listener_keycode_pressed(const struct zmk_keycode_state_changed *ev) { int err, explicit_mods_changed, implicit_mods_changed; + if (zmk_hid_is_pressed(ZMK_HID_USAGE(ev->usage_page, ev->keycode))) { + LOG_DBG("unregistering usage_page 0x%02X keycode 0x%02X since it was already pressed", + ev->usage_page, ev->keycode); + err = zmk_hid_release(ZMK_HID_USAGE(ev->usage_page, ev->keycode)); + if (err < 0) { + LOG_DBG("Unable to pre-release keycode (%d)", err); + return err; + } + err = zmk_endpoints_send_report(ev->usage_page); + if (err < 0) { + LOG_ERR("Failed to send key report for pre-releasing keycode (%d)", err); + } + } + LOG_DBG("usage_page 0x%02X keycode 0x%02X implicit_mods 0x%02X explicit_mods 0x%02X", ev->usage_page, ev->keycode, ev->implicit_modifiers, ev->explicit_modifiers); err = zmk_hid_press(ZMK_HID_USAGE(ev->usage_page, ev->keycode)); From dffdb2365e1ff977020a1c62967512b196edee2c Mon Sep 17 00:00:00 2001 From: Andrew Rae Date: Tue, 6 Jun 2023 20:42:52 -0400 Subject: [PATCH 06/49] test(core): Adding coverage for key pre-releasing. Added cases for the two use cases I know of: 1. Rolling with key-repeat behavior 2. Rolling symbols that have the same base key, eg `+=` --- .../tap-when-rolling/events.patterns | 2 ++ .../tap-when-rolling/keycode_events.snapshot | 9 +++++++ .../tap-when-rolling/native_posix.keymap | 13 +++++++++ .../tap-when-rolling/native_posix_64.keymap | 13 +++++++++ .../events.patterns | 4 +++ .../keycode_events.snapshot | 9 +++++++ .../native_posix_64.keymap | 27 +++++++++++++++++++ 7 files changed, 77 insertions(+) create mode 100644 app/tests/key-repeat/tap-when-rolling/events.patterns create mode 100644 app/tests/key-repeat/tap-when-rolling/keycode_events.snapshot create mode 100644 app/tests/key-repeat/tap-when-rolling/native_posix.keymap create mode 100644 app/tests/key-repeat/tap-when-rolling/native_posix_64.keymap create mode 100644 app/tests/modifiers/implicit/kp-rolling-symbols-same-key/events.patterns create mode 100644 app/tests/modifiers/implicit/kp-rolling-symbols-same-key/keycode_events.snapshot create mode 100644 app/tests/modifiers/implicit/kp-rolling-symbols-same-key/native_posix_64.keymap diff --git a/app/tests/key-repeat/tap-when-rolling/events.patterns b/app/tests/key-repeat/tap-when-rolling/events.patterns new file mode 100644 index 00000000000..794719239fc --- /dev/null +++ b/app/tests/key-repeat/tap-when-rolling/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*hid_implicit_modifiers_//p \ No newline at end of file diff --git a/app/tests/key-repeat/tap-when-rolling/keycode_events.snapshot b/app/tests/key-repeat/tap-when-rolling/keycode_events.snapshot new file mode 100644 index 00000000000..7d54e9de9b2 --- /dev/null +++ b/app/tests/key-repeat/tap-when-rolling/keycode_events.snapshot @@ -0,0 +1,9 @@ +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +press: Modifiers set to 0x00 +pressed: unregistering usage_page 0x07 keycode 0x04 since it was already pressed +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +press: Modifiers set to 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +release: Modifiers set to 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +release: Modifiers set to 0x00 diff --git a/app/tests/key-repeat/tap-when-rolling/native_posix.keymap b/app/tests/key-repeat/tap-when-rolling/native_posix.keymap new file mode 100644 index 00000000000..bb129d14c7a --- /dev/null +++ b/app/tests/key-repeat/tap-when-rolling/native_posix.keymap @@ -0,0 +1,13 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/key-repeat/tap-when-rolling/native_posix_64.keymap b/app/tests/key-repeat/tap-when-rolling/native_posix_64.keymap new file mode 100644 index 00000000000..a947f7ab0ed --- /dev/null +++ b/app/tests/key-repeat/tap-when-rolling/native_posix_64.keymap @@ -0,0 +1,13 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,1,9000) + ZMK_MOCK_PRESS(0,0,30) + ZMK_MOCK_RELEASE(0,1,30) + ZMK_MOCK_RELEASE(0,0,3000) + >; +}; diff --git a/app/tests/modifiers/implicit/kp-rolling-symbols-same-key/events.patterns b/app/tests/modifiers/implicit/kp-rolling-symbols-same-key/events.patterns new file mode 100644 index 00000000000..cbf21aff278 --- /dev/null +++ b/app/tests/modifiers/implicit/kp-rolling-symbols-same-key/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode_//p +s/.*hid_register_mod/reg/p +s/.*hid_unregister_mod/unreg/p +s/.*zmk_hid_.*Modifiers set to /mods: Modifiers set to /p \ No newline at end of file diff --git a/app/tests/modifiers/implicit/kp-rolling-symbols-same-key/keycode_events.snapshot b/app/tests/modifiers/implicit/kp-rolling-symbols-same-key/keycode_events.snapshot new file mode 100644 index 00000000000..0b06bd91d88 --- /dev/null +++ b/app/tests/modifiers/implicit/kp-rolling-symbols-same-key/keycode_events.snapshot @@ -0,0 +1,9 @@ +pressed: usage_page 0x07 keycode 0x2E implicit_mods 0x02 explicit_mods 0x00 +mods: Modifiers set to 0x02 +pressed: unregistering usage_page 0x07 keycode 0x2E since it was already pressed +pressed: usage_page 0x07 keycode 0x2E implicit_mods 0x00 explicit_mods 0x00 +mods: Modifiers set to 0x00 +released: usage_page 0x07 keycode 0x2E implicit_mods 0x02 explicit_mods 0x00 +mods: Modifiers set to 0x00 +released: usage_page 0x07 keycode 0x2E implicit_mods 0x00 explicit_mods 0x00 +mods: Modifiers set to 0x00 diff --git a/app/tests/modifiers/implicit/kp-rolling-symbols-same-key/native_posix_64.keymap b/app/tests/modifiers/implicit/kp-rolling-symbols-same-key/native_posix_64.keymap new file mode 100644 index 00000000000..3926eb57a29 --- /dev/null +++ b/app/tests/modifiers/implicit/kp-rolling-symbols-same-key/native_posix_64.keymap @@ -0,0 +1,27 @@ +#include +#include +#include + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; + +/ { + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp PLUS &kp EQUAL + &none &none + >; + }; + }; +}; From 2f05ad55ca9734f1cc890dcd659d0691ce327658 Mon Sep 17 00:00:00 2001 From: Andrew Rae Date: Sat, 16 Sep 2023 17:24:39 -0400 Subject: [PATCH 07/49] fix(core): Prevent pre-releasing explicit mods. --- app/src/hid_listener.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/hid_listener.c b/app/src/hid_listener.c index 796ec19b7a4..2b8470820a1 100644 --- a/app/src/hid_listener.c +++ b/app/src/hid_listener.c @@ -19,7 +19,8 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); static int hid_listener_keycode_pressed(const struct zmk_keycode_state_changed *ev) { int err, explicit_mods_changed, implicit_mods_changed; - if (zmk_hid_is_pressed(ZMK_HID_USAGE(ev->usage_page, ev->keycode))) { + if (!is_mod(ev->usage_page, ev->keycode) && + zmk_hid_is_pressed(ZMK_HID_USAGE(ev->usage_page, ev->keycode))) { LOG_DBG("unregistering usage_page 0x%02X keycode 0x%02X since it was already pressed", ev->usage_page, ev->keycode); err = zmk_hid_release(ZMK_HID_USAGE(ev->usage_page, ev->keycode)); From 651ed05e9aab061703fb6e3736c974702310a9e1 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sun, 14 May 2023 11:55:21 -0500 Subject: [PATCH 08/49] refactor: Split endpoint to transport and instance Changed the endpoints code to rename the existing endpoint types to "transport" and add the concept of "endpoint instances". A transport is the method by which data is sent, while instances allow describing multiple endpoints that use the same transport (e.g. bluetooth profiles) Also added new APIs to get the total number of possible endpoint instances and assign each instance a unique index, which can be used for tracking separate state for each endpoint in other code files. --- .../arm/corneish_zen/widgets/output_status.c | 33 ++- app/include/zmk/endpoints.h | 62 ++++- app/include/zmk/endpoints_types.h | 32 ++- ...selection_changed.h => endpoint_changed.h} | 6 +- app/src/behaviors/behavior_outputs.c | 6 +- app/src/display/widgets/output_status.c | 29 +-- app/src/endpoints.c | 220 ++++++++++++------ app/src/events/endpoint_selection_changed.c | 4 +- 8 files changed, 268 insertions(+), 124 deletions(-) rename app/include/zmk/events/{endpoint_selection_changed.h => endpoint_changed.h} (61%) diff --git a/app/boards/arm/corneish_zen/widgets/output_status.c b/app/boards/arm/corneish_zen/widgets/output_status.c index ad0c2b1a8fb..bdf90cc3d7f 100644 --- a/app/boards/arm/corneish_zen/widgets/output_status.c +++ b/app/boards/arm/corneish_zen/widgets/output_status.c @@ -14,9 +14,8 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include "output_status.h" #include -#include #include -#include +#include #include #include #include @@ -39,31 +38,31 @@ LV_IMG_DECLARE(USB_connected); static sys_slist_t widgets = SYS_SLIST_STATIC_INIT(&widgets); struct output_status_state { - enum zmk_endpoint selected_endpoint; + struct zmk_endpoint_instance selected_endpoint; bool active_profile_connected; bool active_profile_bonded; uint8_t active_profile_index; }; static struct output_status_state get_state(const zmk_event_t *_eh) { - return (struct output_status_state){.selected_endpoint = zmk_endpoints_selected(), - .active_profile_connected = - zmk_ble_active_profile_is_connected(), - .active_profile_bonded = !zmk_ble_active_profile_is_open(), - .active_profile_index = zmk_ble_active_profile_index()}; + return (struct output_status_state){ + .selected_endpoint = zmk_endpoints_selected(), + .active_profile_connected = zmk_ble_active_profile_is_connected(), + .active_profile_bonded = !zmk_ble_active_profile_is_open(), + }; ; } static void set_status_symbol(lv_obj_t *icon, struct output_status_state state) { - switch (state.selected_endpoint) { - case ZMK_ENDPOINT_USB: + switch (state.selected_endpoint.transport) { + case ZMK_TRANSPORT_USB: lv_img_set_src(icon, &USB_connected); break; - case ZMK_ENDPOINT_BLE: + case ZMK_TRANSPORT_BLE: if (state.active_profile_bonded) { if (state.active_profile_connected) { // sprintf(text, LV_SYMBOL_BLUETOOTH "%i " LV_SYMBOL_OK, active_profile_index); - switch (state.active_profile_index) { + switch (state.selected_endpoint.ble.profile_index) { case 0: lv_img_set_src(icon, &bluetooth_connected_1); break; @@ -84,7 +83,7 @@ static void set_status_symbol(lv_obj_t *icon, struct output_status_state state) lv_img_set_src(icon, &bluetooth_disconnected_right); } } else { - switch (state.active_profile_index) { + switch (state.selected_endpoint.ble.profile_index) { case 0: lv_img_set_src(icon, &bluetooth_advertising_1); break; @@ -113,11 +112,9 @@ static void output_status_update_cb(struct output_status_state state) { ZMK_DISPLAY_WIDGET_LISTENER(widget_output_status, struct output_status_state, output_status_update_cb, get_state) -ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_selection_changed); - -#if IS_ENABLED(CONFIG_USB_DEVICE_STACK) -ZMK_SUBSCRIPTION(widget_output_status, zmk_usb_conn_state_changed); -#endif +ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_changed); +// We don't get an endpoint changed event when the active profile connects/disconnects +// but there wasn't another endpoint to switch from/to, so update on BLE events too. #if defined(CONFIG_ZMK_BLE) ZMK_SUBSCRIPTION(widget_output_status, zmk_ble_active_profile_changed); #endif diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h index c8860533e1d..c5964ff8535 100644 --- a/app/include/zmk/endpoints.h +++ b/app/include/zmk/endpoints.h @@ -6,10 +6,66 @@ #pragma once +#include #include -int zmk_endpoints_select(enum zmk_endpoint endpoint); -int zmk_endpoints_toggle(); -enum zmk_endpoint zmk_endpoints_selected(); +/** + * Recommended length of string buffer for printing endpoint identifiers. + */ +#define ZMK_ENDPOINT_STR_LEN 10 + +#ifdef CONFIG_ZMK_USB +#define ZMK_ENDPOINT_USB_COUNT 1 +#else +#define ZMK_ENDPOINT_USB_COUNT 0 +#endif + +#ifdef CONFIG_ZMK_BLE +#define ZMK_ENDPOINT_BLE_COUNT ZMK_BLE_PROFILE_COUNT +#else +#define ZMK_ENDPOINT_BLE_COUNT 0 +#endif + +/** + * The total number of different (struct zmk_endpoint_instance) values that can + * be selected. + * + * Note that this value may change between firmware versions, so it should not + * be used in any persistent storage. + */ +#define ZMK_ENDPOINT_COUNT (ZMK_ENDPOINT_USB_COUNT + ZMK_ENDPOINT_BLE_COUNT) + +bool zmk_endpoint_instance_eq(struct zmk_endpoint_instance a, struct zmk_endpoint_instance b); + +/** + * Writes a string identifying an endpoint instance. + * + * @param str Address of output string buffer + * @param len Length of string buffer. See ZMK_ENDPOINT_STR_LEN for recommended length. + * + * @returns Number of characters written. + */ +int zmk_endpoint_instance_to_str(struct zmk_endpoint_instance endpoint, char *str, size_t len); + +/** + * Gets a unique index for an endpoint instance. This can be used together with + * ZMK_ENDPOINT_COUNT to manage separate state for each endpoint instance. + * + * Note that the index for a specific instance may change between firmware versions, + * so it should not be used in any persistent storage. + */ +int zmk_endpoint_instance_to_index(struct zmk_endpoint_instance endpoint); + +/** + * Sets the preferred endpoint transport to use. (If the preferred endpoint is + * not available, a different one may automatically be selected.) + */ +int zmk_endpoints_select_transport(enum zmk_transport transport); +int zmk_endpoints_toggle_transport(void); + +/** + * Gets the currently-selected endpoint. + */ +struct zmk_endpoint_instance zmk_endpoints_selected(void); int zmk_endpoints_send_report(uint16_t usage_page); diff --git a/app/include/zmk/endpoints_types.h b/app/include/zmk/endpoints_types.h index 70804a613d8..ea51c8aa93d 100644 --- a/app/include/zmk/endpoints_types.h +++ b/app/include/zmk/endpoints_types.h @@ -6,7 +6,33 @@ #pragma once -enum zmk_endpoint { - ZMK_ENDPOINT_USB, - ZMK_ENDPOINT_BLE, +/** + * The method by which data is sent. + */ +enum zmk_transport { + ZMK_TRANSPORT_USB, + ZMK_TRANSPORT_BLE, +}; + +/** + * Configuration to select an endpoint on ZMK_TRANSPORT_USB. + */ +struct zmk_transport_usb_data {}; + +/** + * Configuration to select an endpoint on ZMK_TRANSPORT_BLE. + */ +struct zmk_transport_ble_data { + int profile_index; +}; + +/** + * A specific endpoint to which data may be sent. + */ +struct zmk_endpoint_instance { + enum zmk_transport transport; + union { + struct zmk_transport_usb_data usb; // ZMK_TRANSPORT_USB + struct zmk_transport_ble_data ble; // ZMK_TRANSPORT_BLE + }; }; diff --git a/app/include/zmk/events/endpoint_selection_changed.h b/app/include/zmk/events/endpoint_changed.h similarity index 61% rename from app/include/zmk/events/endpoint_selection_changed.h rename to app/include/zmk/events/endpoint_changed.h index 198fe5a1688..2147009b84b 100644 --- a/app/include/zmk/events/endpoint_selection_changed.h +++ b/app/include/zmk/events/endpoint_changed.h @@ -11,8 +11,8 @@ #include #include -struct zmk_endpoint_selection_changed { - enum zmk_endpoint endpoint; +struct zmk_endpoint_changed { + struct zmk_endpoint_instance endpoint; }; -ZMK_EVENT_DECLARE(zmk_endpoint_selection_changed); +ZMK_EVENT_DECLARE(zmk_endpoint_changed); diff --git a/app/src/behaviors/behavior_outputs.c b/app/src/behaviors/behavior_outputs.c index 7aab8ee32c0..6ae81a0fdd7 100644 --- a/app/src/behaviors/behavior_outputs.c +++ b/app/src/behaviors/behavior_outputs.c @@ -24,11 +24,11 @@ static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, struct zmk_behavior_binding_event event) { switch (binding->param1) { case OUT_TOG: - return zmk_endpoints_toggle(); + return zmk_endpoints_toggle_transport(); case OUT_USB: - return zmk_endpoints_select(ZMK_ENDPOINT_USB); + return zmk_endpoints_select_transport(ZMK_TRANSPORT_USB); case OUT_BLE: - return zmk_endpoints_select(ZMK_ENDPOINT_BLE); + return zmk_endpoints_select_transport(ZMK_TRANSPORT_BLE); default: LOG_ERR("Unknown output command: %d", binding->param1); } diff --git a/app/src/display/widgets/output_status.c b/app/src/display/widgets/output_status.c index 1c6da4b9034..da29a95f393 100644 --- a/app/src/display/widgets/output_status.c +++ b/app/src/display/widgets/output_status.c @@ -12,9 +12,8 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include -#include #include -#include +#include #include #include #include @@ -22,40 +21,38 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); static sys_slist_t widgets = SYS_SLIST_STATIC_INIT(&widgets); struct output_status_state { - enum zmk_endpoint selected_endpoint; + struct zmk_endpoint_instance selected_endpoint; bool active_profile_connected; bool active_profile_bonded; - uint8_t active_profile_index; }; static struct output_status_state get_state(const zmk_event_t *_eh) { return (struct output_status_state){.selected_endpoint = zmk_endpoints_selected(), .active_profile_connected = zmk_ble_active_profile_is_connected(), - .active_profile_bonded = !zmk_ble_active_profile_is_open(), - .active_profile_index = zmk_ble_active_profile_index()}; + .active_profile_bonded = !zmk_ble_active_profile_is_open()}; ; } static void set_status_symbol(lv_obj_t *label, struct output_status_state state) { char text[10] = {}; - switch (state.selected_endpoint) { - case ZMK_ENDPOINT_USB: + switch (state.selected_endpoint.transport) { + case ZMK_TRANSPORT_USB: strcat(text, LV_SYMBOL_USB); break; - case ZMK_ENDPOINT_BLE: + case ZMK_TRANSPORT_BLE: if (state.active_profile_bonded) { if (state.active_profile_connected) { snprintf(text, sizeof(text), LV_SYMBOL_WIFI " %i " LV_SYMBOL_OK, - state.active_profile_index + 1); + state.selected_endpoint.ble.profile_index + 1); } else { snprintf(text, sizeof(text), LV_SYMBOL_WIFI " %i " LV_SYMBOL_CLOSE, - state.active_profile_index + 1); + state.selected_endpoint.ble.profile_index + 1); } } else { snprintf(text, sizeof(text), LV_SYMBOL_WIFI " %i " LV_SYMBOL_SETTINGS, - state.active_profile_index + 1); + state.selected_endpoint.ble.profile_index + 1); } break; } @@ -70,11 +67,9 @@ static void output_status_update_cb(struct output_status_state state) { ZMK_DISPLAY_WIDGET_LISTENER(widget_output_status, struct output_status_state, output_status_update_cb, get_state) -ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_selection_changed); - -#if IS_ENABLED(CONFIG_USB_DEVICE_STACK) -ZMK_SUBSCRIPTION(widget_output_status, zmk_usb_conn_state_changed); -#endif +ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_changed); +// We don't get an endpoint changed event when the active profile connects/disconnects +// but there wasn't another endpoint to switch from/to, so update on BLE events too. #if defined(CONFIG_ZMK_BLE) ZMK_SUBSCRIPTION(widget_output_status, zmk_ble_active_profile_changed); #endif diff --git a/app/src/endpoints.c b/app/src/endpoints.c index dbd1a3e6c0e..e208a36a312 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -16,29 +16,29 @@ #include #include #include -#include +#include #include LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); -#define DEFAULT_ENDPOINT \ - COND_CODE_1(IS_ENABLED(CONFIG_ZMK_BLE), (ZMK_ENDPOINT_BLE), (ZMK_ENDPOINT_USB)) +#define DEFAULT_TRANSPORT \ + COND_CODE_1(IS_ENABLED(CONFIG_ZMK_BLE), (ZMK_TRANSPORT_BLE), (ZMK_TRANSPORT_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 struct zmk_endpoint_instance current_instance = {}; +static enum zmk_transport preferred_transport = + ZMK_TRANSPORT_USB; /* Used if multiple endpoints are ready */ -static void update_current_endpoint(); +static void update_current_endpoint(void); #if IS_ENABLED(CONFIG_SETTINGS) static void endpoints_save_preferred_work(struct k_work *work) { - settings_save_one("endpoints/preferred", &preferred_endpoint, sizeof(preferred_endpoint)); + settings_save_one("endpoints/preferred", &preferred_transport, sizeof(preferred_transport)); } static struct k_work_delayable endpoints_save_work; #endif -static int endpoints_save_preferred() { +static int endpoints_save_preferred(void) { #if IS_ENABLED(CONFIG_SETTINGS) return k_work_reschedule(&endpoints_save_work, K_MSEC(CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE)); #else @@ -46,14 +46,60 @@ static int endpoints_save_preferred() { #endif } -int zmk_endpoints_select(enum zmk_endpoint endpoint) { - LOG_DBG("Selected endpoint %d", endpoint); +bool zmk_endpoint_instance_eq(struct zmk_endpoint_instance a, struct zmk_endpoint_instance b) { + if (a.transport != b.transport) { + return false; + } + + switch (a.transport) { + case ZMK_TRANSPORT_USB: + return true; + + case ZMK_TRANSPORT_BLE: + return a.ble.profile_index == b.ble.profile_index; + } + + LOG_ERR("Invalid transport %d", a.transport); + return false; +} + +int zmk_endpoint_instance_to_str(struct zmk_endpoint_instance endpoint, char *str, size_t len) { + switch (endpoint.transport) { + case ZMK_TRANSPORT_USB: + return snprintf(str, len, "USB"); + + case ZMK_TRANSPORT_BLE: + return snprintf(str, len, "BLE:%d", endpoint.ble.profile_index); + + default: + return snprintf(str, len, "Invalid"); + } +} + +#define INSTANCE_INDEX_OFFSET_USB 0 +#define INSTANCE_INDEX_OFFSET_BLE ZMK_ENDPOINT_USB_COUNT + +int zmk_endpoint_instance_to_index(struct zmk_endpoint_instance endpoint) { + switch (endpoint.transport) { + case ZMK_TRANSPORT_USB: + return INSTANCE_INDEX_OFFSET_USB; + + case ZMK_TRANSPORT_BLE: + return INSTANCE_INDEX_OFFSET_BLE + endpoint.ble.profile_index; + } + + LOG_ERR("Invalid transport %d", endpoint.transport); + return 0; +} - if (preferred_endpoint == endpoint) { +int zmk_endpoints_select_transport(enum zmk_transport transport) { + LOG_DBG("Selected endpoint transport %d", transport); + + if (preferred_transport == transport) { return 0; } - preferred_endpoint = endpoint; + preferred_transport = transport; endpoints_save_preferred(); @@ -62,20 +108,22 @@ int zmk_endpoints_select(enum zmk_endpoint endpoint) { return 0; } -enum zmk_endpoint zmk_endpoints_selected() { return current_endpoint; } +int zmk_endpoints_toggle_transport(void) { + enum zmk_transport new_transport = + (preferred_transport == ZMK_TRANSPORT_USB) ? ZMK_TRANSPORT_BLE : ZMK_TRANSPORT_USB; + return zmk_endpoints_select_transport(new_transport); +} -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); +struct zmk_endpoint_instance zmk_endpoints_selected(void) { + return current_instance; } -static int send_keyboard_report() { +static int send_keyboard_report(void) { struct zmk_hid_keyboard_report *keyboard_report = zmk_hid_get_keyboard_report(); - switch (current_endpoint) { + switch (current_instance.transport) { #if IS_ENABLED(CONFIG_ZMK_USB) - case ZMK_ENDPOINT_USB: { + case ZMK_TRANSPORT_USB: { int err = zmk_usb_hid_send_report((uint8_t *)keyboard_report, sizeof(*keyboard_report)); if (err) { LOG_ERR("FAILED TO SEND OVER USB: %d", err); @@ -85,7 +133,7 @@ static int send_keyboard_report() { #endif /* IS_ENABLED(CONFIG_ZMK_USB) */ #if IS_ENABLED(CONFIG_ZMK_BLE) - case ZMK_ENDPOINT_BLE: { + case ZMK_TRANSPORT_BLE: { int err = zmk_hog_send_keyboard_report(&keyboard_report->body); if (err) { LOG_ERR("FAILED TO SEND OVER HOG: %d", err); @@ -93,19 +141,18 @@ static int send_keyboard_report() { return err; } #endif /* IS_ENABLED(CONFIG_ZMK_BLE) */ - - default: - LOG_ERR("Unsupported endpoint %d", current_endpoint); - return -ENOTSUP; } + + LOG_ERR("Unsupported endpoint transport %d", current_instance.transport); + return -ENOTSUP; } -static int send_consumer_report() { +static int send_consumer_report(void) { struct zmk_hid_consumer_report *consumer_report = zmk_hid_get_consumer_report(); - switch (current_endpoint) { + switch (current_instance.transport) { #if IS_ENABLED(CONFIG_ZMK_USB) - case ZMK_ENDPOINT_USB: { + case ZMK_TRANSPORT_USB: { int err = zmk_usb_hid_send_report((uint8_t *)consumer_report, sizeof(*consumer_report)); if (err) { LOG_ERR("FAILED TO SEND OVER USB: %d", err); @@ -115,7 +162,7 @@ static int send_consumer_report() { #endif /* IS_ENABLED(CONFIG_ZMK_USB) */ #if IS_ENABLED(CONFIG_ZMK_BLE) - case ZMK_ENDPOINT_BLE: { + case ZMK_TRANSPORT_BLE: { int err = zmk_hog_send_consumer_report(&consumer_report->body); if (err) { LOG_ERR("FAILED TO SEND OVER HOG: %d", err); @@ -123,11 +170,10 @@ static int send_consumer_report() { return err; } #endif /* IS_ENABLED(CONFIG_ZMK_BLE) */ - - default: - LOG_ERR("Unsupported endpoint %d", current_endpoint); - return -ENOTSUP; } + + LOG_ERR("Unsupported endpoint transport %d", current_instance.transport); + return -ENOTSUP; } int zmk_endpoints_send_report(uint16_t usage_page) { @@ -136,12 +182,13 @@ int zmk_endpoints_send_report(uint16_t usage_page) { switch (usage_page) { case HID_USAGE_KEY: return send_keyboard_report(); + case HID_USAGE_CONSUMER: return send_consumer_report(); - default: - LOG_ERR("Unsupported usage page %d", usage_page); - return -ENOTSUP; } + + LOG_ERR("Unsupported usage page %d", usage_page); + return -ENOTSUP; } #if IS_ENABLED(CONFIG_SETTINGS) @@ -151,12 +198,12 @@ static int endpoints_handle_set(const char *name, size_t len, settings_read_cb r LOG_DBG("Setting endpoint value %s", 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)); + if (len != sizeof(enum zmk_transport)) { + LOG_ERR("Invalid endpoint size (got %d expected %d)", len, sizeof(enum zmk_transport)); return -EINVAL; } - int err = read_cb(cb_arg, &preferred_endpoint, sizeof(enum zmk_endpoint)); + int err = read_cb(cb_arg, &preferred_transport, sizeof(enum zmk_transport)); if (err <= 0) { LOG_ERR("Failed to read preferred endpoint from settings (err %d)", err); return err; @@ -171,25 +218,7 @@ static int endpoints_handle_set(const char *name, size_t len, settings_read_cb r struct settings_handler endpoints_handler = {.name = "endpoints", .h_set = endpoints_handle_set}; #endif /* IS_ENABLED(CONFIG_SETTINGS) */ -static int zmk_endpoints_init(const 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; - } - - k_work_init_delayable(&endpoints_save_work, endpoints_save_preferred_work); - - settings_load_subtree("endpoints"); -#endif - - return 0; -} - -static bool is_usb_ready() { +static bool is_usb_ready(void) { #if IS_ENABLED(CONFIG_ZMK_USB) return zmk_usb_is_hid_ready(); #else @@ -197,7 +226,7 @@ static bool is_usb_ready() { #endif } -static bool is_ble_ready() { +static bool is_ble_ready(void) { #if IS_ENABLED(CONFIG_ZMK_BLE) return zmk_ble_active_profile_is_connected(); #else @@ -205,24 +234,62 @@ static bool is_ble_ready() { #endif } -static enum zmk_endpoint get_selected_endpoint() { +static enum zmk_transport get_selected_transport(void) { if (is_ble_ready()) { if (is_usb_ready()) { - LOG_DBG("Both endpoints are ready. Using %d", preferred_endpoint); - return preferred_endpoint; + LOG_DBG("Both endpoint transports are ready. Using %d", preferred_transport); + return preferred_transport; } LOG_DBG("Only BLE is ready."); - return ZMK_ENDPOINT_BLE; + return ZMK_TRANSPORT_BLE; } if (is_usb_ready()) { LOG_DBG("Only USB is ready."); - return ZMK_ENDPOINT_USB; + return ZMK_TRANSPORT_USB; + } + + LOG_DBG("No endpoint transports are ready."); + return DEFAULT_TRANSPORT; +} + +static struct zmk_endpoint_instance get_selected_instance(void) { + struct zmk_endpoint_instance instance = {.transport = get_selected_transport()}; + + switch (instance.transport) { +#if IS_ENABLED(CONFIG_ZMK_BLE) + case ZMK_TRANSPORT_BLE: + instance.ble.profile_index = zmk_ble_active_profile_index(); + break; +#endif // IS_ENABLED(CONFIG_ZMK_BLE) + + default: + // No extra data for this transport. + break; + } + + return instance; +} + +static int zmk_endpoints_init(const 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; } - LOG_DBG("No endpoints are ready."); - return DEFAULT_ENDPOINT; + k_work_init_delayable(&endpoints_save_work, endpoints_save_preferred_work); + + settings_load_subtree("endpoints"); +#endif + + current_instance = get_selected_instance(); + + return 0; } static void disconnect_current_endpoint() { @@ -233,18 +300,21 @@ static void disconnect_current_endpoint() { zmk_endpoints_send_report(HID_USAGE_CONSUMER); } -static void update_current_endpoint() { - enum zmk_endpoint new_endpoint = get_selected_endpoint(); +static void update_current_endpoint(void) { + struct zmk_endpoint_instance new_instance = get_selected_instance(); - if (new_endpoint != current_endpoint) { - /* Cancel all current keypresses so keys don't stay held on the old endpoint. */ + if (!zmk_endpoint_instance_eq(new_instance, current_instance)) { + // Cancel all current keypresses so keys don't stay held on the old endpoint. disconnect_current_endpoint(); - current_endpoint = new_endpoint; - LOG_INF("Endpoint changed: %d", current_endpoint); + current_instance = new_instance; + + char endpoint_str[ZMK_ENDPOINT_STR_LEN]; + zmk_endpoint_instance_to_str(current_instance, endpoint_str, sizeof(endpoint_str)); + LOG_INF("Endpoint changed: %s", endpoint_str); - ZMK_EVENT_RAISE(new_zmk_endpoint_selection_changed( - (struct zmk_endpoint_selection_changed){.endpoint = current_endpoint})); + ZMK_EVENT_RAISE( + new_zmk_endpoint_changed((struct zmk_endpoint_changed){.endpoint = current_instance})); } } diff --git a/app/src/events/endpoint_selection_changed.c b/app/src/events/endpoint_selection_changed.c index 34bc39dd2ab..6b152156feb 100644 --- a/app/src/events/endpoint_selection_changed.c +++ b/app/src/events/endpoint_selection_changed.c @@ -5,6 +5,6 @@ */ #include -#include +#include -ZMK_EVENT_IMPL(zmk_endpoint_selection_changed); +ZMK_EVENT_IMPL(zmk_endpoint_changed); From b17d896c5c30dbb038de36e7ed540318f9b048a1 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sun, 27 Aug 2023 18:03:06 -0500 Subject: [PATCH 09/49] fix: Address review comments --- app/CMakeLists.txt | 2 +- app/boards/arm/corneish_zen/widgets/output_status.c | 2 -- app/src/endpoints.c | 2 ++ .../events/{endpoint_selection_changed.c => endpoint_changed.c} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename app/src/events/{endpoint_selection_changed.c => endpoint_changed.c} (100%) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 29944753f80..793f386dbe3 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -59,7 +59,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/behavior_queue.c) target_sources(app PRIVATE src/conditional_layer.c) target_sources(app PRIVATE src/endpoints.c) - target_sources(app PRIVATE src/events/endpoint_selection_changed.c) + target_sources(app PRIVATE src/events/endpoint_changed.c) target_sources(app PRIVATE src/hid_listener.c) target_sources(app PRIVATE src/keymap.c) target_sources(app PRIVATE src/events/layer_state_changed.c) diff --git a/app/boards/arm/corneish_zen/widgets/output_status.c b/app/boards/arm/corneish_zen/widgets/output_status.c index bdf90cc3d7f..8e9457ebefe 100644 --- a/app/boards/arm/corneish_zen/widgets/output_status.c +++ b/app/boards/arm/corneish_zen/widgets/output_status.c @@ -41,7 +41,6 @@ struct output_status_state { struct zmk_endpoint_instance selected_endpoint; bool active_profile_connected; bool active_profile_bonded; - uint8_t active_profile_index; }; static struct output_status_state get_state(const zmk_event_t *_eh) { @@ -50,7 +49,6 @@ static struct output_status_state get_state(const zmk_event_t *_eh) { .active_profile_connected = zmk_ble_active_profile_is_connected(), .active_profile_bonded = !zmk_ble_active_profile_is_open(), }; - ; } static void set_status_symbol(lv_obj_t *icon, struct output_status_state state) { diff --git a/app/src/endpoints.c b/app/src/endpoints.c index e208a36a312..138e790fe72 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -7,6 +7,8 @@ #include #include +#include + #include #include #include diff --git a/app/src/events/endpoint_selection_changed.c b/app/src/events/endpoint_changed.c similarity index 100% rename from app/src/events/endpoint_selection_changed.c rename to app/src/events/endpoint_changed.c From 6a3cc914fcc0ecc01f33f81413080b882c3022c5 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sun, 27 Aug 2023 18:33:29 -0500 Subject: [PATCH 10/49] fix: Fix nice_view display widgets --- .../nice_view/widgets/peripheral_status.c | 8 +- app/boards/shields/nice_view/widgets/status.c | 76 +++++++++---------- app/boards/shields/nice_view/widgets/util.c | 6 +- app/boards/shields/nice_view/widgets/util.h | 5 +- 4 files changed, 46 insertions(+), 49 deletions(-) diff --git a/app/boards/shields/nice_view/widgets/peripheral_status.c b/app/boards/shields/nice_view/widgets/peripheral_status.c index 85c2a1d8793..4c0c22637be 100644 --- a/app/boards/shields/nice_view/widgets/peripheral_status.c +++ b/app/boards/shields/nice_view/widgets/peripheral_status.c @@ -31,7 +31,7 @@ struct peripheral_status_state { bool connected; }; -static void draw_top(lv_obj_t *widget, lv_color_t cbuf[], struct status_state state) { +static void draw_top(lv_obj_t *widget, lv_color_t cbuf[], const struct status_state *state) { lv_obj_t *canvas = lv_obj_get_child(widget, 0); lv_draw_label_dsc_t label_dsc; @@ -47,7 +47,7 @@ static void draw_top(lv_obj_t *widget, lv_color_t cbuf[], struct status_state st // Draw output status lv_canvas_draw_text(canvas, 0, 0, CANVAS_SIZE, &label_dsc, - state.connected ? LV_SYMBOL_WIFI : LV_SYMBOL_CLOSE); + state->connected ? LV_SYMBOL_WIFI : LV_SYMBOL_CLOSE); // Rotate canvas rotate_canvas(canvas, cbuf); @@ -61,7 +61,7 @@ static void set_battery_status(struct zmk_widget_status *widget, widget->state.battery = state.level; - draw_top(widget->obj, widget->cbuf, widget->state); + draw_top(widget->obj, widget->cbuf, &widget->state); } static void battery_status_update_cb(struct battery_status_state state) { @@ -94,7 +94,7 @@ static void set_connection_status(struct zmk_widget_status *widget, struct peripheral_status_state state) { widget->state.connected = state.connected; - draw_top(widget->obj, widget->cbuf, widget->state); + draw_top(widget->obj, widget->cbuf, &widget->state); } static void output_status_update_cb(struct peripheral_status_state state) { diff --git a/app/boards/shields/nice_view/widgets/status.c b/app/boards/shields/nice_view/widgets/status.c index 1ad9e9207d7..c629be50f59 100644 --- a/app/boards/shields/nice_view/widgets/status.c +++ b/app/boards/shields/nice_view/widgets/status.c @@ -17,7 +17,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include -#include +#include #include #include #include @@ -29,10 +29,9 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); static sys_slist_t widgets = SYS_SLIST_STATIC_INIT(&widgets); struct output_status_state { - enum zmk_endpoint selected_endpoint; + struct zmk_endpoint_instance selected_endpoint; bool active_profile_connected; bool active_profile_bonded; - uint8_t active_profile_index; }; struct layer_status_state { @@ -44,7 +43,7 @@ struct wpm_status_state { uint8_t wpm; }; -static void draw_top(lv_obj_t *widget, lv_color_t cbuf[], struct status_state state) { +static void draw_top(lv_obj_t *widget, lv_color_t cbuf[], const struct status_state *state) { lv_obj_t *canvas = lv_obj_get_child(widget, 0); lv_draw_label_dsc_t label_dsc; @@ -67,13 +66,13 @@ static void draw_top(lv_obj_t *widget, lv_color_t cbuf[], struct status_state st // Draw output status char output_text[10] = {}; - switch (state.selected_endpoint) { - case ZMK_ENDPOINT_USB: + switch (state->selected_endpoint.transport) { + case ZMK_TRANSPORT_USB: strcat(output_text, LV_SYMBOL_USB); break; - case ZMK_ENDPOINT_BLE: - if (state.active_profile_bonded) { - if (state.active_profile_connected) { + case ZMK_TRANSPORT_BLE: + if (state->active_profile_bonded) { + if (state->active_profile_connected) { strcat(output_text, LV_SYMBOL_WIFI); } else { strcat(output_text, LV_SYMBOL_CLOSE); @@ -91,18 +90,18 @@ static void draw_top(lv_obj_t *widget, lv_color_t cbuf[], struct status_state st lv_canvas_draw_rect(canvas, 1, 22, 66, 40, &rect_black_dsc); char wpm_text[6] = {}; - snprintf(wpm_text, sizeof(wpm_text), "%d", state.wpm[9]); + snprintf(wpm_text, sizeof(wpm_text), "%d", state->wpm[9]); lv_canvas_draw_text(canvas, 42, 52, 24, &label_dsc_wpm, wpm_text); int max = 0; int min = 256; for (int i = 0; i < 10; i++) { - if (state.wpm[i] > max) { - max = state.wpm[i]; + if (state->wpm[i] > max) { + max = state->wpm[i]; } - if (state.wpm[i] < min) { - min = state.wpm[i]; + if (state->wpm[i] < min) { + min = state->wpm[i]; } } @@ -114,7 +113,7 @@ static void draw_top(lv_obj_t *widget, lv_color_t cbuf[], struct status_state st lv_point_t points[10]; for (int i = 0; i < 10; i++) { points[i].x = 2 + i * 7; - points[i].y = 60 - (state.wpm[i] - min) * 36 / range; + points[i].y = 60 - (state->wpm[i] - min) * 36 / range; } lv_canvas_draw_line(canvas, points, 10, &line_dsc); @@ -122,7 +121,7 @@ static void draw_top(lv_obj_t *widget, lv_color_t cbuf[], struct status_state st rotate_canvas(canvas, cbuf); } -static void draw_middle(lv_obj_t *widget, lv_color_t cbuf[], struct status_state state) { +static void draw_middle(lv_obj_t *widget, lv_color_t cbuf[], const struct status_state *state) { lv_obj_t *canvas = lv_obj_get_child(widget, 1); lv_draw_rect_dsc_t rect_black_dsc; @@ -147,7 +146,7 @@ static void draw_middle(lv_obj_t *widget, lv_color_t cbuf[], struct status_state }; for (int i = 0; i < 5; i++) { - bool selected = state.active_profile_index == i; + bool selected = state->selected_endpoint.ble.profile_index == i; lv_canvas_draw_arc(canvas, circle_offsets[i][0], circle_offsets[i][1], 13, 0, 359, &arc_dsc); @@ -167,7 +166,7 @@ static void draw_middle(lv_obj_t *widget, lv_color_t cbuf[], struct status_state rotate_canvas(canvas, cbuf); } -static void draw_bottom(lv_obj_t *widget, lv_color_t cbuf[], struct status_state state) { +static void draw_bottom(lv_obj_t *widget, lv_color_t cbuf[], const struct status_state *state) { lv_obj_t *canvas = lv_obj_get_child(widget, 2); lv_draw_rect_dsc_t rect_black_dsc; @@ -179,14 +178,14 @@ static void draw_bottom(lv_obj_t *widget, lv_color_t cbuf[], struct status_state lv_canvas_draw_rect(canvas, 0, 0, CANVAS_SIZE, CANVAS_SIZE, &rect_black_dsc); // Draw layer - if (state.layer_label == NULL) { + if (state->layer_label == NULL) { char text[9] = {}; - sprintf(text, "LAYER %i", state.layer_index); + sprintf(text, "LAYER %i", state->layer_index); lv_canvas_draw_text(canvas, 0, 5, 68, &label_dsc, text); } else { - lv_canvas_draw_text(canvas, 0, 5, 68, &label_dsc, state.layer_label); + lv_canvas_draw_text(canvas, 0, 5, 68, &label_dsc, state->layer_label); } // Rotate canvas @@ -201,7 +200,7 @@ static void set_battery_status(struct zmk_widget_status *widget, widget->state.battery = state.level; - draw_top(widget->obj, widget->cbuf, widget->state); + draw_top(widget->obj, widget->cbuf, &widget->state); } static void battery_status_update_cb(struct battery_status_state state) { @@ -226,33 +225,32 @@ ZMK_SUBSCRIPTION(widget_battery_status, zmk_battery_state_changed); ZMK_SUBSCRIPTION(widget_battery_status, zmk_usb_conn_state_changed); #endif /* IS_ENABLED(CONFIG_USB_DEVICE_STACK) */ -static void set_output_status(struct zmk_widget_status *widget, struct output_status_state state) { - widget->state.selected_endpoint = state.selected_endpoint; - widget->state.active_profile_connected = state.active_profile_connected; - widget->state.active_profile_bonded = state.active_profile_bonded; - widget->state.active_profile_index = state.active_profile_index; +static void set_output_status(struct zmk_widget_status *widget, + const struct output_status_state *state) { + widget->state.selected_endpoint = state->selected_endpoint; + widget->state.active_profile_connected = state->active_profile_connected; + widget->state.active_profile_bonded = state->active_profile_bonded; - draw_top(widget->obj, widget->cbuf, widget->state); - draw_middle(widget->obj, widget->cbuf2, widget->state); + draw_top(widget->obj, widget->cbuf, &widget->state); + draw_middle(widget->obj, widget->cbuf2, &widget->state); } static void output_status_update_cb(struct output_status_state state) { struct zmk_widget_status *widget; - SYS_SLIST_FOR_EACH_CONTAINER(&widgets, widget, node) { set_output_status(widget, state); } + SYS_SLIST_FOR_EACH_CONTAINER(&widgets, widget, node) { set_output_status(widget, &state); } } static struct output_status_state output_status_get_state(const zmk_event_t *_eh) { - return (struct output_status_state){.selected_endpoint = zmk_endpoints_selected(), - .active_profile_connected = - zmk_ble_active_profile_is_connected(), - .active_profile_bonded = !zmk_ble_active_profile_is_open(), - .active_profile_index = zmk_ble_active_profile_index()}; - ; + return (struct output_status_state){ + .selected_endpoint = zmk_endpoints_selected(), + .active_profile_connected = zmk_ble_active_profile_is_connected(), + .active_profile_bonded = !zmk_ble_active_profile_is_open(), + }; } ZMK_DISPLAY_WIDGET_LISTENER(widget_output_status, struct output_status_state, output_status_update_cb, output_status_get_state) -ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_selection_changed); +ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_changed); #if IS_ENABLED(CONFIG_USB_DEVICE_STACK) ZMK_SUBSCRIPTION(widget_output_status, zmk_usb_conn_state_changed); @@ -265,7 +263,7 @@ static void set_layer_status(struct zmk_widget_status *widget, struct layer_stat widget->state.layer_index = state.index; widget->state.layer_label = state.label; - draw_bottom(widget->obj, widget->cbuf3, widget->state); + draw_bottom(widget->obj, widget->cbuf3, &widget->state); } static void layer_status_update_cb(struct layer_status_state state) { @@ -289,7 +287,7 @@ static void set_wpm_status(struct zmk_widget_status *widget, struct wpm_status_s } widget->state.wpm[9] = state.wpm; - draw_top(widget->obj, widget->cbuf, widget->state); + draw_top(widget->obj, widget->cbuf, &widget->state); } static void wpm_status_update_cb(struct wpm_status_state state) { diff --git a/app/boards/shields/nice_view/widgets/util.c b/app/boards/shields/nice_view/widgets/util.c index 9655f837f6e..b4915ab767f 100644 --- a/app/boards/shields/nice_view/widgets/util.c +++ b/app/boards/shields/nice_view/widgets/util.c @@ -24,7 +24,7 @@ void rotate_canvas(lv_obj_t *canvas, lv_color_t cbuf[]) { CANVAS_SIZE / 2, true); } -void draw_battery(lv_obj_t *canvas, struct status_state state) { +void draw_battery(lv_obj_t *canvas, const struct status_state *state) { lv_draw_rect_dsc_t rect_black_dsc; init_rect_dsc(&rect_black_dsc, LVGL_BACKGROUND); lv_draw_rect_dsc_t rect_white_dsc; @@ -32,11 +32,11 @@ void draw_battery(lv_obj_t *canvas, struct status_state state) { lv_canvas_draw_rect(canvas, 0, 2, 29, 12, &rect_white_dsc); lv_canvas_draw_rect(canvas, 1, 3, 27, 10, &rect_black_dsc); - lv_canvas_draw_rect(canvas, 2, 4, (state.battery + 2) / 4, 8, &rect_white_dsc); + lv_canvas_draw_rect(canvas, 2, 4, (state->battery + 2) / 4, 8, &rect_white_dsc); lv_canvas_draw_rect(canvas, 30, 5, 3, 6, &rect_white_dsc); lv_canvas_draw_rect(canvas, 31, 6, 1, 4, &rect_black_dsc); - if (state.charging) { + if (state->charging) { lv_draw_img_dsc_t img_dsc; lv_draw_img_dsc_init(&img_dsc); lv_canvas_draw_img(canvas, 9, -1, &bolt, &img_dsc); diff --git a/app/boards/shields/nice_view/widgets/util.h b/app/boards/shields/nice_view/widgets/util.h index e48c1082ce9..e2d2782a3f9 100644 --- a/app/boards/shields/nice_view/widgets/util.h +++ b/app/boards/shields/nice_view/widgets/util.h @@ -19,10 +19,9 @@ struct status_state { uint8_t battery; bool charging; #if !IS_ENABLED(CONFIG_ZMK_SPLIT) || IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) - enum zmk_endpoint selected_endpoint; + struct zmk_endpoint_instance selected_endpoint; bool active_profile_connected; bool active_profile_bonded; - uint8_t active_profile_index; uint8_t layer_index; const char *layer_label; uint8_t wpm[10]; @@ -39,7 +38,7 @@ struct battery_status_state { }; void rotate_canvas(lv_obj_t *canvas, lv_color_t cbuf[]); -void draw_battery(lv_obj_t *canvas, struct status_state state); +void draw_battery(lv_obj_t *canvas, const struct status_state *state); void init_label_dsc(lv_draw_label_dsc_t *label_dsc, lv_color_t color, const lv_font_t *font, lv_text_align_t align); void init_rect_dsc(lv_draw_rect_dsc_t *rect_dsc, lv_color_t bg_color); From aa4cb143bf2de89f06039fd9ba0b259f6d38fc5d Mon Sep 17 00:00:00 2001 From: Flo Kempenich Date: Tue, 3 Oct 2023 09:03:59 +0100 Subject: [PATCH 11/49] fix(combos)Fix bug with overlapping combos timeouts (#1945) * Fix bug with overlapping combos timeouts * Fix trailing whitespace * Fix log format --- app/src/combo.c | 26 +++-- .../events.patterns | 1 + .../keycode_events.snapshot | 8 ++ .../native_posix_64.keymap | 98 +++++++++++++++++++ 4 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 app/tests/combo/overlapping-combos-4-different-timeouts/events.patterns create mode 100644 app/tests/combo/overlapping-combos-4-different-timeouts/keycode_events.snapshot create mode 100644 app/tests/combo/overlapping-combos-4-different-timeouts/native_posix_64.keymap diff --git a/app/src/combo.c b/app/src/combo.c index 90c89c15e73..87a931439a1 100644 --- a/app/src/combo.c +++ b/app/src/combo.c @@ -204,22 +204,34 @@ static inline bool candidate_is_completely_pressed(struct combo_cfg *candidate) static int cleanup(); static int filter_timed_out_candidates(int64_t timestamp) { - int num_candidates = 0; + int remaining_candidates = 0; for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { struct combo_candidate *candidate = &candidates[i]; if (candidate->combo == NULL) { break; } if (candidate->timeout_at > timestamp) { - // reorder candidates so they're contiguous - candidates[num_candidates].combo = candidate->combo; - candidates[num_candidates].timeout_at = candidate->timeout_at; - num_candidates++; + bool need_to_bubble_up = remaining_candidates != i; + if (need_to_bubble_up) { + // bubble up => reorder candidates so they're contiguous + candidates[remaining_candidates].combo = candidate->combo; + candidates[remaining_candidates].timeout_at = candidate->timeout_at; + // clear the previous location + candidates[i].combo = NULL; + candidates[i].timeout_at = 0; + } + + remaining_candidates++; } else { candidate->combo = NULL; } } - return num_candidates; + + LOG_DBG( + "after filtering out timed out combo candidates: remaining_candidates=%d timestamp=%lld", + remaining_candidates, timestamp); + + return remaining_candidates; } static int clear_candidates() { @@ -449,7 +461,7 @@ static void combo_timeout_handler(struct k_work *item) { // timer was cancelled or rescheduled. return; } - if (filter_timed_out_candidates(timeout_task_timeout_at) < 2) { + if (filter_timed_out_candidates(timeout_task_timeout_at) == 0) { cleanup(); } update_timeout_task(); diff --git a/app/tests/combo/overlapping-combos-4-different-timeouts/events.patterns b/app/tests/combo/overlapping-combos-4-different-timeouts/events.patterns new file mode 100644 index 00000000000..89015deee06 --- /dev/null +++ b/app/tests/combo/overlapping-combos-4-different-timeouts/events.patterns @@ -0,0 +1 @@ +s/.*\(hid_listener_keycode_pressed\|filter_timed_out_candidates\): //p \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-4-different-timeouts/keycode_events.snapshot b/app/tests/combo/overlapping-combos-4-different-timeouts/keycode_events.snapshot new file mode 100644 index 00000000000..8fe441ff46e --- /dev/null +++ b/app/tests/combo/overlapping-combos-4-different-timeouts/keycode_events.snapshot @@ -0,0 +1,8 @@ +after filtering out timed out combo candidates: remaining_candidates=2 timestamp=71 +after filtering out timed out combo candidates: remaining_candidates=1 timestamp=81 +after filtering out timed out combo candidates: remaining_candidates=0 timestamp=91 +usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +after filtering out timed out combo candidates: remaining_candidates=2 timestamp=143 +after filtering out timed out combo candidates: remaining_candidates=1 timestamp=153 +after filtering out timed out combo candidates: remaining_candidates=1 timestamp=159 +usage_page 0x07 keycode 0x1D implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/combo/overlapping-combos-4-different-timeouts/native_posix_64.keymap b/app/tests/combo/overlapping-combos-4-different-timeouts/native_posix_64.keymap new file mode 100644 index 00000000000..8967207987b --- /dev/null +++ b/app/tests/combo/overlapping-combos-4-different-timeouts/native_posix_64.keymap @@ -0,0 +1,98 @@ +#include +#include +#include + +#define kA 0 +#define kB 1 +#define kC 2 +#define kD 3 + +/ { + combos { + compatible = "zmk,combos"; + + // Intentionally out of order in the config, to make sure 'combo.c' handles it properly + combo_40 { + timeout-ms = <40>; + key-positions = ; + bindings = <&kp Z>; + }; + combo_20 { + timeout-ms = <20>; + key-positions = ; + bindings = <&kp X>; + }; + combo_30 { + timeout-ms = <30>; + key-positions = ; + bindings = <&kp Y>; + }; + + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &kp D + >; + }; + }; +}; + +#define press_A_and_wait(delay_next) \ + ZMK_MOCK_PRESS(0,0,delay_next) +#define press_B_and_wait(delay_next) \ + ZMK_MOCK_PRESS(0,1,delay_next) +#define press_C_and_wait(delay_next) \ + ZMK_MOCK_PRESS(1,0,delay_next) +#define press_D_and_wait(delay_next) \ + ZMK_MOCK_PRESS(1,1,delay_next) + +#define release_A_and_wait(delay_next) \ + ZMK_MOCK_RELEASE(0,0,delay_next) +#define release_D_and_wait(delay_next) \ + ZMK_MOCK_RELEASE(1,1,delay_next) + +&kscan { + events = < + /* Note: This starts at T+50 because the ZMK_MOCK_PRESS seems to launch the first event at T+(first wait duration). So in our case T+50 */ + + + + /*** First Phase: All 3 combos expire ***/ + + /* T+50+0= T+50: Press A and wait 50ms */ + press_A_and_wait(50) + + /* T+50+20= T+70: 'combo_20' should expire */ + /* T+50+30= T+80: 'combo_30' should expire */ + /* T+50+40= T+90: 'combo_40' should expire, and we should send the keycode 'A' */ + + /* T+50+50= T+100: We release A and wait 20ms */ + release_A_and_wait(20) + + + + /*** Second Phase: 2 combo expire, 1 combo triggers ***/ + + /* T+120+0= T+120: Press A and wait 35ms */ + press_A_and_wait(35) + + /* T+120+20= T+140: 'combo_20' should expire */ + /* T+120+30= T+150: 'combo_30' should expire */ + + /* T+120+35= T+155: We press 'D', this should trigger 'combo_40' and send the keycode 'Z'. We wait 15ms */ + press_D_and_wait(15) + + + + /*** Cleanup ***/ + /* T+120+50= T+170: We release both keys */ + release_A_and_wait(20) + release_D_and_wait(0) + >; +}; \ No newline at end of file From 2f6abff3bcb4f157d7b0d8eaa53faf2afebe7878 Mon Sep 17 00:00:00 2001 From: Andrew Rae Date: Sat, 16 Jul 2022 21:51:25 -0700 Subject: [PATCH 12/49] refactor(behaviors): Giving global-quick-tap its own term Detaching the global-quick-tap functionality from the quick-tap term. This makes way for two improvements: 1. This functionality can be added to combos under a unified name 'global-quick-tap-ms'. 2. This allows users to set a lower term for the 'global-quick-tap' (typically ~100ms), and a higher term for the regular quick-tap (typically ~200ms) This deprecates the global-quick-tap option, however if it is set, the quick-tap-ms value will be copied to global-quick-tap-ms. --- .../behaviors/zmk,behavior-hold-tap.yaml | 5 ++++- app/src/behaviors/behavior_hold_tap.c | 17 +++++++++++------ .../1-basic/native_posix_64.keymap | 2 +- .../8-global-quick-tap/behavior_keymap.dtsi | 2 +- .../1-basic/native_posix_64.keymap | 2 +- .../8-global-quick-tap/behavior_keymap.dtsi | 2 +- .../1-basic/native_posix_64.keymap | 2 +- .../2-double-hold/native_posix_64.keymap | 2 +- .../8-global-quick-tap/behavior_keymap.dtsi | 2 +- .../1-basic/native_posix_64.keymap | 2 +- .../6-global-quick-tap/behavior_keymap.dtsi | 2 +- 11 files changed, 24 insertions(+), 16 deletions(-) diff --git a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml index a2affbf2147..cca0e96951a 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml @@ -20,8 +20,11 @@ properties: default: -1 quick_tap_ms: # deprecated type: int - global-quick-tap: + global-quick-tap: # deprecated type: boolean + global-quick-tap-ms: + type: int + default: -1 flavor: type: string required: false diff --git a/app/src/behaviors/behavior_hold_tap.c b/app/src/behaviors/behavior_hold_tap.c index 30350ef24ce..1f172e44c46 100644 --- a/app/src/behaviors/behavior_hold_tap.c +++ b/app/src/behaviors/behavior_hold_tap.c @@ -57,7 +57,7 @@ struct behavior_hold_tap_config { char *hold_behavior_dev; char *tap_behavior_dev; int quick_tap_ms; - bool global_quick_tap; + int global_quick_tap_ms; enum flavor flavor; bool retro_tap; bool hold_trigger_on_release; @@ -97,7 +97,9 @@ struct last_tapped { int64_t timestamp; }; -struct last_tapped last_tapped = {INT32_MIN, INT64_MIN}; +// Set time stamp to large negative number initially for test suites, but not +// int64 min since it will overflow if -1 is added +struct last_tapped last_tapped = {INT32_MIN, INT32_MIN}; static void store_last_tapped(int64_t timestamp) { if (timestamp > last_tapped.timestamp) { @@ -112,10 +114,11 @@ static void store_last_hold_tapped(struct active_hold_tap *hold_tap) { } static bool is_quick_tap(struct active_hold_tap *hold_tap) { - if (hold_tap->config->global_quick_tap || last_tapped.position == hold_tap->position) { - return (last_tapped.timestamp + hold_tap->config->quick_tap_ms) > hold_tap->timestamp; + if ((last_tapped.timestamp + hold_tap->config->global_quick_tap_ms) > hold_tap->timestamp) { + return true; } else { - return false; + return (last_tapped.position == hold_tap->position) && + (last_tapped.timestamp + hold_tap->config->quick_tap_ms) > hold_tap->timestamp; } } @@ -703,7 +706,9 @@ static int behavior_hold_tap_init(const struct device *dev) { .hold_behavior_dev = DT_PROP(DT_INST_PHANDLE_BY_IDX(n, bindings, 0), label), \ .tap_behavior_dev = DT_PROP(DT_INST_PHANDLE_BY_IDX(n, bindings, 1), label), \ .quick_tap_ms = DT_INST_PROP(n, quick_tap_ms), \ - .global_quick_tap = DT_INST_PROP(n, global_quick_tap), \ + .global_quick_tap_ms = DT_INST_PROP(n, global_quick_tap) \ + ? DT_INST_PROP(n, quick_tap_ms) \ + : DT_INST_PROP(n, global_quick_tap_ms), \ .flavor = DT_ENUM_IDX(DT_DRV_INST(n), flavor), \ .retro_tap = DT_INST_PROP(n, retro_tap), \ .hold_trigger_on_release = DT_INST_PROP(n, hold_trigger_on_release), \ diff --git a/app/tests/hold-tap/balanced/8-global-quick-tap/1-basic/native_posix_64.keymap b/app/tests/hold-tap/balanced/8-global-quick-tap/1-basic/native_posix_64.keymap index 5af001f6be1..cdbe51bfe3c 100644 --- a/app/tests/hold-tap/balanced/8-global-quick-tap/1-basic/native_posix_64.keymap +++ b/app/tests/hold-tap/balanced/8-global-quick-tap/1-basic/native_posix_64.keymap @@ -7,7 +7,7 @@ events = < /* tap */ ZMK_MOCK_PRESS(0,0,10) - ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,0,250) /* normal quick tap */ ZMK_MOCK_PRESS(0,0,400) ZMK_MOCK_RELEASE(0,0,400) diff --git a/app/tests/hold-tap/balanced/8-global-quick-tap/behavior_keymap.dtsi b/app/tests/hold-tap/balanced/8-global-quick-tap/behavior_keymap.dtsi index ef8efd437d7..8fa363a22e6 100644 --- a/app/tests/hold-tap/balanced/8-global-quick-tap/behavior_keymap.dtsi +++ b/app/tests/hold-tap/balanced/8-global-quick-tap/behavior_keymap.dtsi @@ -11,8 +11,8 @@ flavor = "balanced"; tapping-term-ms = <300>; quick-tap-ms = <300>; + global-quick-tap-ms = <100>; bindings = <&kp>, <&kp>; - global-quick-tap; }; }; diff --git a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap b/app/tests/hold-tap/hold-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap index e28eb4c37e8..a7ba7304c35 100644 --- a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap +++ b/app/tests/hold-tap/hold-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap @@ -7,7 +7,7 @@ events = < /* tap */ ZMK_MOCK_PRESS(0,0,10) - ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,0,250) /* normal quick tap */ ZMK_MOCK_PRESS(0,0,400) ZMK_MOCK_RELEASE(0,0,400) diff --git a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/behavior_keymap.dtsi b/app/tests/hold-tap/hold-preferred/8-global-quick-tap/behavior_keymap.dtsi index 392a5f83435..8b162bd6a10 100644 --- a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/behavior_keymap.dtsi +++ b/app/tests/hold-tap/hold-preferred/8-global-quick-tap/behavior_keymap.dtsi @@ -11,8 +11,8 @@ flavor = "hold-preferred"; tapping-term-ms = <300>; quick-tap-ms = <300>; + global-quick-tap-ms = <100>; bindings = <&kp>, <&kp>; - global-quick-tap; }; }; diff --git a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap b/app/tests/hold-tap/tap-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap index 5af001f6be1..cdbe51bfe3c 100644 --- a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap +++ b/app/tests/hold-tap/tap-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap @@ -7,7 +7,7 @@ events = < /* tap */ ZMK_MOCK_PRESS(0,0,10) - ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,0,250) /* normal quick tap */ ZMK_MOCK_PRESS(0,0,400) ZMK_MOCK_RELEASE(0,0,400) diff --git a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/2-double-hold/native_posix_64.keymap b/app/tests/hold-tap/tap-preferred/8-global-quick-tap/2-double-hold/native_posix_64.keymap index 69d691cee8e..068ae81a8c6 100644 --- a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/2-double-hold/native_posix_64.keymap +++ b/app/tests/hold-tap/tap-preferred/8-global-quick-tap/2-double-hold/native_posix_64.keymap @@ -6,7 +6,7 @@ &kscan { events = < /* hold the first mod tap */ - ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_PRESS(0,0,10) /* hold the second mod tap */ ZMK_MOCK_PRESS(0,1,400) /* press the normal key */ diff --git a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/behavior_keymap.dtsi b/app/tests/hold-tap/tap-preferred/8-global-quick-tap/behavior_keymap.dtsi index 02362ef2b09..9268da077a4 100644 --- a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/behavior_keymap.dtsi +++ b/app/tests/hold-tap/tap-preferred/8-global-quick-tap/behavior_keymap.dtsi @@ -11,8 +11,8 @@ flavor = "tap-preferred"; tapping-term-ms = <300>; quick-tap-ms = <300>; + global-quick-tap-ms = <100>; bindings = <&kp>, <&kp>; - global-quick-tap; }; }; diff --git a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/1-basic/native_posix_64.keymap b/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/1-basic/native_posix_64.keymap index 5af001f6be1..cdbe51bfe3c 100644 --- a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/1-basic/native_posix_64.keymap +++ b/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/1-basic/native_posix_64.keymap @@ -7,7 +7,7 @@ events = < /* tap */ ZMK_MOCK_PRESS(0,0,10) - ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,0,250) /* normal quick tap */ ZMK_MOCK_PRESS(0,0,400) ZMK_MOCK_RELEASE(0,0,400) diff --git a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/behavior_keymap.dtsi b/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/behavior_keymap.dtsi index 029a8128292..0ee84a0d908 100644 --- a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/behavior_keymap.dtsi +++ b/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/behavior_keymap.dtsi @@ -11,8 +11,8 @@ flavor = "tap-unless-interrupted"; tapping-term-ms = <300>; quick-tap-ms = <300>; + global-quick-tap-ms = <100>; bindings = <&kp>, <&kp>; - global-quick-tap; }; }; From 77eb44ba9b14d0c47eb81001d6a90a3ec4e82b19 Mon Sep 17 00:00:00 2001 From: Andrew Rae Date: Mon, 18 Jul 2022 18:27:44 -0700 Subject: [PATCH 13/49] feat(behaviors): Adding global-quick-tap-ms for combos This brings the 'global-quick-tap' functionality to combos by filtering out candidate combos that fell within their own quick tap term. I also replaced `return 0` with `return ZMK_EV_EVENT_BUBBLE` where appropriate. (I assume this was done in past as it is similar to errno returning, but being that this is to signify an event type I find this more clear) --- app/dts/bindings/zmk,combos.yaml | 3 + app/src/combo.c | 50 +++++++++++++-- .../combo/global-quick-tap/events.patterns | 1 + .../global-quick-tap/keycode_events.snapshot | 14 ++++ .../global-quick-tap/native_posix_64.keymap | 64 +++++++++++++++++++ 5 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 app/tests/combo/global-quick-tap/events.patterns create mode 100644 app/tests/combo/global-quick-tap/keycode_events.snapshot create mode 100644 app/tests/combo/global-quick-tap/native_posix_64.keymap diff --git a/app/dts/bindings/zmk,combos.yaml b/app/dts/bindings/zmk,combos.yaml index d094b5c42af..6f6794baa49 100644 --- a/app/dts/bindings/zmk,combos.yaml +++ b/app/dts/bindings/zmk,combos.yaml @@ -18,6 +18,9 @@ child-binding: timeout-ms: type: int default: 50 + global-quick-tap-ms: + type: int + default: -1 slow-release: type: boolean layers: diff --git a/app/src/combo.c b/app/src/combo.c index 87a931439a1..0d6de4f11b3 100644 --- a/app/src/combo.c +++ b/app/src/combo.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,7 @@ struct combo_cfg { int32_t key_position_len; struct zmk_behavior_binding behavior; int32_t timeout_ms; + int32_t global_quick_tap_ms; // if slow release is set, the combo releases when the last key is released. // otherwise, the combo releases when the first key is released. bool slow_release; @@ -72,6 +74,17 @@ int active_combo_count = 0; struct k_work_delayable timeout_task; int64_t timeout_task_timeout_at; +// this keeps track of the last non-combo, non-mod key tap +int64_t last_tapped_timestamp = INT32_MIN; +// this keeps track of the last time a combo was pressed +int64_t last_combo_timestamp = INT32_MIN; + +static void store_last_tapped(int64_t timestamp) { + if (timestamp > last_combo_timestamp) { + last_tapped_timestamp = timestamp; + } +} + // Store the combo key pointer in the combos array, one pointer for each key position // The combos are sorted shortest-first, then by virtual-key-position. static int initialize_combo(struct combo_cfg *new_combo) { @@ -122,6 +135,10 @@ static bool combo_active_on_layer(struct combo_cfg *combo, uint8_t layer) { return false; } +static bool is_quick_tap(struct combo_cfg *combo, int64_t timestamp) { + return (last_tapped_timestamp + combo->global_quick_tap_ms) > timestamp; +} + static int setup_candidates_for_first_keypress(int32_t position, int64_t timestamp) { int number_of_combo_candidates = 0; uint8_t highest_active_layer = zmk_keymap_highest_layer_active(); @@ -130,7 +147,7 @@ static int setup_candidates_for_first_keypress(int32_t position, int64_t timesta if (combo == NULL) { return number_of_combo_candidates; } - if (combo_active_on_layer(combo, highest_active_layer)) { + if (combo_active_on_layer(combo, highest_active_layer) && !is_quick_tap(combo, timestamp)) { candidates[number_of_combo_candidates].combo = combo; candidates[number_of_combo_candidates].timeout_at = timestamp + combo->timeout_ms; number_of_combo_candidates++; @@ -252,7 +269,7 @@ static int capture_pressed_key(const zmk_event_t *ev) { pressed_keys[i] = ev; return ZMK_EV_EVENT_CAPTURED; } - return 0; + return ZMK_EV_EVENT_BUBBLE; } const struct zmk_listener zmk_listener_combo; @@ -284,6 +301,8 @@ static inline int press_combo_behavior(struct combo_cfg *combo, int32_t timestam .timestamp = timestamp, }; + last_combo_timestamp = timestamp; + return behavior_keymap_binding_pressed(&combo->behavior, event); } @@ -413,7 +432,7 @@ static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_ if (candidates[0].combo == NULL) { num_candidates = setup_candidates_for_first_keypress(data->position, data->timestamp); if (num_candidates == 0) { - return 0; + return ZMK_EV_EVENT_BUBBLE; } } else { filter_timed_out_candidates(data->timestamp); @@ -453,7 +472,7 @@ static int position_state_up(const zmk_event_t *ev, struct zmk_position_state_ch ZMK_EVENT_RAISE(ev); return ZMK_EV_EVENT_CAPTURED; } - return 0; + return ZMK_EV_EVENT_BUBBLE; } static void combo_timeout_handler(struct k_work *item) { @@ -470,7 +489,7 @@ static void combo_timeout_handler(struct k_work *item) { static int position_state_changed_listener(const zmk_event_t *ev) { struct zmk_position_state_changed *data = as_zmk_position_state_changed(ev); if (data == NULL) { - return 0; + return ZMK_EV_EVENT_BUBBLE; } if (data->state) { // keydown @@ -480,12 +499,31 @@ static int position_state_changed_listener(const zmk_event_t *ev) { } } -ZMK_LISTENER(combo, position_state_changed_listener); +static int keycode_state_changed_listener(const zmk_event_t *eh) { + struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh); + if (ev->state && !is_mod(ev->usage_page, ev->keycode)) { + store_last_tapped(ev->timestamp); + } + return ZMK_EV_EVENT_BUBBLE; +} + +int behavior_combo_listener(const zmk_event_t *eh) { + if (as_zmk_position_state_changed(eh) != NULL) { + return position_state_changed_listener(eh); + } else if (as_zmk_keycode_state_changed(eh) != NULL) { + return keycode_state_changed_listener(eh); + } + return ZMK_EV_EVENT_BUBBLE; +} + +ZMK_LISTENER(combo, behavior_combo_listener); ZMK_SUBSCRIPTION(combo, zmk_position_state_changed); +ZMK_SUBSCRIPTION(combo, zmk_keycode_state_changed); #define COMBO_INST(n) \ static struct combo_cfg combo_config_##n = { \ .timeout_ms = DT_PROP(n, timeout_ms), \ + .global_quick_tap_ms = DT_PROP(n, global_quick_tap_ms), \ .key_positions = DT_PROP(n, key_positions), \ .key_position_len = DT_PROP_LEN(n, key_positions), \ .behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, n), \ diff --git a/app/tests/combo/global-quick-tap/events.patterns b/app/tests/combo/global-quick-tap/events.patterns new file mode 100644 index 00000000000..833100f6ac4 --- /dev/null +++ b/app/tests/combo/global-quick-tap/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p \ No newline at end of file diff --git a/app/tests/combo/global-quick-tap/keycode_events.snapshot b/app/tests/combo/global-quick-tap/keycode_events.snapshot new file mode 100644 index 00000000000..ee4dd064c61 --- /dev/null +++ b/app/tests/combo/global-quick-tap/keycode_events.snapshot @@ -0,0 +1,14 @@ +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/combo/global-quick-tap/native_posix_64.keymap b/app/tests/combo/global-quick-tap/native_posix_64.keymap new file mode 100644 index 00000000000..92b5a8ae88e --- /dev/null +++ b/app/tests/combo/global-quick-tap/native_posix_64.keymap @@ -0,0 +1,64 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <50>; + key-positions = <0 1>; + bindings = <&kp X>; + global-quick-tap-ms = <100>; + }; + + combo_two { + timeout-ms = <50>; + key-positions = <0 2>; + bindings = <&kp Y>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &kp D + >; + }; + }; +}; + +&kscan { + events = < + /* Tap A */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,60) + /* Quick Tap A and B */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,200) + /* Combo One */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + /* Combo One Again (shouldn't quick tap) */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + /* Tap A */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,60) + /* Combo 2 */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(1,0,10) + >; +}; From 1e84e265b144602431d723e1080ebe32d87871e6 Mon Sep 17 00:00:00 2001 From: Andrew Rae Date: Sun, 28 May 2023 15:35:51 -0400 Subject: [PATCH 14/49] feat(docs): Adding global-quick-tap-ms docs --- docs/docs/behaviors/hold-tap.md | 10 +++++----- docs/docs/config/combos.md | 15 ++++++++------- docs/docs/features/combos.md | 1 + 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md index 2a8489a15ab..66cf5958224 100644 --- a/docs/docs/behaviors/hold-tap.md +++ b/docs/docs/behaviors/hold-tap.md @@ -49,11 +49,11 @@ Defines how long a key must be pressed to trigger Hold behavior. If you press a tapped hold-tap again within `quick-tap-ms` milliseconds, it will always trigger the tap behavior. This is useful for things like a backspace, where a quick tap+hold holds backspace pressed. Set this to a negative value to disable. The default is -1 (disabled). -#### `global-quick-tap` +#### `global-quick-tap-ms` -If `global-quick-tap` is enabled, then `quick-tap-ms` will apply not only when the given hold-tap is tapped, but for any key tapped before it. This effectively disables the hold-tap when typing quickly, which can be quite useful for homerow mods. It can also have the effect of removing the input delay when typing quickly. +If `global-quick-tap-ms` is like `quick-tap-ms` however it will apply for _any_ key tapped before it. This effectively disables the hold-tap when typing quickly, which can be quite useful for homerow mods. It can also have the effect of removing the input delay when typing quickly. -For example, the following hold-tap configuration enables `global-quick-tap` with a 125 millisecond `quick-tap-ms` term. +For example, the following hold-tap configuration enables `global-quick-tap-ms` with a 125 millisecond term, alongside a regular `quick-tap-ms` with a 200 millisecond term. ``` gqt: global-quick-tap { @@ -62,8 +62,8 @@ gqt: global-quick-tap { #binding-cells = <2>; flavor = "tap-preferred"; tapping-term-ms = <200>; - quick-tap-ms = <125>; - global-quick-tap; + quick-tap-ms = <200>; + global-quick-tap-ms = <125>; bindings = <&kp>, <&kp>; }; ``` diff --git a/docs/docs/config/combos.md b/docs/docs/config/combos.md index cd351125450..ca80245019e 100644 --- a/docs/docs/config/combos.md +++ b/docs/docs/config/combos.md @@ -31,12 +31,13 @@ The `zmk,combos` node itself has no properties. It should have one child node pe Each child node can have the following properties: -| Property | Type | Description | Default | -| --------------- | ------------- | ----------------------------------------------------------------------------------------------------- | ------- | -| `bindings` | phandle-array | A [behavior](../features/keymaps.md#behaviors) to run when the combo is triggered | | -| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | -| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | -| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | -| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | +| Property | Type | Description | Default | +| --------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------- | ------- | +| `bindings` | phandle-array | A [behavior](../features/keymaps.md#behaviors) to run when the combo is triggered | | +| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | +| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | +| `global-quick-tap-ms` | int | If any key is tapped within `global-quick-tap-ms` before a key in the combo, the key will not be considered for the combo | -1 | +| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | +| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | The `key-positions` array must not be longer than the `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` setting, which defaults to 4. If you want a combo that triggers when pressing 5 keys, then you must change the setting to 5. diff --git a/docs/docs/features/combos.md b/docs/docs/features/combos.md index 44313cc1dac..263c1a5383c 100644 --- a/docs/docs/features/combos.md +++ b/docs/docs/features/combos.md @@ -30,6 +30,7 @@ Combos configured in your `.keymap` file, but are separate from the `keymap` nod - `layers = <0 1...>` will allow limiting a combo to specific layers. This is an _optional_ parameter, when omitted it defaults to global scope. - `bindings` is the behavior that is activated when the behavior is pressed. - (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released. +- (advanced) you can specify `global-quick-tap-ms` much like in [hold-taps](behaviors/hold-tap.md#global-quick-tap-ms). If any key is tapped within `global-quick-tap-ms` before a key in the combo, the key will not be considered for the combo. :::info From 49c393e8f80701a8736b3e07bf7479c5b9a6592c Mon Sep 17 00:00:00 2001 From: Andrew Rae <56003701+andrewjrae@users.noreply.github.com> Date: Sun, 28 May 2023 21:35:02 -0400 Subject: [PATCH 15/49] refactor(docs): Applying suggestions for gqt from @caksoylar Co-authored-by: Cem Aksoylar --- docs/docs/behaviors/hold-tap.md | 4 ++-- docs/docs/config/behaviors.md | 22 +++++++++++----------- docs/docs/config/combos.md | 16 ++++++++-------- docs/docs/features/combos.md | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md index 66cf5958224..e1b1059508a 100644 --- a/docs/docs/behaviors/hold-tap.md +++ b/docs/docs/behaviors/hold-tap.md @@ -47,11 +47,11 @@ Defines how long a key must be pressed to trigger Hold behavior. #### `quick-tap-ms` -If you press a tapped hold-tap again within `quick-tap-ms` milliseconds, it will always trigger the tap behavior. This is useful for things like a backspace, where a quick tap+hold holds backspace pressed. Set this to a negative value to disable. The default is -1 (disabled). +If you press a tapped hold-tap again within `quick-tap-ms` milliseconds of the first press, it will always trigger the tap behavior. This is useful for things like a backspace, where a quick tap+hold holds backspace pressed. Set this to a negative value to disable. The default is -1 (disabled). #### `global-quick-tap-ms` -If `global-quick-tap-ms` is like `quick-tap-ms` however it will apply for _any_ key tapped before it. This effectively disables the hold-tap when typing quickly, which can be quite useful for homerow mods. It can also have the effect of removing the input delay when typing quickly. +`global-quick-tap-ms` is like `quick-tap-ms` however it will apply for _any_ non-modifier key pressed before it. This effectively disables the hold-tap when typing quickly, which can be quite useful for homerow mods. It can also have the effect of removing the input delay when typing quickly. For example, the following hold-tap configuration enables `global-quick-tap-ms` with a 125 millisecond term, alongside a regular `quick-tap-ms` with a 200 millisecond term. diff --git a/docs/docs/config/behaviors.md b/docs/docs/config/behaviors.md index 60e8b72abca..172a0f13fee 100644 --- a/docs/docs/config/behaviors.md +++ b/docs/docs/config/behaviors.md @@ -58,17 +58,17 @@ Definition file: [zmk/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml](htt Applies to: `compatible = "zmk,behavior-hold-tap"` -| Property | Type | Description | Default | -| ---------------------------- | ------------- | ----------------------------------------------------------------------------------------- | ------------------ | -| `label` | string | Unique label for the node | | -| `#binding-cells` | int | Must be `<2>` | | -| `bindings` | phandle array | A list of two behaviors (without parameters): one for hold and one for tap | | -| `flavor` | string | Adjusts how the behavior chooses between hold and tap | `"hold-preferred"` | -| `tapping-term-ms` | int | How long in milliseconds the key must be held to trigger a hold | | -| `quick-tap-ms` | int | Tap twice within this period (in milliseconds) to trigger a tap, even when held | -1 (disabled) | -| `global-quick-tap` | bool | If enabled, `quick-tap-ms` also applies when tapping another key and then this one. | false | -| `retro-tap` | bool | Triggers the tap behavior on release if no other key was pressed during a hold | false | -| `hold-trigger-key-positions` | array | If set, pressing the hold-tap and then any key position _not_ in the list triggers a tap. | | +| Property | Type | Description | Default | +| ---------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------ | ------------------ | +| `label` | string | Unique label for the node | | +| `#binding-cells` | int | Must be `<2>` | | +| `bindings` | phandle array | A list of two behaviors (without parameters): one for hold and one for tap | | +| `flavor` | string | Adjusts how the behavior chooses between hold and tap | `"hold-preferred"` | +| `tapping-term-ms` | int | How long in milliseconds the key must be held to trigger a hold | | +| `quick-tap-ms` | int | Tap twice within this period (in milliseconds) to trigger a tap, even when held | -1 (disabled) | +| `global-quick-tap-ms` | int | Triggers a tap immediately if any non-modifier key was pressed within `global-quick-tap-ms` of the hold-tap. | -1 (disabled) | +| `retro-tap` | bool | Triggers the tap behavior on release if no other key was pressed during a hold | false | +| `hold-trigger-key-positions` | array | If set, pressing the hold-tap and then any key position _not_ in the list triggers a tap. | | The `flavor` property may be one of: diff --git a/docs/docs/config/combos.md b/docs/docs/config/combos.md index ca80245019e..33622a7a869 100644 --- a/docs/docs/config/combos.md +++ b/docs/docs/config/combos.md @@ -31,13 +31,13 @@ The `zmk,combos` node itself has no properties. It should have one child node pe Each child node can have the following properties: -| Property | Type | Description | Default | -| --------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------- | ------- | -| `bindings` | phandle-array | A [behavior](../features/keymaps.md#behaviors) to run when the combo is triggered | | -| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | -| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | -| `global-quick-tap-ms` | int | If any key is tapped within `global-quick-tap-ms` before a key in the combo, the key will not be considered for the combo | -1 | -| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | -| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | +| Property | Type | Description | Default | +| --------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `bindings` | phandle-array | A [behavior](../features/keymaps.md#behaviors) to run when the combo is triggered | | +| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | +| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | +| `global-quick-tap-ms` | int | If any non-modifier key is pressed within `global-quick-tap-ms` before a key in the combo, the key will not be considered for the combo | -1 (disabled) | +| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | +| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | The `key-positions` array must not be longer than the `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` setting, which defaults to 4. If you want a combo that triggers when pressing 5 keys, then you must change the setting to 5. diff --git a/docs/docs/features/combos.md b/docs/docs/features/combos.md index 263c1a5383c..5ad061686a2 100644 --- a/docs/docs/features/combos.md +++ b/docs/docs/features/combos.md @@ -30,7 +30,7 @@ Combos configured in your `.keymap` file, but are separate from the `keymap` nod - `layers = <0 1...>` will allow limiting a combo to specific layers. This is an _optional_ parameter, when omitted it defaults to global scope. - `bindings` is the behavior that is activated when the behavior is pressed. - (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released. -- (advanced) you can specify `global-quick-tap-ms` much like in [hold-taps](behaviors/hold-tap.md#global-quick-tap-ms). If any key is tapped within `global-quick-tap-ms` before a key in the combo, the key will not be considered for the combo. +- (advanced) you can specify a `global-quick-tap-ms` value much like for [hold-taps](behaviors/hold-tap.md#global-quick-tap-ms). If any non-modifier key is pressed within `global-quick-tap-ms` before a key in the combo, the combo will not trigger. :::info From b85ffa4b6cfb5e2a8c735534ac1b46dc52f8ad86 Mon Sep 17 00:00:00 2001 From: Andrew Rae Date: Sun, 24 Sep 2023 09:38:45 -0400 Subject: [PATCH 16/49] refactor(behaviors): global-quick-tap -> require-prior-idle Renaming global-quick-tap-ms to require-prior-idle. --- .../behaviors/zmk,behavior-hold-tap.yaml | 2 +- app/dts/bindings/zmk,combos.yaml | 2 +- app/src/behaviors/behavior_hold_tap.c | 10 ++++----- app/src/combo.c | 6 ++--- .../events.patterns | 0 .../keycode_events.snapshot | 0 .../native_posix_64.keymap | 2 +- .../1-basic/events.patterns | 0 .../1-basic/keycode_events.snapshot | 0 .../1-basic/native_posix_64.keymap | 0 .../2-double-hold/events.patterns | 0 .../2-double-hold/keycode_events.snapshot | 0 .../2-double-hold/native_posix_64.keymap | 0 .../behavior_keymap.dtsi | 2 +- .../1-basic/events.patterns | 0 .../1-basic/keycode_events.snapshot | 0 .../1-basic/native_posix_64.keymap | 0 .../2-double-hold/events.patterns | 0 .../2-double-hold/keycode_events.snapshot | 0 .../2-double-hold/native_posix_64.keymap | 0 .../behavior_keymap.dtsi | 2 +- .../1-basic/events.patterns | 0 .../1-basic/keycode_events.snapshot | 0 .../1-basic/native_posix_64.keymap | 0 .../2-double-hold/events.patterns | 0 .../2-double-hold/keycode_events.snapshot | 0 .../2-double-hold/native_posix_64.keymap | 0 .../behavior_keymap.dtsi | 2 +- .../1-basic/events.patterns | 0 .../1-basic/keycode_events.snapshot | 0 .../1-basic/native_posix_64.keymap | 0 .../2-double-hold/events.patterns | 0 .../2-double-hold/keycode_events.snapshot | 0 .../2-double-hold/native_posix_64.keymap | 0 .../behavior_keymap.dtsi | 2 +- docs/docs/behaviors/hold-tap.md | 12 +++++----- docs/docs/config/behaviors.md | 22 +++++++++---------- docs/docs/config/combos.md | 16 +++++++------- docs/docs/features/combos.md | 2 +- 39 files changed, 41 insertions(+), 41 deletions(-) rename app/tests/combo/{global-quick-tap => require-prior-idle}/events.patterns (100%) rename app/tests/combo/{global-quick-tap => require-prior-idle}/keycode_events.snapshot (100%) rename app/tests/combo/{global-quick-tap => require-prior-idle}/native_posix_64.keymap (97%) rename app/tests/hold-tap/balanced/{8-global-quick-tap => 8-require-prior-idle}/1-basic/events.patterns (100%) rename app/tests/hold-tap/balanced/{8-global-quick-tap => 8-require-prior-idle}/1-basic/keycode_events.snapshot (100%) rename app/tests/hold-tap/balanced/{8-global-quick-tap => 8-require-prior-idle}/1-basic/native_posix_64.keymap (100%) rename app/tests/hold-tap/balanced/{8-global-quick-tap => 8-require-prior-idle}/2-double-hold/events.patterns (100%) rename app/tests/hold-tap/balanced/{8-global-quick-tap => 8-require-prior-idle}/2-double-hold/keycode_events.snapshot (100%) rename app/tests/hold-tap/balanced/{8-global-quick-tap => 8-require-prior-idle}/2-double-hold/native_posix_64.keymap (100%) rename app/tests/hold-tap/balanced/{8-global-quick-tap => 8-require-prior-idle}/behavior_keymap.dtsi (94%) rename app/tests/hold-tap/hold-preferred/{8-global-quick-tap => 8-require-prior-idle}/1-basic/events.patterns (100%) rename app/tests/hold-tap/hold-preferred/{8-global-quick-tap => 8-require-prior-idle}/1-basic/keycode_events.snapshot (100%) rename app/tests/hold-tap/hold-preferred/{8-global-quick-tap => 8-require-prior-idle}/1-basic/native_posix_64.keymap (100%) rename app/tests/hold-tap/hold-preferred/{8-global-quick-tap => 8-require-prior-idle}/2-double-hold/events.patterns (100%) rename app/tests/hold-tap/hold-preferred/{8-global-quick-tap => 8-require-prior-idle}/2-double-hold/keycode_events.snapshot (100%) rename app/tests/hold-tap/hold-preferred/{8-global-quick-tap => 8-require-prior-idle}/2-double-hold/native_posix_64.keymap (100%) rename app/tests/hold-tap/hold-preferred/{8-global-quick-tap => 8-require-prior-idle}/behavior_keymap.dtsi (94%) rename app/tests/hold-tap/tap-preferred/{8-global-quick-tap => 8-require-prior-idle}/1-basic/events.patterns (100%) rename app/tests/hold-tap/tap-preferred/{8-global-quick-tap => 8-require-prior-idle}/1-basic/keycode_events.snapshot (100%) rename app/tests/hold-tap/tap-preferred/{8-global-quick-tap => 8-require-prior-idle}/1-basic/native_posix_64.keymap (100%) rename app/tests/hold-tap/tap-preferred/{8-global-quick-tap => 8-require-prior-idle}/2-double-hold/events.patterns (100%) rename app/tests/hold-tap/tap-preferred/{8-global-quick-tap => 8-require-prior-idle}/2-double-hold/keycode_events.snapshot (100%) rename app/tests/hold-tap/tap-preferred/{8-global-quick-tap => 8-require-prior-idle}/2-double-hold/native_posix_64.keymap (100%) rename app/tests/hold-tap/tap-preferred/{8-global-quick-tap => 8-require-prior-idle}/behavior_keymap.dtsi (93%) rename app/tests/hold-tap/tap-unless-interrupted/{6-global-quick-tap => 6-require-prior-idle}/1-basic/events.patterns (100%) rename app/tests/hold-tap/tap-unless-interrupted/{6-global-quick-tap => 6-require-prior-idle}/1-basic/keycode_events.snapshot (100%) rename app/tests/hold-tap/tap-unless-interrupted/{6-global-quick-tap => 6-require-prior-idle}/1-basic/native_posix_64.keymap (100%) rename app/tests/hold-tap/tap-unless-interrupted/{6-global-quick-tap => 6-require-prior-idle}/2-double-hold/events.patterns (100%) rename app/tests/hold-tap/tap-unless-interrupted/{6-global-quick-tap => 6-require-prior-idle}/2-double-hold/keycode_events.snapshot (100%) rename app/tests/hold-tap/tap-unless-interrupted/{6-global-quick-tap => 6-require-prior-idle}/2-double-hold/native_posix_64.keymap (100%) rename app/tests/hold-tap/tap-unless-interrupted/{6-global-quick-tap => 6-require-prior-idle}/behavior_keymap.dtsi (94%) diff --git a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml index cca0e96951a..7a140f9134d 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml @@ -22,7 +22,7 @@ properties: type: int global-quick-tap: # deprecated type: boolean - global-quick-tap-ms: + require-prior-idle-ms: type: int default: -1 flavor: diff --git a/app/dts/bindings/zmk,combos.yaml b/app/dts/bindings/zmk,combos.yaml index 6f6794baa49..f146ab7a230 100644 --- a/app/dts/bindings/zmk,combos.yaml +++ b/app/dts/bindings/zmk,combos.yaml @@ -18,7 +18,7 @@ child-binding: timeout-ms: type: int default: 50 - global-quick-tap-ms: + require-prior-idle-ms: type: int default: -1 slow-release: diff --git a/app/src/behaviors/behavior_hold_tap.c b/app/src/behaviors/behavior_hold_tap.c index 1f172e44c46..d4aa0dce036 100644 --- a/app/src/behaviors/behavior_hold_tap.c +++ b/app/src/behaviors/behavior_hold_tap.c @@ -57,7 +57,7 @@ struct behavior_hold_tap_config { char *hold_behavior_dev; char *tap_behavior_dev; int quick_tap_ms; - int global_quick_tap_ms; + int require_prior_idle_ms; enum flavor flavor; bool retro_tap; bool hold_trigger_on_release; @@ -114,7 +114,7 @@ static void store_last_hold_tapped(struct active_hold_tap *hold_tap) { } static bool is_quick_tap(struct active_hold_tap *hold_tap) { - if ((last_tapped.timestamp + hold_tap->config->global_quick_tap_ms) > hold_tap->timestamp) { + if ((last_tapped.timestamp + hold_tap->config->require_prior_idle_ms) > hold_tap->timestamp) { return true; } else { return (last_tapped.position == hold_tap->position) && @@ -706,9 +706,9 @@ static int behavior_hold_tap_init(const struct device *dev) { .hold_behavior_dev = DT_PROP(DT_INST_PHANDLE_BY_IDX(n, bindings, 0), label), \ .tap_behavior_dev = DT_PROP(DT_INST_PHANDLE_BY_IDX(n, bindings, 1), label), \ .quick_tap_ms = DT_INST_PROP(n, quick_tap_ms), \ - .global_quick_tap_ms = DT_INST_PROP(n, global_quick_tap) \ - ? DT_INST_PROP(n, quick_tap_ms) \ - : DT_INST_PROP(n, global_quick_tap_ms), \ + .require_prior_idle_ms = DT_INST_PROP(n, global_quick_tap) \ + ? DT_INST_PROP(n, quick_tap_ms) \ + : DT_INST_PROP(n, require_prior_idle_ms), \ .flavor = DT_ENUM_IDX(DT_DRV_INST(n), flavor), \ .retro_tap = DT_INST_PROP(n, retro_tap), \ .hold_trigger_on_release = DT_INST_PROP(n, hold_trigger_on_release), \ diff --git a/app/src/combo.c b/app/src/combo.c index 0d6de4f11b3..0d5c2a6e237 100644 --- a/app/src/combo.c +++ b/app/src/combo.c @@ -31,7 +31,7 @@ struct combo_cfg { int32_t key_position_len; struct zmk_behavior_binding behavior; int32_t timeout_ms; - int32_t global_quick_tap_ms; + int32_t require_prior_idle_ms; // if slow release is set, the combo releases when the last key is released. // otherwise, the combo releases when the first key is released. bool slow_release; @@ -136,7 +136,7 @@ static bool combo_active_on_layer(struct combo_cfg *combo, uint8_t layer) { } static bool is_quick_tap(struct combo_cfg *combo, int64_t timestamp) { - return (last_tapped_timestamp + combo->global_quick_tap_ms) > timestamp; + return (last_tapped_timestamp + combo->require_prior_idle_ms) > timestamp; } static int setup_candidates_for_first_keypress(int32_t position, int64_t timestamp) { @@ -523,7 +523,7 @@ ZMK_SUBSCRIPTION(combo, zmk_keycode_state_changed); #define COMBO_INST(n) \ static struct combo_cfg combo_config_##n = { \ .timeout_ms = DT_PROP(n, timeout_ms), \ - .global_quick_tap_ms = DT_PROP(n, global_quick_tap_ms), \ + .require_prior_idle_ms = DT_PROP(n, require_prior_idle_ms), \ .key_positions = DT_PROP(n, key_positions), \ .key_position_len = DT_PROP_LEN(n, key_positions), \ .behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, n), \ diff --git a/app/tests/combo/global-quick-tap/events.patterns b/app/tests/combo/require-prior-idle/events.patterns similarity index 100% rename from app/tests/combo/global-quick-tap/events.patterns rename to app/tests/combo/require-prior-idle/events.patterns diff --git a/app/tests/combo/global-quick-tap/keycode_events.snapshot b/app/tests/combo/require-prior-idle/keycode_events.snapshot similarity index 100% rename from app/tests/combo/global-quick-tap/keycode_events.snapshot rename to app/tests/combo/require-prior-idle/keycode_events.snapshot diff --git a/app/tests/combo/global-quick-tap/native_posix_64.keymap b/app/tests/combo/require-prior-idle/native_posix_64.keymap similarity index 97% rename from app/tests/combo/global-quick-tap/native_posix_64.keymap rename to app/tests/combo/require-prior-idle/native_posix_64.keymap index 92b5a8ae88e..206b230a7fa 100644 --- a/app/tests/combo/global-quick-tap/native_posix_64.keymap +++ b/app/tests/combo/require-prior-idle/native_posix_64.keymap @@ -9,7 +9,7 @@ timeout-ms = <50>; key-positions = <0 1>; bindings = <&kp X>; - global-quick-tap-ms = <100>; + require-prior-idle-ms = <100>; }; combo_two { diff --git a/app/tests/hold-tap/balanced/8-global-quick-tap/1-basic/events.patterns b/app/tests/hold-tap/balanced/8-require-prior-idle/1-basic/events.patterns similarity index 100% rename from app/tests/hold-tap/balanced/8-global-quick-tap/1-basic/events.patterns rename to app/tests/hold-tap/balanced/8-require-prior-idle/1-basic/events.patterns diff --git a/app/tests/hold-tap/balanced/8-global-quick-tap/1-basic/keycode_events.snapshot b/app/tests/hold-tap/balanced/8-require-prior-idle/1-basic/keycode_events.snapshot similarity index 100% rename from app/tests/hold-tap/balanced/8-global-quick-tap/1-basic/keycode_events.snapshot rename to app/tests/hold-tap/balanced/8-require-prior-idle/1-basic/keycode_events.snapshot diff --git a/app/tests/hold-tap/balanced/8-global-quick-tap/1-basic/native_posix_64.keymap b/app/tests/hold-tap/balanced/8-require-prior-idle/1-basic/native_posix_64.keymap similarity index 100% rename from app/tests/hold-tap/balanced/8-global-quick-tap/1-basic/native_posix_64.keymap rename to app/tests/hold-tap/balanced/8-require-prior-idle/1-basic/native_posix_64.keymap diff --git a/app/tests/hold-tap/balanced/8-global-quick-tap/2-double-hold/events.patterns b/app/tests/hold-tap/balanced/8-require-prior-idle/2-double-hold/events.patterns similarity index 100% rename from app/tests/hold-tap/balanced/8-global-quick-tap/2-double-hold/events.patterns rename to app/tests/hold-tap/balanced/8-require-prior-idle/2-double-hold/events.patterns diff --git a/app/tests/hold-tap/balanced/8-global-quick-tap/2-double-hold/keycode_events.snapshot b/app/tests/hold-tap/balanced/8-require-prior-idle/2-double-hold/keycode_events.snapshot similarity index 100% rename from app/tests/hold-tap/balanced/8-global-quick-tap/2-double-hold/keycode_events.snapshot rename to app/tests/hold-tap/balanced/8-require-prior-idle/2-double-hold/keycode_events.snapshot diff --git a/app/tests/hold-tap/balanced/8-global-quick-tap/2-double-hold/native_posix_64.keymap b/app/tests/hold-tap/balanced/8-require-prior-idle/2-double-hold/native_posix_64.keymap similarity index 100% rename from app/tests/hold-tap/balanced/8-global-quick-tap/2-double-hold/native_posix_64.keymap rename to app/tests/hold-tap/balanced/8-require-prior-idle/2-double-hold/native_posix_64.keymap diff --git a/app/tests/hold-tap/balanced/8-global-quick-tap/behavior_keymap.dtsi b/app/tests/hold-tap/balanced/8-require-prior-idle/behavior_keymap.dtsi similarity index 94% rename from app/tests/hold-tap/balanced/8-global-quick-tap/behavior_keymap.dtsi rename to app/tests/hold-tap/balanced/8-require-prior-idle/behavior_keymap.dtsi index 8fa363a22e6..670bdcc2650 100644 --- a/app/tests/hold-tap/balanced/8-global-quick-tap/behavior_keymap.dtsi +++ b/app/tests/hold-tap/balanced/8-require-prior-idle/behavior_keymap.dtsi @@ -11,7 +11,7 @@ flavor = "balanced"; tapping-term-ms = <300>; quick-tap-ms = <300>; - global-quick-tap-ms = <100>; + require-prior-idle-ms = <100>; bindings = <&kp>, <&kp>; }; }; diff --git a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/1-basic/events.patterns b/app/tests/hold-tap/hold-preferred/8-require-prior-idle/1-basic/events.patterns similarity index 100% rename from app/tests/hold-tap/hold-preferred/8-global-quick-tap/1-basic/events.patterns rename to app/tests/hold-tap/hold-preferred/8-require-prior-idle/1-basic/events.patterns diff --git a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/1-basic/keycode_events.snapshot b/app/tests/hold-tap/hold-preferred/8-require-prior-idle/1-basic/keycode_events.snapshot similarity index 100% rename from app/tests/hold-tap/hold-preferred/8-global-quick-tap/1-basic/keycode_events.snapshot rename to app/tests/hold-tap/hold-preferred/8-require-prior-idle/1-basic/keycode_events.snapshot diff --git a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap b/app/tests/hold-tap/hold-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap similarity index 100% rename from app/tests/hold-tap/hold-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap rename to app/tests/hold-tap/hold-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap diff --git a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/2-double-hold/events.patterns b/app/tests/hold-tap/hold-preferred/8-require-prior-idle/2-double-hold/events.patterns similarity index 100% rename from app/tests/hold-tap/hold-preferred/8-global-quick-tap/2-double-hold/events.patterns rename to app/tests/hold-tap/hold-preferred/8-require-prior-idle/2-double-hold/events.patterns diff --git a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/2-double-hold/keycode_events.snapshot b/app/tests/hold-tap/hold-preferred/8-require-prior-idle/2-double-hold/keycode_events.snapshot similarity index 100% rename from app/tests/hold-tap/hold-preferred/8-global-quick-tap/2-double-hold/keycode_events.snapshot rename to app/tests/hold-tap/hold-preferred/8-require-prior-idle/2-double-hold/keycode_events.snapshot diff --git a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/2-double-hold/native_posix_64.keymap b/app/tests/hold-tap/hold-preferred/8-require-prior-idle/2-double-hold/native_posix_64.keymap similarity index 100% rename from app/tests/hold-tap/hold-preferred/8-global-quick-tap/2-double-hold/native_posix_64.keymap rename to app/tests/hold-tap/hold-preferred/8-require-prior-idle/2-double-hold/native_posix_64.keymap diff --git a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/behavior_keymap.dtsi b/app/tests/hold-tap/hold-preferred/8-require-prior-idle/behavior_keymap.dtsi similarity index 94% rename from app/tests/hold-tap/hold-preferred/8-global-quick-tap/behavior_keymap.dtsi rename to app/tests/hold-tap/hold-preferred/8-require-prior-idle/behavior_keymap.dtsi index 8b162bd6a10..a99eb3f56f8 100644 --- a/app/tests/hold-tap/hold-preferred/8-global-quick-tap/behavior_keymap.dtsi +++ b/app/tests/hold-tap/hold-preferred/8-require-prior-idle/behavior_keymap.dtsi @@ -11,7 +11,7 @@ flavor = "hold-preferred"; tapping-term-ms = <300>; quick-tap-ms = <300>; - global-quick-tap-ms = <100>; + require-prior-idle-ms = <100>; bindings = <&kp>, <&kp>; }; }; diff --git a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/1-basic/events.patterns b/app/tests/hold-tap/tap-preferred/8-require-prior-idle/1-basic/events.patterns similarity index 100% rename from app/tests/hold-tap/tap-preferred/8-global-quick-tap/1-basic/events.patterns rename to app/tests/hold-tap/tap-preferred/8-require-prior-idle/1-basic/events.patterns diff --git a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/1-basic/keycode_events.snapshot b/app/tests/hold-tap/tap-preferred/8-require-prior-idle/1-basic/keycode_events.snapshot similarity index 100% rename from app/tests/hold-tap/tap-preferred/8-global-quick-tap/1-basic/keycode_events.snapshot rename to app/tests/hold-tap/tap-preferred/8-require-prior-idle/1-basic/keycode_events.snapshot diff --git a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap b/app/tests/hold-tap/tap-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap similarity index 100% rename from app/tests/hold-tap/tap-preferred/8-global-quick-tap/1-basic/native_posix_64.keymap rename to app/tests/hold-tap/tap-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap diff --git a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/2-double-hold/events.patterns b/app/tests/hold-tap/tap-preferred/8-require-prior-idle/2-double-hold/events.patterns similarity index 100% rename from app/tests/hold-tap/tap-preferred/8-global-quick-tap/2-double-hold/events.patterns rename to app/tests/hold-tap/tap-preferred/8-require-prior-idle/2-double-hold/events.patterns diff --git a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/2-double-hold/keycode_events.snapshot b/app/tests/hold-tap/tap-preferred/8-require-prior-idle/2-double-hold/keycode_events.snapshot similarity index 100% rename from app/tests/hold-tap/tap-preferred/8-global-quick-tap/2-double-hold/keycode_events.snapshot rename to app/tests/hold-tap/tap-preferred/8-require-prior-idle/2-double-hold/keycode_events.snapshot diff --git a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/2-double-hold/native_posix_64.keymap b/app/tests/hold-tap/tap-preferred/8-require-prior-idle/2-double-hold/native_posix_64.keymap similarity index 100% rename from app/tests/hold-tap/tap-preferred/8-global-quick-tap/2-double-hold/native_posix_64.keymap rename to app/tests/hold-tap/tap-preferred/8-require-prior-idle/2-double-hold/native_posix_64.keymap diff --git a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/behavior_keymap.dtsi b/app/tests/hold-tap/tap-preferred/8-require-prior-idle/behavior_keymap.dtsi similarity index 93% rename from app/tests/hold-tap/tap-preferred/8-global-quick-tap/behavior_keymap.dtsi rename to app/tests/hold-tap/tap-preferred/8-require-prior-idle/behavior_keymap.dtsi index 9268da077a4..c66dc934092 100644 --- a/app/tests/hold-tap/tap-preferred/8-global-quick-tap/behavior_keymap.dtsi +++ b/app/tests/hold-tap/tap-preferred/8-require-prior-idle/behavior_keymap.dtsi @@ -11,7 +11,7 @@ flavor = "tap-preferred"; tapping-term-ms = <300>; quick-tap-ms = <300>; - global-quick-tap-ms = <100>; + require-prior-idle-ms = <100>; bindings = <&kp>, <&kp>; }; }; diff --git a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/1-basic/events.patterns b/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/1-basic/events.patterns similarity index 100% rename from app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/1-basic/events.patterns rename to app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/1-basic/events.patterns diff --git a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/1-basic/keycode_events.snapshot b/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/1-basic/keycode_events.snapshot similarity index 100% rename from app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/1-basic/keycode_events.snapshot rename to app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/1-basic/keycode_events.snapshot diff --git a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/1-basic/native_posix_64.keymap b/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/1-basic/native_posix_64.keymap similarity index 100% rename from app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/1-basic/native_posix_64.keymap rename to app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/1-basic/native_posix_64.keymap diff --git a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/2-double-hold/events.patterns b/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/2-double-hold/events.patterns similarity index 100% rename from app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/2-double-hold/events.patterns rename to app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/2-double-hold/events.patterns diff --git a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/2-double-hold/keycode_events.snapshot b/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/2-double-hold/keycode_events.snapshot similarity index 100% rename from app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/2-double-hold/keycode_events.snapshot rename to app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/2-double-hold/keycode_events.snapshot diff --git a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/2-double-hold/native_posix_64.keymap b/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/2-double-hold/native_posix_64.keymap similarity index 100% rename from app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/2-double-hold/native_posix_64.keymap rename to app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/2-double-hold/native_posix_64.keymap diff --git a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/behavior_keymap.dtsi b/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/behavior_keymap.dtsi similarity index 94% rename from app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/behavior_keymap.dtsi rename to app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/behavior_keymap.dtsi index 0ee84a0d908..7aa3940833f 100644 --- a/app/tests/hold-tap/tap-unless-interrupted/6-global-quick-tap/behavior_keymap.dtsi +++ b/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/behavior_keymap.dtsi @@ -11,7 +11,7 @@ flavor = "tap-unless-interrupted"; tapping-term-ms = <300>; quick-tap-ms = <300>; - global-quick-tap-ms = <100>; + require-prior-idle-ms = <100>; bindings = <&kp>, <&kp>; }; }; diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md index e1b1059508a..e8192f684b1 100644 --- a/docs/docs/behaviors/hold-tap.md +++ b/docs/docs/behaviors/hold-tap.md @@ -49,11 +49,11 @@ Defines how long a key must be pressed to trigger Hold behavior. If you press a tapped hold-tap again within `quick-tap-ms` milliseconds of the first press, it will always trigger the tap behavior. This is useful for things like a backspace, where a quick tap+hold holds backspace pressed. Set this to a negative value to disable. The default is -1 (disabled). -#### `global-quick-tap-ms` +#### `require-prior-idle-ms` -`global-quick-tap-ms` is like `quick-tap-ms` however it will apply for _any_ non-modifier key pressed before it. This effectively disables the hold-tap when typing quickly, which can be quite useful for homerow mods. It can also have the effect of removing the input delay when typing quickly. +`require-prior-idle-ms` is like `quick-tap-ms` however it will apply for _any_ non-modifier key pressed before it. This effectively disables the hold-tap when typing quickly, which can be quite useful for homerow mods. It can also have the effect of removing the input delay when typing quickly. -For example, the following hold-tap configuration enables `global-quick-tap-ms` with a 125 millisecond term, alongside a regular `quick-tap-ms` with a 200 millisecond term. +For example, the following hold-tap configuration enables `require-prior-idle-ms` with a 125 millisecond term, alongside `quick-tap-ms` with a 200 millisecond term. ``` gqt: global-quick-tap { @@ -63,14 +63,14 @@ gqt: global-quick-tap { flavor = "tap-preferred"; tapping-term-ms = <200>; quick-tap-ms = <200>; - global-quick-tap-ms = <125>; + require-prior-idle-ms = <125>; bindings = <&kp>, <&kp>; }; ``` -If you press `&kp A` and then `&gqt LEFT_SHIFT B` **within** 125 ms, then `ab` will be output. Importantly, `b` will be output immediately since it was within the `quick-tap-ms`. This quick-tap behavior will work for any key press, whether it is within a behavior like hold-tap, or a simple `&kp`. This means the `&gqt LEFT_SHIFT B` binding will only have its underlying hold-tap behavior if it is pressed 125 ms **after** a key press. +If you press `&kp A` and then `&gqt LEFT_SHIFT B` **within** 125 ms, then `ab` will be output. Importantly, `b` will be output immediately since it was within the `require-prior-idle-ms`. This "quick-tap" behavior will work for any key press, whether it is within a behavior like hold-tap, or a simple `&kp`. This means the `&gqt LEFT_SHIFT B` binding will only have its underlying hold-tap behavior if it is pressed 125 ms **after** a key press. -Note that the greater the value of `quick-tap-ms` is, the harder it will be to invoke the hold behavior, making this feature less applicable for use-cases like capitalizing letters while typing normally. However, if the hold behavior isn't used during fast typing, then it can be an effective way to mitigate misfires. +Note that the greater the value of `require-prior-idle-ms` is, the harder it will be to invoke the hold behavior, making this feature less applicable for use-cases like capitalizing letters while typing normally. However, if the hold behavior isn't used during fast typing, then it can be an effective way to mitigate misfires. #### `retro-tap` diff --git a/docs/docs/config/behaviors.md b/docs/docs/config/behaviors.md index 172a0f13fee..f3f1f563ec8 100644 --- a/docs/docs/config/behaviors.md +++ b/docs/docs/config/behaviors.md @@ -58,17 +58,17 @@ Definition file: [zmk/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml](htt Applies to: `compatible = "zmk,behavior-hold-tap"` -| Property | Type | Description | Default | -| ---------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------ | ------------------ | -| `label` | string | Unique label for the node | | -| `#binding-cells` | int | Must be `<2>` | | -| `bindings` | phandle array | A list of two behaviors (without parameters): one for hold and one for tap | | -| `flavor` | string | Adjusts how the behavior chooses between hold and tap | `"hold-preferred"` | -| `tapping-term-ms` | int | How long in milliseconds the key must be held to trigger a hold | | -| `quick-tap-ms` | int | Tap twice within this period (in milliseconds) to trigger a tap, even when held | -1 (disabled) | -| `global-quick-tap-ms` | int | Triggers a tap immediately if any non-modifier key was pressed within `global-quick-tap-ms` of the hold-tap. | -1 (disabled) | -| `retro-tap` | bool | Triggers the tap behavior on release if no other key was pressed during a hold | false | -| `hold-trigger-key-positions` | array | If set, pressing the hold-tap and then any key position _not_ in the list triggers a tap. | | +| Property | Type | Description | Default | +| ---------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------- | ------------------ | +| `label` | string | Unique label for the node | | +| `#binding-cells` | int | Must be `<2>` | | +| `bindings` | phandle array | A list of two behaviors (without parameters): one for hold and one for tap | | +| `flavor` | string | Adjusts how the behavior chooses between hold and tap | `"hold-preferred"` | +| `tapping-term-ms` | int | How long in milliseconds the key must be held to trigger a hold | | +| `quick-tap-ms` | int | Tap twice within this period (in milliseconds) to trigger a tap, even when held | -1 (disabled) | +| `require-prior-idle-ms` | int | Triggers a tap immediately if any non-modifier key was pressed within `require-prior-idle-ms` of the hold-tap. | -1 (disabled) | +| `retro-tap` | bool | Triggers the tap behavior on release if no other key was pressed during a hold | false | +| `hold-trigger-key-positions` | array | If set, pressing the hold-tap and then any key position _not_ in the list triggers a tap. | | The `flavor` property may be one of: diff --git a/docs/docs/config/combos.md b/docs/docs/config/combos.md index 33622a7a869..4f5ebba3d34 100644 --- a/docs/docs/config/combos.md +++ b/docs/docs/config/combos.md @@ -31,13 +31,13 @@ The `zmk,combos` node itself has no properties. It should have one child node pe Each child node can have the following properties: -| Property | Type | Description | Default | -| --------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `bindings` | phandle-array | A [behavior](../features/keymaps.md#behaviors) to run when the combo is triggered | | -| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | -| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | -| `global-quick-tap-ms` | int | If any non-modifier key is pressed within `global-quick-tap-ms` before a key in the combo, the key will not be considered for the combo | -1 (disabled) | -| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | -| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | +| Property | Type | Description | Default | +| ----------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `bindings` | phandle-array | A [behavior](../features/keymaps.md#behaviors) to run when the combo is triggered | | +| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | +| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | +| `require-prior-idle-ms` | int | If any non-modifier key is pressed within `require-prior-idle-ms` before a key in the combo, the key will not be considered for the combo | -1 (disabled) | +| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | +| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | The `key-positions` array must not be longer than the `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` setting, which defaults to 4. If you want a combo that triggers when pressing 5 keys, then you must change the setting to 5. diff --git a/docs/docs/features/combos.md b/docs/docs/features/combos.md index 5ad061686a2..bc1353b416e 100644 --- a/docs/docs/features/combos.md +++ b/docs/docs/features/combos.md @@ -30,7 +30,7 @@ Combos configured in your `.keymap` file, but are separate from the `keymap` nod - `layers = <0 1...>` will allow limiting a combo to specific layers. This is an _optional_ parameter, when omitted it defaults to global scope. - `bindings` is the behavior that is activated when the behavior is pressed. - (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released. -- (advanced) you can specify a `global-quick-tap-ms` value much like for [hold-taps](behaviors/hold-tap.md#global-quick-tap-ms). If any non-modifier key is pressed within `global-quick-tap-ms` before a key in the combo, the combo will not trigger. +- (advanced) you can specify a `require-prior-idle-ms` value much like for [hold-taps](behaviors/hold-tap.md#require-prior-idle-ms). If any non-modifier key is pressed within `require-prior-idle-ms` before a key in the combo, the combo will not trigger. :::info From f0f6d61e794633a06b04246ee31e276c9fbf3040 Mon Sep 17 00:00:00 2001 From: Andrew Rae Date: Sun, 24 Sep 2023 10:19:58 -0400 Subject: [PATCH 17/49] fix(tests): Updating old tests includes --- app/tests/combo/require-prior-idle/native_posix_64.keymap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/combo/require-prior-idle/native_posix_64.keymap b/app/tests/combo/require-prior-idle/native_posix_64.keymap index 206b230a7fa..fcd94056c4d 100644 --- a/app/tests/combo/require-prior-idle/native_posix_64.keymap +++ b/app/tests/combo/require-prior-idle/native_posix_64.keymap @@ -1,6 +1,6 @@ #include #include -#include +#include / { combos { From 2234be0871cdab4c0d8bfc6abee3013716e5601a Mon Sep 17 00:00:00 2001 From: Andrew Rae <56003701+andrewjrae@users.noreply.github.com> Date: Mon, 25 Sep 2023 09:15:57 -0400 Subject: [PATCH 18/49] refactor(docs): Apply suggestions from @caksoylar Co-authored-by: Cem Aksoylar --- docs/docs/behaviors/hold-tap.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md index e8192f684b1..d9d86cea4a5 100644 --- a/docs/docs/behaviors/hold-tap.md +++ b/docs/docs/behaviors/hold-tap.md @@ -56,7 +56,7 @@ If you press a tapped hold-tap again within `quick-tap-ms` milliseconds of the f For example, the following hold-tap configuration enables `require-prior-idle-ms` with a 125 millisecond term, alongside `quick-tap-ms` with a 200 millisecond term. ``` -gqt: global-quick-tap { +rpi: require_prior_idle { compatible = "zmk,behavior-hold-tap"; label = "GLOBAL_QUICK_TAP"; #binding-cells = <2>; @@ -68,7 +68,7 @@ gqt: global-quick-tap { }; ``` -If you press `&kp A` and then `&gqt LEFT_SHIFT B` **within** 125 ms, then `ab` will be output. Importantly, `b` will be output immediately since it was within the `require-prior-idle-ms`. This "quick-tap" behavior will work for any key press, whether it is within a behavior like hold-tap, or a simple `&kp`. This means the `&gqt LEFT_SHIFT B` binding will only have its underlying hold-tap behavior if it is pressed 125 ms **after** a key press. +If you press `&kp A` and then `&rpi LEFT_SHIFT B` **within** 125 ms, then `ab` will be output. Importantly, `b` will be output immediately since it was within the `require-prior-idle-ms`, without waiting for a timeout or an interrupting key. In other words, the `&rpi LEFT_SHIFT B` binding will only have its underlying hold-tap behavior if it is pressed 125 ms **after** the previous key press; otherwise it will act like `&kp B`. Note that the greater the value of `require-prior-idle-ms` is, the harder it will be to invoke the hold behavior, making this feature less applicable for use-cases like capitalizing letters while typing normally. However, if the hold behavior isn't used during fast typing, then it can be an effective way to mitigate misfires. From 11996ff7f05bdbe0d5ee59dc3b695d5e9d8c5c8d Mon Sep 17 00:00:00 2001 From: Andrew Rae Date: Tue, 26 Sep 2023 22:00:23 -0400 Subject: [PATCH 19/49] refactor(behaviors): Final global-quick-tap deprecation. --- app/boards/shields/cradio/cradio.keymap | 2 +- app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml | 9 ++++++--- .../8-require-prior-idle/1-basic/native_posix_64.keymap | 2 +- .../8-require-prior-idle/1-basic/native_posix_64.keymap | 2 +- .../8-require-prior-idle/1-basic/native_posix_64.keymap | 2 +- .../6-require-prior-idle/1-basic/native_posix_64.keymap | 2 +- docs/docs/behaviors/hold-tap.md | 2 +- 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/boards/shields/cradio/cradio.keymap b/app/boards/shields/cradio/cradio.keymap index 3f6670da725..47bf0422a5a 100644 --- a/app/boards/shields/cradio/cradio.keymap +++ b/app/boards/shields/cradio/cradio.keymap @@ -18,7 +18,7 @@ flavor = "tap-preferred"; tapping-term-ms = <220>; quick-tap-ms = <150>; - global-quick-tap; + require-prior-idle-ms = <100>; bindings = <&kp>, <&kp>; }; }; diff --git a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml index 7a140f9134d..575754116b1 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml @@ -13,15 +13,18 @@ properties: required: true tapping-term-ms: type: int - tapping_term_ms: # deprecated + tapping_term_ms: type: int + deprecated: true quick-tap-ms: type: int default: -1 - quick_tap_ms: # deprecated + quick_tap_ms: type: int - global-quick-tap: # deprecated + deprecated: true + global-quick-tap: type: boolean + deprecated: true require-prior-idle-ms: type: int default: -1 diff --git a/app/tests/hold-tap/balanced/8-require-prior-idle/1-basic/native_posix_64.keymap b/app/tests/hold-tap/balanced/8-require-prior-idle/1-basic/native_posix_64.keymap index cdbe51bfe3c..aa629498073 100644 --- a/app/tests/hold-tap/balanced/8-require-prior-idle/1-basic/native_posix_64.keymap +++ b/app/tests/hold-tap/balanced/8-require-prior-idle/1-basic/native_posix_64.keymap @@ -16,7 +16,7 @@ ZMK_MOCK_PRESS(1,0,10) ZMK_MOCK_RELEASE(1,0,10) ZMK_MOCK_RELEASE(0,0,400) - /* global quick tap */ + /* require-prior-idle */ ZMK_MOCK_PRESS(1,0,10) ZMK_MOCK_PRESS(0,0,400) ZMK_MOCK_RELEASE(1,0,10) diff --git a/app/tests/hold-tap/hold-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap b/app/tests/hold-tap/hold-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap index a7ba7304c35..6db79abc381 100644 --- a/app/tests/hold-tap/hold-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap +++ b/app/tests/hold-tap/hold-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap @@ -16,7 +16,7 @@ ZMK_MOCK_PRESS(1,0,10) ZMK_MOCK_RELEASE(1,0,10) ZMK_MOCK_RELEASE(0,0,400) - /* global quick tap */ + /* require-prior-idle */ ZMK_MOCK_PRESS(1,0,10) ZMK_MOCK_PRESS(0,0,400) ZMK_MOCK_RELEASE(1,0,10) diff --git a/app/tests/hold-tap/tap-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap b/app/tests/hold-tap/tap-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap index cdbe51bfe3c..aa629498073 100644 --- a/app/tests/hold-tap/tap-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap +++ b/app/tests/hold-tap/tap-preferred/8-require-prior-idle/1-basic/native_posix_64.keymap @@ -16,7 +16,7 @@ ZMK_MOCK_PRESS(1,0,10) ZMK_MOCK_RELEASE(1,0,10) ZMK_MOCK_RELEASE(0,0,400) - /* global quick tap */ + /* require-prior-idle */ ZMK_MOCK_PRESS(1,0,10) ZMK_MOCK_PRESS(0,0,400) ZMK_MOCK_RELEASE(1,0,10) diff --git a/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/1-basic/native_posix_64.keymap b/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/1-basic/native_posix_64.keymap index cdbe51bfe3c..aa629498073 100644 --- a/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/1-basic/native_posix_64.keymap +++ b/app/tests/hold-tap/tap-unless-interrupted/6-require-prior-idle/1-basic/native_posix_64.keymap @@ -16,7 +16,7 @@ ZMK_MOCK_PRESS(1,0,10) ZMK_MOCK_RELEASE(1,0,10) ZMK_MOCK_RELEASE(0,0,400) - /* global quick tap */ + /* require-prior-idle */ ZMK_MOCK_PRESS(1,0,10) ZMK_MOCK_PRESS(0,0,400) ZMK_MOCK_RELEASE(1,0,10) diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md index d9d86cea4a5..ec66b34f2af 100644 --- a/docs/docs/behaviors/hold-tap.md +++ b/docs/docs/behaviors/hold-tap.md @@ -58,7 +58,7 @@ For example, the following hold-tap configuration enables `require-prior-idle-ms ``` rpi: require_prior_idle { compatible = "zmk,behavior-hold-tap"; - label = "GLOBAL_QUICK_TAP"; + label = "REQUIRE_PRIOR_IDLE"; #binding-cells = <2>; flavor = "tap-preferred"; tapping-term-ms = <200>; From df92b0e37d5c53e23ffe60c7a8005eb311cff785 Mon Sep 17 00:00:00 2001 From: Seth Milliken Date: Wed, 4 Oct 2023 08:48:46 -0700 Subject: [PATCH 20/49] feat(shields): sofle: add underglow support with `&pinctrl` update * Sofle Shield: Add underglow support --------- Co-authored-by: Kim Streich Co-authored-by: Seth Milliken --- app/boards/shields/sofle/Kconfig.defconfig | 6 ++ .../shields/sofle/boards/nice_nano.overlay | 51 +++++++++++++ .../shields/sofle/boards/nice_nano_v2.overlay | 51 +++++++++++++ .../shields/sofle/boards/nrfmicro_11.overlay | 51 +++++++++++++ .../shields/sofle/boards/nrfmicro_13.overlay | 51 +++++++++++++ app/boards/shields/sofle/sofle.conf | 8 ++ app/boards/shields/sofle/sofle.keymap | 76 ++++++++++++++----- app/boards/shields/sofle/sofle.zmk.yml | 1 + 8 files changed, 276 insertions(+), 19 deletions(-) create mode 100644 app/boards/shields/sofle/boards/nice_nano.overlay create mode 100644 app/boards/shields/sofle/boards/nice_nano_v2.overlay create mode 100644 app/boards/shields/sofle/boards/nrfmicro_11.overlay create mode 100644 app/boards/shields/sofle/boards/nrfmicro_13.overlay diff --git a/app/boards/shields/sofle/Kconfig.defconfig b/app/boards/shields/sofle/Kconfig.defconfig index cc598d6720f..4e7bf88488e 100644 --- a/app/boards/shields/sofle/Kconfig.defconfig +++ b/app/boards/shields/sofle/Kconfig.defconfig @@ -46,4 +46,10 @@ endchoice endif # LVGL +if ZMK_RGB_UNDERGLOW + +config WS2812_STRIP + default y +endif + endif diff --git a/app/boards/shields/sofle/boards/nice_nano.overlay b/app/boards/shields/sofle/boards/nice_nano.overlay new file mode 100644 index 00000000000..336be4b0d96 --- /dev/null +++ b/app/boards/shields/sofle/boards/nice_nano.overlay @@ -0,0 +1,51 @@ +#include + +&pinctrl { + spi3_default: spi3_default { + group1 { + psels = ; + }; + }; + + spi3_sleep: spi3_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; +}; + +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + + pinctrl-0 = <&spi3_default>; + pinctrl-1 = <&spi3_sleep>; + pinctrl-names = "default", "sleep"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + label = "WS2812"; + + /* SPI */ + reg = <0>; /* ignored, but necessary for SPI bindings */ + spi-max-frequency = <4000000>; + + /* WS2812 */ + chain-length = <36>; /* arbitrary; change at will */ + spi-one-frame = <0x70>; + spi-zero-frame = <0x40>; + + color-mapping = < + LED_COLOR_ID_GREEN + LED_COLOR_ID_RED + LED_COLOR_ID_BLUE + >; + }; +}; + +/ { + chosen { + zmk,underglow = &led_strip; + }; +}; diff --git a/app/boards/shields/sofle/boards/nice_nano_v2.overlay b/app/boards/shields/sofle/boards/nice_nano_v2.overlay new file mode 100644 index 00000000000..336be4b0d96 --- /dev/null +++ b/app/boards/shields/sofle/boards/nice_nano_v2.overlay @@ -0,0 +1,51 @@ +#include + +&pinctrl { + spi3_default: spi3_default { + group1 { + psels = ; + }; + }; + + spi3_sleep: spi3_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; +}; + +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + + pinctrl-0 = <&spi3_default>; + pinctrl-1 = <&spi3_sleep>; + pinctrl-names = "default", "sleep"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + label = "WS2812"; + + /* SPI */ + reg = <0>; /* ignored, but necessary for SPI bindings */ + spi-max-frequency = <4000000>; + + /* WS2812 */ + chain-length = <36>; /* arbitrary; change at will */ + spi-one-frame = <0x70>; + spi-zero-frame = <0x40>; + + color-mapping = < + LED_COLOR_ID_GREEN + LED_COLOR_ID_RED + LED_COLOR_ID_BLUE + >; + }; +}; + +/ { + chosen { + zmk,underglow = &led_strip; + }; +}; diff --git a/app/boards/shields/sofle/boards/nrfmicro_11.overlay b/app/boards/shields/sofle/boards/nrfmicro_11.overlay new file mode 100644 index 00000000000..336be4b0d96 --- /dev/null +++ b/app/boards/shields/sofle/boards/nrfmicro_11.overlay @@ -0,0 +1,51 @@ +#include + +&pinctrl { + spi3_default: spi3_default { + group1 { + psels = ; + }; + }; + + spi3_sleep: spi3_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; +}; + +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + + pinctrl-0 = <&spi3_default>; + pinctrl-1 = <&spi3_sleep>; + pinctrl-names = "default", "sleep"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + label = "WS2812"; + + /* SPI */ + reg = <0>; /* ignored, but necessary for SPI bindings */ + spi-max-frequency = <4000000>; + + /* WS2812 */ + chain-length = <36>; /* arbitrary; change at will */ + spi-one-frame = <0x70>; + spi-zero-frame = <0x40>; + + color-mapping = < + LED_COLOR_ID_GREEN + LED_COLOR_ID_RED + LED_COLOR_ID_BLUE + >; + }; +}; + +/ { + chosen { + zmk,underglow = &led_strip; + }; +}; diff --git a/app/boards/shields/sofle/boards/nrfmicro_13.overlay b/app/boards/shields/sofle/boards/nrfmicro_13.overlay new file mode 100644 index 00000000000..336be4b0d96 --- /dev/null +++ b/app/boards/shields/sofle/boards/nrfmicro_13.overlay @@ -0,0 +1,51 @@ +#include + +&pinctrl { + spi3_default: spi3_default { + group1 { + psels = ; + }; + }; + + spi3_sleep: spi3_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; +}; + +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + + pinctrl-0 = <&spi3_default>; + pinctrl-1 = <&spi3_sleep>; + pinctrl-names = "default", "sleep"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + label = "WS2812"; + + /* SPI */ + reg = <0>; /* ignored, but necessary for SPI bindings */ + spi-max-frequency = <4000000>; + + /* WS2812 */ + chain-length = <36>; /* arbitrary; change at will */ + spi-one-frame = <0x70>; + spi-zero-frame = <0x40>; + + color-mapping = < + LED_COLOR_ID_GREEN + LED_COLOR_ID_RED + LED_COLOR_ID_BLUE + >; + }; +}; + +/ { + chosen { + zmk,underglow = &led_strip; + }; +}; diff --git a/app/boards/shields/sofle/sofle.conf b/app/boards/shields/sofle/sofle.conf index fe3f0f4f8a7..1f74aa339fc 100644 --- a/app/boards/shields/sofle/sofle.conf +++ b/app/boards/shields/sofle/sofle.conf @@ -7,3 +7,11 @@ # Uncomment these two lines to add support for encoders # CONFIG_EC11=y # CONFIG_EC11_TRIGGER_GLOBAL_THREAD=y + +# Uncomment this line below to add rgb underglow / backlight support +# CONFIG_ZMK_RGB_UNDERGLOW=y + +# Uncomment the line below to disable external power toggling by the underglow. +# By default toggling the underglow on and off also toggles external power +# on and off. This also causes the display to turn off. +# CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER=n diff --git a/app/boards/shields/sofle/sofle.keymap b/app/boards/shields/sofle/sofle.keymap index 395ecf1ddd7..fbb0af7fb00 100644 --- a/app/boards/shields/sofle/sofle.keymap +++ b/app/boards/shields/sofle/sofle.keymap @@ -7,34 +7,53 @@ #include #include #include +#include +#include + +#define BASE 0 +#define LOWER 1 +#define RAISE 2 +#define ADJUST 3 / { + + // Activate ADJUST layer by pressing raise and lower + conditional_layers { + compatible = "zmk,conditional-layers"; + adjust_layer { + if-layers = ; + then-layer = ; + }; + }; + keymap { compatible = "zmk,keymap"; default_layer { + label = "default"; // ------------------------------------------------------------------------------------------------------------ -// | ` | 1 | 2 | 3 | 4 | 5 | | 6 | 7 | 8 | 9 | 0 | | +// | ` | 1 | 2 | 3 | 4 | 5 | | 6 | 7 | 8 | 9 | 0 | | // | ESC | Q | W | E | R | T | | Y | U | I | O | P | BKSPC | // | TAB | A | S | D | F | G | | H | J | K | L | ; | ' | // | SHIFT | Z | X | C | V | B | MUTE | | | N | M | , | . | / | SHIFT | // | GUI | ALT | CTRL | LOWER| ENTER | | SPACE | RAISE| CTRL | ALT | GUI | bindings = < -&kp GRAVE &kp N1 &kp N2 &kp N3 &kp N4 &kp N5 &kp N6 &kp N7 &kp N8 &kp N9 &kp N0 &none -&kp ESC &kp Q &kp W &kp E &kp R &kp T &kp Y &kp U &kp I &kp O &kp P &kp BSPC -&kp TAB &kp A &kp S &kp D &kp F &kp G &kp H &kp J &kp K &kp L &kp SEMI &kp SQT -&kp LSHFT &kp Z &kp X &kp C &kp V &kp B &kp C_MUTE &none &kp N &kp M &kp COMMA &kp DOT &kp FSLH &kp RSHFT - &kp LGUI &kp LALT &kp LCTRL &mo 1 &kp RET &kp SPACE &mo 2 &kp RCTRL &kp RALT &kp RGUI +&kp GRAVE &kp N1 &kp N2 &kp N3 &kp N4 &kp N5 &kp N6 &kp N7 &kp N8 &kp N9 &kp N0 &none +&kp ESC &kp Q &kp W &kp E &kp R &kp T &kp Y &kp U &kp I &kp O &kp P &kp BSPC +&kp TAB &kp A &kp S &kp D &kp F &kp G &kp H &kp J &kp K &kp L &kp SEMI &kp SQT +&kp LSHFT &kp Z &kp X &kp C &kp V &kp B &kp C_MUTE &none &kp N &kp M &kp COMMA &kp DOT &kp FSLH &kp RSHFT + &kp LGUI &kp LALT &kp LCTRL &mo LOWER &kp RET &kp SPACE &mo RAISE &kp RCTRL &kp RALT &kp RGUI >; sensor-bindings = <&inc_dec_kp C_VOL_UP C_VOL_DN &inc_dec_kp PG_UP PG_DN>; }; lower_layer { + label = "lower"; // TODO: Some binds are waiting for shifted keycode support. // ------------------------------------------------------------------------------------------------------------ // | | F1 | F2 | F3 | F4 | F5 | | F6 | F7 | F8 | F9 | F10 | F11 | -// | ` | 1 | 2 | 3 | 4 | 5 | | 6 | 7 | 8 | 9 | 0 | F12 | +// | ` | 1 | 2 | 3 | 4 | 5 | | 6 | 7 | 8 | 9 | 0 | F12 | // | | ! | @ | # | $ | % | | ^ | & | * | ( | ) | | | // | | = | - | + | { | } | | | | [ | ] | ; | : | \ | | // | | | | | | | | | | | | @@ -42,29 +61,48 @@ &trans &kp F1 &kp F2 &kp F3 &kp F4 &kp F5 &kp F6 &kp F7 &kp F8 &kp F9 &kp F10 &kp F11 &kp GRAVE &kp N1 &kp N2 &kp N3 &kp N4 &kp N5 &kp N6 &kp N7 &kp N8 &kp N9 &kp N0 &kp F12 &trans &kp EXCL &kp AT &kp HASH &kp DLLR &kp PRCNT &kp CARET &kp AMPS &kp KP_MULTIPLY &kp LPAR &kp RPAR &kp PIPE -&trans &kp EQUAL &kp MINUS &kp KP_PLUS &kp LBRC &kp RBRC &trans &trans &kp LBKT &kp RBKT &kp SEMI &kp COLON &kp BSLH &trans - &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans +&trans &kp EQUAL &kp MINUS &kp KP_PLUS &kp LBRC &kp RBRC &trans &trans &kp LBKT &kp RBKT &kp SEMI &kp COLON &kp BSLH &trans + &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans >; sensor-bindings = <&inc_dec_kp C_VOL_UP C_VOL_DN &inc_dec_kp PG_UP PG_DN>; }; raise_layer { + label = "raise"; // ------------------------------------------------------------------------------------------------------------ -// |BTCLR| BT1 | BT2 | BT3 | BT4 | BT5 | | | | | | | | -// | | INS | PSCR | GUI | | | | PGUP | | ^ | | | | -// | | ALT | CTRL | SHIFT | | CAPS | | PGDN | <- | v | -> | DEL | BKSPC | -// | | UNDO | CUT | COPY | PASTE | | | | | | | | | | | -// | | | | | | | | | | | | +// | BTCLR | BT1 | BT2 | BT3 | BT4 | BT5 | | | | | | | | +// | | INS | PSCR | GUI | | | | PGUP | | ^ | | | | +// | | ALT | CTRL | SHIFT | | CAPS | | PGDN | <- | v | -> | DEL | BKSPC | +// | | UNDO | CUT | COPY | PASTE | | | | | | | | | | | +// | | | | | | | | | | | | bindings = < -&bt BT_CLR &bt BT_SEL 0 &bt BT_SEL 1 &bt BT_SEL 2 &bt BT_SEL 3 &bt BT_SEL 4 &trans &trans &trans &trans &trans &trans -&trans &kp INS &kp PSCRN &kp K_CMENU &trans &trans &kp PG_UP &trans &kp UP &trans &kp N0 &trans -&trans &kp LALT &kp LCTRL &kp LSHFT &trans &kp CLCK &kp PG_DN &kp LEFT &kp DOWN &kp RIGHT &kp DEL &kp BSPC -&trans &kp K_UNDO &kp K_CUT &kp K_COPY &kp K_PASTE &trans &trans &trans &trans &trans &trans &trans &trans &trans - &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans +&bt BT_CLR &bt BT_SEL 0 &bt BT_SEL 1 &bt BT_SEL 2 &bt BT_SEL 3 &bt BT_SEL 4 &trans &trans &trans &trans &trans &trans +&trans &kp INS &kp PSCRN &kp K_CMENU &trans &trans &kp PG_UP &trans &kp UP &trans &kp N0 &trans +&trans &kp LALT &kp LCTRL &kp LSHFT &trans &kp CLCK &kp PG_DN &kp LEFT &kp DOWN &kp RIGHT &kp DEL &kp BSPC +&trans &kp K_UNDO &kp K_CUT &kp K_COPY &kp K_PASTE &trans &trans &trans &trans &trans &trans &trans &trans &trans + &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans >; sensor-bindings = <&inc_dec_kp C_VOL_UP C_VOL_DN &inc_dec_kp PG_UP PG_DN>; }; + + adjust_layer { +// ---------------------------------------------------------------------------------------------------------------------------- +// | BTCLR | BT1 | BT2 | BT3 | BT4 | BT5 | | | | | | | | +// | EXTPWR | RGB_HUD | RGB_HUI | RGB_SAD | RGB_SAI | RGB_EFF | | | | | | | | +// | | RGB_BRD | RGB_BRI | | | | | | | | | | | +// | | | | | | | RGB_TOG | | | | | | | | | +// | | | | | | | | | | | | + label = "adjust"; + bindings = < +&bt BT_CLR &bt BT_SEL 0 &bt BT_SEL 1 &bt BT_SEL 2 &bt BT_SEL 3 &bt BT_SEL 4 &none &none &none &none &none &none +&ext_power EP_TOG &rgb_ug RGB_HUD &rgb_ug RGB_HUI &rgb_ug RGB_SAD &rgb_ug RGB_SAI &rgb_ug RGB_EFF &none &none &none &none &none &none +&none &rgb_ug RGB_BRD &rgb_ug RGB_BRI &none &none &none &none &none &none &none &none &none +&none &none &none &none &none &none &rgb_ug RGB_TOG &none &none &none &none &none &none &none + &none &none &none &none &none &none &none &none &none &none + >; + }; + }; }; diff --git a/app/boards/shields/sofle/sofle.zmk.yml b/app/boards/shields/sofle/sofle.zmk.yml index 5f6f99c3fed..47b66d6777c 100644 --- a/app/boards/shields/sofle/sofle.zmk.yml +++ b/app/boards/shields/sofle/sofle.zmk.yml @@ -9,6 +9,7 @@ features: - keys - display - encoder + - underglow siblings: - sofle_left - sofle_right From 913fdb831e4445bf26df74881fa74666a590020b Mon Sep 17 00:00:00 2001 From: Amettler Thierry Date: Wed, 4 Oct 2023 22:29:06 +0200 Subject: [PATCH 21/49] feat(docs): Add configuration snippet for layer-taps --- docs/docs/behaviors/layers.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/docs/behaviors/layers.md b/docs/docs/behaviors/layers.md index cf793089be6..7d7901568f9 100644 --- a/docs/docs/behaviors/layers.md +++ b/docs/docs/behaviors/layers.md @@ -57,6 +57,22 @@ Example: < LOWER SPACE ``` +### Configuration + +You can configure a different tapping term or tweak other properties noted in the [hold-tap](hold-tap.md#advanced-configuration) documentation page in your keymap: + +``` +< { + tapping-term-ms = <200>; +}; + +/ { + keymap { + ... + }; +}; +``` + :::info Functionally, the layer-tap is a [hold-tap](hold-tap.md) of the ["tap-preferred" flavor](hold-tap.md/#flavors) and a [`tapping-term-ms`](hold-tap.md/#tapping-term-ms) of 200 that takes in a [`momentary layer`](#momentary-layer) and a [keypress](key-press.md) as its "hold" and "tap" parameters, respectively. From ca5c9b4ae6b7008b81c9f9cb02fb7d295b3290b0 Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Thu, 5 Oct 2023 19:53:15 -0700 Subject: [PATCH 22/49] feat(blog): Add SOTF #6 (#1943) Co-authored-by: Pete Johanson --- docs/blog/2023-10-05-zmk-sotf-6.md | 295 +++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 docs/blog/2023-10-05-zmk-sotf-6.md diff --git a/docs/blog/2023-10-05-zmk-sotf-6.md b/docs/blog/2023-10-05-zmk-sotf-6.md new file mode 100644 index 00000000000..18a52a859fd --- /dev/null +++ b/docs/blog/2023-10-05-zmk-sotf-6.md @@ -0,0 +1,295 @@ +--- +title: "ZMK State Of The Firmware #6" +author: Cem Aksoylar +author_title: Documentation maintainer +author_url: https://github.com/caksoylar +author_image_url: https://avatars.githubusercontent.com/u/7876996 +tags: [SOTF, keyboards, firmware, oss, ble] +--- + +Welcome to the sixth ZMK "State Of The Firmware" (SOTF)! + +This update will cover all the major activity since [SOTF #5](/blog/2022/04/10/zmk-sotf-5). That was over a year ago (again!), so there are many new exciting features and plenty of improvements to cover! + +## Recent Activity + +Here's a summary of the various major changes since last time, broken down by theme: + +### Keymaps/Behaviors + +#### Hold-tap improvements + +[andrewjrae] added the [`require-prior-idle-ms` property](/docs/behaviors/hold-tap#require-prior-idle-ms) to the hold-tap behavior in [#1187](https://github.com/zmkfirmware/zmk/pull/1187) and [#1387](https://github.com/zmkfirmware/zmk/pull/1387), which prevents the hold behavior from triggering if it hasn't been a certain duration since the last key press. This is a useful feature to prevent accidental hold activations during quick typing and made its way into many keymaps! The same property was added to [combos](/docs/features/combos#configuration) as well to help prevent false combo activations. + +Note that an earlier iteration of this feature was supported with the `global-quick-tap` property, which did not allow customizing the timeout and used the value of `tapping-term-ms` for it. This property is now deprecated and users are encouraged to use `require-prior-idle-ms` instead. + +[urob] added the [`hold-trigger-on-release` property](/docs/behaviors/hold-tap#positional-hold-tap-and-hold-trigger-key-positions) in [#1423](https://github.com/zmkfirmware/zmk/pull/1423). This significantly increases the usefulness of positional constraints on hold-taps, since it allows combining multiple holds such as different modifiers for home row mods usage. + +#### Masking mods in mod-morphs + +[aumuell](https://github.com/aumuell), [vrinek](https://github.com/vrinek) and [urob] contributed to improving the behavior of [mod-morphs](/docs/behaviors/mod-morph) by masking the triggering modifiers and added `keep-mods` property in [#1412](https://github.com/zmkfirmware/zmk/pull/1412). This unlocks more use cases for mod-morphs, since you are no longer constrained to emitting keycodes that work well with the triggering modifier keycodes. + +As an example, you can now define a mod-morph that swaps `;` and `:` so that the former is the shifted version of the latter, which wasn't previously possible: + +```dts + col_semi: colon_semicolon { + compatible = "zmk,behavior-mod-morph"; + label = "COLON_SEMICOLON"; + #binding-cells = <0>; + bindings = <&kp COLON>, <&kp SEMI>; + mods = <(MOD_LSFT|MOD_RSFT)>; + }; +``` + +#### Parameterized macros + +[petejohanson] added [macros that can be parameterized](/docs/behaviors/macros#parameterized-macros) with one or two parameters in [#1232](https://github.com/zmkfirmware/zmk/pull/1232). This allows users to define macros in a more modular way and is a nice quality-of-life improvement. + +As a simple example, you could define a macro that puts any keycode provided between double quotes as below, then use it like `&ql A` in your keymap: + +```dts + ql: quoted_letter_macro { + #binding-cells = <1>; + label = "QUOTED_LETTER"; + compatible = "zmk,behavior-macro-one-param"; + bindings = + <&kp DQT>, + <¯o_param_1to1 &kp MACRO_PLACEHOLDER>, + <&kp DQT>; + }; +``` + +Please see the documentation page linked above for usage and more examples. + +#### Arbitrary behaviors on encoder rotation + +[nickconway](https://github.com/nickconway) and [petejohanson] added [sensor rotation behaviors](/docs/behaviors/sensor-rotate) to allow invoking arbitrary behaviors from encoders [#1758](https://github.com/zmkfirmware/zmk/pull/1758). Previously encoder rotations could only invoke the key-press behavior `&kp` through the `&inc_dec_kp` binding, whereas now you can define new sensor rotation behaviors to invoke others. + +(Note that currently behaviors that have "locality" such as `&rgb_ug` do not work as expected via encoder rotation bindings in split keyboards, due to issue [#1494](https://github.com/zmkfirmware/zmk/issues/1494).) + +#### Pre-releasing already pressed keys + +[andrewjrae] contributed a tweak to emitting keycodes in [#1828](https://github.com/zmkfirmware/zmk/pull/1828), where rolling multiple keys that involve the same keycode now releases the keycode before sending a press event again. While this might sound like a technical distinction, it leads to more correct behavior when quickly typing sequences like `+=` and makes the [key repeat behavior](/docs/behaviors/key-repeat) work properly when it is pressed before the previous key is released. + +#### Key toggle behavior + +[cgoates](https://github.com/cgoates) added the [key toggle behavior](/docs/behaviors/key-toggle) in [#1278](https://github.com/zmkfirmware/zmk/pull/1278), which can be used via its `&kt` binding to toggle the state of a keycode between pressed and released. + +#### Apple Globe key + +[ReFil] added support for the `C_AC_NEXT_KEYBOARD_LAYOUT_SELECT` keycode with alias `GLOBE` which acts as the Globe key in macOS and iOS in [#1938](https://github.com/zmkfirmware/zmk/pull/1938). Note that this keycode doesn't exactly behave like a Globe key that is present on an Apple keyboard and its limitations are documented in [this comment](https://github.com/zmkfirmware/zmk/pull/1938#issuecomment-1744579039) thanks to testing by [SethMilliken](https://github.com/SethMilliken). These limitations will be noted in the official [keycodes documentation](/docs/codes/applications) shortly. + +#### Bug fixes and other improvements + +[petejohanson], [andrewjrae] and [okke-formsma] tracked down and fixed an issue causing stuck keys when there are combos on key positions involving hold-tap behaviors in [#1411](https://github.com/zmkfirmware/zmk/pull/1411). This was an elusive bug that took a lot of effort from the community to nail down and fix! + +[nguyendown](https://github.com/nguyendown) and [joelspadin] tracked down and fixed a couple issues causing stuck keys with [sticky keys](/docs/behaviors/sticky-key) in [#1586](https://github.com/zmkfirmware/zmk/pull/1586), [#1745](https://github.com/zmkfirmware/zmk/pull/1745). + +[okke-formsma] fixed an issue allowing tap dances to be invoked by combos in [#1518](https://github.com/zmkfirmware/zmk/pull/1518). + +[petejohanson] tweaked the caps word behavior to ignore modifiers in [#1330](https://github.com/zmkfirmware/zmk/pull/1330). + +[HelloThisIsFlo](https://github.com/HelloThisIsFlo) documented a bug with combos involving overlapping keys and different timeouts, produced a reproducing unit test, then proceeded to fix it in [#1945](https://github.com/zmkfirmware/zmk/pull/1945). + +### Bluetooth and Split Improvements + +#### Multiple peripherals + +[xudongzheng] contributed to add support for more than one peripheral per keyboard in [#836](https://github.com/zmkfirmware/zmk/pull/836). This allows setups such as split keyboards with more than two halves, or enable a BLE-based "dongle mode" via a third device running ZMK that can stay connected to a computer via USB. + +Note that documentation is still lacking for utilizing more than one peripheral and there will potentially be future changes in the build system to allow for more seamless configuration. + +#### Pairing passkey requirement + +[petejohanson] added [the option to require passkey input](/docs/config/bluetooth) while pairing to new devices in [#1822](https://github.com/zmkfirmware/zmk/pull/1822). Enabling this will require you to enter a six digit passcode via the number keys on your keymap and press enter when pairing to a new device, enhancing security during the pairing procedure. + +#### Split keyboard improvements + +[petejohanson] contributed a fix to release held keys on peripheral disconnect [#1340](https://github.com/zmkfirmware/zmk/pull/1340), which makes scenarios where a split disconnects unexpectedly less painful. + +[petejohanson] also improved [the `settings_reset` shield](/docs/troubleshooting#split-keyboard-halves-unable-to-pair) by making it clear bonds more reliably, and allow it to build for all boards in [#1879](https://github.com/zmkfirmware/zmk/pull/1879). + +[petejohanson] and [xudongzheng] contributed additional split connectivity improvements, via using directed advertising in [#1913](https://github.com/zmkfirmware/zmk/pull/1913) and improving the robustness of central scanning in [#1912](https://github.com/zmkfirmware/zmk/pull/1912). + +### Hardware Support + +#### Encoders + +[petejohanson] contributed a major refactor of encoder (and more generally sensor) functionality in [#1039](https://github.com/zmkfirmware/zmk/pull/1039). While the documentation for these changes are still in progress, check out the [dedicated blog post](/blog/2023/06/18/encoder-refactors) for more details. + +This refactor paved way to implementing a long-awaited feature, encoder support in peripheral halves of split keyboards! Building upon the work by [stephen](https://github.com/stephen) in [#728](https://github.com/zmkfirmware/zmk/pull/728), [petejohanson] implemented support in [#1841](https://github.com/zmkfirmware/zmk/pull/1841). + +#### Direct GPIO driver + +[joelspadin] extended the comprehensive debouncing framework used for matrix scan driver to the [direct GPIO driver](/docs/config/kscan#direct-gpio-driver) in [#1288](https://github.com/zmkfirmware/zmk/pull/1288). + +[kurtis-lew] added toggle mode support for direct GPIO driver in [#1305](https://github.com/zmkfirmware/zmk/pull/1305). This allows for adding toggle switches to a keyboard, by properly reading their initial state on boot and making sure the power use is efficient. + +#### IO peripheral drivers + +[petejohanson] added support for the 595 shift register commonly used with smaller controllers like Seeeduino Xiaos, in [#1325](https://github.com/zmkfirmware/zmk/pull/1325). + +[zhiayang](https://github.com/zhiayang) added the driver for the MAX7318 GPIO expander in [#1295](https://github.com/zmkfirmware/zmk/pull/1295). + +#### Underglow auto-off options + +[ReFil] added two [new RGB auto off options](/docs/config/underglow), one using an idle timeout and the other USB status in [#1010](https://github.com/zmkfirmware/zmk/pull/1010). + +#### nice!view support + +[nicell] added support for nice!view, a memory display optimized for low power use in [#1462](https://github.com/zmkfirmware/zmk/pull/1462). +He also contributed a custom vertically-oriented status screen that is automatically enabled when the `nice_view` shield is used in [#1768](https://github.com/zmkfirmware/zmk/pull/1768), since the default status screen has a horizontal orientation. +Please see the instructions in the [nice!view README](https://github.com/zmkfirmware/zmk/blob/main/app/boards/shields/nice_view/README.md) if you would like to restore the stock status screen. + +#### E-paper display initialization + +[petejohanson] contributed EPD initialization improvements in [#1098](https://github.com/zmkfirmware/zmk/pull/1098), which makes the keyboards using slow refresh displays such as the Corne-ish Zen much more responsive during initial boot. + +#### Xiao BLE improvements + +Various improvements were made for the Seeeduino Xiao BLE board in [#1293](https://github.com/zmkfirmware/zmk/pull/1293), [d0176f36](https://github.com/zmkfirmware/zmk/commit/d0176f36), [#1545](https://github.com/zmkfirmware/zmk/pull/1545) and [#1927](https://github.com/zmkfirmware/zmk/pull/1927) by [petejohanson] and [caksoylar], enabling features necessary for ZMK and improving its power use. + +### Zephyr 3.2 Upgrade + +[petejohanson] once again contributed the massive work necessary for upgrading ZMK to Zephyr 3.2 in [#1499](https://github.com/zmkfirmware/zmk/pull/1499), with review help from [joelspadin] and testing by the community. This Zephyr release brings with it upgrades to the display library LVGL, adds official support for the RP2040 controllers and many internal refactors to help future development. +Check out the [dedicated blog post](/blog/2023/04/06/zephyr-3-2) for more details! + +### Documentation + +#### Configuration docs + +[joelspadin], through a massive amount of work in [#722](https://github.com/zmkfirmware/zmk/pull/722), contributed a whole new section to the documentation: [configuration](/docs/config)! It enumerates the configuration options for each ZMK feature that might be relevant to users in dedicated pages, making it a very handy reference. + +In addition, the [overview page](/docs/config) presents an overview of how configuration works in Zephyr in the context of ZMK, in terms of devicetree files (like the keymap files or shield overlays), and Kconfig ones (like the `.conf` files). It is very helpful in de-mystifying what the various files do and what syntax is expected in them. + +#### New behavior guide + +For users or future contributors that might want to dive into writing their own ZMK behaviors, [kurtis-lew] wrote a useful [guide on how to create new behaviors](/docs/development/new-behavior) in [#1268](https://github.com/zmkfirmware/zmk/pull/1268). + +#### Tap dance and hold-tap documentation improvements + +[kurtis-lew] also improved the documentation for these two behaviors in [#1298](https://github.com/zmkfirmware/zmk/pull/1298), by updating the diagrams to better clarify how their timings work and adding examples for scenarios that are frequently asked by users. + +#### Battery sensor documentation + +[joelspadin] also added documentation for setting up battery sensors, typically required for new boards, in [#868](https://github.com/zmkfirmware/zmk/pull/868). + +#### Shield interconnects + +[petejohanson] updated the [new shield guide](/docs/development/new-shield) for non-Pro Micro interconnects including Xiao, Arduino Uno and Blackpill in [#1607](https://github.com/zmkfirmware/zmk/pull/1607). + +#### Bluetooth feature page + +[petejohanson] and [caksoylar] added a new [Bluetooth feature page](/docs/features/bluetooth) as part of [#1499](https://github.com/zmkfirmware/zmk/pull/1499) and in [#1818](https://github.com/zmkfirmware/zmk/pull/1499), detailing ZMK's Bluetooth implementation and troubleshooting for common problems. + +In addition to the specific contributions listed above, various improvements and fixes to documentation are made by many users from the community, including but not limited to [kurtis-lew], [joelspadin], [filterpaper], [byran.tech](https://github.com/byran.tech), [dxmh] and [caksoylar]. These contributions are are all very appreciated! + +### Miscellaneous + +#### Reusable GitHub build workflow + +[elagil](https://github.com/elagil) helped switch the build workflow used by the [user config repos](/docs/user-setup) to a reusable one in [#1183](https://github.com/zmkfirmware/zmk/pull/1183) and it was further tweaked by [filterpaper] in [#1258](https://github.com/zmkfirmware/zmk/pull/1258). This allows any changes in the workflow to be propagated automatically to users, rather than requiring them to make the updates. The build workflow can be customized by the users [using input parameters](https://github.com/zmkfirmware/zmk/blob/main/.github/workflows/build-user-config.yml#L5) if desired. + +#### Pre-commit hooks + +[joelspadin] added various [pre-commit](https://pre-commit.com/) hooks and added checks to the repo to run them for each commit in [#1651](https://github.com/zmkfirmware/zmk/pull/1651). These hooks and resulting updates standardize formatting across devicetree and other source files, reducing busywork on both contributors and reviewers. + +#### Zephyr usage and other refactors + +[joelspadin] also contributed a few refactor PRs such as [#1269](https://github.com/zmkfirmware/zmk/pull/1269), [#1255](https://github.com/zmkfirmware/zmk/pull/1255) and [#1803](https://github.com/zmkfirmware/zmk/pull/1803), generally improving code quality and bringing the codebase in line with the latest Zephyr conventions. + +[petejohanson] refactored the drivers structure to bring it in line with the current Zephyr conventions for out-of-tree drivers in [#1919](https://github.com/zmkfirmware/zmk/pull/1919). + +#### Updated USB polling interval default + +USB HID polling interval now defaults to 1 ms, i.e. a 1000Hz polling rate, thanks to [joelspadin]'s tweak in [#1271](https://github.com/zmkfirmware/zmk/pull/1271). + +#### Additional display config options + +[caksoylar] added a couple configuration options for displays, including a setting to invert display colors in [#1754](https://github.com/zmkfirmware/zmk/pull/1754) and an option to display the battery percentage for the stock status screen in [#1563](https://github.com/zmkfirmware/zmk/pull/1563). + +## New Shields + +- Eternal keypad [#1136](https://github.com/zmkfirmware/zmk/pull/1136) - [halcyonCorsair](https://github.com/halcyonCorsair) +- nullbits SNAP [#1319](https://github.com/zmkfirmware/zmk/pull/1319) - [jaygreco](https://github.com/jaygreco) +- Aurora Sweep [#1504](https://github.com/zmkfirmware/zmk/pull/1504), Corne [#1520](https://github.com/zmkfirmware/zmk/pull/1520), Lily58 [#1553](https://github.com/zmkfirmware/zmk/pull/1553), Sofle [#1864](https://github.com/zmkfirmware/zmk/pull/1864), and Helix [#1873](https://github.com/zmkfirmware/zmk/pull/1873) - [petejohanson] +- ZMK Uno shield [#1576](https://github.com/zmkfirmware/zmk/pull/1576) - [petejohanson] +- Waterfowl [#1554](https://github.com/zmkfirmware/zmk/pull/1554) - [JW2586](https://github.com/JW2586) +- Kyria Rev 3 [#1627](https://github.com/zmkfirmware/zmk/pull/1627) - [petejohanson] +- Leeloo v2 and Leeloo-Micro [#1762](https://github.com/zmkfirmware/zmk/pull/1762) - [ClicketySplit](https://github.com/ClicketySplit) +- Spaceman Pancake [#1400](https://github.com/zmkfirmware/zmk/pull/1400) - [jasonhazel](https://github.com/jasonhazel) +- Reviung5 [#1548](https://github.com/zmkfirmware/zmk/pull/1548) - [zblesk](https://github.com/zblesk) + +## New Boards + +- RP2040 boards, including Sparkfun Pro Micro, Adafruit KB2040 and Seeeduino Xiao RP2040 were added as part of the Zephyr 3.2 upgrade in [#1499](https://github.com/zmkfirmware/zmk/pull/1499) - [petejohanson] +- Puchi BLE [#1445](https://github.com/zmkfirmware/zmk/pull/1445) - [BenRoe](https://github.com/BenRoe) +- nRFMicro 1.3/1.4 (nRF52833) [#912](https://github.com/zmkfirmware/zmk/pull/912) - [pashutk](https://github.com/pashutk) +- nRF5340 DK [#1562](https://github.com/zmkfirmware/zmk/pull/1562) - [joelspadin] +- PillBug [#1530](https://github.com/zmkfirmware/zmk/pull/1530) - [kylemccreery](https://github.com/kylemccreery) +- Preonic Rev 3 [#1575](https://github.com/zmkfirmware/zmk/pull/1575) - [jeromeOlivier](https://github.com/jeromeOlivier) +- Corne-ish Zen v2 [#1498](https://github.com/zmkfirmware/zmk/pull/1498) and v1 [#1593](https://github.com/zmkfirmware/zmk/pull/1593) - [LOWPROKB](https://github.com/LOWPROKB) and [caksoylar] +- Polarity Works CKP family of boards [#1547](https://github.com/zmkfirmware/zmk/pull/1547) - [ReFil] + +## Coming Soon! + +Some items listed in the last coming soon section are still under active development and other new exciting items are in progress: + +- Automatic/simple BLE profile management +- Soft off support for turning the keyboard "off" through firmware +- Improved automatic power management for devices with multiple peripherals, e.g. OLED displays and RGB LEDs +- Caps/Scroll/Num Lock LED support +- Mouse keys +- Wired split support +- More modular approach to external boards/shields, custom code, user keymaps, etc. +- More shields and boards + +## Statistics + +Some statistics of interest for ZMK: + +- GitHub (lifetime stats) + - 166 Contributors + - 1256 Closed PRs + - 1883 Stars + - 1949 Forks +- Discord Chat + - 8055 total registered (130% up from last SOTF!) +- Website (last 30 days) + - 52K page views + - 4.7K new users + +## Sponsorship + +While ZMK is an open source project that uses the permissive MIT license, below are opportunities for anyone who would like to show their support to the project financially. + +### Open Collective + +The ZMK project has an [Open Collective sponsorship](https://opencollective.com/zmkfirmware) that has been going for two and a half years. +This fund helps pay for project costs like domain registration or development of hardware such as the [ZMK Uno shield](https://github.com/zmkfirmware/zmk-uno). +Note that donations to this fund do _not_ pay for the work of any individual contributor directly. + +### Contributor sponsorships + +Project creator and lead Pete Johanson has a [GitHub sponsorship](https://github.com/sponsors/petejohanson) set up that you can contribute to, in order to directly support his time and efforts in developing and maintaining ZMK. +He has also been traveling full time while focusing on ZMK and keyboard hardware design for more than a year now! +If you are curious, you can check out [his blog post](https://petejohanson.dev/blog/new-journey-2022) on deciding to embark on this adventure, in addition to his thoughts on contributor vs. project sponsorship, and sustainability of open source projects in general. + +## Thanks! + +As the first person to author a State Of The Firmware post besides Pete, I'd like to take the opportunity to thank him for his efforts on leading and developing ZMK, along with fostering a great community of contributors and users around it. + +Also a big thank you to contributors that submit patches and perform reviews, testers that help validate changes, and users that take time out of their day to help out folks with ZMK usage on [Discord channels](https://zmk.dev/community/discord/invite), GitHub issues and other communities. + +[okke-formsma]: https://github.com/okke-formsma +[andrewjrae]: https://github.com/andrewjrae +[xudongzheng]: https://github.com/xudongzheng +[nicell]: https://github.com/Nicell +[petejohanson]: https://github.com/petejohanson +[kurtis-lew]: https://github.com/kurtis-lew +[joelspadin]: https://github.com/joelspadin +[dxmh]: https://github.com/dxmh +[caksoylar]: https://github.com/caksoylar +[urob]: https://github.com/urob +[filterpaper]: https://github.com/filterpaper +[ReFil]: https://github.com/ReFil From fd05478897567bff34d607229636ae4d4507ef0f Mon Sep 17 00:00:00 2001 From: Alex Kang Date: Fri, 6 Oct 2023 12:27:38 -0700 Subject: [PATCH 23/49] feat(shields): Microdox V2 shield definition * Refactor common parts of the Microdox sheild into a separate file. This is in preparation for adding Microdox V2 as another shield in the same directory. * Refactor Microdox keymap into a common file in preparation for Microdox V2 * Add Microdox V2 shield definition * Added a README to explain v1/v2 differences. --- app/boards/shields/microdox/Kconfig.defconfig | 4 +- app/boards/shields/microdox/Kconfig.shield | 6 +++ app/boards/shields/microdox/README.md | 8 +++ app/boards/shields/microdox/microdox.dtsi | 46 +---------------- .../shields/microdox/microdox_common.dtsi | 51 +++++++++++++++++++ app/boards/shields/microdox/microdox_v2.conf | 6 +++ app/boards/shields/microdox/microdox_v2.dtsi | 15 ++++++ .../shields/microdox/microdox_v2.zmk.yml | 13 +++++ .../shields/microdox/microdox_v2_left.conf | 0 .../shields/microdox/microdox_v2_left.overlay | 23 +++++++++ .../shields/microdox/microdox_v2_right.conf | 0 .../microdox/microdox_v2_right.overlay | 32 ++++++++++++ 12 files changed, 157 insertions(+), 47 deletions(-) create mode 100644 app/boards/shields/microdox/README.md create mode 100644 app/boards/shields/microdox/microdox_common.dtsi create mode 100644 app/boards/shields/microdox/microdox_v2.conf create mode 100644 app/boards/shields/microdox/microdox_v2.dtsi create mode 100644 app/boards/shields/microdox/microdox_v2.zmk.yml create mode 100644 app/boards/shields/microdox/microdox_v2_left.conf create mode 100644 app/boards/shields/microdox/microdox_v2_left.overlay create mode 100644 app/boards/shields/microdox/microdox_v2_right.conf create mode 100644 app/boards/shields/microdox/microdox_v2_right.overlay diff --git a/app/boards/shields/microdox/Kconfig.defconfig b/app/boards/shields/microdox/Kconfig.defconfig index d05ae045803..e355c6411ee 100644 --- a/app/boards/shields/microdox/Kconfig.defconfig +++ b/app/boards/shields/microdox/Kconfig.defconfig @@ -1,7 +1,7 @@ # Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT -if SHIELD_MICRODOX_LEFT +if SHIELD_MICRODOX_LEFT || SHIELD_MICRODOX_V2_LEFT config ZMK_KEYBOARD_NAME default "Microdox" @@ -11,7 +11,7 @@ config ZMK_SPLIT_ROLE_CENTRAL endif -if SHIELD_MICRODOX_LEFT || SHIELD_MICRODOX_RIGHT +if SHIELD_MICRODOX_LEFT || SHIELD_MICRODOX_RIGHT || SHIELD_MICRODOX_V2_LEFT || SHIELD_MICRODOX_V2_RIGHT config ZMK_SPLIT default y diff --git a/app/boards/shields/microdox/Kconfig.shield b/app/boards/shields/microdox/Kconfig.shield index 47543760b6a..e0f461ff56f 100644 --- a/app/boards/shields/microdox/Kconfig.shield +++ b/app/boards/shields/microdox/Kconfig.shield @@ -6,3 +6,9 @@ config SHIELD_MICRODOX_LEFT config SHIELD_MICRODOX_RIGHT def_bool $(shields_list_contains,microdox_right) + +config SHIELD_MICRODOX_V2_LEFT + def_bool $(shields_list_contains,microdox_v2_left) + +config SHIELD_MICRODOX_V2_RIGHT + def_bool $(shields_list_contains,microdox_v2_right) diff --git a/app/boards/shields/microdox/README.md b/app/boards/shields/microdox/README.md new file mode 100644 index 00000000000..f92807bb7eb --- /dev/null +++ b/app/boards/shields/microdox/README.md @@ -0,0 +1,8 @@ +# Microdox + +Microdox is a 36 key split keyboard by Boardsource. + +Two variants are defined for this shield – V1 and V2. The layout is exactly the same between the +two. Per [help documentation](https://www.boardsource.xyz/help/6129be4a9c85c6050be190d2), if you +purchased your PCB before April 2022, use `microdox_left`/`microdox_right`. Otherwise, use +`microdox_v2_left`/`microdox_v2_right`. diff --git a/app/boards/shields/microdox/microdox.dtsi b/app/boards/shields/microdox/microdox.dtsi index e02aa5546f4..57247e68159 100644 --- a/app/boards/shields/microdox/microdox.dtsi +++ b/app/boards/shields/microdox/microdox.dtsi @@ -4,35 +4,12 @@ * SPDX-License-Identifier: MIT */ -#include +#include "microdox_common.dtsi" / { - chosen { - zephyr,display = &oled; - zmk,kscan = &kscan0; - zmk,matrix_transform = &default_transform; - }; - - default_transform: keymap_transform_0 { - compatible = "zmk,matrix-transform"; - columns = <10>; - rows = <4>; -// | SW1 | SW2 | SW3 | SW4 | SW5 | | SW5 | SW4 | SW3 | SW2 | SW1 | -// | SW6 | SW7 | SW8 | SW9 | SW10 | | SW10 | SW9 | SW8 | SW7 | SW6 | -// | SW11 | SW12 | SW13 | SW14 | SW15 | | SW15 | SW14 | SW13 | SW12 | SW11 | -// | SW16 | SW17 | SW18 | | SW18 | SW17 | SW16 | - map = < -RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,5) RC(0,6) RC(0,7) RC(0,8) RC(0,9) -RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,5) RC(1,6) RC(1,7) RC(1,8) RC(1,9) -RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,6) RC(2,7) RC(2,8) RC(2,9) - RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(3,6) RC(3,7) - >; - }; - kscan0: kscan { compatible = "zmk,kscan-gpio-matrix"; label = "KSCAN"; - diode-direction = "col2row"; row-gpios = <&pro_micro 16 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> @@ -40,26 +17,5 @@ RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,6) RC(2,7) RC(2,8) RC(2,9) , <&pro_micro 8 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> , <&pro_micro 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> ; - - }; - - // TODO: per-key RGB node(s)? -}; - -&pro_micro_i2c { - status = "okay"; - - oled: ssd1306@3c { - compatible = "solomon,ssd1306fb"; - reg = <0x3c>; - label = "DISPLAY"; - width = <128>; - height = <32>; - segment-offset = <0>; - page-offset = <0>; - display-offset = <0>; - multiplex-ratio = <31>; - com-sequential; - prechargep = <0x22>; }; }; diff --git a/app/boards/shields/microdox/microdox_common.dtsi b/app/boards/shields/microdox/microdox_common.dtsi new file mode 100644 index 00000000000..0460e012287 --- /dev/null +++ b/app/boards/shields/microdox/microdox_common.dtsi @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + chosen { + zephyr,display = &oled; + zmk,kscan = &kscan0; + zmk,matrix_transform = &default_transform; + + }; + + default_transform: keymap_transform_0 { + compatible = "zmk,matrix-transform"; + columns = <10>; + rows = <4>; +// | SW1 | SW2 | SW3 | SW4 | SW5 | | SW5 | SW4 | SW3 | SW2 | SW1 | +// | SW6 | SW7 | SW8 | SW9 | SW10 | | SW10 | SW9 | SW8 | SW7 | SW6 | +// | SW11 | SW12 | SW13 | SW14 | SW15 | | SW15 | SW14 | SW13 | SW12 | SW11 | +// | SW16 | SW17 | SW18 | | SW18 | SW17 | SW16 | + map = < +RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,5) RC(0,6) RC(0,7) RC(0,8) RC(0,9) +RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,5) RC(1,6) RC(1,7) RC(1,8) RC(1,9) +RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,6) RC(2,7) RC(2,8) RC(2,9) + RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(3,6) RC(3,7) + >; + }; + + // TODO: per-key RGB node(s)? +}; + +&pro_micro_i2c { + status = "okay"; + oled: ssd1306@3c { + compatible = "solomon,ssd1306fb"; + reg = <0x3c>; + label = "DISPLAY"; + width = <128>; + height = <32>; + segment-offset = <0>; + page-offset = <0>; + display-offset = <0>; + multiplex-ratio = <31>; + com-sequential; + prechargep = <0x22>; + }; +}; diff --git a/app/boards/shields/microdox/microdox_v2.conf b/app/boards/shields/microdox/microdox_v2.conf new file mode 100644 index 00000000000..0d38398c029 --- /dev/null +++ b/app/boards/shields/microdox/microdox_v2.conf @@ -0,0 +1,6 @@ +# Uncomment the following lines to enable the Microdox RGB Underglow +# CONFIG_ZMK_RGB_UNDERGLOW=y +# CONFIG_WS2812_STRIP=y + +# Uncomment the following line to enable the Microdox OLED Display +# CONFIG_ZMK_DISPLAY=y diff --git a/app/boards/shields/microdox/microdox_v2.dtsi b/app/boards/shields/microdox/microdox_v2.dtsi new file mode 100644 index 00000000000..93fa44459db --- /dev/null +++ b/app/boards/shields/microdox/microdox_v2.dtsi @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include "microdox_common.dtsi" + +/ { + kscan0: kscan { + compatible = "zmk,kscan-gpio-matrix"; + label = "KSCAN"; + diode-direction = "col2row"; + }; +}; diff --git a/app/boards/shields/microdox/microdox_v2.zmk.yml b/app/boards/shields/microdox/microdox_v2.zmk.yml new file mode 100644 index 00000000000..1b9b65b33e4 --- /dev/null +++ b/app/boards/shields/microdox/microdox_v2.zmk.yml @@ -0,0 +1,13 @@ +file_format: "1" +id: microdox_v2 +name: Microdox V2 +type: shield +url: https://boardsource.xyz/store/5f2e7e4a2902de7151494f92 +requires: [pro_micro] +exposes: [i2c_oled] +features: + - keys + - display +siblings: + - microdox_v2_left + - microdox_v2_right diff --git a/app/boards/shields/microdox/microdox_v2_left.conf b/app/boards/shields/microdox/microdox_v2_left.conf new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/boards/shields/microdox/microdox_v2_left.overlay b/app/boards/shields/microdox/microdox_v2_left.overlay new file mode 100644 index 00000000000..83326f6fdc2 --- /dev/null +++ b/app/boards/shields/microdox/microdox_v2_left.overlay @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include "microdox_v2.dtsi" + +&kscan0 { + col-gpios + = <&pro_micro 4 GPIO_ACTIVE_HIGH> + , <&pro_micro 6 GPIO_ACTIVE_HIGH> + , <&pro_micro 14 GPIO_ACTIVE_HIGH> + , <&pro_micro 18 GPIO_ACTIVE_HIGH> + , <&pro_micro 19 GPIO_ACTIVE_HIGH> + ; + row-gpios + = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 0 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 5 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 15 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + ; +}; diff --git a/app/boards/shields/microdox/microdox_v2_right.conf b/app/boards/shields/microdox/microdox_v2_right.conf new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/boards/shields/microdox/microdox_v2_right.overlay b/app/boards/shields/microdox/microdox_v2_right.overlay new file mode 100644 index 00000000000..412c42f63f4 --- /dev/null +++ b/app/boards/shields/microdox/microdox_v2_right.overlay @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include "microdox.dtsi" + +&default_transform { + col-offset = <5>; +}; + +&oled { + segment-remap; + com-invdir; +}; + +&kscan0 { + col-gpios + = <&pro_micro 14 GPIO_ACTIVE_HIGH> + , <&pro_micro 5 GPIO_ACTIVE_HIGH> + , <&pro_micro 6 GPIO_ACTIVE_HIGH> + , <&pro_micro 15 GPIO_ACTIVE_HIGH> + , <&pro_micro 21 GPIO_ACTIVE_HIGH> + ; + row-gpios + = <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 18 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 19 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 7 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + ; +}; From 8a84b7dd5b843e69c8a2e74409c282cd3056c4ce Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sun, 8 Oct 2023 18:30:23 -0500 Subject: [PATCH 24/49] fix(shields): Fix incorrect union access in nice view Fixed an error in a previous commit where a member of the selected endpoint was used without checking if it was the correct transport type. The nice!view status screen displays the active BLE profile regardless of whether BLE is active, so we have to get that data directly from the BLE code instead of from the selected endpoint. --- app/boards/shields/nice_view/widgets/status.c | 5 ++++- app/boards/shields/nice_view/widgets/util.h | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/boards/shields/nice_view/widgets/status.c b/app/boards/shields/nice_view/widgets/status.c index c629be50f59..453fd650039 100644 --- a/app/boards/shields/nice_view/widgets/status.c +++ b/app/boards/shields/nice_view/widgets/status.c @@ -30,6 +30,7 @@ static sys_slist_t widgets = SYS_SLIST_STATIC_INIT(&widgets); struct output_status_state { struct zmk_endpoint_instance selected_endpoint; + int active_profile_index; bool active_profile_connected; bool active_profile_bonded; }; @@ -146,7 +147,7 @@ static void draw_middle(lv_obj_t *widget, lv_color_t cbuf[], const struct status }; for (int i = 0; i < 5; i++) { - bool selected = state->selected_endpoint.ble.profile_index == i; + bool selected = i == state->active_profile_index; lv_canvas_draw_arc(canvas, circle_offsets[i][0], circle_offsets[i][1], 13, 0, 359, &arc_dsc); @@ -228,6 +229,7 @@ ZMK_SUBSCRIPTION(widget_battery_status, zmk_usb_conn_state_changed); static void set_output_status(struct zmk_widget_status *widget, const struct output_status_state *state) { widget->state.selected_endpoint = state->selected_endpoint; + widget->state.active_profile_index = state->active_profile_index; widget->state.active_profile_connected = state->active_profile_connected; widget->state.active_profile_bonded = state->active_profile_bonded; @@ -243,6 +245,7 @@ static void output_status_update_cb(struct output_status_state state) { static struct output_status_state output_status_get_state(const zmk_event_t *_eh) { return (struct output_status_state){ .selected_endpoint = zmk_endpoints_selected(), + .active_profile_index = zmk_ble_active_profile_index(), .active_profile_connected = zmk_ble_active_profile_is_connected(), .active_profile_bonded = !zmk_ble_active_profile_is_open(), }; diff --git a/app/boards/shields/nice_view/widgets/util.h b/app/boards/shields/nice_view/widgets/util.h index e2d2782a3f9..fbcb616fad3 100644 --- a/app/boards/shields/nice_view/widgets/util.h +++ b/app/boards/shields/nice_view/widgets/util.h @@ -20,6 +20,7 @@ struct status_state { bool charging; #if !IS_ENABLED(CONFIG_ZMK_SPLIT) || IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) struct zmk_endpoint_instance selected_endpoint; + int active_profile_index; bool active_profile_connected; bool active_profile_bonded; uint8_t layer_index; From 791711b5556adee55ff9f9c68cdf4ab7fa0be117 Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Sat, 2 Sep 2023 14:22:18 -0700 Subject: [PATCH 25/49] feat(docs): Note how to use GPIO outside interconnect definition --- docs/docs/development/new-shield.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/docs/development/new-shield.md b/docs/docs/development/new-shield.md index 186169234f2..055afcf6ec7 100644 --- a/docs/docs/development/new-shield.md +++ b/docs/docs/development/new-shield.md @@ -120,6 +120,10 @@ endif +To use GPIO pins that are not part of the interconnects as described above, you can use the GPIO labels that are specific to each controller type. +For instance, pins numbered `PX.Y` in nRF52840-based boards can be referred to via `&gpioX Y` labels. +An example is `&gpio1 7` for the `P1.07` pin that the nice!nano exposes in the middle of the board. + Date: Tue, 21 Mar 2023 14:36:53 +0100 Subject: [PATCH 26/49] feat(shields): add right encoders support to waterfowl --- app/boards/shields/waterfowl/waterfowl.dtsi | 43 +++++++++++++++---- app/boards/shields/waterfowl/waterfowl.keymap | 30 ++++++++++--- .../shields/waterfowl/waterfowl_left.overlay | 6 ++- .../shields/waterfowl/waterfowl_right.overlay | 5 ++- 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/app/boards/shields/waterfowl/waterfowl.dtsi b/app/boards/shields/waterfowl/waterfowl.dtsi index 7f4929b3451..3d9140941b4 100644 --- a/app/boards/shields/waterfowl/waterfowl.dtsi +++ b/app/boards/shields/waterfowl/waterfowl.dtsi @@ -40,28 +40,53 @@ RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) , <&pro_micro 6 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> , <&pro_micro 7 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> ; + }; + roller_left_encoder: encoder_left_roller { + compatible = "alps,ec11"; + label = "ROLLER_LEFT_ENCODER"; + a-gpios = <&pro_micro 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; + b-gpios = <&pro_micro 8 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; + steps = <80>; + status = "disabled"; + }; + + dial_left_encoder: encoder_left_dial { + compatible = "alps,ec11"; + label = "DIAL_LEFT_ENCODER"; + a-gpios = <&pro_micro 14 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; + b-gpios = <&pro_micro 16 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; + steps = <80>; + status = "disabled"; }; - left_encoder: encoder_left { //roller + roller_right_encoder: encoder_right_roller { compatible = "alps,ec11"; - label = "LEFT_ENCODER"; + label = "ROLLER_RIGHT_ENCODER"; a-gpios = <&pro_micro 8 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; + status = "disabled"; }; - right_encoder: encoder_right { //Standard encoder on left half + dial_right_encoder: encoder_right_dial { compatible = "alps,ec11"; - label = "RIGHT_ENCODER"; - a-gpios = <&pro_micro 14 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - b-gpios = <&pro_micro 16 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <2>; + label = "DIAL_RIGHT_ENCODER"; + a-gpios = <&pro_micro 16 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; + b-gpios = <&pro_micro 14 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; + steps = <80>; + status = "disabled"; }; sensors { compatible = "zmk,keymap-sensors"; - sensors = <&left_encoder &right_encoder>; + triggers-per-rotation = <20>; + sensors = < + &roller_left_encoder + &dial_left_encoder + &dial_right_encoder + &roller_right_encoder + >; }; // TODO: RGB node(s) diff --git a/app/boards/shields/waterfowl/waterfowl.keymap b/app/boards/shields/waterfowl/waterfowl.keymap index c47f188b07e..197a34fa182 100644 --- a/app/boards/shields/waterfowl/waterfowl.keymap +++ b/app/boards/shields/waterfowl/waterfowl.keymap @@ -33,7 +33,12 @@ &kp N1 < 3 DEL < 1 SPACE &kp TAB &kp N2 &kp N3 &kp ESC &kp BSPC < 2 RET &kp N4 >; - sensor-bindings = <&inc_dec_kp PAGE_UP PAGE_DOWN &inc_dec_kp TAB LS(TAB)>; + sensor-bindings = < + &inc_dec_kp PAGE_DOWN PAGE_UP + &inc_dec_kp C_VOL_DN C_VOL_UP + &inc_dec_kp DOWN UP + &inc_dec_kp LEFT RIGHT + >; }; navnum_layer { @@ -57,7 +62,12 @@ &kp N1 < 3 DEL < 1 SPACE &kp TAB &kp N2 &kp N3 &kp ESC &kp BSPC < 2 RET &kp N4 >; - sensor-bindings = <&inc_dec_kp PAGE_UP PAGE_DOWN &inc_dec_kp TAB LS(TAB)>; + sensor-bindings = < + &inc_dec_kp PAGE_DOWN PAGE_UP + &inc_dec_kp C_VOL_DN C_VOL_UP + &inc_dec_kp DOWN UP + &inc_dec_kp LEFT RIGHT + >; }; symbol_layer { @@ -81,7 +91,12 @@ &kp N1 < 3 DEL < 1 SPACE &kp TAB &kp N2 &kp N3 &kp ESC &kp BSPC < 2 RET &kp N4 >; - sensor-bindings = <&inc_dec_kp PAGE_UP PAGE_DOWN &inc_dec_kp TAB LS(TAB)>; + sensor-bindings = < + &inc_dec_kp PAGE_DOWN PAGE_UP + &inc_dec_kp C_VOL_DN C_VOL_UP + &inc_dec_kp DOWN UP + &inc_dec_kp LEFT RIGHT + >; }; function_layer { @@ -105,8 +120,13 @@ &kp N1 < 3 DEL < 1 SPACE &kp TAB &kp N2 &kp N3 &kp ESC &kp BSPC < 2 RET &kp N4 >; - sensor-bindings = <&inc_dec_kp PAGE_UP PAGE_DOWN &inc_dec_kp TAB LS(TAB)>; + sensor-bindings = < + &inc_dec_kp PAGE_DOWN PAGE_UP + &inc_dec_kp C_VOL_DN C_VOL_UP + &inc_dec_kp DOWN UP + &inc_dec_kp LEFT RIGHT + >; }; }; -}; \ No newline at end of file +}; diff --git a/app/boards/shields/waterfowl/waterfowl_left.overlay b/app/boards/shields/waterfowl/waterfowl_left.overlay index 3b9fd42d404..d58c2876374 100644 --- a/app/boards/shields/waterfowl/waterfowl_left.overlay +++ b/app/boards/shields/waterfowl/waterfowl_left.overlay @@ -16,6 +16,10 @@ ; }; -&left_encoder { +&roller_left_encoder { + status = "okay"; +}; + +&dial_left_encoder { status = "okay"; }; diff --git a/app/boards/shields/waterfowl/waterfowl_right.overlay b/app/boards/shields/waterfowl/waterfowl_right.overlay index bf8f3a446b1..cb23b29ac9f 100644 --- a/app/boards/shields/waterfowl/waterfowl_right.overlay +++ b/app/boards/shields/waterfowl/waterfowl_right.overlay @@ -20,7 +20,10 @@ ; }; +&roller_right_encoder { + status = "okay"; +}; -&right_encoder { +&dial_right_encoder { status = "okay"; }; From 4a339093cebd9e89e531f78dc218907a292cbb1b Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Fri, 6 Oct 2023 22:02:23 -0500 Subject: [PATCH 27/49] docs: Add highlighting for devicetree and kconfig Added syntax highlighting for devicetree and kconfig files. The PrismJS project is not accepting contributions right now as they work on a version 2 of the library, so the new language files are added directly here. Also enabled syntax highlighting for various languages that are used in the docs but aren't enabled in Docusaurus by default. --- docs/docusaurus.config.js | 15 ++++ docs/src/theme/prism-include-languages.js | 23 ++++++ .../prism/components/prism-devicetree.js | 41 +++++++++++ .../theme/prism/components/prism-kconfig.js | 44 +++++++++++ .../theme/prism/themes/github-dark-dimmed.js | 73 +++++++++++++++++++ docs/src/theme/prism/themes/github.js | 73 +++++++++++++++++++ 6 files changed, 269 insertions(+) create mode 100644 docs/src/theme/prism-include-languages.js create mode 100644 docs/src/theme/prism/components/prism-devicetree.js create mode 100644 docs/src/theme/prism/components/prism-kconfig.js create mode 100644 docs/src/theme/prism/themes/github-dark-dimmed.js create mode 100644 docs/src/theme/prism/themes/github.js diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index e039d3691f6..701b5b997bb 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -1,4 +1,6 @@ const path = require("path"); +const theme = require("./src/theme/prism/themes/github"); +const darkTheme = require("./src/theme/prism/themes/github-dark-dimmed"); module.exports = { title: "ZMK Firmware", @@ -20,6 +22,19 @@ module.exports = { colorMode: { respectPrefersColorScheme: true, }, + prism: { + additionalLanguages: [ + "bash", + "c", + "cmake", + "ini", + "linker-script", + "log", + "powershell", + ], + theme, + darkTheme, + }, // sidebarCollapsible: false, navbar: { title: "ZMK Firmware", diff --git a/docs/src/theme/prism-include-languages.js b/docs/src/theme/prism-include-languages.js new file mode 100644 index 00000000000..c073923b92a --- /dev/null +++ b/docs/src/theme/prism-include-languages.js @@ -0,0 +1,23 @@ +import siteConfig from "@generated/docusaurus.config"; +export default function prismIncludeLanguages(PrismObject) { + const { + themeConfig: { prism }, + } = siteConfig; + const { additionalLanguages } = prism; + // Prism components work on the Prism instance on the window, while prism- + // react-renderer uses its own Prism instance. We temporarily mount the + // instance onto window, import components to enhance it, then remove it to + // avoid polluting global namespace. + // You can mutate PrismObject: registering plugins, deleting languages... As + // long as you don't re-assign it + globalThis.Prism = PrismObject; + additionalLanguages.forEach((lang) => { + // eslint-disable-next-line global-require + require(`prismjs/components/prism-${lang}`); + }); + + require("./prism/components/prism-devicetree.js"); + require("./prism/components/prism-kconfig.js"); + + delete globalThis.Prism; +} diff --git a/docs/src/theme/prism/components/prism-devicetree.js b/docs/src/theme/prism/components/prism-devicetree.js new file mode 100644 index 00000000000..22e638c6ae4 --- /dev/null +++ b/docs/src/theme/prism/components/prism-devicetree.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/* eslint-disable no-undef */ + +Prism.languages.devicetree = { + comment: Prism.languages.c["comment"], + string: Prism.languages.c["string"], + keyword: + /\/(?:bits|delete-node|delete-property|dts-v1|incbin|include|memreserve|omit-if-no-ref|plugin)\//, + label: { + pattern: /\b(?:[a-z_]\w*):/i, + alias: "symbol", + }, + reference: { + pattern: /&(?:[a-z_]\w*|\{[\w,.+*#?@/-]*\})/i, + alias: "variable", + }, + node: { + pattern: /(?:\/|\b[\w,.+\-@]+)(?=\s*\{)/, + alias: "class-name", + inside: { + // Node address + number: { + pattern: /(@)[0-9a-f,]/i, + lookbehind: true, + }, + }, + }, + function: Prism.languages.c["function"], + "attr-name": /\\?[\w,.+*#?@-]+(?=\s*[=;])/, + number: [/\b[0-9a-f]{2}\b/i, /\b(?:0[xX][0-9a-fA-F]+|\d+)(?:ULL|UL|LL|U|L)?/], + macro: Prism.languages.c["macro"], + operator: /<<|>>|[<>]=?|[!=]=?|&&?|\|\|?|[+\-*/%~?^]/, + punctuation: /[{}[\];(),.]/, +}; + +Prism.languages.dts = Prism.languages.devicetree; diff --git a/docs/src/theme/prism/components/prism-kconfig.js b/docs/src/theme/prism/components/prism-kconfig.js new file mode 100644 index 00000000000..f911d6e32be --- /dev/null +++ b/docs/src/theme/prism/components/prism-kconfig.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/* eslint-disable no-undef */ + +Prism.languages.kconfig = { + comment: { + pattern: /(^|[^\\])#.*/, + lookbehind: true, + greedy: true, + }, + string: /"(?:\\.|[^\\\r\n"])*"/, + helptext: { + // help text ends at the first line at a lower level of indentation than the + // first line of text. + pattern: /(^\s*)(?:help|---help---)\s*^(\s+)(?:.+)(?:\s*^\2[^\n]*)*/m, + lookbehind: true, + alias: "string", + inside: { + keyword: /^(?:help|---help---)/, + }, + }, + keyword: + /\b(?:allnoconfig_y|bool|boolean|choice|comment|config|def_bool|def_hex|def_int|def_string|def_tristate|default|defconfig_list|depends|endchoice|endif|endmenu|env|hex|if|imply|int|mainmenu|menu|menuconfig|modules|on|option|optional|orsource|osource|prompt|range|rsource|select|source|string|tristate|visible)\b/, + expansion: { + pattern: /\$\([\s\S]+\)/, + alias: "variable", + inside: { + function: /\$\(|\)/, + punctuation: /,/, + }, + }, + number: /\b(?:0[xX][0-9a-fA-F]+|\d+)/, + boolean: { + pattern: /\b(?:y|n|m)\b/, + alias: "number", + }, + variable: /\b[A-Z_]+\b/, + operator: /[<>]=?|[!=]=?|&&|\|\|/, + punctuation: /[()]/, +}; diff --git a/docs/src/theme/prism/themes/github-dark-dimmed.js b/docs/src/theme/prism/themes/github-dark-dimmed.js new file mode 100644 index 00000000000..210742bba0b --- /dev/null +++ b/docs/src/theme/prism/themes/github-dark-dimmed.js @@ -0,0 +1,73 @@ +/* + Converted from https://github.com/highlightjs/highlight.js/blob/main/src/styles/github-dark-dimmed.css + + BSD 3-Clause License + + Copyright (c) 2006, Ivan Sagalaev. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** @type {import("prism-react-renderer").PrismTheme} */ +const theme = { + plain: { color: "#adbac7", backgroundColor: "#22272e" }, + styles: [ + { types: ["keyword", "atrule"], style: { color: "#f47067" } }, + { types: ["class-name", "function"], style: { color: "#dcbdfb" } }, + { + types: [ + "attr-name", + "boolean", + "important", + "doctype", + "prolog", + "cdata", + "number", + "operator", + "variable", + "selector", + ], + style: { color: "#6cb6ff" }, + }, + { types: ["regex", "string", "char", "url"], style: { color: "#96d0ff" } }, + { types: ["builtin", "symbol", "entity"], style: { color: "#f69d50" } }, + { types: ["comment"], style: { color: "#768390" } }, + { types: ["italic"], style: { color: "#adbac7", fontStyle: "italic" } }, + { types: ["bold"], style: { color: "#adbac7", fontWeight: "bold" } }, + { + types: ["inserted"], + style: { color: "#b4f1b4", backgroundColor: "#1b4721" }, + }, + { + types: ["deleted"], + style: { color: "#ffd8d3", backgroundColor: "#78191b" }, + }, + { types: ["property", "punctuation", "tag"], style: {} }, + ], +}; + +module.exports = theme; diff --git a/docs/src/theme/prism/themes/github.js b/docs/src/theme/prism/themes/github.js new file mode 100644 index 00000000000..b0be14c35ab --- /dev/null +++ b/docs/src/theme/prism/themes/github.js @@ -0,0 +1,73 @@ +/* + Converted from https://github.com/highlightjs/highlight.js/blob/main/src/styles/github.css + + BSD 3-Clause License + + Copyright (c) 2006, Ivan Sagalaev. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** @type {import("prism-react-renderer").PrismTheme} */ +const theme = { + plain: { color: "#24292e", backgroundColor: "#f9f9f9" }, + styles: [ + { types: ["keyword", "atrule"], style: { color: "#d73a49" } }, + { types: ["class-name", "function"], style: { color: "#6f42c1" } }, + { + types: [ + "attr-name", + "boolean", + "important", + "doctype", + "prolog", + "cdata", + "number", + "operator", + "variable", + "selector", + ], + style: { color: "#005cc5" }, + }, + { types: ["regex", "string", "char", "url"], style: { color: "#032f62" } }, + { types: ["builtin", "symbol", "entity"], style: { color: "#e36209" } }, + { types: ["comment"], style: { color: "#6a737d" } }, + { types: ["italic"], style: { color: "#24292e", fontStyle: "italic" } }, + { types: ["bold"], style: { color: "#24292e", fontWeight: "bold" } }, + { + types: ["inserted"], + style: { color: "#22863a", backgroundColor: "#f0fff4" }, + }, + { + types: ["deleted"], + style: { color: "#b31d28", backgroundColor: "#ffeef0" }, + }, + { types: ["property", "punctuation", "tag"], style: {} }, + ], +}; + +module.exports = theme; From 65667b863ad380955da60d38ac2ae81b4dab06f5 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Fri, 6 Oct 2023 22:05:49 -0500 Subject: [PATCH 28/49] docs: Add syntax highlighting to more code blocks Added language tags to more code blocks in the documentation to enable syntax highlighting. --- docs/README.md | 6 ++-- docs/blog/2020-10-03-bootloader-fix.md | 6 ++-- docs/blog/2020-11-09-zmk-sotf-3.md | 4 +-- docs/blog/2021-01-27-zmk-sotf-4.md | 8 ++--- docs/blog/2022-04-02-zephyr-3-0.md | 26 +++++++------- docs/blog/2022-04-10-zmk-sotf-5.md | 10 +++--- docs/blog/2023-04-06-zephyr-3-2.md | 30 ++++++++-------- docs/blog/2023-06-18-encoder-refactors.md | 10 +++--- docs/docs/behaviors/backlight.md | 6 ++-- docs/docs/behaviors/bluetooth.md | 10 +++--- docs/docs/behaviors/caps-word.md | 8 ++--- docs/docs/behaviors/hold-tap.md | 18 +++++----- docs/docs/behaviors/key-press.md | 4 +-- docs/docs/behaviors/key-repeat.md | 4 +-- docs/docs/behaviors/key-toggle.md | 2 +- docs/docs/behaviors/layers.md | 14 ++++---- docs/docs/behaviors/macros.md | 26 +++++++------- docs/docs/behaviors/misc.md | 4 +-- docs/docs/behaviors/mod-morph.md | 8 ++--- docs/docs/behaviors/mod-tap.md | 4 +-- docs/docs/behaviors/outputs.md | 8 ++--- docs/docs/behaviors/power.md | 8 ++--- docs/docs/behaviors/reset.md | 4 +-- docs/docs/behaviors/sensor-rotate.md | 4 +-- docs/docs/behaviors/sticky-key.md | 8 ++--- docs/docs/behaviors/sticky-layer.md | 4 +-- docs/docs/behaviors/tap-dance.md | 4 +-- docs/docs/behaviors/underglow.md | 6 ++-- docs/docs/config/index.md | 16 ++++----- docs/docs/config/kscan.md | 12 +++---- docs/docs/customization.md | 4 +-- docs/docs/development/build-flash.md | 10 +++--- docs/docs/development/ide-integration.md | 2 +- docs/docs/development/new-behavior.md | 4 +-- docs/docs/development/new-shield.md | 44 ++++++++++------------- docs/docs/development/posix-board.md | 4 +-- docs/docs/development/usb-logging.md | 6 ++-- docs/docs/features/backlight.md | 24 ++++++------- docs/docs/features/battery.md | 2 +- docs/docs/features/beta-testing.md | 6 ++-- docs/docs/features/bluetooth.md | 4 +-- docs/docs/features/combos.md | 2 +- docs/docs/features/conditional-layers.md | 2 +- docs/docs/features/debouncing.md | 4 +-- docs/docs/features/encoders.md | 4 +-- docs/docs/features/keymaps.md | 10 +++--- docs/docs/features/underglow.md | 12 +++---- docs/docs/keymap-example-file.md | 2 +- docs/docs/keymap-example.md | 2 +- docs/docs/user-setup.md | 6 ++-- 50 files changed, 215 insertions(+), 221 deletions(-) diff --git a/docs/README.md b/docs/README.md index fab8b874e03..1fd6775d534 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,13 +8,13 @@ The ZMK Documentation is licensed [CC-BY-NC-SA](http://creativecommons.org/licen ### Installation -``` +```sh $ npm ci ``` ### Local Development -``` +```sh $ npm start ``` @@ -22,7 +22,7 @@ This command starts a local development server and open up a browser window. Mos ### Build -``` +```sh $ npm build ``` diff --git a/docs/blog/2020-10-03-bootloader-fix.md b/docs/blog/2020-10-03-bootloader-fix.md index ec1d7b0ba72..435034cfbfb 100644 --- a/docs/blog/2020-10-03-bootloader-fix.md +++ b/docs/blog/2020-10-03-bootloader-fix.md @@ -77,7 +77,7 @@ So first I enabled logging of the NVS module by adding `CONFIG_NVS_LOG_LEVEL_DBG=y` to my `.conf` file. I repeated the same test of spamming RGB underglow effect cycle and the resulting logs I got were this: -``` +```log [00:00:00.000,671] fs_nvs: 8 Sectors of 4096 bytes [00:00:00.000,671] fs_nvs: alloc wra: 3, f70 [00:00:00.000,671] fs_nvs: data wra: 3, f40 @@ -130,7 +130,7 @@ and [Dan Halbert](https://github.com/dhalbert) pointed me towards the [linker map](https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/master/linker/nrf52840.ld) of the nRF52840. Let's take a look. -``` +```linker-script FLASH (rx) : ORIGIN = 0xF4000, LENGTH = 0xFE000-0xF4000-2048 /* 38 KB */ BOOTLOADER_CONFIG (r): ORIGIN = 0xFE000 - 2048, LENGTH = 2048 @@ -166,7 +166,7 @@ Now that we've found the issue, we can pretty easily fix this. We'll need to move the settings flash area back so that it doesn't overlap with the bootloader. First we calculate the size of the of flash area the bootloader is using. -``` +```linker-script 0x100000 (end of flash) - 0x0F4000 (start of bootloader) = 0xC000 (48KB) ``` diff --git a/docs/blog/2020-11-09-zmk-sotf-3.md b/docs/blog/2020-11-09-zmk-sotf-3.md index 9e250a99c86..11d67040346 100644 --- a/docs/blog/2020-11-09-zmk-sotf-3.md +++ b/docs/blog/2020-11-09-zmk-sotf-3.md @@ -30,7 +30,7 @@ This also laid the foundation for the other keymap related changes that are now [okke-formsma] added the ability to apply modifiers to a code, e.g.: -``` +```dts &kp LC(C) ``` @@ -63,7 +63,7 @@ and nice!nano that have specialized hardware for this purpose. With this change, you can add -``` +```dts &ext_power EP_TOG ``` diff --git a/docs/blog/2021-01-27-zmk-sotf-4.md b/docs/blog/2021-01-27-zmk-sotf-4.md index e7ff3232b51..fbb53e4031e 100644 --- a/docs/blog/2021-01-27-zmk-sotf-4.md +++ b/docs/blog/2021-01-27-zmk-sotf-4.md @@ -25,7 +25,7 @@ The initial [combos](/docs/features/combos) work has landed! The amazing [okke-f An example, that would send the `ESC` keycode when pressing both the first and second positions on your keyboard: -``` +```dts / { combos { compatible = "zmk,combos"; @@ -46,13 +46,13 @@ Combos currently are "global", and not scoped to a given active layer. There is [okke-formsma] also contributed the initial "sticky keys" behavior, which can be used for functionality sometimes called "one shot mods" or "one shot layers". In your keymap, this would like like: -``` +```dts &sk LEFT_CONTROL ``` for a sticky key/modifier, or: -``` +```dts &sl NAV ``` @@ -68,7 +68,7 @@ This is most frequently used when using multiple core base layers with different [okke-formsma] added an implementation of the "Grave Escape" behavior, developing a more generic "mod-morph" behavior to do so. Adding -``` +```dts &gresc ``` diff --git a/docs/blog/2022-04-02-zephyr-3-0.md b/docs/blog/2022-04-02-zephyr-3-0.md index 103573c663c..6ec4c904f87 100644 --- a/docs/blog/2022-04-02-zephyr-3-0.md +++ b/docs/blog/2022-04-02-zephyr-3-0.md @@ -29,12 +29,12 @@ Existing user config repositories using Github Actions to build will pull down Z - Change `zmkfirmware/zmk-build-arm:2.5` to `zmkfirmware/zmk-build-arm:stable` wherever it is found - Locate and delete the lines for the DTS output step, which is no longer needed: - ``` - - name: ${{ steps.variables.outputs.display-name }} DTS File - if: ${{ always() }} - run: | - if [ -f "build/zephyr/${{ matrix.board }}.pre.tmp" ]; then cat -n build/zephyr/${{ matrix.board }}.pre.tmp; fi - if [ -f "build/zephyr/zephyr.dts" ]; then cat -n build/zephyr/zephyr.dts; fi + ```yaml + - name: ${{ steps.variables.outputs.display-name }} DTS File + if: ${{ always() }} + run: | + if [ -f "build/zephyr/${{ matrix.board }}.pre.tmp" ]; then cat -n build/zephyr/${{ matrix.board }}.pre.tmp; fi + if [ -f "build/zephyr/zephyr.dts" ]; then cat -n build/zephyr/zephyr.dts; fi ``` :::note @@ -76,7 +76,7 @@ The following changes have [already been completed](https://github.com/zmkfirmwa Zephyr's WS2812 `led_strip` driver added a new required property. When adding [underglow](/docs/features/underglow#adding-rgb-underglow-to-a-board) to a board, you now must also add the additional include `#include ` at the top of your devicetree file, and add a `color-mapping` property like: -``` +```dts led_strip: ws2812@0 { compatible = "worldsemi,ws2812-spi"; label = "WS2812"; @@ -108,7 +108,7 @@ Zephyr moved to using a `chosen` node named `zephyr,display` to select the displ For example, for a shield with: -``` +```dts &pro_micro_i2c { status = "okay"; @@ -132,7 +132,7 @@ For example, for a shield with: You would add a `chosen` node like: -``` +```dts / { chosen { zephyr,display = &oled; @@ -148,7 +148,7 @@ two sections of the `.dts` file need updating. Underneath the USB device, add the CDC ACM node: -``` +```dts &usbd { status = "okay"; cdc_acm_uart: cdc_acm_uart { @@ -160,7 +160,7 @@ Underneath the USB device, add the CDC ACM node: Then, an additional `chosen` node (near the top of the file) will mark the CDC ACM device as the console: -``` +```dts / { chosen { ... @@ -175,7 +175,7 @@ Then, an additional `chosen` node (near the top of the file) will mark the CDC A Previously, to get ZMK to build a UF2 image to flash to a given board required adding a `CMakeLists.txt` file that added a custom post build command. Now, the only thing necessary to have Zephyr build a UF2 is to add the following to your `_defconfig` file: -``` +```ini CONFIG_BUILD_OUTPUT_UF2=y ``` @@ -187,7 +187,7 @@ For more details on the implementation, see [zephyr#31066](https://github.com/ze Clock configuration moved to devicetree as well, out of the Kconfig files. Here is a sample config for a board that uses the HSI for the PLL source: -``` +```dts &clk_hsi { status = "okay"; }; diff --git a/docs/blog/2022-04-10-zmk-sotf-5.md b/docs/blog/2022-04-10-zmk-sotf-5.md index b0dd6310354..4ea62a31b87 100644 --- a/docs/blog/2022-04-10-zmk-sotf-5.md +++ b/docs/blog/2022-04-10-zmk-sotf-5.md @@ -32,7 +32,7 @@ to the host capitalized until a non-alpha, non-"continue list" keycode is sent. [petejohanson], taking heavy inspiration on the initial work from [okke-formsma], added [macro support](/docs/behaviors/macros) in [#1168](https://github.com/zmkfirmware/zmk/pull/1166). Several [common patterns](/docs/behaviors/macros#common-patterns) are documented, but one example, changing the underglow color as you activate/deactivate a layer, looks like: -``` +```dts ZMK_MACRO(layer_color_macro, wait-ms = <0>; tap-ms = <0>; @@ -50,7 +50,7 @@ ZMK_MACRO(layer_color_macro, [kurtis-lew] worked diligently to add the [tap-dance behavior](/docs/behaviors/tap-dance) in [#1139](https://github.com/zmkfirmware/zmk/pull/1139), allowing different behaviors to be invoked based on the number of times a user taps a single key in their keymap, e.g. -``` +```dts / { behaviors { td0: tap_dance_0 { @@ -80,7 +80,7 @@ a user taps a single key in their keymap, e.g. Example: -``` +```dts / { conditional_layers { compatible = "zmk,conditional-layers"; @@ -164,7 +164,7 @@ using "high voltage mode" with that SoC. [petejohanson]'s work on the HID foundation also included adding support for full NKRO HID in [#726](https://github.com/zmkfirmware/zmk/pull/726) that can be enabled by adding the following to your `.conf` file for your config: -``` +```ini CONFIG_ZMK_HID_REPORT_TYPE_NKRO=y ``` @@ -176,7 +176,7 @@ It's been live for a while, but [nicell] added an amazing [power profiler](/powe [malinges](https://github.com/malinges) added support for configuring min/max underglow brightness in [#944](https://github.com/zmkfirmware/zmk/pull/944) by setting the values in your `.conf` file as percentages of full: -``` +```ini CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN=20 CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX=80 ``` diff --git a/docs/blog/2023-04-06-zephyr-3-2.md b/docs/blog/2023-04-06-zephyr-3-2.md index 693d93dc828..69ecb6dd032 100644 --- a/docs/blog/2023-04-06-zephyr-3-2.md +++ b/docs/blog/2023-04-06-zephyr-3-2.md @@ -26,7 +26,7 @@ Existing user config repositories using Github Actions to build will pull down Z 1. Replace the contents of your `.github/workflows/build.yml` with: - ``` + ```yaml on: [push, pull_request, workflow_dispatch] jobs: @@ -36,7 +36,7 @@ Existing user config repositories using Github Actions to build will pull down Z 1. If it doesn't exist already, add a new file to your repository named `build.yaml`: - ``` + ```yaml # This file generates the GitHub Actions matrix # For simple board + shield combinations, add them # to the top level board and shield arrays, for more @@ -63,12 +63,12 @@ If you have a custom GitHub Actions workflow you need to maintain for some reaso - Change `zmkfirmware/zmk-build-arm:2.5` to `zmkfirmware/zmk-build-arm:stable` wherever it is found - Locate and delete the lines for the DTS output step, which is no longer needed: - ``` - - name: ${{ steps.variables.outputs.display-name }} DTS File - if: ${{ always() }} - run: | - if [ -f "build/zephyr/${{ matrix.board }}.pre.tmp" ]; then cat -n build/zephyr/${{ matrix.board }}.pre.tmp; fi - if [ -f "build/zephyr/zephyr.dts" ]; then cat -n build/zephyr/zephyr.dts; fi + ```yaml + - name: ${{ steps.variables.outputs.display-name }} DTS File + if: ${{ always() }} + run: | + if [ -f "build/zephyr/${{ matrix.board }}.pre.tmp" ]; then cat -n build/zephyr/${{ matrix.board }}.pre.tmp; fi + if [ -f "build/zephyr/zephyr.dts" ]; then cat -n build/zephyr/zephyr.dts; fi ``` ### VS Code & Docker (Dev Container) @@ -107,7 +107,7 @@ A few testers have reported inconsistent issues with bluetooth connections on Wi Zephyr 3.2 introduced [a new Kconfig setting](https://github.com/zephyrproject-rtos/zephyr/pull/48929) that can be used to work around a bug in Windows related to battery reporting. Check out our [bluetooth config](/docs/config/bluetooth) for the full details. The key new configuration that can be set if using Windows is: -``` +```ini CONFIG_BT_GATT_ENFORCE_SUBSCRIPTION=n ``` @@ -133,7 +133,7 @@ The main area this affects existing shields is those with board specific overrid Previously in ZMK, we relied on per-driver devicetree source properties to set the alternate pin functions for things like SPI or I2C. For example, here is the I2C bus setup as it was previously on the `nice_nano` board: -``` +```dts &i2c0 { compatible = "nordic,nrf-twi"; sda-pin = <17>; @@ -143,7 +143,7 @@ Previously in ZMK, we relied on per-driver devicetree source properties to set t With the move to the `pinctrl` system, this setup now look like: -``` +```dts &i2c0 { compatible = "nordic,nrf-twi"; pinctrl-0 = <&i2c0_default>; @@ -154,7 +154,7 @@ With the move to the `pinctrl` system, this setup now look like: which references the `pinctrl` configuration: -``` +```dts &pinctrl { i2c0_default: i2c0_default { group1 { @@ -185,7 +185,7 @@ The approach is the following when updating a _board_: 1. Add a new file with the naming convention `-pinctrl.dtsi` to your board directory. 1. In the new file, add your `pinctrl` entries that set up different pin control configurations for whatever peripherals/buses are needed. Here's the nice!nano file as an example: - ``` + ```dts /* * Copyright (c) 2022 The ZMK Contributors * SPDX-License-Identifier: MIT @@ -230,7 +230,7 @@ The approach is the following when updating a _board_: 1. From the main `.dts` file, add an `#include "-pinctrl.dtsi"` to have the C-preprocessor combine the files. 1. Update the various peripheral nodes to use the new `pinctrl` configurations. For example, the following old configuration: - ``` + ```dts &i2c0 { compatible = "nordic,nrf-twi"; sda-pin = <15>; @@ -240,7 +240,7 @@ The approach is the following when updating a _board_: would be changed to: - ``` + ```dts &i2c0 { compatible = "nordic,nrf-twi"; pinctrl-0 = <&i2c0_default>; diff --git a/docs/blog/2023-06-18-encoder-refactors.md b/docs/blog/2023-06-18-encoder-refactors.md index 26f9cee8176..14be81c8700 100644 --- a/docs/blog/2023-06-18-encoder-refactors.md +++ b/docs/blog/2023-06-18-encoder-refactors.md @@ -35,7 +35,7 @@ Splitting these two parts of the encoder configuration allows greater flexibilit Previously, an encoder configuration looked like: -``` +```dts left_encoder: encoder_left { compatible = "alps,ec11"; label = "LEFT_ENCODER"; @@ -47,7 +47,7 @@ Previously, an encoder configuration looked like: Here, the `resolution` property was used to indicate how many encoder pulses should trigger the sensor behavior one time. Next, the encoder is selected in the sensors node: -``` +```dts sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; @@ -58,7 +58,7 @@ That was the entirety of the configuration for encoders. ### New Configuration -``` +```dts left_encoder: encoder_left { compatible = "alps,ec11"; label = "LEFT_ENCODER"; @@ -70,7 +70,7 @@ That was the entirety of the configuration for encoders. Here, the `steps` property is now used to indicate how many encoder pulses there are in a single complete rotation of the encoder. Next, the encoder is selected in the sensors node as before, but an additional configuration is used to indicate how many times the encoder should trigger the behavior in your keymap per rotation: -``` +```dts sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; @@ -84,7 +84,7 @@ For tactile encoders that have detents, the `triggers-per-rotation` would match The configuration changes bring ZMK's code in line with how upstream Zephyr sensor drivers handle rotations. This has the added advantage of allowing us to leverage other sensor drivers. On Nordic MCUs, like nRF52840, the NRFX QDEC driver can be used, for example: -``` +```dts &pinctrl { qdec_default: qdec_default { group1 { diff --git a/docs/docs/behaviors/backlight.md b/docs/docs/behaviors/backlight.md index 8c613fe6bd1..322530e3763 100644 --- a/docs/docs/behaviors/backlight.md +++ b/docs/docs/behaviors/backlight.md @@ -12,7 +12,7 @@ This page contains [backlight](../features/backlight.md) behaviors supported by Backlight actions defines are provided through the [`dt-bindings/zmk/backlight.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/backlight.h) header, which is added at the top of the keymap file: -``` +```dts #include ``` @@ -46,13 +46,13 @@ However the settings will only be saved after [`CONFIG_ZMK_SETTINGS_SAVE_DEBOUNC 1. Toggle backlight on/off - ``` + ```dts &bl BL_TOG ``` 1. Sets a specific brightness - ``` + ```dts &bl BL_SET 50 ``` diff --git a/docs/docs/behaviors/bluetooth.md b/docs/docs/behaviors/bluetooth.md index 89496948d50..1d461722f5c 100644 --- a/docs/docs/behaviors/bluetooth.md +++ b/docs/docs/behaviors/bluetooth.md @@ -20,7 +20,7 @@ A ZMK device may show as "connected" on multiple hosts at the same time. This is Bluetooth command defines are provided through the [`dt-bindings/zmk/bt.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/bt.h) header, which is added at the top of the keymap file: -``` +```dts #include ``` @@ -54,25 +54,25 @@ The bluetooth behavior completes an bluetooth action given on press. 1. Behavior binding to clear the paired host for the selected profile: - ``` + ```dts &bt BT_CLR ``` 1. Behavior binding to select the next profile: - ``` + ```dts &bt BT_NXT ``` 1. Behavior binding to select the previous profile: - ``` + ```dts &bt BT_PRV ``` 1. Behavior binding to select the 2nd profile (passed parameters are [zero based](https://en.wikipedia.org/wiki/Zero-based_numbering)): - ``` + ```dts &bt BT_SEL 1 ``` diff --git a/docs/docs/behaviors/caps-word.md b/docs/docs/behaviors/caps-word.md index e5d862d785b..0233de526b6 100644 --- a/docs/docs/behaviors/caps-word.md +++ b/docs/docs/behaviors/caps-word.md @@ -15,7 +15,7 @@ The modifiers are applied only to to the alphabetic (`A` to `Z`) keycodes, to av Example: -``` +```dts &caps_word ``` @@ -25,7 +25,7 @@ Example: By default, the caps word will remain active when any alphanumeric character or underscore (`UNDERSCORE`), backspace (`BACKSPACE`), or delete (`DELETE`) characters are pressed. Any other non-modifier keycode sent will turn off caps word. If you would like to override this, you can set a new array of keys in the `continue-list` property in your keymap: -``` +```dts &caps_word { continue-list = ; }; @@ -41,7 +41,7 @@ By default, the caps word will remain active when any alphanumeric character or In addition, if you would like _multiple_ modifiers, instead of just `MOD_LSFT`, you can override the `mods` property: -``` +```dts &caps_word { mods = <(MOD_LSFT | MOD_LALT)>; }; @@ -57,7 +57,7 @@ In addition, if you would like _multiple_ modifiers, instead of just `MOD_LSFT`, If you want to use multiple caps breaks with different codes to break the caps, you can add additional caps words instances to use in your keymap: -``` +```dts / { prog_caps: behavior_prog_caps_word { compatible = "zmk,behavior-caps-word"; diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md index ec66b34f2af..96e03a0e397 100644 --- a/docs/docs/behaviors/hold-tap.md +++ b/docs/docs/behaviors/hold-tap.md @@ -55,7 +55,7 @@ If you press a tapped hold-tap again within `quick-tap-ms` milliseconds of the f For example, the following hold-tap configuration enables `require-prior-idle-ms` with a 125 millisecond term, alongside `quick-tap-ms` with a 200 millisecond term. -``` +```dts rpi: require_prior_idle { compatible = "zmk,behavior-hold-tap"; label = "REQUIRE_PRIOR_IDLE"; @@ -78,7 +78,7 @@ If `retro-tap` is enabled, the tap behavior is triggered when releasing the hold For example, if you press `&mt LEFT_SHIFT A` and then release it without pressing another key, it will output `a`. -``` +```dts &mt { retro-tap; }; @@ -96,7 +96,7 @@ Note that `hold-trigger-key-positions` is an array of key position indexes. Key See the following example, which uses a hold-tap behavior definition, configured with the `hold-preferred` flavor, and with positional hold-tap enabled: -``` +```dts #include #include @@ -141,7 +141,7 @@ The two parameters that are passed to the hold-tap in your keymap will be forwar If you use behaviors that accept no parameters such as [mod-morphs](mod-morph.md) or [macros](macros.md), you can pass a dummy parameter value such as `0` to the hold-tap when you use it in your keymap. For instance, a hold-tap with node label `caps` and `bindings = <&kp>, <&caps_word>;` can be used in the keymap as below to send the caps lock keycode on hold and invoke the [caps word behavior](caps-word.md) on tap: -``` +```dts &caps CAPS 0 ``` @@ -166,7 +166,7 @@ The following are suggested hold-tap configurations that work well with home row ##### Option 1: cross-hand only modifiers, using `tap-unless-interrupted` and positional hold-tap (`hold-trigger-key-positions`) -```dtsi title="Homerow Mods: Cross-hand Example" +```dts title="Homerow Mods: Cross-hand Example" #include #include @@ -198,7 +198,7 @@ The following are suggested hold-tap configurations that work well with home row ##### Option 2: `tap-preferred` -```dtsi title="Homerow Mods: Tap-Preferred Example" +```dts title="Homerow Mods: Tap-Preferred Example" #include #include @@ -228,7 +228,7 @@ The following are suggested hold-tap configurations that work well with home row ##### Option 3: `balanced` -```dtsi title="Homerow Mods: Balanced Example" +```dts title="Homerow Mods: Balanced Example" #include #include @@ -262,7 +262,7 @@ The following are suggested hold-tap configurations that work well with home row A popular method of implementing Autoshift in ZMK involves a C-preprocessor macro, commonly defined as `AS(keycode)`. This macro applies the `LSHIFT` modifier to the specified `keycode` when `AS(keycode)` is held, and simply performs a [keypress](key-press.md), `&kp keycode`, when the `AS(keycode)` binding is tapped. This simplifies the use of Autoshift in a keymap, as the complete hold-tap bindings for each desired Autoshift key, as in `&as LS() &as LS() ... &as LS() `, can be quite cumbersome to use when applied to a large portion of the keymap. -```dtsi title="Hold-Tap Example: Autoshift" +```dts title="Hold-Tap Example: Autoshift" #include #include @@ -298,7 +298,7 @@ A popular method of implementing Autoshift in ZMK involves a C-preprocessor macr This hold-tap example implements a [momentary-layer](layers.md/#momentary-layer) when the keybind is held and a [toggle-layer](layers.md/#toggle-layer) when it is tapped. Similar to the Autoshift and Sticky Hold use-cases, a `MO_TOG(layer)` macro is defined such that the `&mo` and `&tog` behaviors can target a single layer. -```dtsi title="Hold-Tap Example: Momentary layer on Hold, Toggle layer on Tap" +```dts title="Hold-Tap Example: Momentary layer on Hold, Toggle layer on Tap" #include #include diff --git a/docs/docs/behaviors/key-press.md b/docs/docs/behaviors/key-press.md index e8304d7733d..a298d040b45 100644 --- a/docs/docs/behaviors/key-press.md +++ b/docs/docs/behaviors/key-press.md @@ -27,7 +27,7 @@ To make it easier to encode the HID keycode numeric values, most keymaps include the [`dt-bindings/zmk/keys.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/keys.h) header provided by ZMK near the top: -``` +```dts #include ``` @@ -44,6 +44,6 @@ The "key press" behavior sends standard keycodes on press/release. Example: -``` +```dts &kp A ``` diff --git a/docs/docs/behaviors/key-repeat.md b/docs/docs/behaviors/key-repeat.md index 5217bce8b5b..9772fdb4e0a 100644 --- a/docs/docs/behaviors/key-repeat.md +++ b/docs/docs/behaviors/key-repeat.md @@ -13,7 +13,7 @@ The key repeat behavior when triggered will send whatever keycode was last sent/ Example: -``` +```dts &key_repeat ``` @@ -25,7 +25,7 @@ By default, the key repeat will only track the last pressed key from the HID "Ke If you'd rather have the repeat also capture and send Consumer page usages, you can update the existing behavior: -``` +```dts &key_repeat { usage-pages = ; }; diff --git a/docs/docs/behaviors/key-toggle.md b/docs/docs/behaviors/key-toggle.md index 85b328f849b..080b5b53aa6 100644 --- a/docs/docs/behaviors/key-toggle.md +++ b/docs/docs/behaviors/key-toggle.md @@ -18,7 +18,7 @@ Example uses for key toggle include shift lock, or `ALT-TAB` window switching wi Example: -``` +```dts &kt LALT ``` diff --git a/docs/docs/behaviors/layers.md b/docs/docs/behaviors/layers.md index 7d7901568f9..162b792fcd4 100644 --- a/docs/docs/behaviors/layers.md +++ b/docs/docs/behaviors/layers.md @@ -16,7 +16,7 @@ add a set of `#define`s at the top of your keymap file, and use those layer in y For example, if you have three layers, you can add the following to the top of your keymap: -``` +```dts #define DEFAULT 0 #define LOWER 1 #define RAISE 2 @@ -37,7 +37,7 @@ again. Example: -``` +```dts &mo LOWER ``` @@ -53,7 +53,7 @@ The "layer-tap" behavior enables a layer when a key is held, and outputs a [keyp Example: -``` +```dts < LOWER SPACE ``` @@ -61,7 +61,7 @@ Example: You can configure a different tapping term or tweak other properties noted in the [hold-tap](hold-tap.md#advanced-configuration) documentation page in your keymap: -``` +```dts < { tapping-term-ms = <200>; }; @@ -93,7 +93,7 @@ The "to layer" behavior enables a layer and disables _all_ other layers _except_ Example: -``` +```dts &to 3 ``` @@ -108,13 +108,13 @@ The "toggle layer" behavior enables a layer until the layer is manually disabled Example: -``` +```dts &tog LOWER ``` "Toggle layer" for a : -``` +```dts #define DEFAULT 0 #define NAVI 1 diff --git a/docs/docs/behaviors/macros.md b/docs/docs/behaviors/macros.md index 377ce7fecdb..7ce968e9b9a 100644 --- a/docs/docs/behaviors/macros.md +++ b/docs/docs/behaviors/macros.md @@ -14,7 +14,7 @@ Each macro you want to use in your keymap gets defined first, then bound in your A macro definition looks like: -``` +```dts / { macros { zed_em_kay: zed_em_kay { @@ -38,7 +38,7 @@ used to reference the macro in your keymap. The macro can then be bound in your keymap by referencing it by the label `&zed_em_kay`, e.g.: -``` +```dts raise_layer { bindings = <&zed_em_kay>; }; @@ -54,7 +54,7 @@ with [modifier functions](../codes/modifiers.mdx#modifier-functions) can be used Like [hold-taps](/docs/behaviors/hold-tap), macros are created by composing other behaviors, and any of those behaviors can be added to the `bindings` list, e.g.: -``` +```dts bindings = <&to 1> , <&bl BL_ON> @@ -86,7 +86,7 @@ To modify the activation mode, macro controls can be added at any point in the ` A concrete example, used to hold a modifier, tap multiple keys, then release the modifier, would look like: -``` +```dts bindings = <¯o_press &kp LSHFT> , <¯o_tap &kp Z &kp M &kp K> @@ -101,7 +101,7 @@ the macro itself is released. To pause the macro until release, use `¯o_pause_for_release`. For example, this macro will press a modifier and activate a layer when the macro is pressed. Once the macro is released, it will release the modifier and deactivate the layer by releasing the `&mo`: -``` +```dts bindings = <¯o_press &mo 1 &kp LSHFT> , <¯o_pause_for_release> @@ -116,7 +116,7 @@ which is equal to the value of [`CONFIG_ZMK_MACRO_DEFAULT_WAIT_MS`](../config/be be set by assigning a value to the `wait-ms` property of the macro, e.g. `wait-ms = <20>;`. If you want to update the wait time at any point in the macro bindings list, use `¯o_wait_time`, e.g. `¯o_wait_time 30`. A full example: -``` +```dts wait-ms = <10>; bindings = <&kp F &kp A &kp S &kp T> @@ -132,7 +132,7 @@ which is equal to the value of [`CONFIG_ZMK_MACRO_DEFAULT_TAP_MS`](../config/beh be set by assigning a value to the `tap-ms` property of the macro, e.g. `tap-ms = <20>;`. If you want to update the tap time at any point in a macro bindings list, use `¯o_tap_time`, e.g. `¯o_tap_time 30`. A full example: -``` +```dts bindings = <¯o_tap_time 10> , <&kp S &kp H &kp O &kp R &kp T> @@ -153,7 +153,7 @@ Another limit worth noting is that the maximum number of bindings you can pass t Macros can also be "parameterized", allowing them to be bound in your keymap with unique values passed into them, e.g.: -``` +```dts raise_layer { bindings = <&my_one_param_macro A> }; @@ -262,7 +262,7 @@ lm: lm { To trigger a different underglow when the macro is pressed, and when it is released, we use the macro "press" activation mode whenever triggering the `&rgb_ug` behavior: -``` +```dts wait-ms = <0>; tap-ms = <0>; bindings @@ -278,7 +278,7 @@ bindings The other common use case for macros is to sending sequences of keycodes to the connected host. Here, a wait and tap time of at least 30ms is recommended to avoid having HID notifications grouped at the BLE protocol level and then processed out of order: -``` +```dts wait-ms = <40>; tap-ms = <40>; bindings @@ -292,7 +292,7 @@ bindings Many operating systems allow a special sequence to input unicode characters, e.g. [Windows alt codes](https://support.microsoft.com/en-us/office/insert-ascii-or-unicode-latin-based-symbols-and-characters-d13f58d3-7bcb-44a7-a4d5-972ee12e50e0). You can use macros to automate inputting the sequences, e.g. below macro inserts `£` on Windows: -``` +```dts wait-ms = <40>; tap-ms = <40>; bindings @@ -306,7 +306,7 @@ bindings To avoid repetition or possible typos when declaring a **zero parameter macro**, a convenience _C_ macro, named `ZMK_MACRO(name, props)` can be used to simplify things: -``` +```dts ZMK_MACRO(my_zero_param_macro, wait-ms = <30>; tap-ms = <40>; @@ -320,7 +320,7 @@ To avoid repetition or possible typos when declaring a **zero parameter macro**, This can be used instead of a complete macro definition. During the firmware build process, the example above would produce the complete macro definition below: -``` +```dts my_zero_param_macro: my_zero_param_macro { compatible = "zmk,behavior-macro"; label = "ZM_my_macro"; diff --git a/docs/docs/behaviors/misc.md b/docs/docs/behaviors/misc.md index 446ba33c3e4..c71f87c061b 100644 --- a/docs/docs/behaviors/misc.md +++ b/docs/docs/behaviors/misc.md @@ -21,7 +21,7 @@ passed down to the next active layer in the stack. Example: -``` +```dts &trans ``` @@ -37,6 +37,6 @@ be passed down to the next active layer in the stack. Example: -``` +```dts &none ``` diff --git a/docs/docs/behaviors/mod-morph.md b/docs/docs/behaviors/mod-morph.md index 33d18e33a2e..6099b4281fd 100644 --- a/docs/docs/behaviors/mod-morph.md +++ b/docs/docs/behaviors/mod-morph.md @@ -18,7 +18,7 @@ The Mod-Morph behavior acts as one of two keycodes, depending on if the required An example of how to implement the mod-morph "Grave Escape": -``` +```dts / { behaviors { gresc: grave_escape { @@ -41,7 +41,7 @@ Note that this specific mod-morph exists in ZMK by default using code `&gresc`. Example: -``` +```dts &gresc ``` @@ -62,7 +62,7 @@ Available Modifiers: Example: -``` +```dts mods = <(MOD_LGUI|MOD_LSFT|MOD_RGUI|MOD_RSFT)>; ``` @@ -74,7 +74,7 @@ When a modifier specified in `mods` is being held, it won't be sent along with t For example, the following configuration morphs `LEFT_SHIFT` + `BACKSPACE` into `DELETE`, and morphs `RIGHT_SHIFT` + `BACKSPACE` into `RIGHT_SHIFT` + `DELETE`. -``` +```dts / { behaviors { bspc_del: backspace_delete { diff --git a/docs/docs/behaviors/mod-tap.md b/docs/docs/behaviors/mod-tap.md index 5f4fa50653a..d789a8b723c 100644 --- a/docs/docs/behaviors/mod-tap.md +++ b/docs/docs/behaviors/mod-tap.md @@ -24,7 +24,7 @@ The Mod-Tap behavior either acts as a held modifier, or as a tapped keycode. Example: -``` +```dts &mt LSHIFT A ``` @@ -32,7 +32,7 @@ Example: You can configure a different tapping term in your keymap: -``` +```dts &mt { tapping-term-ms = <400>; }; diff --git a/docs/docs/behaviors/outputs.md b/docs/docs/behaviors/outputs.md index dad52be2ccb..46b567cfdfc 100644 --- a/docs/docs/behaviors/outputs.md +++ b/docs/docs/behaviors/outputs.md @@ -23,7 +23,7 @@ to select the BLE output through below behavior to be able to send keystrokes to Output command defines are provided through the [`dt-bindings/zmk/outputs.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/outputs.h) header, which is added at the top of the keymap file: -``` +```dts #include ``` @@ -53,18 +53,18 @@ However it will only be saved after [`CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE`](../con 1. Behavior binding to prefer sending keyboard output to USB - ``` + ```dts &out OUT_USB ``` 1. Behavior binding to prefer sending keyboard output to the current bluetooth profile - ``` + ```dts &out OUT_BLE ``` 1. Behavior binding to toggle between preferring USB and BLE - ``` + ```dts &out OUT_TOG ``` diff --git a/docs/docs/behaviors/power.md b/docs/docs/behaviors/power.md index 11f920845a3..5251d76cb13 100644 --- a/docs/docs/behaviors/power.md +++ b/docs/docs/behaviors/power.md @@ -24,7 +24,7 @@ The following boards currently support this feature: External power control command defines are provided through the [`dt-bindings/zmk/ext_power.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/ext_power.h) header, which is added at the top of the keymap file: -``` +```dts #include ``` @@ -52,19 +52,19 @@ However it will only be saved after [`CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE`](../con 1. Behavior binding to enable the external power - ``` + ```dts &ext_power EP_ON ``` 1. Behavior binding to disable the external power - ``` + ```dts &ext_power EP_OFF ``` 1. Behavior binding to toggle the external power - ``` + ```dts &ext_power EP_TOG ``` diff --git a/docs/docs/behaviors/reset.md b/docs/docs/behaviors/reset.md index 0fc62b2f4e9..b7850eca77d 100644 --- a/docs/docs/behaviors/reset.md +++ b/docs/docs/behaviors/reset.md @@ -22,7 +22,7 @@ to the device Example: -``` +```dts &sys_reset ``` @@ -38,7 +38,7 @@ you to flash a new firmware. Example: -``` +```dts &bootloader ``` diff --git a/docs/docs/behaviors/sensor-rotate.md b/docs/docs/behaviors/sensor-rotate.md index 02e56924bad..3ff69dc6d02 100644 --- a/docs/docs/behaviors/sensor-rotate.md +++ b/docs/docs/behaviors/sensor-rotate.md @@ -19,7 +19,7 @@ The standard sensor rotation behavior allows fully binding behaviors to be invok Here is an example that binds the [RGB Underglow Behavior](/docs/behaviors/underglow.md) to change the RGB brightness: -``` +```dts / { behaviors { rgb_encoder: rgb_encoder { @@ -55,7 +55,7 @@ Here is an example, showing how send key presses on rotation: First, defining the sensor rotation itself, binding the [Key Press Behavior](/docs/behaviors/key-press.md) twice, then binding it in the `sensor-bindings` property of a keymap layer: -``` +```dts / { behaviors { rot_kp: behavior_sensor_rotate_kp { diff --git a/docs/docs/behaviors/sticky-key.md b/docs/docs/behaviors/sticky-key.md index d881e6b0b83..8b003f55e39 100644 --- a/docs/docs/behaviors/sticky-key.md +++ b/docs/docs/behaviors/sticky-key.md @@ -14,13 +14,13 @@ A sticky key stays pressed until another key is pressed. It is often used for 's Example: -``` +```dts &sk LSHIFT ``` You can use any keycode that works for `&kp` as parameter to `&sk`: -``` +```dts &sk LG(LS(LA(LCTRL))) ``` @@ -40,7 +40,7 @@ This setting is enabled by default. It ensures that if a sticky key modifier is #### Example -``` +```dts &sk { release-after-ms = <2000>; quick-release; @@ -56,7 +56,7 @@ This setting is enabled by default. It ensures that if a sticky key modifier is This configuration would apply to all sticky keys. This may not be appropriate if using `quick-release` as you'll lose the ability to chain sticky key modifiers. A better approach for this use case would be to create a new behavior: -``` +```dts / { behaviors { skq: sticky_key_quick_release { diff --git a/docs/docs/behaviors/sticky-layer.md b/docs/docs/behaviors/sticky-layer.md index ac341f7737d..41c2ccf55d7 100644 --- a/docs/docs/behaviors/sticky-layer.md +++ b/docs/docs/behaviors/sticky-layer.md @@ -16,7 +16,7 @@ By default, sticky layers stay pressed for a second if you don't press any other Example: -``` +```dts &sl 1 ``` @@ -24,7 +24,7 @@ Example: You can configure a different `release-after-ms` in your keymap: -``` +```dts &sl { release-after-ms = <2000>; }; diff --git a/docs/docs/behaviors/tap-dance.md b/docs/docs/behaviors/tap-dance.md index c68b51dcd44..ac85b3dab6f 100644 --- a/docs/docs/behaviors/tap-dance.md +++ b/docs/docs/behaviors/tap-dance.md @@ -37,7 +37,7 @@ values={[ This example configures a tap-dance named `td0` that outputs the number of times its binding is pressed from 1-3. -```title="Basic Tap-Dance Example: Counter" +```dts title="Basic Tap-Dance Example: Counter" #include #include @@ -78,7 +78,7 @@ Alphanumeric [`key press`](key-press.md) bindings, like those used for `td0`, wi This example configures a mod-tap inside a tap-dance named `td_mt` that outputs `CAPSLOCK` on a single tap, `LSHIFT` on a single press and hold, and `LCTRL` when the tap-dance is pressed twice. -```title="Advanced Tap-Dance Example: Nested Mod-Tap" +```dts title="Advanced Tap-Dance Example: Nested Mod-Tap" #include #include diff --git a/docs/docs/behaviors/underglow.md b/docs/docs/behaviors/underglow.md index 29f34c2ab0b..f94d9008c04 100644 --- a/docs/docs/behaviors/underglow.md +++ b/docs/docs/behaviors/underglow.md @@ -12,7 +12,7 @@ This page contains [RGB Underglow](../features/underglow.md) behaviors supported RGB actions defines are provided through the [`dt-bindings/zmk/rgb.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/rgb.h) header, which is added at the top of the keymap file: -``` +```dts #include ``` @@ -65,13 +65,13 @@ However the settings will only be saved after [`CONFIG_ZMK_SETTINGS_SAVE_DEBOUNC 1. Toggle underglow on/off - ``` + ```dts &rgb_ug RGB_TOG ``` 1. Set a specific HSB color (green) - ``` + ```dts &rgb_ug RGB_COLOR_HSB(128,100,100) ``` diff --git a/docs/docs/config/index.md b/docs/docs/config/index.md index 1ff1bfa094e..3d7aeb48bc2 100644 --- a/docs/docs/config/index.md +++ b/docs/docs/config/index.md @@ -71,7 +71,7 @@ Kconfig is used to configure global settings such as the keyboard name and enabl Kconfig files look like this: -``` +```ini CONFIG_ZMK_SLEEP=y CONFIG_EC11=y CONFIG_EC11_TRIGGER_GLOBAL_THREAD=y @@ -114,7 +114,7 @@ Devicetree files use various file extensions. These indicate the purpose of the Devicetree files look like this: -```devicetree +```dts / { chosen { zmk,kscan = &kscan0; @@ -138,7 +138,7 @@ search through the `.dts` file for your board, `.overlay` file for your shield, A Devicetree node looks like this: -```devicetree +```dts kscan0: kscan { compatible = "zmk,kscan-gpio-matrix"; // more properties and/or nodes... @@ -157,7 +157,7 @@ followed by the node's label, an opening curly brace (`{`), one or more new prop For example, to adjust the debouncing of the `zmk,kscan-gpio-matrix` node shown above, you could add this to your keymap: -```devicetree +```dts &kscan0 { debounce-press-ms = <0>; }; @@ -165,7 +165,7 @@ For example, to adjust the debouncing of the `zmk,kscan-gpio-matrix` node shown If the node you want to edit doesn't have a label, you can also write a new tree and it will be merged with the existing tree, overriding any properties. Adding this to your keymap would be equivalent to the previous example. -```devicetree +```dts / { kscan { debounce-press-ms = <0>; @@ -185,7 +185,7 @@ Example: `property;` If a property has already been set to true and you need to override it to false, use the following command to delete the existing property: -```devicetree +```dts /delete-property/ the-property-name; ``` @@ -240,7 +240,7 @@ Each item in the array should be a label for a GPIO node (the names of which dif Example: -```devicetree +```dts some-gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>, <&gpio0 1 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)> @@ -253,7 +253,7 @@ A path to a node, either as a node reference or as a string. Examples: -``` +```dts property = &label; property = "/path/to/some/node"; ``` diff --git a/docs/docs/config/kscan.md b/docs/docs/config/kscan.md index 296cb4a179c..67c3765298a 100644 --- a/docs/docs/config/kscan.md +++ b/docs/docs/config/kscan.md @@ -88,7 +88,7 @@ By default, a switch will drain current through the internal pull up/down resist Assuming the switches connect each GPIO pin to the ground, the [GPIO flags](https://docs.zephyrproject.org/3.2.0/hardware/peripherals/gpio.html#api-reference) for the elements in `input-gpios` should be `(GPIO_ACTIVE_LOW | GPIO_PULL_UP)`: -```devicetree +```dts kscan0: kscan { compatible = "zmk,kscan-gpio-direct"; input-gpios @@ -137,7 +137,7 @@ The `diode-direction` property must be one of: Given the `diode-direction`, the [GPIO flags](https://docs.zephyrproject.org/3.2.0/hardware/peripherals/gpio.html#api-reference) for the elements in `row-` and `col-gpios` should be set appropriately. The output pins (e.g. columns for `col2row`) should have the flag `GPIO_ACTIVE_HIGH`, and input pins (e.g. rows for `col2row`) should have the flags `(GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)`: -```devicetree +```dts kscan0: kscan { compatible = "zmk,kscan-gpio-matrix"; diode-direction = "col2row"; @@ -229,7 +229,7 @@ One possible way to do this is a 3x4 matrix where the direct GPIO keys are shift ...which can be configured with the following Devicetree code: -```devicetree +```dts / { chosen { zmk,kscan = &kscan0; @@ -316,7 +316,7 @@ The `map` array should be defined using the `RC()` macro from [dt-bindings/zmk/m Any keyboard which is not a grid of 1 unit keys will likely have some unused positions in the matrix. A matrix transform can be used to skip the unused positions so users don't have to set them to `&none` in keymaps. -```devicetree +```dts // numpad.overlay / { chosen { @@ -355,7 +355,7 @@ Any keyboard which is not a grid of 1 unit keys will likely have some unused pos }; ``` -```devicetree +```dts // numpad.keymap / { keymap { @@ -377,7 +377,7 @@ Any keyboard which is not a grid of 1 unit keys will likely have some unused pos Consider a keyboard with a [duplex matrix](https://wiki.ai03.com/books/pcb-design/page/matrices-and-duplex-matrix), where the matrix has twice as many rows and half as many columns as the keyboard has keys. A matrix transform can be used to correct for this so that keymaps can match the layout of the keys, not the layout of the matrix. -```devicetree +```dts / { chosen { zmk,kscan = &kscan0; diff --git a/docs/docs/customization.md b/docs/docs/customization.md index 6fb39f85015..4f75e743a66 100644 --- a/docs/docs/customization.md +++ b/docs/docs/customization.md @@ -48,7 +48,7 @@ default keyboard settings. For instance, building kyria firmware from a user `myUser`'s `zmk-config` folder on Windows 10 may look something like this: -``` +```bash west build -b nice_nano -- -DSHIELD=kyria_left -DZMK_CONFIG="C:/Users/myUser/Documents/Github/zmk-config/config" ``` @@ -63,7 +63,7 @@ More troubleshooting information for split keyboards can be found [here](trouble You can build additional keyboards with GitHub actions by appending them to `build.yml` in your `zmk-config` folder. For instance assume that we have set up a Corne shield with nice!nano during [initial setup](user-setup.md) and we want to add a Lily58 shield with nice!nano v2. The following is an example `build.yaml` file that would accomplish that: -``` +```yaml include: - board: nice_nano shield: corne_left diff --git a/docs/docs/development/build-flash.md b/docs/docs/development/build-flash.md index f57c4b5caf9..94bbcdf93a8 100644 --- a/docs/docs/development/build-flash.md +++ b/docs/docs/development/build-flash.md @@ -76,13 +76,13 @@ For split keyboards, you will have to build and flash each side separately the f By default, the `build` command outputs a single .uf2 file named `zmk.uf2` so building left and then right immediately after will overwrite your left firmware. In addition, you will need to pristine build each side to ensure the correct files are used. To avoid having to pristine build every time and separate the left and right build files, we recommend setting up separate build directories for each half. You can do this by using the `-d` parameter and first building left into `build/left`: -``` +```sh west build -d build/left -b nice_nano -- -DSHIELD=kyria_left ``` and then building right into `build/right`: -``` +```sh west build -d build/right -b nice_nano -- -DSHIELD=kyria_right ``` @@ -99,7 +99,7 @@ Instead of building .uf2 files using the default keymap and config files, you ca For instance, building kyria firmware from a user `myUser`'s `zmk-config` folder on Windows 10 may look something like this: -``` +```sh west build -b nice_nano -- -DSHIELD=kyria_left -DZMK_CONFIG="C:/Users/myUser/Documents/Github/zmk-config/config" ``` @@ -117,7 +117,7 @@ volume automatically -- we need to delete the default volume before binding it t Then you can bind the `zmk-config` volume to the correct path pointing to your local [zmk-config](customization.md) folder: -``` +```sh docker volume create --driver local -o o=bind -o type=none -o \ device="/full/path/to/your/zmk-config/" zmk-config ``` @@ -137,7 +137,7 @@ Alternatively, if your board supports flashing and you're not developing from within a Dockerized environment, enable Device Firmware Upgrade (DFU) mode on your board and run the following command to flash: -``` +```sh west flash ``` diff --git a/docs/docs/development/ide-integration.md b/docs/docs/development/ide-integration.md index bfcd5def4da..f0403dbba36 100644 --- a/docs/docs/development/ide-integration.md +++ b/docs/docs/development/ide-integration.md @@ -71,7 +71,7 @@ If you are developing inside a Docker container, set the IntelliSense mode to `l Open VS Code's integrated terminal and run the following command: -``` +```sh cmake -P zephyr/cmake/verify-toolchain.cmake ``` diff --git a/docs/docs/development/new-behavior.md b/docs/docs/development/new-behavior.md index 0d70aa3b2a2..aab056c1a93 100644 --- a/docs/docs/development/new-behavior.md +++ b/docs/docs/development/new-behavior.md @@ -357,7 +357,7 @@ For behaviors that do not require central locality, the following options for up For the purpose of this section, we will discuss the structure of `app/dts/behaviors/gresc.dtsi` below. -```dtsi title="app/dts/behaviors/gresc.dtsi" +```dts title="app/dts/behaviors/gresc.dtsi" /* * Copyright (c) 2020 The ZMK Contributors * @@ -383,7 +383,7 @@ The format of a behavior's `.dtsi` file is identical to declaring an instance of After creating the `.dtsi` from above, update `app/dts/behaviors.dtsi` to include your newly predefined behavior instance, making it accessible by the devicetree. -```dtsi title="app/dts/behaviors.dtsi" +```dts title="app/dts/behaviors.dtsi" #include #include #include diff --git a/docs/docs/development/new-shield.md b/docs/docs/development/new-shield.md index 055afcf6ec7..fa30ca38abf 100644 --- a/docs/docs/development/new-shield.md +++ b/docs/docs/development/new-shield.md @@ -53,7 +53,7 @@ shield to get it picked up for ZMK, `Kconfig.shield` and `Kconfig.defconfig`. The `Kconfig.shield` file defines any additional Kconfig settings that may be relevant when using this keyboard. For most keyboards, there is just one additional configuration value for the shield itself. -``` +```kconfig config SHIELD_MY_BOARD def_bool $(shields_list_contains,my_board) ``` @@ -62,7 +62,7 @@ This will make sure that a new configuration value named `SHIELD_MY_BOARD` is se **For split boards**, you will need to add configurations for the left and right sides. For example, if your split halves are named `my_board_left` and `my_board_right`, it would look like this: -``` +```kconfig config SHIELD_MY_BOARD_LEFT def_bool $(shields_list_contains,my_board_left) @@ -83,7 +83,7 @@ The updated new default values should always be wrapped inside a conditional on The keyboard name must be less than or equal to 16 characters in length, otherwise the bluetooth advertising might fail and you will not be able to find your keyboard from your device. ::: -``` +```kconfig if SHIELD_MY_BOARD config ZMK_KEYBOARD_NAME @@ -97,7 +97,7 @@ You'll also want to set which half is the central side. Most boards set it to th Then on the peripheral half, you'll want to turn USB on so that it shows USB status on displays properly. Finally, you'll want to turn on the split option for both sides. This can all be seen below. -``` +```kconfig if SHIELD_MY_BOARD_LEFT config ZMK_KEYBOARD_NAME @@ -136,7 +136,7 @@ values={[ The `.overlay` is the devicetree description of the keyboard shield that is merged with the primary board devicetree description before the build. For ZMK, this file at a minimum should include the chosen node named `zmk,kscan` that references a KSCAN driver instance. For a simple 3x3 macropad matrix, this might look something like: -``` +```dts / { chosen { zmk,kscan = &kscan0; @@ -174,7 +174,7 @@ Unlike unibody keyboards, split keyboards have a core .dtsi file with shield ove It is preferred to define only the `col-gpios` or `row-gpios` in the common shield .dtsi, depending on the `diode-direction` value. For `col2row` directed boards like the iris, the shared .dtsi file may look like this: -``` +```dts #include / { @@ -228,9 +228,7 @@ Furthermore, the column offset for the [matrix transform](#optional-matrix-trans because the keyboard's switch matrix is read from left to right, top to bottom. This is exemplified with the iris .overlay files. -``` -// iris_left.overlay - +```dts title=iris_left.overlay #include "iris.dtsi" // Notice that the main dtsi files are included in the overlay. &kscan0 { @@ -245,9 +243,7 @@ This is exemplified with the iris .overlay files. }; ``` -``` -// iris_right.overlay - +```dts title=iris_right.overlay #include "iris.dtsi" &default_transform { // The matrix transform for this board is 6 columns over because the left half is 6 columns wide according to the matrix. @@ -281,9 +277,7 @@ For example, a split board called `my_awesome_split_board` would have the follow In most case you'll only need to use the .conf file that affects both halves of a split board. It's used for adding features like deep-sleep or rotary encoders. -``` -// my_awesome_split_board.conf - +```ini title=my_awesome_split_board.conf CONFIG_ZMK_SLEEP=y ``` @@ -306,7 +300,7 @@ the logical key location as perceived by the end user. All _keymap_ mappings act _Without_ a matrix transform, that intentionally map each key position to the row/column pair that position corresponds to, the default equation to determine that is: -``` +```c ($row * NUMBER_OF_COLUMNS) + $column ``` @@ -316,7 +310,7 @@ Whenever that default key position mapping is insufficient, the `.o Here is an example for the [nice60](https://github.com/Nicell/nice60), which uses an efficient 8x8 GPIO matrix, and uses a transform: -``` +```dts #include / { @@ -419,7 +413,7 @@ values={[ In your configuration file you will need to add the following lines so that the encoders can be enabled/disabled: -``` +```ini # Uncomment to enable encoder # CONFIG_EC11=y # CONFIG_EC11_TRIGGER_GLOBAL_THREAD=y @@ -435,7 +429,7 @@ If building locally for split boards, you may need to add these lines to the spe In your device tree file you will need to add the following lines to define the encoder sensor: -``` +```dts left_encoder: encoder_left { compatible = "alps,ec11"; label = "LEFT_ENCODER"; @@ -452,8 +446,8 @@ Add additional encoders as necessary by duplicating the above lines, replacing ` Once you have defined the encoder sensors, you will have to add them to the list of sensors: -``` -sensors { +```dts + sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; }; @@ -465,7 +459,7 @@ In this example, a left_encoder and right_encoder are both added. Additional enc Add the following lines to your overlay file(s) to enable the encoder: -``` +```dts &left_encoder { status = "okay"; }; @@ -479,7 +473,7 @@ For split keyboards, make sure to add left hand encoders to the left .overlay fi Add the following line to your keymap file to add default encoder behavior bindings: -``` +```dts sensor-bindings = <&inc_dec_kp C_VOL_UP C_VOL_DN>; ``` @@ -493,7 +487,7 @@ Add additional bindings as necessary to match the default number of encoders on Once you've fully created the new keyboard shield definition, you should be able to test with a build command like: -``` +```sh west build --pristine -b proton_c -- -DSHIELD=my_board ``` @@ -506,7 +500,7 @@ Alternatively, if your board supports flashing and you're not developing from within a Dockerized environment, enable Device Firmware Upgrade (DFU) mode on your board and run the following command to test your build: -``` +```sh west flash ``` diff --git a/docs/docs/development/posix-board.md b/docs/docs/development/posix-board.md index d0ad571a0e2..5a809c02cca 100644 --- a/docs/docs/development/posix-board.md +++ b/docs/docs/development/posix-board.md @@ -14,7 +14,7 @@ with a compiler that can target 32-bit POSIX. On Debian, you can do this with: -``` +```sh apt install -y gcc-multilib ``` @@ -23,7 +23,7 @@ apt install -y gcc-multilib To do this, you can build ZMK targeting the `native_posix_64` board. -``` +```sh west build --pristine --board native_posix_64 -- -DZMK_CONFIG=tests/none/normal/ ``` diff --git a/docs/docs/development/usb-logging.md b/docs/docs/development/usb-logging.md index 87cd0e7d853..e50e78241a6 100644 --- a/docs/docs/development/usb-logging.md +++ b/docs/docs/development/usb-logging.md @@ -33,7 +33,7 @@ In Github Actions, you can check the ` Kconfig file` step output to ve for you successfully. ::: -``` +```ini # Turn on logging, and set ZMK logging to debug output CONFIG_ZMK_USB_LOGGING=y ``` @@ -53,7 +53,7 @@ values={[ On Linux, this should be a device like `/dev/ttyACM0` and you can connect with `minicom` or `tio` as usual, e.g.: -``` +```sh sudo tio /dev/ttyACM0 ``` @@ -74,7 +74,7 @@ If you already have the Ardunio IDE installed you can also use its built-in Seri On macOS, the device name is something like `/dev/tty.usbmodemXXXXX` where `XXXXX` is some numerical ID. You can connect to the device with [tio](https://tio.github.io/) (can be installed via [Homebrew](https://formulae.brew.sh/formula/tio)): -``` +```sh sudo tio /dev/tty.usbmodem14401 ``` diff --git a/docs/docs/features/backlight.md b/docs/docs/features/backlight.md index 717361a2085..c1b5db5dd9b 100644 --- a/docs/docs/features/backlight.md +++ b/docs/docs/features/backlight.md @@ -16,7 +16,7 @@ Unlike [RGB Underglow](underglow.md), backlight can only control single color LE To enable backlight on your board or shield, add the following line to your `.conf` file of your user config directory as such: -``` +```ini CONFIG_ZMK_BACKLIGHT=y ``` @@ -46,7 +46,7 @@ values={[ First, you must enable PWM by adding the following lines to your `Kconfig.defconfig` file: -``` +```kconfig if ZMK_BACKLIGHT config PWM @@ -60,7 +60,7 @@ endif # ZMK_BACKLIGHT Then you have to add the following lines to your `.dts` file: -``` +```dts &pwm0 { status = "okay"; ch0-pin = <45>; @@ -77,7 +77,7 @@ If your board uses a P-channel MOSFET to control backlight instead of a N-channe Then you have to add the following lines inside the root devicetree node on the same file as before: -``` +```dts / { backlight: pwmleds { compatible = "pwm-leds"; @@ -98,7 +98,7 @@ Note that every LED inside of the backlight node will be treated as a backlight Finally you need to add backlight to the `chosen` element of the root devicetree node: -``` +```dts / { chosen { zmk,backlight = &backlight; @@ -113,7 +113,7 @@ You must first add a `boards/` directory within your shield folder. For each boa Inside your `.defconfig` file, add the following lines: -``` +```kconfig if ZMK_BACKLIGHT config PWM @@ -127,7 +127,7 @@ endif # ZMK_BACKLIGHT Then add the following lines to your `.overlay` file: -``` +```dts &pwm0 { status = "okay"; ch0-pin = <45>; @@ -144,7 +144,7 @@ If your shield uses a P-channel MOSFET to control backlight instead of a N-chann Then you have to add the following lines inside the root devicetree node on the same file: -``` +```dts / { backlight: pwmleds { compatible = "pwm-leds"; @@ -165,7 +165,7 @@ Note that every LED inside of the backlight node will be treated as a backlight Finally you need to add backlight to the `chosen` element of the root devicetree node: -``` +```dts / { chosen { zmk,backlight = &backlight; @@ -175,7 +175,7 @@ Finally you need to add backlight to the `chosen` element of the root devicetree Optionally, on Pro Micro compatible shields you can add a LED GPIO node to your devicetree, this could be useful if you want your shield to be compatible with newer or untested boards. To do that you have to enable `CONFIG_LED_GPIO` in your `.conf` file and then add the following lines inside the root devicetree node of your `.dtsi` or `.dts` file: -``` +```dts / { backlight: gpioleds { compatible = "gpio-leds"; @@ -199,7 +199,7 @@ It is possible to control multiple backlight LEDs at the same time. This is usef In order to do that, first you need to enable PWM for each pin: -``` +```dts &pwm0 { status = "okay"; ch0-pin = <45>; /* LED 0 */ @@ -213,7 +213,7 @@ This part may vary based on your MCU as different MCUs may have a different numb Then you can simply add each of your LED to the backlight node: -``` +```dts backlight: pwmleds { compatible = "pwm-leds"; label = "Backlight LEDs"; diff --git a/docs/docs/features/battery.md b/docs/docs/features/battery.md index 42ba6d40f26..8bf7820799c 100644 --- a/docs/docs/features/battery.md +++ b/docs/docs/features/battery.md @@ -26,7 +26,7 @@ Zephyr also provides some drivers for fuel gauge ICs such as the TI bq274xx seri Once you have the sensor driver defined, add a `zmk,battery` property to the `chosen` node and set it to reference the sensor node. For example: -``` +```dts / { chosen { zmk,battery = &vbatt; diff --git a/docs/docs/features/beta-testing.md b/docs/docs/features/beta-testing.md index 4328ccbf68d..4a159362f13 100644 --- a/docs/docs/features/beta-testing.md +++ b/docs/docs/features/beta-testing.md @@ -44,7 +44,7 @@ values={[ ]}> -``` +```yaml manifest: remotes: - name: zmkfirmware @@ -61,7 +61,7 @@ manifest: -``` +```yaml manifest: remotes: - name: zmkfirmware @@ -80,7 +80,7 @@ manifest: -``` +```yaml manifest: remotes: - name: zmkfirmware diff --git a/docs/docs/features/bluetooth.md b/docs/docs/features/bluetooth.md index b75b89537d4..e58e16735ef 100644 --- a/docs/docs/features/bluetooth.md +++ b/docs/docs/features/bluetooth.md @@ -42,7 +42,7 @@ Management of the bluetooth in ZMK is accomplished using the [`&bt` behavior](.. Some users may experience a poor connection between the keyboard and the host. This might be due to poor quality BLE hardware, a metal enclosure on the keyboard or host, or the distance between them. Increasing the transmit power of the keyboard's BLE radio may reduce the severity of this problem. To do this, set the `CONFIG_BT_CTLR_TX_PWR_PLUS_8` configuration value in the `.conf` file of your user config directory as such: -``` +```ini CONFIG_BT_CTLR_TX_PWR_PLUS_8=y ``` @@ -64,7 +64,7 @@ There are a few known issues related to BLE and ZMK: There is a known issue with Windows failing to update the battery information after connecting to a ZMK keyboard. You can work around this Windows bug by overriding a [Bluetooth config variable](../config/bluetooth.md) to force battery notifications even if a host neglects to subscribe to them: -``` +```ini CONFIG_BT_GATT_ENFORCE_SUBSCRIPTION=n ``` diff --git a/docs/docs/features/combos.md b/docs/docs/features/combos.md index bc1353b416e..f4664c546f1 100644 --- a/docs/docs/features/combos.md +++ b/docs/docs/features/combos.md @@ -10,7 +10,7 @@ Combo keys are a way to combine multiple keypresses to output a different key. F Combos configured in your `.keymap` file, but are separate from the `keymap` node found there, since they are processed before the normal keymap. They are specified like this: -``` +```dts / { combos { compatible = "zmk,combos"; diff --git a/docs/docs/features/conditional-layers.md b/docs/docs/features/conditional-layers.md index a685bcabd93..7ccfdf2338a 100644 --- a/docs/docs/features/conditional-layers.md +++ b/docs/docs/features/conditional-layers.md @@ -14,7 +14,7 @@ Another way to think of this feature is as a simple combo system for layers, jus Conditional layers are configured via a `conditional_layers` node in your `.keymap` file as follows: -``` +```dts / { conditional_layers { compatible = "zmk,conditional-layers"; diff --git a/docs/docs/features/debouncing.md b/docs/docs/features/debouncing.md index 40170739fcb..cbea7092884 100644 --- a/docs/docs/features/debouncing.md +++ b/docs/docs/features/debouncing.md @@ -56,7 +56,7 @@ per-driver option. For example, if your board/shield has a kscan driver labeled `kscan0` in its `.overlay`, `.dts`, or `.dtsi` files, -```devicetree +```dts kscan0: kscan { compatible = "zmk,kscan-gpio-matrix"; ... @@ -65,7 +65,7 @@ kscan0: kscan { then you could add this to your `.keymap`: -```devicetree +```dts &kscan0 { debounce-press-ms = <3>; debounce-release-ms = <3>; diff --git a/docs/docs/features/encoders.md b/docs/docs/features/encoders.md index 0c493330cc6..105757634b1 100644 --- a/docs/docs/features/encoders.md +++ b/docs/docs/features/encoders.md @@ -21,7 +21,7 @@ Keyboards and macropads with encoder support will typically take the two EC11 pi Rotation is handled separately as a type of sensor. The behavior for this is set in `sensor-bindings`. See [Sensor Rotation](../behaviors/sensor-rotate.md) for customizing this behavior. -``` +```dts sensor-bindings = ; ``` @@ -33,7 +33,7 @@ Additional encoders can be configured by adding more bindings immediately after As an example, a complete `sensor-bindings` for a Kyria with two encoders could look like: -``` +```dts sensor-bindings = <&inc_dec_kp C_VOL_UP C_VOL_DN &inc_dec_kp PG_UP PG_DN>; ``` diff --git a/docs/docs/features/keymaps.md b/docs/docs/features/keymaps.md index 93c2c8250fa..9778ecbab18 100644 --- a/docs/docs/features/keymaps.md +++ b/docs/docs/features/keymaps.md @@ -61,7 +61,7 @@ alter the behavior when that specific key position is activated/deactivated. For the "key press" (`kp`) behavior at a certain key position, you must specify _which_ keycode should be used for that key position. -``` +```dts &kp A ``` @@ -69,7 +69,7 @@ In this case, the `A` is actually a define for the raw HID keycode, to make keym For example of a binding that uses two parameters, you can see how "mod-tap" (`mt`) is bound: -``` +```dts &mt LSHIFT D ``` @@ -87,7 +87,7 @@ for what would otherwise be cryptic integer keycodes, etc. This also allows brin The top two lines of most keymaps should include: -``` +```dts #include #include ``` @@ -100,7 +100,7 @@ The second include brings in the defines for all the keycodes (e.g. `A`, `N1`, ` All the remaining keymap nodes will be nested inside of the root devicetree node, like so: -```devicetree +```dts / { // Everything else goes here! }; @@ -111,7 +111,7 @@ All the remaining keymap nodes will be nested inside of the root devicetree node Nested under the devicetree root, is the keymap node. The node _name_ itself is not critical, but the node **MUST** have a property `compatible = "zmk,keymap"` in order to be used by ZMK. -``` +```dts keymap { compatible = "zmk,keymap"; diff --git a/docs/docs/features/underglow.md b/docs/docs/features/underglow.md index 2b1400fc7e8..b5c4c70392e 100644 --- a/docs/docs/features/underglow.md +++ b/docs/docs/features/underglow.md @@ -28,7 +28,7 @@ Here you can see the RGB underglow feature in action using WS2812 LEDs. To enable RGB underglow on your board or shield, simply enable the `CONFIG_ZMK_RGB_UNDERGLOW` and `CONFIG_*_STRIP` configuration values in the `.conf` file for your board or shield. For example: -``` +```ini CONFIG_ZMK_RGB_UNDERGLOW=y # Use the STRIP config specific to the LEDs you're using CONFIG_WS2812_STRIP=y @@ -45,7 +45,7 @@ A common issue when enabling underglow is that some of the installed LEDs do not The number of underglow LEDs is controlled by the `chain-length` property in the `led_strip` node. You can [change the value of this property](../config/index.md#changing-devicetree-properties) in the `.keymap` file by adding a stanza like this one outside of any other node (i.e. above or below the `/` node): -``` +```dts &led_strip { chain-length = <21>; }; @@ -70,7 +70,7 @@ With nRF52 boards, you can just use `&spi3` and define the pins you want to use. Here's an example on a definition that uses P0.06: -``` +```dts #include &pinctrl { @@ -135,7 +135,7 @@ For other boards, you must select an SPI definition that has the `MOSI` pin as y Here's another example for a non-nRF52 board on `spi3`: -``` +```dts #include &spi3 { @@ -161,7 +161,7 @@ Here's another example for a non-nRF52 board on `spi3`: Once you have your `led_strip` properly defined you need to add it to the root devicetree node `chosen` element: -``` +```dts / { chosen { zmk,underglow = &led_strip; @@ -171,7 +171,7 @@ Once you have your `led_strip` properly defined you need to add it to the root d Finally you need to enable the `CONFIG_ZMK_RGB_UNDERGLOW` and `CONFIG_*_STRIP` configuration values in the `.conf` file of your board (or set a default in the `Kconfig.defconfig`): -``` +```ini CONFIG_ZMK_RGB_UNDERGLOW=y # Use the STRIP config specific to the LEDs you're using CONFIG_WS2812_STRIP=y diff --git a/docs/docs/keymap-example-file.md b/docs/docs/keymap-example-file.md index d8d201aff93..91213f1510b 100644 --- a/docs/docs/keymap-example-file.md +++ b/docs/docs/keymap-example-file.md @@ -1,4 +1,4 @@ -``` +```dts #include #include diff --git a/docs/docs/keymap-example.md b/docs/docs/keymap-example.md index 47d1c06bca6..e526d5427a9 100644 --- a/docs/docs/keymap-example.md +++ b/docs/docs/keymap-example.md @@ -1,4 +1,4 @@ -``` +```dts keymap { compatible = "zmk,keymap"; diff --git a/docs/docs/user-setup.md b/docs/docs/user-setup.md index 9d04c347d58..2532fe8b128 100644 --- a/docs/docs/user-setup.md +++ b/docs/docs/user-setup.md @@ -67,21 +67,21 @@ values={[ ]}> -``` +```bash bash -c "$(curl -fsSL https://zmk.dev/setup.sh)" ``` -``` +```bash bash -c "$(wget https://zmk.dev/setup.sh -O -)" '' --wget ``` -``` +```powershell powershell -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://zmk.dev/setup.ps1'))" ``` From 6c75d6986c3dfaf7562783476059349c0814792a Mon Sep 17 00:00:00 2001 From: ReFil <31960031+ReFil@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:08:45 +0100 Subject: [PATCH 29/49] feat(docs): Document globe key specific quirks --- docs/docs/codes/_footnotes/globe.mdx | 1 + docs/src/data/footnotes.js | 2 ++ docs/src/data/hid.js | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/docs/codes/_footnotes/globe.mdx diff --git a/docs/docs/codes/_footnotes/globe.mdx b/docs/docs/codes/_footnotes/globe.mdx new file mode 100644 index 00000000000..f267a0ee0d3 --- /dev/null +++ b/docs/docs/codes/_footnotes/globe.mdx @@ -0,0 +1 @@ +Does not exactly replicate original key behavior on macOS, works for Globe+key modifiers but not Fn+key ([#1938](https://github.com/zmkfirmware/zmk/pull/1938#issuecomment-1744579039)). diff --git a/docs/src/data/footnotes.js b/docs/src/data/footnotes.js index cf91ecdb431..ab7f27243e8 100644 --- a/docs/src/data/footnotes.js +++ b/docs/src/data/footnotes.js @@ -8,10 +8,12 @@ import example from "@site/docs/codes/_footnotes/example.mdx"; import iosApplication from "@site/docs/codes/_footnotes/ios-application.mdx"; import iosPower from "@site/docs/codes/_footnotes/ios-power.mdx"; import macosPower from "@site/docs/codes/_footnotes/macos-power.mdx"; +import globe from "@site/docs/codes/_footnotes/globe.mdx"; export default { example, iosApplication, iosPower, macosPower, + globe, }; diff --git a/docs/src/data/hid.js b/docs/src/data/hid.js index fc61555c8eb..96697beebb6 100644 --- a/docs/src/data/hid.js +++ b/docs/src/data/hid.js @@ -7884,6 +7884,8 @@ export default [ macos: true, ios: true, }, - footnotes: {}, + footnotes: { + macos: ["globe"], + }, }, ]; From 0c06023ca03fe2e7e7d5f7abe24b3f5d835b5b5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 22:58:15 +0000 Subject: [PATCH 30/49] chore(deps): bump postcss from 8.4.18 to 8.4.31 in /docs Bumps [postcss](https://github.com/postcss/postcss) from 8.4.18 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.18...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/package-lock.json | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index f17adc4bde9..6e5973726bf 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -11189,9 +11189,15 @@ } }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -11938,9 +11944,9 @@ } }, "node_modules/postcss": { - "version": "8.4.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", - "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -11949,10 +11955,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -24533,9 +24543,9 @@ } }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" }, "natural-compare": { "version": "1.4.0", @@ -25063,11 +25073,11 @@ } }, "postcss": { - "version": "8.4.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", - "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } From a9f677007bccc3d2c0d988d3b1f45bb7d79d6131 Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Tue, 10 Oct 2023 20:09:15 -0700 Subject: [PATCH 31/49] fix(blog): Correct incorrect info re: global-quick-tap --- docs/blog/2023-10-05-zmk-sotf-6.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/2023-10-05-zmk-sotf-6.md b/docs/blog/2023-10-05-zmk-sotf-6.md index 18a52a859fd..5aa1567a90a 100644 --- a/docs/blog/2023-10-05-zmk-sotf-6.md +++ b/docs/blog/2023-10-05-zmk-sotf-6.md @@ -21,7 +21,7 @@ Here's a summary of the various major changes since last time, broken down by th [andrewjrae] added the [`require-prior-idle-ms` property](/docs/behaviors/hold-tap#require-prior-idle-ms) to the hold-tap behavior in [#1187](https://github.com/zmkfirmware/zmk/pull/1187) and [#1387](https://github.com/zmkfirmware/zmk/pull/1387), which prevents the hold behavior from triggering if it hasn't been a certain duration since the last key press. This is a useful feature to prevent accidental hold activations during quick typing and made its way into many keymaps! The same property was added to [combos](/docs/features/combos#configuration) as well to help prevent false combo activations. -Note that an earlier iteration of this feature was supported with the `global-quick-tap` property, which did not allow customizing the timeout and used the value of `tapping-term-ms` for it. This property is now deprecated and users are encouraged to use `require-prior-idle-ms` instead. +Note that an earlier iteration of this feature was supported with the `global-quick-tap` property, which did not allow customizing the timeout and used the value of `quick-tap-ms` for it. This property is now deprecated and users are encouraged to use `require-prior-idle-ms` instead. [urob] added the [`hold-trigger-on-release` property](/docs/behaviors/hold-tap#positional-hold-tap-and-hold-trigger-key-positions) in [#1423](https://github.com/zmkfirmware/zmk/pull/1423). This significantly increases the usefulness of positional constraints on hold-taps, since it allows combining multiple holds such as different modifiers for home row mods usage. From 3ab922822c0b1bbf99e9ea50f17fe15dcef6c3f3 Mon Sep 17 00:00:00 2001 From: Peter Johanson Date: Mon, 2 Oct 2023 22:05:24 -0700 Subject: [PATCH 32/49] feat(shields): Add ZMK Uno nice!view support. Export the `nice_view_spi` node properly from the ZMK Uno overlay to ensure the shield will work when built along with the `nice_view` shield. --- app/boards/shields/zmk_uno/zmk_uno.overlay | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/boards/shields/zmk_uno/zmk_uno.overlay b/app/boards/shields/zmk_uno/zmk_uno.overlay index 78f3b4a7146..02bb4ad8e81 100644 --- a/app/boards/shields/zmk_uno/zmk_uno.overlay +++ b/app/boards/shields/zmk_uno/zmk_uno.overlay @@ -11,9 +11,17 @@ status = "okay"; }; -&arduino_spi { +nice_view_spi: &arduino_spi { status = "okay"; + cs-gpios = <&arduino_header 16 GPIO_ACTIVE_HIGH>; + + // Needed so the nice_view shield will enhance the existing node which falls *first* + // on the bus, properly picking up the first `cs-gpios` specifier. + ls0xx@0 { + reg = <0>; + }; + led_strip: ws2812@0 { compatible = "worldsemi,ws2812-spi"; From a9a53e6da490e319bae5efa85c221010a2578b15 Mon Sep 17 00:00:00 2001 From: Alex Kang Date: Sat, 14 Oct 2023 19:39:42 -0700 Subject: [PATCH 33/49] feat(shields): Reviung34 shield definition * Initial implementation of REVIUNG34 shield. * Add copyright information to files * Added a README with instructions on how to enable the 1x2u layout. * Add a default chosen matrix transform in the default keymap, alongside a commented out version for the alternate layout. --------- Co-authored-by: Peter Johanson Co-authored-by: Cem Aksoylar --- .../shields/reviung34/Kconfig.defconfig | 9 +++ app/boards/shields/reviung34/Kconfig.shield | 5 ++ app/boards/shields/reviung34/README.md | 13 ++++ .../reviung34/boards/nice_nano_v2.overlay | 48 ++++++++++++ app/boards/shields/reviung34/reviung34.conf | 3 + app/boards/shields/reviung34/reviung34.keymap | 77 +++++++++++++++++++ .../shields/reviung34/reviung34.overlay | 63 +++++++++++++++ .../shields/reviung34/reviung34.zmk.yml | 9 +++ 8 files changed, 227 insertions(+) create mode 100644 app/boards/shields/reviung34/Kconfig.defconfig create mode 100644 app/boards/shields/reviung34/Kconfig.shield create mode 100644 app/boards/shields/reviung34/README.md create mode 100644 app/boards/shields/reviung34/boards/nice_nano_v2.overlay create mode 100644 app/boards/shields/reviung34/reviung34.conf create mode 100644 app/boards/shields/reviung34/reviung34.keymap create mode 100644 app/boards/shields/reviung34/reviung34.overlay create mode 100644 app/boards/shields/reviung34/reviung34.zmk.yml diff --git a/app/boards/shields/reviung34/Kconfig.defconfig b/app/boards/shields/reviung34/Kconfig.defconfig new file mode 100644 index 00000000000..5dc26b4f065 --- /dev/null +++ b/app/boards/shields/reviung34/Kconfig.defconfig @@ -0,0 +1,9 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +if SHIELD_REVIUNG34 + +config ZMK_KEYBOARD_NAME + default "REVIUNG34" + +endif diff --git a/app/boards/shields/reviung34/Kconfig.shield b/app/boards/shields/reviung34/Kconfig.shield new file mode 100644 index 00000000000..f3c63a157c1 --- /dev/null +++ b/app/boards/shields/reviung34/Kconfig.shield @@ -0,0 +1,5 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +config SHIELD_REVIUNG34 + def_bool $(shields_list_contains,reviung34) diff --git a/app/boards/shields/reviung34/README.md b/app/boards/shields/reviung34/README.md new file mode 100644 index 00000000000..e62c52d65e3 --- /dev/null +++ b/app/boards/shields/reviung34/README.md @@ -0,0 +1,13 @@ +# REVIUNG34 + +REVIUNG34 is a 33-34 key unibody split keyboard by [gtips](https://github.com/gtips). An in-stock version can be found at [Little Keyboards](https://www.littlekeyboards.com/products/reviung34-analyst-keyboard-kit). + +By default, the 2x1u layout is used. To use to the 1x2u layout, add the following to your keymap: + +``` +/ { + chosen { + zmk,matrix_transform = &single_2u_transform; + }; +}; +``` diff --git a/app/boards/shields/reviung34/boards/nice_nano_v2.overlay b/app/boards/shields/reviung34/boards/nice_nano_v2.overlay new file mode 100644 index 00000000000..b8d21398448 --- /dev/null +++ b/app/boards/shields/reviung34/boards/nice_nano_v2.overlay @@ -0,0 +1,48 @@ +#include + +&pinctrl { + spi3_default: spi3_default { + group1 { + psels = ; + }; + }; + + spi3_sleep: spi3_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; +}; + +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + + pinctrl-0 = <&spi3_default>; + pinctrl-1 = <&spi3_sleep>; + pinctrl-names = "default", "sleep"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + label = "WS2812"; + + /* SPI */ + reg = <0>; /* ignored, but necessary for SPI bindings */ + spi-max-frequency = <4000000>; + + /* WS2812 */ + chain-length = <9>; /* number of LEDs */ + spi-one-frame = <0x70>; + spi-zero-frame = <0x40>; + color-mapping = ; + }; +}; + +/ { + chosen { + zmk,underglow = &led_strip; + }; +}; diff --git a/app/boards/shields/reviung34/reviung34.conf b/app/boards/shields/reviung34/reviung34.conf new file mode 100644 index 00000000000..289f070ba3f --- /dev/null +++ b/app/boards/shields/reviung34/reviung34.conf @@ -0,0 +1,3 @@ +# Uncomment the following lines to enable RGB underglow +# CONFIG_ZMK_RGB_UNDERGLOW=y +# CONFIG_WS2812_STRIP=y diff --git a/app/boards/shields/reviung34/reviung34.keymap b/app/boards/shields/reviung34/reviung34.keymap new file mode 100644 index 00000000000..eefc510a1c4 --- /dev/null +++ b/app/boards/shields/reviung34/reviung34.keymap @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +/ { + chosen { + // 34 keys. + zmk,matrix_transform = &dual_1u_transform; + + // 33 keys. Center two thumb keys replaced by a single 2u key. Remember to adjust your + // keymap accordingly! + // zmk,matrix_transform = &single_2u_transform; + }; +}; + +/ { + keymap { + compatible = "zmk,keymap"; + + base { + label = "Base"; + bindings = < +&kp Q &kp W &kp E &kp R &kp T &kp Y &kp U &kp I &kp O &kp P +&kp A &kp S &kp D &kp F &kp G &kp H &kp J &kp K &kp L &kp SEMI +&mt LSHFT Z &mt LCTRL X &mt LALT C &kp V &kp B &kp N &kp M &mt RALT COMMA &mt RCTRL DOT &mt RSHFT SLASH + &kp LGUI < 1 BSPC < 2 SPACE &mo 3 + >; + }; + + lower { + label = "Lower"; + bindings = < +&kp EXCL &kp AT &kp HASH &kp DLLR &kp PRCNT &kp CARET &kp AMPS &kp ASTRK &kp LPAR &kp RPAR +&trans &kp TILDE &kp DQT &kp PIPE &trans &trans &kp UNDER &kp PLUS &kp LBRC &kp RBRC +&trans &trans &trans &trans &trans &trans &trans &trans &trans &trans + &trans &trans &mo 4 &trans + >; + }; + + upper { + label = "Upper"; + bindings = < +&kp N1 &kp N2 &kp N3 &kp N4 &kp N5 &kp N6 &kp N7 &kp N8 &kp N9 &kp N0 +&trans &kp GRAVE &kp SQT &kp BSLH &trans &trans &kp MINUS &kp EQUAL &kp LBKT &kp RBKT +&trans &trans &trans &trans &trans &trans &trans &trans &trans &trans + &trans &mo 4 &trans &trans + >; + }; + + function { + label = "Function"; + bindings = < +&kp TAB &trans &kp C_VOL_UP &trans &trans &trans &trans &trans &trans &kp ENTER +&kp ESC &kp C_BRI_DN &kp C_VOL_DN &kp C_BRI_UP &trans &trans &kp LEFT &kp DOWN &kp UP &kp RIGHT +&trans &trans &trans &trans &trans &trans &trans &trans &trans &trans + &trans &kp C_PWR &trans &trans + >; + }; + + meta { + label = "Meta"; + bindings = < +&rgb_ug RGB_HUI &rgb_ug RGB_SAI &rgb_ug RGB_BRI &rgb_ug RGB_SPI &rgb_ug RGB_EFF &none &none &none &none &none +&rgb_ug RGB_HUD &rgb_ug RGB_SAD &rgb_ug RGB_BRD &rgb_ug RGB_SPD &rgb_ug RGB_EFR &bt BT_SEL 0 &bt BT_SEL 1 &bt BT_SEL 2 &bt BT_SEL 3 &bt BT_SEL 4 +&none &none &rgb_ug RGB_TOG &none &none &none &none &bt BT_CLR &none &none + &none &trans &trans &none + >; + }; + }; +}; diff --git a/app/boards/shields/reviung34/reviung34.overlay b/app/boards/shields/reviung34/reviung34.overlay new file mode 100644 index 00000000000..46d85996738 --- /dev/null +++ b/app/boards/shields/reviung34/reviung34.overlay @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + chosen { + zmk,kscan = &kscan0; + zmk,matrix_transform = &dual_1u_transform; + }; + + dual_1u_transform: keymap_transform_0 { + compatible = "zmk,matrix-transform"; + columns = <9>; + rows = <4>; + map = < +RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,5) RC(0,6) RC(0,7) RC(0,8) RC(3,5) +RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,5) RC(1,6) RC(1,7) RC(1,8) RC(3,6) +RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,6) RC(2,7) RC(2,8) RC(3,7) + RC(3,2) RC(3,3) RC(3,4) RC(3,8) + >; + }; + + single_2u_transform: keymap_transform_1 { + compatible = "zmk,matrix-transform"; + columns = <9>; + rows = <4>; + map = < +RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,5) RC(0,6) RC(0,7) RC(0,8) RC(3,5) +RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,5) RC(1,6) RC(1,7) RC(1,8) RC(3,6) +RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,6) RC(2,7) RC(2,8) RC(3,7) + RC(3,2) RC(3,4) RC(3,8) + >; + }; + + kscan0: kscan_0 { + compatible = "zmk,kscan-gpio-matrix"; + label = "KSCAN"; + diode-direction = "col2row"; + + col-gpios + = <&pro_micro 4 GPIO_ACTIVE_HIGH> + , <&pro_micro 5 GPIO_ACTIVE_HIGH> + , <&pro_micro 6 GPIO_ACTIVE_HIGH> + , <&pro_micro 7 GPIO_ACTIVE_HIGH> + , <&pro_micro 8 GPIO_ACTIVE_HIGH> + , <&pro_micro 15 GPIO_ACTIVE_HIGH> + , <&pro_micro 14 GPIO_ACTIVE_HIGH> + , <&pro_micro 16 GPIO_ACTIVE_HIGH> + , <&pro_micro 10 GPIO_ACTIVE_HIGH> + ; + + row-gpios + = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 19 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 18 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + ; + }; +}; diff --git a/app/boards/shields/reviung34/reviung34.zmk.yml b/app/boards/shields/reviung34/reviung34.zmk.yml new file mode 100644 index 00000000000..76ed745d979 --- /dev/null +++ b/app/boards/shields/reviung34/reviung34.zmk.yml @@ -0,0 +1,9 @@ +file_format: "1" +id: reviung34 +name: REVIUNG34 +type: shield +url: https://github.com/gtips/reviung/tree/master/reviung34 +requires: [pro_micro] +features: + - keys + - underglow From 3b1d04372b0a013ec2738019166c1c2e85bf0ba7 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Fri, 13 Oct 2023 14:58:27 -0500 Subject: [PATCH 34/49] feat: Print Kconfig and DTS for failed builds Added the combined devicetree file to the user config build action. Set it and the Kconfig output to run even on a failed build. --- .github/workflows/build-user-config.yml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-user-config.yml b/.github/workflows/build-user-config.yml index 5891ddc1e26..c1a97b4d6e0 100644 --- a/.github/workflows/build-user-config.yml +++ b/.github/workflows/build-user-config.yml @@ -97,7 +97,27 @@ jobs: run: west build -s zmk/app -b "${{ matrix.board }}" -- -DZMK_CONFIG="${GITHUB_WORKSPACE}/${{ inputs.config_path }}" ${{ env.extra_cmake_args }} ${{ matrix.cmake-args }} - name: ${{ env.display_name }} Kconfig file - run: grep -v -e "^#" -e "^$" build/zephyr/.config | sort + run: | + if [ -f build/zephyr/.config ] + then + grep -v -e "^#" -e "^$" build/zephyr/.config | sort + else + echo "No Kconfig output" + fi + if: ${{ !cancelled() }} + + - name: ${{ env.display_name }} Devicetree file + run: | + if [ -f build/zephyr/zephyr.dts ] + then + cat build/zephyr/zephyr.dts + elif [ -f build/zephyr/zephyr.dts.pre ] + then + cat -s build/zephyr/zephyr.dts.pre + else + echo "No Devicetree output" + fi + if: ${{ !cancelled() }} - name: Rename artifacts shell: sh -x {0} From 7fe9ecd87f086fe6fcf253559e751fab575faa5c Mon Sep 17 00:00:00 2001 From: Khalid Aj Date: Wed, 18 Oct 2023 05:43:50 +0700 Subject: [PATCH 35/49] feat(shields): Add Reviung53 shield. * Initial Reviung53 shield --------- Co-authored-by: Cem Aksoylar --- .../shields/reviung53/Kconfig.defconfig | 9 ++ app/boards/shields/reviung53/Kconfig.shield | 5 + .../reviung53/boards/nice_nano.overlay | 47 ++++++++ .../reviung53/boards/nice_nano_v2.overlay | 47 ++++++++ app/boards/shields/reviung53/reviung53.conf | 3 + app/boards/shields/reviung53/reviung53.keymap | 109 ++++++++++++++++++ .../shields/reviung53/reviung53.overlay | 56 +++++++++ .../shields/reviung53/reviung53.zmk.yml | 9 ++ 8 files changed, 285 insertions(+) create mode 100644 app/boards/shields/reviung53/Kconfig.defconfig create mode 100644 app/boards/shields/reviung53/Kconfig.shield create mode 100644 app/boards/shields/reviung53/boards/nice_nano.overlay create mode 100644 app/boards/shields/reviung53/boards/nice_nano_v2.overlay create mode 100644 app/boards/shields/reviung53/reviung53.conf create mode 100644 app/boards/shields/reviung53/reviung53.keymap create mode 100644 app/boards/shields/reviung53/reviung53.overlay create mode 100644 app/boards/shields/reviung53/reviung53.zmk.yml diff --git a/app/boards/shields/reviung53/Kconfig.defconfig b/app/boards/shields/reviung53/Kconfig.defconfig new file mode 100644 index 00000000000..efac69b7f40 --- /dev/null +++ b/app/boards/shields/reviung53/Kconfig.defconfig @@ -0,0 +1,9 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +if SHIELD_REVIUNG53 + +config ZMK_KEYBOARD_NAME + default "Reviung53" + +endif diff --git a/app/boards/shields/reviung53/Kconfig.shield b/app/boards/shields/reviung53/Kconfig.shield new file mode 100644 index 00000000000..0b0613e2331 --- /dev/null +++ b/app/boards/shields/reviung53/Kconfig.shield @@ -0,0 +1,5 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +config SHIELD_REVIUNG53 + def_bool $(shields_list_contains,reviung53) diff --git a/app/boards/shields/reviung53/boards/nice_nano.overlay b/app/boards/shields/reviung53/boards/nice_nano.overlay new file mode 100644 index 00000000000..4df91903c68 --- /dev/null +++ b/app/boards/shields/reviung53/boards/nice_nano.overlay @@ -0,0 +1,47 @@ +#include + +&pinctrl { + spi3_default: spi3_default { + group1 { + psels = ; + }; + }; + + spi3_sleep: spi3_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; +}; + +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + + pinctrl-0 = <&spi3_default>; + pinctrl-1 = <&spi3_sleep>; + pinctrl-names = "default", "sleep"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + label = "WS2812"; + + /* SPI */ + reg = <0>; /* ignored, but necessary for SPI bindings */ + spi-max-frequency = <4000000>; + + /* WS2812 */ + chain-length = <10>; + spi-one-frame = <0x70>; + spi-zero-frame = <0x40>; + + color-mapping = ; + }; +}; + +/ { + chosen { + zmk,underglow = &led_strip; + }; +}; diff --git a/app/boards/shields/reviung53/boards/nice_nano_v2.overlay b/app/boards/shields/reviung53/boards/nice_nano_v2.overlay new file mode 100644 index 00000000000..4df91903c68 --- /dev/null +++ b/app/boards/shields/reviung53/boards/nice_nano_v2.overlay @@ -0,0 +1,47 @@ +#include + +&pinctrl { + spi3_default: spi3_default { + group1 { + psels = ; + }; + }; + + spi3_sleep: spi3_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; +}; + +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + + pinctrl-0 = <&spi3_default>; + pinctrl-1 = <&spi3_sleep>; + pinctrl-names = "default", "sleep"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + label = "WS2812"; + + /* SPI */ + reg = <0>; /* ignored, but necessary for SPI bindings */ + spi-max-frequency = <4000000>; + + /* WS2812 */ + chain-length = <10>; + spi-one-frame = <0x70>; + spi-zero-frame = <0x40>; + + color-mapping = ; + }; +}; + +/ { + chosen { + zmk,underglow = &led_strip; + }; +}; diff --git a/app/boards/shields/reviung53/reviung53.conf b/app/boards/shields/reviung53/reviung53.conf new file mode 100644 index 00000000000..289f070ba3f --- /dev/null +++ b/app/boards/shields/reviung53/reviung53.conf @@ -0,0 +1,3 @@ +# Uncomment the following lines to enable RGB underglow +# CONFIG_ZMK_RGB_UNDERGLOW=y +# CONFIG_WS2812_STRIP=y diff --git a/app/boards/shields/reviung53/reviung53.keymap b/app/boards/shields/reviung53/reviung53.keymap new file mode 100644 index 00000000000..d00ca6b975b --- /dev/null +++ b/app/boards/shields/reviung53/reviung53.keymap @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +/ { + conditional_layers { + compatible = "zmk,conditional-layers"; + tri_layer { + if-layers = <1 2>; + then-layer = <3>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { +// ---------------------------------------------------------------------------------------- +// | | | ESC | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | DEL | +// | TAB | Q | W | E | R | T | Y | U | I | O | P | BKSP | +// | CAPS | A | S | D | F | G | H | J | K | L | ; | RET | +// | SHFT | Z | X | C | V | B | N | M | , | . | SHFT(/) | +// | CTRL | GUI | ALT | LOWER(SPACE) | RAISE(SPACE)| ALT | GUI | CTRL(\) | +// | + bindings = < + &kp ESC &kp F1 &kp F2 &kp F3 &kp F4 &kp F5 &kp F6 &kp F7 &kp F8 &kp DEL + &kp TAB &kp Q &kp W &kp E &kp R &kp T &kp Y &kp U &kp I &kp O &kp P &kp BSPC + &kp CAPS &kp A &kp S &kp D &kp F &kp G &kp H &kp J &kp K &kp L &kp SEMI &kp RET + &kp LSHFT &kp Z &kp X &kp C &kp V &kp B &kp N &kp M &kp COMMA &kp DOT &mt RSHFT FSLH + &kp LCTRL &kp LCMD &kp LALT < 1 SPACE < 2 SPACE &kp RALT &kp RCMD &mt RCTRL BSLH + >; + }; + + lower_layer { +// -------------------------------------------------------------------------------------------- +// | | | | F9 | F10 | F11 | F12 | INS | PAU | SCR | PSCR | | +// | ~ | ! | @ | # | $ | % | ^ | & | * | ( | ) | | +// | NAV | | | | | | | _ | + | { | } | " | +// | | | | | | | | | | | ? | +// | | | | | | | | | | +// | + bindings = < + &trans &kp F9 &kp F10 &kp F11 &kp F12 &kp INS &kp PAUSE_BREAK &kp SLCK &kp PSCRN &trans + &kp TILDE &kp EXCL &kp AT &kp HASH &kp DLLR &kp PRCNT &kp CARET &kp AMPS &kp ASTRK &kp LPAR &kp RPAR &trans + &mo 4 &none &none &none &none &none &none &kp UNDER &kp PLUS &kp LBRC &kp RBRC &kp DQT + &trans &none &none &none &none &none &none &none &none &none &kp QMARK + &trans &trans &trans &trans &trans &trans &trans &kp PIPE + >; + }; + + raise_layer { +// -------------------------------------------------------------------------------------- +// | | | | F9 | F10 | F11 | F12 | MUTE | VOL+ | VOL- | PLAY | | +// | ` | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | | +// | NAV | | | | | | | - | = | [ | ] | ' | +// | | | | | | | | + | < | > | : | +// | | | | | | | | | | +// | + bindings = < + &trans &kp F9 &kp F10 &kp F11 &kp F12 &kp C_MUTE &kp C_VOL_UP &kp C_VOL_DN &kp C_PLAY &trans + &kp GRAVE &kp N1 &kp N2 &kp N3 &kp N4 &kp N5 &kp N6 &kp N7 &kp N8 &kp N9 &kp N0 &trans + &mo 4 &none &none &none &none &none &none &kp MINUS &kp EQUAL &kp LBKT &kp RBKT &kp SQT + &trans &none &none &none &none &none &none &kp PLUS &kp LT &kp GT &kp COLON + &trans &trans &trans &trans &trans &trans &trans &kp PIPE + >; + }; + + adjust_layer { +// ------------------------------------------------------------------------------------------------------------------------ +// | | | BT CLR | BT1 | BT2 | BT3 | BT4 | BT5 | | | | BT CLR | +// | RGB BRI+ | RGB SAT+ | RGB HUE+ | RGB ANI+ | | RGB TOG | | | | | | | +// | RGB BRI- | RGB SAT- | RGB HUE- | RGB ANI- | | | | | | | | | +// | | | | | | | BOOT | | | | | +// | | | | | | | | | +// | + bindings = < + &bt BT_CLR &bt BT_SEL 0 &bt BT_SEL 1 &bt BT_SEL 2 &bt BT_SEL 3 &bt BT_SEL 4 &none &none &none &bt BT_CLR + &rgb_ug RGB_BRI &rgb_ug RGB_SAI &rgb_ug RGB_HUI &rgb_ug RGB_EFF &none &rgb_ug RGB_TOG &none &none &none &none &none &none + &rgb_ug RGB_BRD &rgb_ug RGB_SAD &rgb_ug RGB_HUD &rgb_ug RGB_EFR &none &none &none &none &none &none &none &none + &trans &none &none &none &none &none &bootloader &none &none &none &none + &trans &trans &trans &trans &trans &none &none &none + >; + }; + + nav_layer { +// ------------------------------------------------------------------------------------------------------------------------ +// | | | ESC | | | | | | | | | DEL | +// | TAB | | UP | | | | | | | | | BSPC | +// | NAV | LEFT | DOWN | RIGHT | | | LEFT | DOWN | UP | RIGHT | | ENTER | +// | SHIFT | | | | | | HOME | END | PGUP | PGDN | SHIFT | +// | CTRL | GUI | ALT | SPACE | SPACE | ALT | GUI | CTRL | +// | + bindings = < + &kp ESC &none &none &none &none &none &none &none &none &kp DEL + &kp TAB &none &kp UP &none &none &none &none &none &none &none &none &kp BSPC + &trans &kp LEFT &kp DOWN &kp RIGHT &none &none &kp LEFT &kp DOWN &kp UP &kp RIGHT &none &kp RET + &kp LSHFT &none &none &none &none &none &kp HOME &kp END &kp PG_UP &kp PG_DN &kp RSHFT + &kp LCTRL &kp LCMD &kp LALT &kp SPACE &kp SPACE &kp LALT &kp RCMD &kp RCTRL + >; + }; + }; +}; \ No newline at end of file diff --git a/app/boards/shields/reviung53/reviung53.overlay b/app/boards/shields/reviung53/reviung53.overlay new file mode 100644 index 00000000000..213b3b81c72 --- /dev/null +++ b/app/boards/shields/reviung53/reviung53.overlay @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + chosen { + zmk,kscan = &kscan0; + zmk,matrix_transform = &default_transform; + }; + + default_transform: keymap_transform_0 { + compatible = "zmk,matrix-transform"; + columns = <8>; + rows = <7>; + + map = < + RC(0,2) RC(0,3) RC(0,4) RC(0,5) RC(0,6) RC(0,7) RC(4,0) RC(4,1) RC(4,2) RC(4,3) +RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,5) RC(1,6) RC(1,7) RC(4,4) RC(4,5) RC(4,6) RC(4,7) +RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,6) RC(2,7) RC(5,0) RC(5,1) RC(5,2) RC(5,3) +RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(3,6) RC(3,7) RC(5,4) RC(5,5) RC(5,6) +RC(6,0) RC(6,1) RC(6,2) RC(6,3) RC(6,4) RC(6,5) RC(6,6) RC(6,7) + >; + }; + + kscan0: kscan_0 { + compatible = "zmk,kscan-gpio-matrix"; + label = "KSCAN"; + diode-direction = "col2row"; + + col-gpios + = <&pro_micro 21 GPIO_ACTIVE_HIGH> + , <&pro_micro 20 GPIO_ACTIVE_HIGH> + , <&pro_micro 19 GPIO_ACTIVE_HIGH> + , <&pro_micro 18 GPIO_ACTIVE_HIGH> + , <&pro_micro 15 GPIO_ACTIVE_HIGH> + , <&pro_micro 14 GPIO_ACTIVE_HIGH> + , <&pro_micro 16 GPIO_ACTIVE_HIGH> + , <&pro_micro 10 GPIO_ACTIVE_HIGH> + ; + + row-gpios + = <&pro_micro 3 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 4 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 5 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 6 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 7 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 8 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + , <&pro_micro 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> + ; + + }; +}; diff --git a/app/boards/shields/reviung53/reviung53.zmk.yml b/app/boards/shields/reviung53/reviung53.zmk.yml new file mode 100644 index 00000000000..e670755c774 --- /dev/null +++ b/app/boards/shields/reviung53/reviung53.zmk.yml @@ -0,0 +1,9 @@ +file_format: "1" +id: reviung53 +name: REVIUNG53 +type: shield +url: https://github.com/gtips/reviung/tree/master/reviung53 +requires: [pro_micro] +features: + - keys + - underglow From 82e85699edd7261500ea2feb0697f8a6a5bbd42c Mon Sep 17 00:00:00 2001 From: ReFil <31960031+ReFil@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:04:04 +0100 Subject: [PATCH 36/49] feat(docs): Document ZMK_BATTERY_REPORTING config (#1971) `CONFIG_ZMK_BATTERY_REPORTING` is currently undocumented, A new KConfig section for battery has been added in line with the other sections in the configuration section of the docs, `CONFIG_ZMK_BATTERY_REPORT_INTERVAL` has been moved from system to battery for consistency --- docs/docs/config/battery.md | 15 +++++++++++++++ docs/docs/config/system.md | 13 ++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/docs/config/battery.md b/docs/docs/config/battery.md index 73b4ee92933..7b7e9436503 100644 --- a/docs/docs/config/battery.md +++ b/docs/docs/config/battery.md @@ -7,6 +7,21 @@ See the [battery level feature page](../features/battery.md) for more details on See [Configuration Overview](index.md) for instructions on how to change these settings. +### Kconfig + +Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/Kconfig) + +| Config | Type | Description | Default | +| ------------------------------------ | ---- | ------------------------------------------------------ | ------- | +| `CONFIG_ZMK_BATTERY_REPORTING` | bool | Enables/disables all battery level detection/reporting | n | +| `CONFIG_ZMK_BATTERY_REPORT_INTERVAL` | int | Battery level report interval in seconds | 60 | + +:::note Default setting + +While `CONFIG_ZMK_BATTERY_REPORTING` is disabled by default it is implied by `CONFIG_ZMK_BLE`, thus any board with BLE enabled will have this automatically enabled unless explicitly overriden. + +::: + ### Devicetree Applies to: [`/chosen` node](https://docs.zephyrproject.org/latest/guides/dts/intro.html#aliases-and-chosen-nodes) diff --git a/docs/docs/config/system.md b/docs/docs/config/system.md index 5a5ae5a55a0..25d51940afb 100644 --- a/docs/docs/config/system.md +++ b/docs/docs/config/system.md @@ -13,13 +13,12 @@ Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/ ### General -| Config | Type | Description | Default | -| ------------------------------------ | ------ | ----------------------------------------------------------------------------- | ------- | -| `CONFIG_ZMK_KEYBOARD_NAME` | string | The name of the keyboard (max 16 characters) | | -| `CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE` | int | Milliseconds to wait after a setting change before writing it to flash memory | 60000 | -| `CONFIG_ZMK_WPM` | bool | Enable calculating words per minute | n | -| `CONFIG_HEAP_MEM_POOL_SIZE` | int | Size of the heap memory pool | 8192 | -| `CONFIG_ZMK_BATTERY_REPORT_INTERVAL` | int | Battery level report interval in seconds | 60 | +| Config | Type | Description | Default | +| ----------------------------------- | ------ | ----------------------------------------------------------------------------- | ------- | +| `CONFIG_ZMK_KEYBOARD_NAME` | string | The name of the keyboard (max 16 characters) | | +| `CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE` | int | Milliseconds to wait after a setting change before writing it to flash memory | 60000 | +| `CONFIG_ZMK_WPM` | bool | Enable calculating words per minute | n | +| `CONFIG_HEAP_MEM_POOL_SIZE` | int | Size of the heap memory pool | 8192 | ### HID From 9e8ee8684252aecca36c1e6f2403b0a9fd3ebf4f Mon Sep 17 00:00:00 2001 From: Nick Coutsos Date: Sun, 22 Oct 2023 12:05:49 -0400 Subject: [PATCH 37/49] chore(docs): Fix links to relocated files (#1975) --- docs/docs/config/battery.md | 2 +- docs/docs/config/displays.md | 4 ++-- docs/docs/config/encoders.md | 4 ++-- docs/docs/config/kscan.md | 14 +++++++------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/docs/config/battery.md b/docs/docs/config/battery.md index 7b7e9436503..c56a30efd67 100644 --- a/docs/docs/config/battery.md +++ b/docs/docs/config/battery.md @@ -48,7 +48,7 @@ Driver for reading the voltage of a battery using a Nordic nRF52's VDDH pin. Thi Applies to: `compatible = "zmk,battery-nrf-vddh"` -Definition file: [zmk/app/drivers/zephyr/dts/bindings/sensor/zmk,battery-nrf-vddh.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/zephyr/dts/bindings/sensor/zmk%2Cbattery-nrf-vddh.yaml) +Definition file: [zmk/app/module/dts/bindings/sensor/zmk,battery-nrf-vddh.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/module/dts/bindings/sensor/zmk%2Cbattery-nrf-vddh.yaml) | Property | Type | Description | | -------- | ------ | ------------------------- | diff --git a/docs/docs/config/displays.md b/docs/docs/config/displays.md index a68bf26a9b9..e22f0da8fbb 100644 --- a/docs/docs/config/displays.md +++ b/docs/docs/config/displays.md @@ -49,7 +49,7 @@ Using a dedicated thread requires more memory but prevents displays with slow up You must also configure the driver for your display. ZMK provides the following display drivers: -- [IL0323](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/display/Kconfig.il0323) +- [IL0323](https://github.com/zmkfirmware/zmk/blob/main/app/module/drivers/display/Kconfig.il0323) Zephyr provides several display drivers as well. Search for the name of your display in [Zephyr's Kconfig options](https://docs.zephyrproject.org/latest/kconfig.html) documentation. @@ -57,7 +57,7 @@ Zephyr provides several display drivers as well. Search for the name of your dis See the Devicetree bindings for your display. Here are the bindings for common displays: -- [IL0323](https://github.com/zmkfirmware/zmk/blob/main/app/dts/bindings/display/gooddisplay%2Cil0323.yaml) +- [IL0323](https://github.com/zmkfirmware/zmk/blob/main/app/module/dts/bindings/display/gooddisplay%2Cil0323.yaml) - [SSD1306 (i2c)](https://docs.zephyrproject.org/latest/build/dts/api/bindings/display/solomon,ssd1306fb-i2c.html) - [SSD1306 (spi)](https://docs.zephyrproject.org/latest/build/dts/api/bindings/display/solomon,ssd1306fb-spi.html) diff --git a/docs/docs/config/encoders.md b/docs/docs/config/encoders.md index f6bd6de1495..97a0cc38e95 100644 --- a/docs/docs/config/encoders.md +++ b/docs/docs/config/encoders.md @@ -11,7 +11,7 @@ See [Configuration Overview](index.md) for instructions on how to change these s ### Kconfig -Definition file: [zmk/app/drivers/sensor/ec11/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/sensor/ec11/Kconfig) +Definition file: [zmk/app/module/drivers/sensor/ec11/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/module/drivers/sensor/ec11/Kconfig) | Config | Type | Description | Default | | ------------------------------- | ---- | -------------------------------- | ------- | @@ -31,7 +31,7 @@ If `CONFIG_EC11` is enabled, exactly one of the following options must be set to Applies to: `compatible = "alps,ec11"` -Definition file: [zmk/app/drivers/zephyr/dts/bindings/sensor/alps,ec11.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/zephyr/dts/bindings/sensor/alps%2Cec11.yaml) +Definition file: [zmk/app/module/dts/bindings/sensor/alps,ec11.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/module/dts/bindings/sensor/alps%2Cec11.yaml) | Property | Type | Description | Default | | ------------ | ---------- | ------------------------------------- | ------- | diff --git a/docs/docs/config/kscan.md b/docs/docs/config/kscan.md index 67c3765298a..00360c46978 100644 --- a/docs/docs/config/kscan.md +++ b/docs/docs/config/kscan.md @@ -12,7 +12,7 @@ See [Configuration Overview](index.md) for instructions on how to change these s Definition files: - [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/Kconfig) -- [zmk/app/drivers/kscan/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/kscan/Kconfig) +- [zmk/app/module/drivers/kscan/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/module/drivers/kscan/Kconfig) | Config | Type | Description | Default | | -------------------------------------- | ---- | ---------------------------------------------------- | ------- | @@ -44,7 +44,7 @@ Currently this driver does not honor the `CONFIG_ZMK_KSCAN_DEBOUNCE_*` settings. Applies to: `compatible = "zmk,kscan-gpio-demux"` -Definition file: [zmk/app/drivers/zephyr/dts/bindings/kscan/zmk,kscan-gpio-demux.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/zephyr/dts/bindings/kscan/zmk%2Ckscan-gpio-demux.yaml) +Definition file: [zmk/app/module/dts/bindings/kscan/zmk,kscan-gpio-demux.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/module/dts/bindings/kscan/zmk%2Ckscan-gpio-demux.yaml) | Property | Type | Description | Default | | ----------------------- | ---------- | -------------------------------- | ------- | @@ -60,7 +60,7 @@ Keyboard scan driver where each key has a dedicated GPIO. ### Kconfig -Definition file: [zmk/app/drivers/kscan/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/kscan/Kconfig) +Definition file: [zmk/app/module/drivers/kscan/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/module/drivers/kscan/Kconfig) | Config | Type | Description | Default | | --------------------------------- | ---- | ------------------------------------------------ | ------- | @@ -70,7 +70,7 @@ Definition file: [zmk/app/drivers/kscan/Kconfig](https://github.com/zmkfirmware/ Applies to: `compatible = "zmk,kscan-gpio-direct"` -Definition file: [zmk/app/drivers/zephyr/dts/bindings/kscan/zmk,kscan-gpio-direct.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/zephyr/dts/bindings/kscan/zmk%2Ckscan-gpio-direct.yaml) +Definition file: [zmk/app/module/dts/bindings/kscan/zmk,kscan-gpio-direct.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/module/dts/bindings/kscan/zmk%2Ckscan-gpio-direct.yaml) | Property | Type | Description | Default | | ------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------- | ------- | @@ -102,7 +102,7 @@ Assuming the switches connect each GPIO pin to the ground, the [GPIO flags](http Keyboard scan driver where keys are arranged on a matrix with one GPIO per row and column. -Definition file: [zmk/app/drivers/kscan/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/kscan/Kconfig) +Definition file: [zmk/app/module/drivers/kscan/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/module/drivers/kscan/Kconfig) | Config | Type | Description | Default | | ---------------------------------------------- | ----------- | ------------------------------------------------------------------------- | ------- | @@ -114,7 +114,7 @@ Definition file: [zmk/app/drivers/kscan/Kconfig](https://github.com/zmkfirmware/ Applies to: `compatible = "zmk,kscan-gpio-matrix"` -Definition file: [zmk/app/drivers/zephyr/dts/bindings/kscan/zmk,kscan-gpio-matrix.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/zephyr/dts/bindings/kscan/zmk%2Ckscan-gpio-matrix.yaml) +Definition file: [zmk/app/module/dts/bindings/kscan/zmk,kscan-gpio-matrix.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/module/dts/bindings/kscan/zmk%2Ckscan-gpio-matrix.yaml) | Property | Type | Description | Default | | ------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------- | ----------- | @@ -284,7 +284,7 @@ Definition file: [zmk/app/dts/bindings/zmk,kscan-mock.yaml](https://github.com/z | `cols` | int | The number of columns in the composite matrix | | | `exit-after` | bool | Exit the program after running all events | false | -The `events` array should be defined using the macros from [dt-bindings/zmk/kscan_mock.h](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/kscan_mock.h). +The `events` array should be defined using the macros from [app/module/include/dt-bindings/zmk/kscan_mock.h](https://github.com/zmkfirmware/zmk/blob/main/app/module/include/dt-bindings/zmk/kscan_mock.h). ## Matrix Transform From 8d09809ef0cfb29c4481e1b31327e0866c2fb902 Mon Sep 17 00:00:00 2001 From: Peter Johanson Date: Wed, 18 Oct 2023 01:19:14 +0000 Subject: [PATCH 38/49] fix(docs): Updated encoder config docs. * Update new shield guide for new sensor/encoder settings. * Add DTS section to encoder config docs. Co-authored-by: Cem Aksoylar --- docs/docs/config/encoders.md | 54 +++++++++++++++++++++++++---- docs/docs/development/new-shield.md | 13 ++++--- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/docs/docs/config/encoders.md b/docs/docs/config/encoders.md index 97a0cc38e95..3044b392943 100644 --- a/docs/docs/config/encoders.md +++ b/docs/docs/config/encoders.md @@ -29,13 +29,55 @@ If `CONFIG_EC11` is enabled, exactly one of the following options must be set to ### Devicetree +#### Keymap Sensor Config + +For shields/boards that export a `sensors` node configuration label, both global and per-sensor settings can be set by overriding the properties there. + +To override the general settings, update them on the exported `sensors` node, e.g.: + +``` +&sensors { + triggers-per-rotation = <18>; +}; +``` + +Per sensor overrides can be added with ordered nested nodes with the correct overrides, e.g.: + +``` +&sensors { + left_config { + triggers-per-rotation = <18>; + }; + + right_config { + triggers-per-rotation = <24>; + }; +}; +``` + +:::note + +The names of the child nodes are not important, and are applied in order to the sensors listed in the `sensors` property of the sensors node. + +::: + +Applies to the node and child nodes of: `compatible = "zmk,keymap-sensors"` + +Definition file: [zmk/app/drivers/zephyr/dts/bindings/zmk,keymap-sensors.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/drivers/zephyr/dts/bindings/zmk%2Ckeymap-sensors.yaml) + +| Property | Type | Description | Default | +| ----------------------- | ---- | --------------------------------------------------------------- | ------- | +| `triggers-per-rotation` | int | Number of times to trigger the bound behavior per full rotation | | + +#### EC11 Nodes + Applies to: `compatible = "alps,ec11"` Definition file: [zmk/app/module/dts/bindings/sensor/alps,ec11.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/module/dts/bindings/sensor/alps%2Cec11.yaml) -| Property | Type | Description | Default | -| ------------ | ---------- | ------------------------------------- | ------- | -| `label` | string | Unique label for the node | | -| `a-gpios` | GPIO array | GPIO connected to the encoder's A pin | | -| `b-gpios` | GPIO array | GPIO connected to the encoder's B pin | | -| `resolution` | int | Number of encoder pulses per tick | 1 | +| Property | Type | Description | Default | +| --------- | ---------- | ---------------------------------------------- | ------- | +| `label` | string | Unique label for the node | | +| `a-gpios` | GPIO array | GPIO connected to the encoder's A pin | | +| `b-gpios` | GPIO array | GPIO connected to the encoder's B pin | | +| `steps` | int | Number of encoder pulses per complete rotation | | diff --git a/docs/docs/development/new-shield.md b/docs/docs/development/new-shield.md index fa30ca38abf..0771122985b 100644 --- a/docs/docs/development/new-shield.md +++ b/docs/docs/development/new-shield.md @@ -430,31 +430,36 @@ If building locally for split boards, you may need to add these lines to the spe In your device tree file you will need to add the following lines to define the encoder sensor: ```dts -left_encoder: encoder_left { + left_encoder: encoder_left { compatible = "alps,ec11"; label = "LEFT_ENCODER"; a-gpios = ; b-gpios = ; - resolution = <4>; + steps = <80>; status = "disabled"; }; ``` -Here you will have to replace PIN_A and PIN_B with the appropriate pins that your PCB utilizes for the encoder(s). For keyboards that use the Pro Micro or any of the Pro Micro replacements, Sparkfun's [Pro Micro Hookup Guide](https://learn.sparkfun.com/tutorials/pro-micro--fio-v3-hookup-guide/hardware-overview-pro-micro) has a pinout diagram that can be useful to determine the right pins. Reference either the blue numbers labeled "Arduino" (digital pins) or the green numbers labeled "Analog" (analog pins). For pins that are labeled as both digital and analog, refer to your specific board's .dtsi file to determine how you should refer to that pin. +Here you need to replace `PIN_A` and `PIN_B` with the appropriate pins that your PCB utilizes for the encoder(s). See [shield overlays section above](#shield-overlays) on the appropriate node label and pin number to use for GPIOs. + +The `steps` property should corresponded to the documented pulses per rotation for the encoders used on the keyboard, typically found on the datasheet of the component. If users use different encoders when they build, the value can be overridden in their keymap. Add additional encoders as necessary by duplicating the above lines, replacing `left` with whatever you would like to call your encoder, and updating the pins. Note that support for peripheral (right) side sensors over BLE is still in progress. Once you have defined the encoder sensors, you will have to add them to the list of sensors: ```dts - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; + triggers-per-rotation = <20>; }; ``` In this example, a left_encoder and right_encoder are both added. Additional encoders can be added with spaces separating each, and the order they are added here determines the order in which you define their behavior in your keymap. +In addition, a default value for the number of times the sensors trigger the bound behavior per full rotation is set via the `triggers-per-rotation` property. See [Encoders Config](../config/encoders.md#devicetree) for more details. + Add the following lines to your overlay file(s) to enable the encoder: From 34c8b3f1e30725d2711d5a75cd2da8e60da7b995 Mon Sep 17 00:00:00 2001 From: Peter Johanson Date: Wed, 18 Oct 2023 02:33:03 +0000 Subject: [PATCH 39/49] refactor: Update boards/shields for encoders. * Update existing boards/shields for new `steps` and `triggers-per-rotation` set up. --- app/boards/arm/bdn9/bdn9_rev2.dts | 7 ++++--- app/boards/arm/bt60/bt60.dtsi | 5 +++-- app/boards/arm/ckp/ckp.dtsi | 9 +++++---- app/boards/shields/knob_goblin/knob_goblin.overlay | 7 ++++--- app/boards/shields/kyria/kyria_common.dtsi | 7 ++++--- app/boards/shields/lily58/lily58.dtsi | 5 +++-- app/boards/shields/lotus58/lotus58.dtsi | 7 ++++--- app/boards/shields/murphpad/murphpad.keymap | 1 + app/boards/shields/murphpad/murphpad.overlay | 4 ++-- app/boards/shields/nibble/nibble.keymap | 1 + app/boards/shields/nibble/nibble.overlay | 2 +- app/boards/shields/reviung5/reviung5.overlay | 5 +++-- app/boards/shields/romac_plus/romac_plus.dtsi | 5 +++-- app/boards/shields/snap/snap.dtsi | 4 ++-- app/boards/shields/snap/snap.keymap | 1 + app/boards/shields/sofle/sofle.dtsi | 7 ++++--- .../splitkb_aurora_corne/splitkb_aurora_corne.dtsi | 7 ++++--- .../splitkb_aurora_lily58/splitkb_aurora_lily58.dtsi | 7 ++++--- .../splitkb_aurora_sweep/splitkb_aurora_sweep.dtsi | 11 ++++++----- app/boards/shields/tidbit/tidbit.dtsi | 10 +++++----- app/boards/shields/tidbit/tidbit.keymap | 1 + app/boards/shields/zodiark/zodiark.dtsi | 7 ++++--- 22 files changed, 69 insertions(+), 51 deletions(-) diff --git a/app/boards/arm/bdn9/bdn9_rev2.dts b/app/boards/arm/bdn9/bdn9_rev2.dts index d2d1c65cb8f..1b85067ce13 100644 --- a/app/boards/arm/bdn9/bdn9_rev2.dts +++ b/app/boards/arm/bdn9/bdn9_rev2.dts @@ -43,7 +43,7 @@ label = "LEFT_ENCODER"; a-gpios = <&gpioa 4 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&gpioa 8 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; mid_encoder: encoder_mid { @@ -51,7 +51,7 @@ label = "MID_ENCODER"; a-gpios = <&gpioa 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&gpioa 10 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; right_encoder: encoder_right { @@ -59,7 +59,7 @@ label = "RIGHT_ENCODER"; a-gpios = <&gpioa 15 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&gpiob 3 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; @@ -67,6 +67,7 @@ compatible = "zmk,keymap-sensors"; status = "disabled"; sensors = <>; + triggers-per-rotation = <20>; }; }; diff --git a/app/boards/arm/bt60/bt60.dtsi b/app/boards/arm/bt60/bt60.dtsi index 6e4900af53c..8a270250957 100644 --- a/app/boards/arm/bt60/bt60.dtsi +++ b/app/boards/arm/bt60/bt60.dtsi @@ -22,9 +22,10 @@ zmk,matrix_transform = &default_transform; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder>; + triggers-per-rotation = <20>; }; @@ -34,7 +35,7 @@ label = "LEFT_ENCODER"; a-gpios = <&gpio1 1 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&gpio1 3 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "okay"; }; diff --git a/app/boards/arm/ckp/ckp.dtsi b/app/boards/arm/ckp/ckp.dtsi index 6c52d620367..ec2bd6a87b6 100644 --- a/app/boards/arm/ckp/ckp.dtsi +++ b/app/boards/arm/ckp/ckp.dtsi @@ -26,9 +26,10 @@ zmk,battery = &vbatt; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&encoder_1>; + triggers-per-rotation = <20>; }; kscan0: kscan_0 { @@ -76,7 +77,7 @@ label = "ENCODER_ONE"; a-gpios = <&gpio0 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&gpio0 19 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "okay"; }; @@ -85,7 +86,7 @@ label = "ENCODER_TWO"; a-gpios = <&gpio0 26 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&gpio0 6 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "okay"; }; @@ -94,7 +95,7 @@ label = "encoder_3"; a-gpios = <&gpio0 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&gpio0 10 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "okay"; }; diff --git a/app/boards/shields/knob_goblin/knob_goblin.overlay b/app/boards/shields/knob_goblin/knob_goblin.overlay index d3ba8c5be79..03051ce5820 100644 --- a/app/boards/shields/knob_goblin/knob_goblin.overlay +++ b/app/boards/shields/knob_goblin/knob_goblin.overlay @@ -39,7 +39,7 @@ label = "TOP_ENCODER"; a-gpios = <&pro_micro 19 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; b-gpios = <&pro_micro 18 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "okay"; }; @@ -48,13 +48,14 @@ label = "BOTTOM_ENCODER"; a-gpios = <&pro_micro 20 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; b-gpios = <&pro_micro 21 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "okay"; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&top_encoder &bottom_encoder>; + triggers-per-rotation = <20>; }; }; diff --git a/app/boards/shields/kyria/kyria_common.dtsi b/app/boards/shields/kyria/kyria_common.dtsi index 2e10cd37df4..1056794d584 100644 --- a/app/boards/shields/kyria/kyria_common.dtsi +++ b/app/boards/shields/kyria/kyria_common.dtsi @@ -23,20 +23,21 @@ left_encoder: encoder_left { compatible = "alps,ec11"; label = "LEFT_ENCODER"; - resolution = <4>; + steps = <80>; status = "disabled"; }; right_encoder: encoder_right { compatible = "alps,ec11"; label = "RIGHT_ENCODER"; - resolution = <4>; + steps = <80>; status = "disabled"; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; + triggers-per-rotation = <20>; }; // TODO: RGB node(s) diff --git a/app/boards/shields/lily58/lily58.dtsi b/app/boards/shields/lily58/lily58.dtsi index ec520f6bb8a..eb427a581ba 100644 --- a/app/boards/shields/lily58/lily58.dtsi +++ b/app/boards/shields/lily58/lily58.dtsi @@ -51,12 +51,13 @@ RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(4,5) RC(4,6) RC(3,6) RC(3,7) label = "LEFT_ENCODER"; a-gpios = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder>; + triggers-per-rotation = <20>; }; }; diff --git a/app/boards/shields/lotus58/lotus58.dtsi b/app/boards/shields/lotus58/lotus58.dtsi index e24d75e70d4..8b1c66f98df 100644 --- a/app/boards/shields/lotus58/lotus58.dtsi +++ b/app/boards/shields/lotus58/lotus58.dtsi @@ -50,7 +50,7 @@ RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(4,5) RC(4,6) RC(3,6) RC(3,7 label = "LEFT_ENCODER"; a-gpios = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; @@ -59,13 +59,14 @@ RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(4,5) RC(4,6) RC(3,6) RC(3,7 label = "RIGHT_ENCODER"; a-gpios = <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; + triggers-per-rotation = <20>; }; }; diff --git a/app/boards/shields/murphpad/murphpad.keymap b/app/boards/shields/murphpad/murphpad.keymap index ccdff9f7a71..74a852a8719 100644 --- a/app/boards/shields/murphpad/murphpad.keymap +++ b/app/boards/shields/murphpad/murphpad.keymap @@ -48,6 +48,7 @@ sensors { compatible = "zmk,keymap-sensors"; sensors = <&encoder_1 &encoder_2>; + triggers-per-rotation = <20>; }; diff --git a/app/boards/shields/murphpad/murphpad.overlay b/app/boards/shields/murphpad/murphpad.overlay index 13905092e64..f175c55e1c0 100644 --- a/app/boards/shields/murphpad/murphpad.overlay +++ b/app/boards/shields/murphpad/murphpad.overlay @@ -39,7 +39,7 @@ label = "Encoder 1"; a-gpios = <&pro_micro 8 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; b-gpios = <&pro_micro 7 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; @@ -48,7 +48,7 @@ label = "Encoder 2"; a-gpios = <&pro_micro 1 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; b-gpios = <&pro_micro 0 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; diff --git a/app/boards/shields/nibble/nibble.keymap b/app/boards/shields/nibble/nibble.keymap index 5b90f6c19ae..4cb6f5b021a 100644 --- a/app/boards/shields/nibble/nibble.keymap +++ b/app/boards/shields/nibble/nibble.keymap @@ -12,6 +12,7 @@ sensors { compatible = "zmk,keymap-sensors"; sensors = <&encoder_1>; + triggers-per-rotation = <20>; }; keymap { diff --git a/app/boards/shields/nibble/nibble.overlay b/app/boards/shields/nibble/nibble.overlay index cd1176401b7..43be6c766fc 100644 --- a/app/boards/shields/nibble/nibble.overlay +++ b/app/boards/shields/nibble/nibble.overlay @@ -18,7 +18,7 @@ label = "Encoder 1"; a-gpios = <&pro_micro 9 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; b-gpios = <&pro_micro 8 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "okay"; }; diff --git a/app/boards/shields/reviung5/reviung5.overlay b/app/boards/shields/reviung5/reviung5.overlay index 24b0f582b0e..8b885245485 100644 --- a/app/boards/shields/reviung5/reviung5.overlay +++ b/app/boards/shields/reviung5/reviung5.overlay @@ -43,12 +43,13 @@ label = "encoder"; a-gpios = <&pro_micro 3 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 2 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "okay"; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&encoder>; + triggers-per-rotation = <20>; }; }; \ No newline at end of file diff --git a/app/boards/shields/romac_plus/romac_plus.dtsi b/app/boards/shields/romac_plus/romac_plus.dtsi index 71ec87b2b94..5324174b54f 100644 --- a/app/boards/shields/romac_plus/romac_plus.dtsi +++ b/app/boards/shields/romac_plus/romac_plus.dtsi @@ -43,13 +43,14 @@ RC(3,0) RC(3,1) RC(3,2) label = "LEFT_ENCODER"; a-gpios = <&pro_micro 16 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 14 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder>; + triggers-per-rotation = <20>; }; // TODO: per-key RGB node(s)? diff --git a/app/boards/shields/snap/snap.dtsi b/app/boards/shields/snap/snap.dtsi index a374ad1768b..7523f35b06c 100644 --- a/app/boards/shields/snap/snap.dtsi +++ b/app/boards/shields/snap/snap.dtsi @@ -16,14 +16,14 @@ left_encoder: encoder_left { compatible = "alps,ec11"; label = "LEFT_ENCODER"; - resolution = <4>; + steps = <80>; status = "disabled"; }; right_encoder: encoder_right { compatible = "alps,ec11"; label = "RIGHT_ENCODER"; - resolution = <4>; + steps = <80>; status = "disabled"; }; diff --git a/app/boards/shields/snap/snap.keymap b/app/boards/shields/snap/snap.keymap index 7c750f9f14c..febaff97bba 100644 --- a/app/boards/shields/snap/snap.keymap +++ b/app/boards/shields/snap/snap.keymap @@ -13,6 +13,7 @@ sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; + triggers-per-rotation = <20>; }; keymap { diff --git a/app/boards/shields/sofle/sofle.dtsi b/app/boards/shields/sofle/sofle.dtsi index 71dc04d87a1..4917ca321ed 100644 --- a/app/boards/shields/sofle/sofle.dtsi +++ b/app/boards/shields/sofle/sofle.dtsi @@ -50,7 +50,7 @@ RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(4,5) RC(4,6) RC(3,6) RC(3,7) label = "LEFT_ENCODER"; a-gpios = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; @@ -59,13 +59,14 @@ RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(4,5) RC(4,6) RC(3,6) RC(3,7) label = "RIGHT_ENCODER"; a-gpios = <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; + triggers-per-rotation = <20>; }; }; diff --git a/app/boards/shields/splitkb_aurora_corne/splitkb_aurora_corne.dtsi b/app/boards/shields/splitkb_aurora_corne/splitkb_aurora_corne.dtsi index a1b7b64382c..3eefdc6aeed 100644 --- a/app/boards/shields/splitkb_aurora_corne/splitkb_aurora_corne.dtsi +++ b/app/boards/shields/splitkb_aurora_corne/splitkb_aurora_corne.dtsi @@ -48,7 +48,7 @@ RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,6) RC(2,7) RC(2,8) RC(2,9) RC(2,10 left_encoder: left_encoder { compatible = "alps,ec11"; label = "L_ENCODER"; - resolution = <4>; + steps = <80>; status = "disabled"; a-gpios = <&pro_micro 4 GPIO_PULL_UP>; @@ -58,16 +58,17 @@ RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,6) RC(2,7) RC(2,8) RC(2,9) RC(2,10 right_encoder: right_encoder { compatible = "alps,ec11"; label = "R_ENCODER"; - resolution = <4>; + steps = <80>; status = "disabled"; a-gpios = <&pro_micro 19 GPIO_PULL_UP>; b-gpios = <&pro_micro 18 GPIO_PULL_UP>; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; + triggers-per-rotation = <20>; }; }; diff --git a/app/boards/shields/splitkb_aurora_lily58/splitkb_aurora_lily58.dtsi b/app/boards/shields/splitkb_aurora_lily58/splitkb_aurora_lily58.dtsi index 908356c757b..06b3ef3955c 100644 --- a/app/boards/shields/splitkb_aurora_lily58/splitkb_aurora_lily58.dtsi +++ b/app/boards/shields/splitkb_aurora_lily58/splitkb_aurora_lily58.dtsi @@ -34,7 +34,7 @@ RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(4,1) RC(4,10) RC(3,6) RC(3,7) left_encoder: left_encoder { compatible = "alps,ec11"; label = "L_ENCODER"; - resolution = <4>; + steps = <80>; status = "disabled"; a-gpios = <&pro_micro 5 GPIO_PULL_UP>; @@ -44,16 +44,17 @@ RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(4,1) RC(4,10) RC(3,6) RC(3,7) right_encoder: right_encoder { compatible = "alps,ec11"; label = "R_ENCODER"; - resolution = <4>; + steps = <80>; status = "disabled"; a-gpios = <&pro_micro 18 GPIO_PULL_UP>; b-gpios = <&pro_micro 19 GPIO_PULL_UP>; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; + triggers-per-rotation = <20>; }; }; diff --git a/app/boards/shields/splitkb_aurora_sweep/splitkb_aurora_sweep.dtsi b/app/boards/shields/splitkb_aurora_sweep/splitkb_aurora_sweep.dtsi index ab568a0986d..c5483af5476 100644 --- a/app/boards/shields/splitkb_aurora_sweep/splitkb_aurora_sweep.dtsi +++ b/app/boards/shields/splitkb_aurora_sweep/splitkb_aurora_sweep.dtsi @@ -28,34 +28,35 @@ left_encoder1: left_encoder1 { compatible = "alps,ec11"; label = "L_ENCODER1"; - resolution = <4>; + steps = <80>; status = "disabled"; }; left_encoder2: left_encoder2 { compatible = "alps,ec11"; label = "L_ENCODER2"; - resolution = <4>; + steps = <80>; status = "disabled"; }; right_encoder1: right_encoder1 { compatible = "alps,ec11"; label = "R_ENCODER1"; - resolution = <4>; + steps = <80>; status = "disabled"; }; right_encoder2: right_encoder2 { compatible = "alps,ec11"; label = "R_ENCODER2"; - resolution = <4>; + steps = <80>; status = "disabled"; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder1 &right_encoder1>; + triggers-per-rotation = <20>; }; }; diff --git a/app/boards/shields/tidbit/tidbit.dtsi b/app/boards/shields/tidbit/tidbit.dtsi index fb84c89ed41..c7af200137d 100644 --- a/app/boards/shields/tidbit/tidbit.dtsi +++ b/app/boards/shields/tidbit/tidbit.dtsi @@ -49,7 +49,7 @@ label = "Top Row Encoder"; a-gpios = <&pro_micro 16 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; b-gpios = <&pro_micro 14 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; @@ -58,7 +58,7 @@ label = "Encoder 1"; a-gpios = <&pro_micro 14 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; b-gpios = <&pro_micro 16 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; @@ -67,7 +67,7 @@ label = "Encoder 2"; a-gpios = <&pro_micro 8 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; b-gpios = <&pro_micro 9 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; @@ -76,7 +76,7 @@ label = "Encoder 3"; a-gpios = <&pro_micro 3 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; b-gpios = <&pro_micro 2 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; @@ -85,7 +85,7 @@ label = "Encoder 4"; a-gpios = <&pro_micro 1 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; b-gpios = <&pro_micro 0 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; diff --git a/app/boards/shields/tidbit/tidbit.keymap b/app/boards/shields/tidbit/tidbit.keymap index 2e415bf8f4d..a98a2eaa28e 100644 --- a/app/boards/shields/tidbit/tidbit.keymap +++ b/app/boards/shields/tidbit/tidbit.keymap @@ -17,6 +17,7 @@ sensors { compatible = "zmk,keymap-sensors"; sensors = <&encoder_1_top_row>; + triggers-per-rotation = <20>; }; keymap { diff --git a/app/boards/shields/zodiark/zodiark.dtsi b/app/boards/shields/zodiark/zodiark.dtsi index cda0b1a6dae..66ebb7b47f9 100644 --- a/app/boards/shields/zodiark/zodiark.dtsi +++ b/app/boards/shields/zodiark/zodiark.dtsi @@ -50,7 +50,7 @@ RC(4,0) RC(4,1) RC(4,2) RC(4,3) RC(4,4) RC(4,5) RC(4,6) RC(4,7) R label = "LEFT_ENCODER"; a-gpios = <&pro_micro 2 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 4 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; @@ -59,13 +59,14 @@ RC(4,0) RC(4,1) RC(4,2) RC(4,3) RC(4,4) RC(4,5) RC(4,6) RC(4,7) R label = "RIGHT_ENCODER"; a-gpios = <&pro_micro 4 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <&pro_micro 2 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; - resolution = <4>; + steps = <80>; status = "disabled"; }; - sensors { + sensors: sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; + triggers-per-rotation = <20>; }; }; From c2d220fbdf2c9ae6957d080a47545aa28e3ec275 Mon Sep 17 00:00:00 2001 From: Peter Johanson Date: Wed, 8 Nov 2023 22:05:53 +0000 Subject: [PATCH 40/49] refactor(hid): Use proper defines for HID values. * Add report ID defines and use them consistently. * Add defines for main item value flags to avoid magic constants. --- app/include/zmk/hid.h | 51 ++++++++++++++++++++++++++++++++----------- app/src/hid.c | 5 +++-- app/src/hog.c | 4 ++-- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index ab42adaa13e..da6bfa65a68 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -15,13 +15,43 @@ #define ZMK_HID_KEYBOARD_NKRO_MAX_USAGE HID_USAGE_KEY_KEYPAD_EQUAL -#define COLLECTION_REPORT 0x03 +// See https://www.usb.org/sites/default/files/hid1_11.pdf section 6.2.2.4 Main Items + +#define ZMK_HID_MAIN_VAL_DATA (0x00 << 0) +#define ZMK_HID_MAIN_VAL_CONST (0x01 << 0) + +#define ZMK_HID_MAIN_VAL_ARRAY (0x00 << 1) +#define ZMK_HID_MAIN_VAL_VAR (0x01 << 1) + +#define ZMK_HID_MAIN_VAL_ABS (0x00 << 2) +#define ZMK_HID_MAIN_VAL_REL (0x01 << 2) + +#define ZMK_HID_MAIN_VAL_NO_WRAP (0x00 << 3) +#define ZMK_HID_MAIN_VAL_WRAP (0x01 << 3) + +#define ZMK_HID_MAIN_VAL_LIN (0x00 << 4) +#define ZMK_HID_MAIN_VAL_NON_LIN (0x01 << 4) + +#define ZMK_HID_MAIN_VAL_PREFERRED (0x00 << 5) +#define ZMK_HID_MAIN_VAL_NO_PREFERRED (0x01 << 5) + +#define ZMK_HID_MAIN_VAL_NO_NULL (0x00 << 6) +#define ZMK_HID_MAIN_VAL_NULL (0x01 << 6) + +#define ZMK_HID_MAIN_VAL_NON_VOL (0x00 << 7) +#define ZMK_HID_MAIN_VAL_VOL (0x01 << 7) + +#define ZMK_HID_MAIN_VAL_BIT_FIELD (0x00 << 8) +#define ZMK_HID_MAIN_VAL_BUFFERED_BYTES (0x01 << 8) + +#define ZMK_HID_REPORT_ID_KEYBOARD 0x01 +#define ZMK_HID_REPORT_ID_CONSUMER 0x02 static const uint8_t zmk_hid_report_desc[] = { HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), HID_USAGE(HID_USAGE_GD_KEYBOARD), HID_COLLECTION(HID_COLLECTION_APPLICATION), - HID_REPORT_ID(0x01), + HID_REPORT_ID(ZMK_HID_REPORT_ID_KEYBOARD), HID_USAGE_PAGE(HID_USAGE_KEY), HID_USAGE_MIN8(HID_USAGE_KEY_KEYBOARD_LEFTCONTROL), HID_USAGE_MAX8(HID_USAGE_KEY_KEYBOARD_RIGHT_GUI), @@ -30,14 +60,12 @@ static const uint8_t zmk_hid_report_desc[] = { HID_REPORT_SIZE(0x01), HID_REPORT_COUNT(0x08), - /* INPUT (Data,Var,Abs) */ - HID_INPUT(0x02), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), HID_USAGE_PAGE(HID_USAGE_KEY), HID_REPORT_SIZE(0x08), HID_REPORT_COUNT(0x01), - /* INPUT (Cnst,Var,Abs) */ - HID_INPUT(0x03), + HID_INPUT(ZMK_HID_MAIN_VAL_CONST | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), HID_USAGE_PAGE(HID_USAGE_KEY), @@ -48,8 +76,7 @@ static const uint8_t zmk_hid_report_desc[] = { HID_USAGE_MAX8(ZMK_HID_KEYBOARD_NKRO_MAX_USAGE), HID_REPORT_SIZE(0x01), HID_REPORT_COUNT(ZMK_HID_KEYBOARD_NKRO_MAX_USAGE + 1), - /* INPUT (Data,Ary,Abs) */ - HID_INPUT(0x02), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), #elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) HID_LOGICAL_MIN8(0x00), HID_LOGICAL_MAX16(0xFF, 0x00), @@ -57,8 +84,7 @@ static const uint8_t zmk_hid_report_desc[] = { HID_USAGE_MAX8(0xFF), HID_REPORT_SIZE(0x08), HID_REPORT_COUNT(CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE), - /* INPUT (Data,Ary,Abs) */ - HID_INPUT(0x00), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_ARRAY | ZMK_HID_MAIN_VAL_ABS), #else #error "A proper HID report type must be selected" #endif @@ -67,7 +93,7 @@ static const uint8_t zmk_hid_report_desc[] = { HID_USAGE_PAGE(HID_USAGE_CONSUMER), HID_USAGE(HID_USAGE_CONSUMER_CONSUMER_CONTROL), HID_COLLECTION(HID_COLLECTION_APPLICATION), - HID_REPORT_ID(0x02), + HID_REPORT_ID(ZMK_HID_REPORT_ID_CONSUMER), HID_USAGE_PAGE(HID_USAGE_CONSUMER), #if IS_ENABLED(CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC) @@ -86,8 +112,7 @@ static const uint8_t zmk_hid_report_desc[] = { #error "A proper consumer HID report usage range must be selected" #endif HID_REPORT_COUNT(CONFIG_ZMK_HID_CONSUMER_REPORT_SIZE), - /* INPUT (Data,Ary,Abs) */ - HID_INPUT(0x00), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_ARRAY | ZMK_HID_MAIN_VAL_ABS), HID_END_COLLECTION, }; diff --git a/app/src/hid.c b/app/src/hid.c index 2a6b5d39da2..58e5824d2e4 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -12,9 +12,10 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include static struct zmk_hid_keyboard_report keyboard_report = { - .report_id = 1, .body = {.modifiers = 0, ._reserved = 0, .keys = {0}}}; + .report_id = ZMK_HID_REPORT_ID_KEYBOARD, .body = {.modifiers = 0, ._reserved = 0, .keys = {0}}}; -static struct zmk_hid_consumer_report consumer_report = {.report_id = 2, .body = {.keys = {0}}}; +static struct zmk_hid_consumer_report consumer_report = {.report_id = ZMK_HID_REPORT_ID_CONSUMER, + .body = {.keys = {0}}}; // Keep track of how often a modifier was pressed. // Only release the modifier if the count is 0. diff --git a/app/src/hog.c b/app/src/hog.c index 930714b092a..9ccfd9d2c0e 100644 --- a/app/src/hog.c +++ b/app/src/hog.c @@ -47,12 +47,12 @@ enum { }; static struct hids_report input = { - .id = 0x01, + .id = ZMK_HID_REPORT_ID_KEYBOARD, .type = HIDS_INPUT, }; static struct hids_report consumer_input = { - .id = 0x02, + .id = ZMK_HID_REPORT_ID_CONSUMER, .type = HIDS_INPUT, }; From 5b49bc10cd7251bb9a90c018410abfc557fa0e9c Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Fri, 3 Nov 2023 23:20:42 -0700 Subject: [PATCH 41/49] feat(blog): Show all posts on sidebar --- docs/docusaurus.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 701b5b997bb..20e6a75c90f 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -146,6 +146,7 @@ module.exports = { showReadingTime: true, // Please change this to your repo. editUrl: "https://github.com/zmkfirmware/zmk/edit/main/docs/", + blogSidebarCount: "ALL", }, theme: { customCss: [ From b80c0be0cedf42b58be2fc431b2a673ab9760c64 Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Fri, 3 Nov 2023 23:20:23 -0700 Subject: [PATCH 42/49] feat(blog): Add keymap editor post for spotlight series Co-authored-by: Nick Coutsos --- docs/blog/2023-11-09-keymap-editor.md | 109 ++++++++++++++++++ .../editor-screenshot-dark.png | Bin 0 -> 74997 bytes .../editor-screenshot-light.png | Bin 0 -> 75097 bytes 3 files changed, 109 insertions(+) create mode 100644 docs/blog/2023-11-09-keymap-editor.md create mode 100644 docs/blog/assets/2023-11-09-keymap-editor/editor-screenshot-dark.png create mode 100644 docs/blog/assets/2023-11-09-keymap-editor/editor-screenshot-light.png diff --git a/docs/blog/2023-11-09-keymap-editor.md b/docs/blog/2023-11-09-keymap-editor.md new file mode 100644 index 00000000000..1fe3b86772d --- /dev/null +++ b/docs/blog/2023-11-09-keymap-editor.md @@ -0,0 +1,109 @@ +--- +title: "Community Spotlight Series #1: Keymap Editor" +author: Cem Aksoylar +author_title: Documentation maintainer +author_url: https://github.com/caksoylar +author_image_url: https://avatars.githubusercontent.com/u/7876996 +tags: [keyboards, firmware, community] +--- + +import ThemedImage from '@theme/ThemedImage'; + + + +This blog post is the first in a series of posts where we highlight projects within the ZMK ecosystem that we think are cool and that the users might benefit from knowing about them. We are starting the series with a big one, [Keymap Editor] by [Nick Coutsos](https://github.com/nickcoutsos)! + +In the rest of the post we leave it to Nick himself to introduce the project, detail his goals and motivation in developing such a tool, and talk about the future of the project. Stay tuned for future installments in the series! + +## What is Keymap Editor? + +_[Keymap Editor]_ is a web based graphical editor for ZMK keymaps. It provides a visual way to manage the contents of your keymap and if nothing else offers two critical features: + +1. Automatic formatting of the keymap file, so that bindings arrays remain readable +2. Searchable behaviors, keycodes, commands, etc, so you won't have to remember if it's `LCTL` or `LCTRL` (I just had to double check myself and I guessed wrong, apparently) + +## What can Keymap Editor do? + +- Render [devicetree keymaps](/docs/features/keymaps) using pre-defined, auto-generated, or side-loadable keyboard layouts +- Integrate with a GitHub repo to streamline firmware builds, or FileSystem/Clipboard if you'd still rather build locally +- Edit [combos](/docs/features/combos), [behaviors](/docs/behaviors/key-press), [macros](/docs/behaviors/macros), [conditional layers](/docs/features/conditional-layers) and [rotary encoder bindings](/docs/behaviors/sensor-rotate) +- Manage references: moving a layer or renaming a behavior will look for references throughout your keymap and update them. + +But check back regularly, because I update pretty often. A recent significant achievement was enabling [parameterized macros](/docs/behaviors/macros#parameterized-macros) and tying it in with my existing parameter type resolution so, yeah, you can finally create that reusable macro combining bluetooth profile selection with RGB backlight colour. Or use it for an actual useful thing, even. _(See also: [Using Parameterized Macros in Keymap Editor](https://github.com/nickcoutsos/keymap-editor/wiki/Using-Parameterized-Macros-in-Keymap-Editor))_ + +My goals are, broadly: + +- **Treat code as a first-class entity:** as long as ZMK keymaps are described in devicetree code then an editor needs to produce readable devicetree code. +- **Flexibly support ZMK features:** use of any ZMK keymap feature should theoretically be achievable within the app. In some cases this can mean more initial setup _(See also: [my thoughts on implementing "autoshift"](https://github.com/nickcoutsos/keymap-editor/wiki/Autoshift-using-ZMK-behaviors))_ but having that foundation makes its easier to add shortcuts and niceties — something I do quite often now. +- **Don't get in the way of not-yet-supported features:** If a new ZMK feature is released and the app isn't able to add it natively, you can always edit your keymap file directly. While the app may not _recognize_ the new features, further changes through the app should not break your keymap. + +## History of Keymap Editor + +When I started writing Keymap Editor I had a handwired Dactyl variant running QMK. Manually editing keymap code was fine, but keeping things readable was important to me, and automating that was the best way to ensure consistency. Programmatically modifying source code was beyond me at the time so the first version persisted keymap data in JSON and spat out formatted versions of both the JSON and C keymaps. + +After switching to ZMK I added a few more features, I guess as a pandemic project, and then gradually migrated from generating a templated keymap file to manipulating devicetree syntax directly, and that has made a big difference in adding new ZMK features. + +## Why am I doing this? + +It started out as a useful tool for me. I shared it with the ZMK community and gained a little traction, and then apparently quite a bit of traction — turns out it's useful for a lot of people. + +I'm a software developer because I enjoy building things. Much of my day-to-day work isn't user facing, so seeing how helpful the keymap editor has been for people in the ZMK community is a big motivator to keep improving it. + +## Future plans + +### Runtime updates + +Streamlining the keymap update process is probably top of mind for most users, but that involves a really big _firmware_ feature, and I'm the wrong person to tackle it. + +That said, once there's a protocol I would _absolutely_ be down to integrate it as an additional keymap source. Being able to pull data directly from the keyboard should unlock a lot of possibilities and ease some of the constraints imposed by using devicetree code as a medium. + +### Simplifying behavior use + +I think a lot of people would like to see the concept of behaviors abstracted away for new users and to prompt them with + +- _"When the key is tapped..."_, +- _"When the key is held..."_, +- _"When the key is double-tapped..."_ and so on. + +Users who are less familiar with ZMK's behaviors and how they are composed may find these prompts to be more intuitive, and their answers could be mapped to an appropriate combination of behaviors managed internally by an editor. + +### Uh, what else? + +This has been long enough already, if you're looking for a feature I haven't mentioned don't assume I won't add it. Feel free to make feature requests on the GitHub repo, and I'd be happy to discuss it! + +## About Me And My Keebs + +I like computers and write software. Many in this field enjoy using mechanical +keyboards for their feel or aesthetics, but what piqued my interest was the +Dactyl keyboard. I think, ergonomics aside, I'm more interested in the DIY/maker +aspect than the collecting of keyboards and switches. + +So [I made a Dactyl](https://github.com/nickcoutsos/dactyl-flatpacked/), and +then [I made another Dactyl](https://github.com/nickcoutsos/dactyl-deskmount/) +and I made a third Dactyl that isn't interesting enough to photograph, but now +I'm using ZMK so I left room for 18650 cells. + +That last Dactyl (with MX browns and a cheap blank XDA keycap set) serves me +well the eight or so hours a day I'll spend at my desk, but I also spend a good +deal of time computing on my couch where I'll use... my Macbook's built-in +keyboard. + +In case that's not surprising enough I'll leave you with this: despite all of +the work and testing I've put into the keymap editor project, I've only updated +an actual keymap once in the last year. + +Thank you and good night. + +## More information + +- [Keymap Editor Wiki](https://github.com/nickcoutsos/keymap-editor/wiki) +- [Keymap Editor Discussions](https://github.com/nickcoutsos/keymap-editor/discussions) +- (YouTube video) [Ben Frain's overview of the Keymap Editor](https://www.youtube.com/watch?v=Vy7IoQAe3oU) + +[Keymap Editor]: http://nickcoutsos.github.io/keymap-editor diff --git a/docs/blog/assets/2023-11-09-keymap-editor/editor-screenshot-dark.png b/docs/blog/assets/2023-11-09-keymap-editor/editor-screenshot-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..166edf8ae806520abd95ab0eb7889e3b64fcc1c1 GIT binary patch literal 74997 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
~y{` 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

n{U;|*<63&-Enc$9Bc{ia~fEJN>9 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 literal 0 HcmV?d00001 diff --git a/docs/blog/assets/2023-11-09-keymap-editor/editor-screenshot-light.png b/docs/blog/assets/2023-11-09-keymap-editor/editor-screenshot-light.png new file mode 100644 index 0000000000000000000000000000000000000000..cdaae5c799da8f63a5a4d1af865e63441c949049 GIT binary patch literal 75097 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 +knUbIx+ui8%&y{z+C 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 I^qph>14ln7O8@`> literal 0 HcmV?d00001 From 91aa3378f32f5688329a663fdaba4772e069d548 Mon Sep 17 00:00:00 2001 From: Chris Andreae Date: Tue, 14 Nov 2023 03:04:04 +0900 Subject: [PATCH 43/49] feat(usb): Add boot protocol support * USB boot protocol support * Use a single definition of a boot report, used for regular reports in non-6KRO, and for rollover in all branches. * Handle gaps in the zmk report when producing a boot report in HKRO mode. For .example, if it was 8KRO, it would be possible to have the state 0 0 0 0 0 0 0 17 (by pressing 8 keys, and letting go of the first 7). Copying the first 6 bytes would not show up the single pressed key. * Disable usb status change and callback on SOF events: SOF events were introduced by the boot protocol changes, and required internally by Zephyr's idle support, but are unused within ZMK itself. Ignore them in the usb status callback. --------- Co-authored-by: Andrew Childs --- app/Kconfig | 6 ++- app/include/zmk/hid.h | 28 +++++++++--- app/include/zmk/usb_hid.h | 6 ++- app/src/endpoints.c | 10 ++--- app/src/hid.c | 92 +++++++++++++++++++++++++++++++++++++++ app/src/usb.c | 14 ++++++ app/src/usb_hid.c | 88 ++++++++++++++++++++++++++++++++++++- 7 files changed, 229 insertions(+), 15 deletions(-) diff --git a/app/Kconfig b/app/Kconfig index 0dd9316a2a7..798cb7855d5 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -103,6 +103,11 @@ config USB_NUMOF_EP_WRITE_RETRIES config USB_HID_POLL_INTERVAL_MS default 1 +config ZMK_USB_BOOT + bool "USB Boot Protocol Support" + default y + select USB_HID_BOOT_PROTOCOL + select USB_DEVICE_SOF #ZMK_USB endif @@ -575,4 +580,3 @@ osource "$(ZMK_CONFIG)/boards/shields/*/Kconfig.shield" source "Kconfig.zephyr" - diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index da6bfa65a68..afe9210199f 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -116,12 +116,24 @@ static const uint8_t zmk_hid_report_desc[] = { HID_END_COLLECTION, }; -// struct zmk_hid_boot_report -// { -// uint8_t modifiers; -// uint8_t _unused; -// uint8_t keys[6]; -// } __packed; +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + +#define HID_ERROR_ROLLOVER 0x1 +#define HID_BOOT_KEY_LEN 6 + +#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO) && \ + CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE == HID_BOOT_KEY_LEN +typedef struct zmk_hid_keyboard_report_body zmk_hid_boot_report_t; +#else +struct zmk_hid_boot_report { + zmk_mod_flags_t modifiers; + uint8_t _reserved; + uint8_t keys[HID_BOOT_KEY_LEN]; +} __packed; + +typedef struct zmk_hid_boot_report zmk_hid_boot_report_t; +#endif +#endif struct zmk_hid_keyboard_report_body { zmk_mod_flags_t modifiers; @@ -179,3 +191,7 @@ bool zmk_hid_is_pressed(uint32_t usage); struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report(); struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(); + +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) +zmk_hid_boot_report_t *zmk_hid_get_boot_report(); +#endif diff --git a/app/include/zmk/usb_hid.h b/app/include/zmk/usb_hid.h index 1748835eef2..777f2b48a03 100644 --- a/app/include/zmk/usb_hid.h +++ b/app/include/zmk/usb_hid.h @@ -6,4 +6,8 @@ #pragma once -int zmk_usb_hid_send_report(const uint8_t *report, size_t len); \ No newline at end of file +#include + +int zmk_usb_hid_send_keyboard_report(); +int zmk_usb_hid_send_consumer_report(); +void zmk_usb_hid_set_protocol(uint8_t protocol); diff --git a/app/src/endpoints.c b/app/src/endpoints.c index 138e790fe72..10357d4e02d 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -121,12 +121,10 @@ struct zmk_endpoint_instance zmk_endpoints_selected(void) { } static int send_keyboard_report(void) { - struct zmk_hid_keyboard_report *keyboard_report = zmk_hid_get_keyboard_report(); - switch (current_instance.transport) { #if IS_ENABLED(CONFIG_ZMK_USB) case ZMK_TRANSPORT_USB: { - int err = zmk_usb_hid_send_report((uint8_t *)keyboard_report, sizeof(*keyboard_report)); + int err = zmk_usb_hid_send_keyboard_report(); if (err) { LOG_ERR("FAILED TO SEND OVER USB: %d", err); } @@ -136,6 +134,7 @@ static int send_keyboard_report(void) { #if IS_ENABLED(CONFIG_ZMK_BLE) case ZMK_TRANSPORT_BLE: { + struct zmk_hid_keyboard_report *keyboard_report = zmk_hid_get_keyboard_report(); int err = zmk_hog_send_keyboard_report(&keyboard_report->body); if (err) { LOG_ERR("FAILED TO SEND OVER HOG: %d", err); @@ -150,12 +149,10 @@ static int send_keyboard_report(void) { } static int send_consumer_report(void) { - struct zmk_hid_consumer_report *consumer_report = zmk_hid_get_consumer_report(); - switch (current_instance.transport) { #if IS_ENABLED(CONFIG_ZMK_USB) case ZMK_TRANSPORT_USB: { - int err = zmk_usb_hid_send_report((uint8_t *)consumer_report, sizeof(*consumer_report)); + int err = zmk_usb_hid_send_consumer_report(); if (err) { LOG_ERR("FAILED TO SEND OVER USB: %d", err); } @@ -165,6 +162,7 @@ static int send_consumer_report(void) { #if IS_ENABLED(CONFIG_ZMK_BLE) case ZMK_TRANSPORT_BLE: { + struct zmk_hid_consumer_report *consumer_report = zmk_hid_get_consumer_report(); int err = zmk_hog_send_consumer_report(&consumer_report->body); if (err) { LOG_ERR("FAILED TO SEND OVER HOG: %d", err); diff --git a/app/src/hid.c b/app/src/hid.c index 58e5824d2e4..689a2361e13 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -17,6 +17,13 @@ static struct zmk_hid_keyboard_report keyboard_report = { static struct zmk_hid_consumer_report consumer_report = {.report_id = ZMK_HID_REPORT_ID_CONSUMER, .body = {.keys = {0}}}; +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + +static zmk_hid_boot_report_t boot_report = {.modifiers = 0, ._reserved = 0, .keys = {0}}; +static uint8_t keys_held = 0; + +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + // Keep track of how often a modifier was pressed. // Only release the modifier if the count is 0. static int explicit_modifier_counts[8] = {0, 0, 0, 0, 0, 0, 0, 0}; @@ -85,15 +92,58 @@ int zmk_hid_unregister_mods(zmk_mod_flags_t modifiers) { return ret; } +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + +static zmk_hid_boot_report_t *boot_report_rollover(uint8_t modifiers) { + boot_report.modifiers = modifiers; + for (int i = 0; i < HID_BOOT_KEY_LEN; i++) { + boot_report.keys[i] = HID_ERROR_ROLLOVER; + } + return &boot_report; +} + +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + #if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO) #define TOGGLE_KEYBOARD(code, val) WRITE_BIT(keyboard_report.body.keys[code / 8], code % 8, val) +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) +zmk_hid_boot_report_t *zmk_hid_get_boot_report() { + if (keys_held > HID_BOOT_KEY_LEN) { + return boot_report_rollover(keyboard_report.body.modifiers); + } + + boot_report.modifiers = keyboard_report.body.modifiers; + memset(&boot_report.keys, 0, HID_BOOT_KEY_LEN); + int ix = 0; + uint8_t base_code = 0; + for (int i = 0; i < (ZMK_HID_KEYBOARD_NKRO_MAX_USAGE + 1) / 8; ++i) { + if (ix == keys_held) { + break; + } + if (!keyboard_report.body.keys[i]) { + continue; + } + base_code = i * 8; + for (int j = 0; j < 8; ++j) { + if (keyboard_report.body.keys[i] & BIT(j)) { + boot_report.keys[ix++] = base_code + j; + } + } + } + return &boot_report; +} +#endif + static inline int select_keyboard_usage(zmk_key_t usage) { if (usage > ZMK_HID_KEYBOARD_NKRO_MAX_USAGE) { return -EINVAL; } TOGGLE_KEYBOARD(usage, 1); +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + ++keys_held; +#endif return 0; } @@ -102,6 +152,9 @@ static inline int deselect_keyboard_usage(zmk_key_t usage) { return -EINVAL; } TOGGLE_KEYBOARD(usage, 0); +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + --keys_held; +#endif return 0; } @@ -125,13 +178,52 @@ static inline bool check_keyboard_usage(zmk_key_t usage) { } \ } +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) +zmk_hid_boot_report_t *zmk_hid_get_boot_report() { + if (keys_held > HID_BOOT_KEY_LEN) { + return boot_report_rollover(keyboard_report.body.modifiers); + } + +#if CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE != HID_BOOT_KEY_LEN + // Form a boot report from a report of different size. + + boot_report.modifiers = keyboard_report.body.modifiers; + + int out = 0; + for (int i = 0; i < CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE; i++) { + uint8_t key = keyboard_report.body.keys[i]; + if (key) { + boot_report.keys[out++] = key; + if (out == keys_held) { + break; + } + } + } + + while (out < HID_BOOT_KEY_LEN) { + boot_report.keys[out++] = 0; + } + + return &boot_report; +#else + return &keyboard_report.body; +#endif /* CONFIG_ZMK_HID_KEYBOARD_REPORT_SIZE != HID_BOOT_KEY_LEN */ +} +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + static inline int select_keyboard_usage(zmk_key_t usage) { TOGGLE_KEYBOARD(0U, usage); +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + ++keys_held; +#endif return 0; } static inline int deselect_keyboard_usage(zmk_key_t usage) { TOGGLE_KEYBOARD(usage, 0U); +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + --keys_held; +#endif return 0; } diff --git a/app/src/usb.c b/app/src/usb.c index cf04ef46c8f..9d27900c3c6 100644 --- a/app/src/usb.c +++ b/app/src/usb.c @@ -15,6 +15,8 @@ #include #include +#include + LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); static enum usb_dc_status_code usb_status = USB_DC_UNKNOWN; @@ -35,6 +37,7 @@ enum zmk_usb_conn_state zmk_usb_get_conn_state() { case USB_DC_CONFIGURED: case USB_DC_RESUME: case USB_DC_CLEAR_HALT: + case USB_DC_SOF: return ZMK_USB_CONN_HID; case USB_DC_DISCONNECTED: @@ -47,6 +50,17 @@ enum zmk_usb_conn_state zmk_usb_get_conn_state() { } void usb_status_cb(enum usb_dc_status_code status, const uint8_t *params) { + // Start-of-frame events are too frequent and noisy to notify, and they're + // not used within ZMK + if (status == USB_DC_SOF) { + return; + } + +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + if (status == USB_DC_RESET) { + zmk_usb_hid_set_protocol(HID_PROTOCOL_REPORT); + } +#endif usb_status = status; k_work_submit(&usb_status_notifier_work); }; diff --git a/app/src/usb_hid.c b/app/src/usb_hid.c index f46c70a0a92..c72bb36c9ef 100644 --- a/app/src/usb_hid.c +++ b/app/src/usb_hid.c @@ -23,11 +23,75 @@ static K_SEM_DEFINE(hid_sem, 1, 1); static void in_ready_cb(const struct device *dev) { k_sem_give(&hid_sem); } +#define HID_GET_REPORT_TYPE_MASK 0xff00 +#define HID_GET_REPORT_ID_MASK 0x00ff + +#define HID_REPORT_TYPE_INPUT 0x100 +#define HID_REPORT_TYPE_OUTPUT 0x200 +#define HID_REPORT_TYPE_FEATURE 0x300 + +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) +static uint8_t hid_protocol = HID_PROTOCOL_REPORT; + +static void set_proto_cb(const struct device *dev, uint8_t protocol) { hid_protocol = protocol; } + +void zmk_usb_hid_set_protocol(uint8_t protocol) { hid_protocol = protocol; } +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + +static uint8_t *get_keyboard_report(size_t *len) { + if (hid_protocol == HID_PROTOCOL_REPORT) { + struct zmk_hid_keyboard_report *report = zmk_hid_get_keyboard_report(); + *len = sizeof(*report); + return (uint8_t *)report; + } +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + zmk_hid_boot_report_t *boot_report = zmk_hid_get_boot_report(); + *len = sizeof(*boot_report); + return (uint8_t *)boot_report; +#endif +} + +static int get_report_cb(const struct device *dev, struct usb_setup_packet *setup, int32_t *len, + uint8_t **data) { + + /* + * 7.2.1 of the HID v1.11 spec is unclear about handling requests for reports that do not exist + * For requested reports that aren't input reports, return -ENOTSUP like the Zephyr subsys does + */ + if ((setup->wValue & HID_GET_REPORT_TYPE_MASK) != HID_REPORT_TYPE_INPUT) { + LOG_ERR("Unsupported report type %d requested", (setup->wValue & HID_GET_REPORT_TYPE_MASK) + << 8); + return -ENOTSUP; + } + + switch (setup->wValue & HID_GET_REPORT_ID_MASK) { + case ZMK_HID_REPORT_ID_KEYBOARD: { + *data = get_keyboard_report(len); + break; + } + case ZMK_HID_REPORT_ID_CONSUMER: { + struct zmk_hid_consumer_report *report = zmk_hid_get_consumer_report(); + *data = (uint8_t *)report; + *len = sizeof(*report); + break; + } + default: + LOG_ERR("Invalid report ID %d requested", setup->wValue & HID_GET_REPORT_ID_MASK); + return -EINVAL; + } + + return 0; +} + static const struct hid_ops ops = { +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + .protocol_change = set_proto_cb, +#endif .int_in_ready = in_ready_cb, + .get_report = get_report_cb, }; -int zmk_usb_hid_send_report(const uint8_t *report, size_t len) { +static int zmk_usb_hid_send_report(const uint8_t *report, size_t len) { switch (zmk_usb_get_status()) { case USB_DC_SUSPEND: return usb_wakeup_request(); @@ -48,6 +112,23 @@ int zmk_usb_hid_send_report(const uint8_t *report, size_t len) { } } +int zmk_usb_hid_send_keyboard_report() { + size_t len; + uint8_t *report = get_keyboard_report(&len); + return zmk_usb_hid_send_report(report, len); +} + +int zmk_usb_hid_send_consumer_report() { +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + if (hid_protocol == HID_PROTOCOL_BOOT) { + return -ENOTSUP; + } +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + + struct zmk_hid_consumer_report *report = zmk_hid_get_consumer_report(); + return zmk_usb_hid_send_report((uint8_t *)report, sizeof(*report)); +} + static int zmk_usb_hid_init(const struct device *_arg) { hid_dev = device_get_binding("HID_0"); if (hid_dev == NULL) { @@ -56,6 +137,11 @@ static int zmk_usb_hid_init(const struct device *_arg) { } usb_hid_register_device(hid_dev, zmk_hid_report_desc, sizeof(zmk_hid_report_desc), &ops); + +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + usb_hid_set_proto_code(hid_dev, HID_BOOT_IFACE_CODE_KEYBOARD); +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + usb_hid_init(hid_dev); return 0; From c1bf35ce1de4b7d99c034300c9a813a0a3d11669 Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Fri, 10 Nov 2023 22:40:55 -0800 Subject: [PATCH 44/49] feat(build): Add support for artifact-name in build.yaml --- .github/workflows/build-user-config.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-user-config.yml b/.github/workflows/build-user-config.yml index c1a97b4d6e0..5aacadca90c 100644 --- a/.github/workflows/build-user-config.yml +++ b/.github/workflows/build-user-config.yml @@ -55,12 +55,14 @@ jobs: - name: Prepare variables shell: sh -x {0} env: + board: ${{ matrix.board }} shield: ${{ matrix.shield }} + artifact_name: ${{ matrix.artifact-name }} run: | echo "zephyr_version=${ZEPHYR_VERSION}" >> $GITHUB_ENV echo "extra_cmake_args=${shield:+-DSHIELD=\"$shield\"}" >> $GITHUB_ENV - echo "display_name=${shield:+$shield - }${{ matrix.board }}" >> $GITHUB_ENV - echo "artifact_name=${shield:+$shield-}${{ matrix.board }}-zmk" >> $GITHUB_ENV + echo "display_name=${shield:+$shield - }${board}" >> $GITHUB_ENV + echo "artifact_name=${artifact_name:-\"${shield:+$shield-}${board}-zmk\"}" >> $GITHUB_ENV - name: Checkout uses: actions/checkout@v3 From 2554b5c88f4d304f311d9f29017ac6fbe948ca7a Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Fri, 10 Nov 2023 22:08:37 -0800 Subject: [PATCH 45/49] fix(docs): Update boards in build examples to common+uf2 ones --- docs/docs/development/build-flash.md | 6 +++--- docs/docs/development/new-shield.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docs/development/build-flash.md b/docs/docs/development/build-flash.md index 94bbcdf93a8..0243983c9d8 100644 --- a/docs/docs/development/build-flash.md +++ b/docs/docs/development/build-flash.md @@ -65,7 +65,7 @@ west build -b planck_rev6 When building for a new board and/or shield after having built one previously, you may need to enable the pristine build option. This option removes all existing files in the build directory before regenerating them, and can be enabled by adding either --pristine or -p to the command: ```sh -west build -p -b proton_c -- -DSHIELD=kyria_left +west build -p -b nice_nano_v2 -- -DSHIELD=kyria_left ``` ### Building For Split Keyboards @@ -77,13 +77,13 @@ For split keyboards, you will have to build and flash each side separately the f By default, the `build` command outputs a single .uf2 file named `zmk.uf2` so building left and then right immediately after will overwrite your left firmware. In addition, you will need to pristine build each side to ensure the correct files are used. To avoid having to pristine build every time and separate the left and right build files, we recommend setting up separate build directories for each half. You can do this by using the `-d` parameter and first building left into `build/left`: ```sh -west build -d build/left -b nice_nano -- -DSHIELD=kyria_left +west build -d build/left -b nice_nano_v2 -- -DSHIELD=kyria_left ``` and then building right into `build/right`: ```sh -west build -d build/right -b nice_nano -- -DSHIELD=kyria_right +west build -d build/right -b nice_nano_v2 -- -DSHIELD=kyria_right ``` This produces `left` and `right` subfolders under the `build` directory and two separate .uf2 files. For future work on a specific half, use the `-d` parameter again to ensure you are building into the correct location. diff --git a/docs/docs/development/new-shield.md b/docs/docs/development/new-shield.md index 0771122985b..7f6a8644e11 100644 --- a/docs/docs/development/new-shield.md +++ b/docs/docs/development/new-shield.md @@ -493,7 +493,7 @@ Once you've fully created the new keyboard shield definition, you should be able to test with a build command like: ```sh -west build --pristine -b proton_c -- -DSHIELD=my_board +west build --pristine -b nice_nano_v2 -- -DSHIELD=my_board ``` The above build command generates `build/zephyr/zmk.uf2`. If your board From a5c3edd51b46f45a7fdf9ff157f69296d7a9fddb Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Fri, 10 Nov 2023 22:22:05 -0800 Subject: [PATCH 46/49] refactor(docs): Remove local build section in customization --- docs/docs/customization.md | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/docs/docs/customization.md b/docs/docs/customization.md index 4f75e743a66..a68e8595fa9 100644 --- a/docs/docs/customization.md +++ b/docs/docs/customization.md @@ -11,15 +11,15 @@ This makes flashing ZMK to your keyboard much easier, especially because you don By default, the `zmk-config` folder should contain two files: -- `.conf` -- `.keymap` +- `.conf` +- `.keymap` However, your config folder can also be modified to include a `boards/` directory for keymaps and configurations for multiple boards/shields outside of the default keyboard setting definitions. ## Configuration Changes -The setup script creates a `config/.conf` file that allows you to add additional configuration options to +The setup script creates a `config/.conf` file that allows you to add additional configuration options to control what features and options are built into your firmware. Opening that file with your text editor will allow you to see the various config settings that can be commented/uncommented to modify how your firmware is built. @@ -27,7 +27,7 @@ Refer to the [Configuration](/docs/config) documentation for more details on thi ## Keymap -Once you have the basic user config completed, you can find the keymap file in `config/.keymap` and customize from there. +Once you have the basic user config completed, you can find the keymap file in `config/.keymap` and customize from there. Refer to the [Keymap](features/keymaps.md) documentation to learn more. ## Publishing @@ -39,18 +39,11 @@ GitHub Actions job to build your firmware which you can download once it complet If you need to, a review of [Learn The Basics Of Git In Under 10 Minutes](https://www.freecodecamp.org/news/learn-the-basics-of-git-in-under-10-minutes-da548267cc91/) will help you get these steps right. ::: -## Building from a local `zmk` fork using `zmk-config` - -[As outlined here](development/build-flash.md), firmware comes in the form of .uf2 files, which can be built locally using the command `west build`. Normally, -`west build` will default to using the in-tree .keymap and .conf files found in your local copy of the `zmk` repository. However, you can append the command, `-DZMK_CONFIG="C:/the/absolute/path/config"` to `west build` in order to use the contents of your `zmk-config` folder instead of the -default keyboard settings. -**Notice that this path should point to the folder labelled `config` within your `zmk-config` folder.** - -For instance, building kyria firmware from a user `myUser`'s `zmk-config` folder on Windows 10 may look something like this: - -```bash -west build -b nice_nano -- -DSHIELD=kyria_left -DZMK_CONFIG="C:/Users/myUser/Documents/Github/zmk-config/config" -``` +:::note +It is also possible to build firmware locally on your computer by following the [toolchain setup](development/setup.md) and +[building instructions](development/build-flash.md), which includes pointers to +[building using your `zmk-config` folder](development/build-flash.md#building-from-zmk-config-folder). +::: ## Flashing Your Changes From 7b4b5d4ff20957c3cba0d3e3eb679dd45c3181f5 Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Fri, 10 Nov 2023 22:01:31 -0800 Subject: [PATCH 47/49] fix(docs): Fix debouncing driver support note --- docs/docs/features/debouncing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/features/debouncing.md b/docs/docs/features/debouncing.md index cbea7092884..38ded2d7952 100644 --- a/docs/docs/features/debouncing.md +++ b/docs/docs/features/debouncing.md @@ -20,7 +20,7 @@ socket or using some sharp tweezers to bend the contacts back together. ## Debounce Configuration :::note -Currently only the `zmk,kscan-gpio-matrix` driver supports these options. The other drivers have not yet been updated to use the new debouncing code. +Currently the `zmk,kscan-gpio-matrix` and `zmk,kscan-gpio-direct` [drivers](../config/kscan.md) supports these options, while `zmk,kscan-gpio-demux` driver does not. ::: ### Global Options From 964c54139dcc5fca466eb2a6dc72b4092c7152a0 Mon Sep 17 00:00:00 2001 From: Peter Johanson Date: Mon, 13 Nov 2023 11:00:28 -0800 Subject: [PATCH 48/49] fix(usb): Tweak how ZMK_USB gets enabled. * Previous version of multiple overrides of the default value of ZMK_USB were problematic. Move to using board _defconfig files for the defaults for those, along with proper `depends on` for ZMK_USB that accounts for split and split roles. --- app/Kconfig | 13 ++++++++----- app/boards/arm/bdn9/Kconfig.defconfig | 3 --- app/boards/arm/bdn9/bdn9_rev2_defconfig | 1 + app/boards/arm/bluemicro840/Kconfig.defconfig | 6 ------ .../arm/bluemicro840/bluemicro840_v1_defconfig | 3 +++ app/boards/arm/bt60/Kconfig.defconfig | 6 ------ app/boards/arm/bt60/bt60_v1_defconfig | 3 +++ app/boards/arm/bt60/bt60_v1_hs_defconfig | 3 +++ app/boards/arm/ckp/Kconfig.defconfig | 6 ------ app/boards/arm/ckp/bt60_v2_defconfig | 3 +++ app/boards/arm/ckp/bt65_v1_defconfig | 3 +++ app/boards/arm/ckp/bt75_v1_defconfig | 3 +++ app/boards/arm/corneish_zen/Kconfig.defconfig | 6 ------ .../arm/corneish_zen/corneish_zen_v1_left_defconfig | 3 +++ .../corneish_zen/corneish_zen_v1_right_defconfig | 3 +++ .../arm/corneish_zen/corneish_zen_v2_left_defconfig | 3 +++ .../corneish_zen/corneish_zen_v2_right_defconfig | 3 +++ app/boards/arm/dz60rgb/Kconfig.defconfig | 3 --- app/boards/arm/dz60rgb/dz60rgb_rev1_defconfig | 2 ++ app/boards/arm/ferris/Kconfig.defconfig | 3 --- app/boards/arm/kbdfans_tofu65/Kconfig.defconfig | 3 --- .../arm/kbdfans_tofu65/kbdfans_tofu65_v2_defconfig | 2 ++ app/boards/arm/mikoto/Kconfig.defconfig | 6 ------ app/boards/arm/mikoto/mikoto_520_defconfig | 3 +++ app/boards/arm/nice60/Kconfig.defconfig | 6 ------ app/boards/arm/nice60/nice60_defconfig | 4 ++++ app/boards/arm/nice_nano/Kconfig.defconfig | 6 ------ app/boards/arm/nice_nano/nice_nano_defconfig | 3 +++ app/boards/arm/nice_nano/nice_nano_v2_defconfig | 3 +++ app/boards/arm/nrf52840_m2/Kconfig.defconfig | 6 ------ app/boards/arm/nrf52840_m2/nrf52840_m2_defconfig | 3 +++ app/boards/arm/nrfmicro/Kconfig.defconfig | 6 ------ app/boards/arm/nrfmicro/nrfmicro_11_defconfig | 3 +++ .../arm/nrfmicro/nrfmicro_11_flipped_defconfig | 3 +++ app/boards/arm/nrfmicro/nrfmicro_13_52833_defconfig | 3 +++ app/boards/arm/nrfmicro/nrfmicro_13_defconfig | 3 +++ app/boards/arm/pillbug/Kconfig.defconfig | 6 ------ app/boards/arm/pillbug/pillbug_defconfig | 3 +++ app/boards/arm/planck/Kconfig.defconfig | 3 --- app/boards/arm/planck/planck_rev6_defconfig | 2 ++ app/boards/arm/preonic/Kconfig.defconfig | 3 --- app/boards/arm/preonic/preonic_rev3_defconfig | 2 ++ app/boards/arm/proton_c/Kconfig.defconfig | 3 --- app/boards/arm/proton_c/proton_c_defconfig | 1 + app/boards/arm/puchi_ble/Kconfig.defconfig | 6 ------ app/boards/arm/puchi_ble/puchi_ble_v1_defconfig | 3 +++ app/boards/arm/s40nc/Kconfig.defconfig | 6 ------ app/boards/arm/s40nc/s40nc_defconfig | 3 +++ app/boards/shields/nibble/Kconfig.defconfig | 4 ---- app/src/split/bluetooth/Kconfig | 3 --- 50 files changed, 85 insertions(+), 105 deletions(-) diff --git a/app/Kconfig b/app/Kconfig index 798cb7855d5..282339e3e40 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -91,10 +91,18 @@ menu "Output Types" config ZMK_USB bool "USB" + depends on (!ZMK_SPLIT || (ZMK_SPLIT && ZMK_SPLIT_ROLE_CENTRAL)) select USB select USB_DEVICE_STACK select USB_DEVICE_HID +config ZMK_USB_BOOT + bool "USB Boot Protocol Support" + default y + depends on ZMK_USB + select USB_HID_BOOT_PROTOCOL + select USB_DEVICE_SOF + if ZMK_USB config USB_NUMOF_EP_WRITE_RETRIES @@ -103,11 +111,6 @@ config USB_NUMOF_EP_WRITE_RETRIES config USB_HID_POLL_INTERVAL_MS default 1 -config ZMK_USB_BOOT - bool "USB Boot Protocol Support" - default y - select USB_HID_BOOT_PROTOCOL - select USB_DEVICE_SOF #ZMK_USB endif diff --git a/app/boards/arm/bdn9/Kconfig.defconfig b/app/boards/arm/bdn9/Kconfig.defconfig index d1c82811d28..96b7fe55329 100644 --- a/app/boards/arm/bdn9/Kconfig.defconfig +++ b/app/boards/arm/bdn9/Kconfig.defconfig @@ -11,9 +11,6 @@ config BOARD config ZMK_KEYBOARD_NAME default "BDN9 Rev2" -config ZMK_USB - default y - config ZMK_RGB_UNDERGLOW select SPI select WS2812_STRIP diff --git a/app/boards/arm/bdn9/bdn9_rev2_defconfig b/app/boards/arm/bdn9/bdn9_rev2_defconfig index 24dddb932d5..05087eeb4c6 100644 --- a/app/boards/arm/bdn9/bdn9_rev2_defconfig +++ b/app/boards/arm/bdn9/bdn9_rev2_defconfig @@ -23,3 +23,4 @@ CONFIG_HEAP_MEM_POOL_SIZE=1024 # clock configuration CONFIG_CLOCK_CONTROL=y +CONFIG_ZMK_USB=y \ No newline at end of file diff --git a/app/boards/arm/bluemicro840/Kconfig.defconfig b/app/boards/arm/bluemicro840/Kconfig.defconfig index 732805ae199..ff61ec92f81 100644 --- a/app/boards/arm/bluemicro840/Kconfig.defconfig +++ b/app/boards/arm/bluemicro840/Kconfig.defconfig @@ -18,10 +18,4 @@ endif # USB_DEVICE_STACK config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - endif # BOARD_BLUEMICRO840_V1 diff --git a/app/boards/arm/bluemicro840/bluemicro840_v1_defconfig b/app/boards/arm/bluemicro840/bluemicro840_v1_defconfig index 99d51a94334..3e13e77d0b5 100644 --- a/app/boards/arm/bluemicro840/bluemicro840_v1_defconfig +++ b/app/boards/arm/bluemicro840/bluemicro840_v1_defconfig @@ -21,3 +21,6 @@ CONFIG_SETTINGS_NVS=y CONFIG_FLASH=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y diff --git a/app/boards/arm/bt60/Kconfig.defconfig b/app/boards/arm/bt60/Kconfig.defconfig index e7cf1a48cff..c44901bd8d4 100644 --- a/app/boards/arm/bt60/Kconfig.defconfig +++ b/app/boards/arm/bt60/Kconfig.defconfig @@ -19,12 +19,6 @@ endif # USB config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - config ZMK_KEYBOARD_NAME default "BT60" diff --git a/app/boards/arm/bt60/bt60_v1_defconfig b/app/boards/arm/bt60/bt60_v1_defconfig index 813dcecea5a..04adb8a3cd8 100644 --- a/app/boards/arm/bt60/bt60_v1_defconfig +++ b/app/boards/arm/bt60/bt60_v1_defconfig @@ -23,3 +23,6 @@ CONFIG_SETTINGS_NVS=y CONFIG_FLASH=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/bt60/bt60_v1_hs_defconfig b/app/boards/arm/bt60/bt60_v1_hs_defconfig index f2327fd3f42..f16d82ac41d 100644 --- a/app/boards/arm/bt60/bt60_v1_hs_defconfig +++ b/app/boards/arm/bt60/bt60_v1_hs_defconfig @@ -23,3 +23,6 @@ CONFIG_SETTINGS_NVS=y CONFIG_FLASH=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/ckp/Kconfig.defconfig b/app/boards/arm/ckp/Kconfig.defconfig index d5bf4ded1eb..376d4619fde 100644 --- a/app/boards/arm/ckp/Kconfig.defconfig +++ b/app/boards/arm/ckp/Kconfig.defconfig @@ -25,10 +25,4 @@ endif # USB config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - endif # BOARD_BT60_V2 || BOARD_BT65_V1 || BOARD_BT75_V1 diff --git a/app/boards/arm/ckp/bt60_v2_defconfig b/app/boards/arm/ckp/bt60_v2_defconfig index f6dc7e09a68..fd1ae985995 100644 --- a/app/boards/arm/ckp/bt60_v2_defconfig +++ b/app/boards/arm/ckp/bt60_v2_defconfig @@ -36,3 +36,6 @@ CONFIG_WS2812_STRIP=y CONFIG_SPI=y CONFIG_BT_CTLR_TX_PWR_PLUS_8=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y diff --git a/app/boards/arm/ckp/bt65_v1_defconfig b/app/boards/arm/ckp/bt65_v1_defconfig index e40ae2dbb54..be5f17eb54f 100644 --- a/app/boards/arm/ckp/bt65_v1_defconfig +++ b/app/boards/arm/ckp/bt65_v1_defconfig @@ -36,3 +36,6 @@ CONFIG_WS2812_STRIP=y CONFIG_SPI=y CONFIG_BT_CTLR_TX_PWR_PLUS_8=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/ckp/bt75_v1_defconfig b/app/boards/arm/ckp/bt75_v1_defconfig index 510d6994dc1..b4d85338aec 100644 --- a/app/boards/arm/ckp/bt75_v1_defconfig +++ b/app/boards/arm/ckp/bt75_v1_defconfig @@ -36,3 +36,6 @@ CONFIG_WS2812_STRIP=y CONFIG_SPI=y CONFIG_BT_CTLR_TX_PWR_PLUS_8=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/corneish_zen/Kconfig.defconfig b/app/boards/arm/corneish_zen/Kconfig.defconfig index feab3eca773..f3cc959edd0 100644 --- a/app/boards/arm/corneish_zen/Kconfig.defconfig +++ b/app/boards/arm/corneish_zen/Kconfig.defconfig @@ -25,12 +25,6 @@ config ZMK_SPLIT config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - if USB config USB_NRFX diff --git a/app/boards/arm/corneish_zen/corneish_zen_v1_left_defconfig b/app/boards/arm/corneish_zen/corneish_zen_v1_left_defconfig index a71ac680bb6..d738255601b 100644 --- a/app/boards/arm/corneish_zen/corneish_zen_v1_left_defconfig +++ b/app/boards/arm/corneish_zen/corneish_zen_v1_left_defconfig @@ -39,6 +39,9 @@ CONFIG_FLASH_MAP=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_30PPM=y +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y + # enable display drivers CONFIG_ZMK_DISPLAY_WORK_QUEUE_DEDICATED=y CONFIG_ZMK_DISPLAY_DEDICATED_THREAD_STACK_SIZE=2048 diff --git a/app/boards/arm/corneish_zen/corneish_zen_v1_right_defconfig b/app/boards/arm/corneish_zen/corneish_zen_v1_right_defconfig index f099392ff6a..5284159d83f 100644 --- a/app/boards/arm/corneish_zen/corneish_zen_v1_right_defconfig +++ b/app/boards/arm/corneish_zen/corneish_zen_v1_right_defconfig @@ -39,6 +39,9 @@ CONFIG_FLASH_MAP=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_30PPM=y +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y + # enable display drivers CONFIG_ZMK_DISPLAY_WORK_QUEUE_DEDICATED=y CONFIG_ZMK_DISPLAY_DEDICATED_THREAD_STACK_SIZE=2048 diff --git a/app/boards/arm/corneish_zen/corneish_zen_v2_left_defconfig b/app/boards/arm/corneish_zen/corneish_zen_v2_left_defconfig index 3b7b4d9e276..29a5f878ac2 100644 --- a/app/boards/arm/corneish_zen/corneish_zen_v2_left_defconfig +++ b/app/boards/arm/corneish_zen/corneish_zen_v2_left_defconfig @@ -35,6 +35,9 @@ CONFIG_FLASH_MAP=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_30PPM=y +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y + # enable display drivers CONFIG_ZMK_DISPLAY_WORK_QUEUE_DEDICATED=y CONFIG_ZMK_DISPLAY_DEDICATED_THREAD_STACK_SIZE=2048 diff --git a/app/boards/arm/corneish_zen/corneish_zen_v2_right_defconfig b/app/boards/arm/corneish_zen/corneish_zen_v2_right_defconfig index b361b08d9a0..506aa67e1b1 100644 --- a/app/boards/arm/corneish_zen/corneish_zen_v2_right_defconfig +++ b/app/boards/arm/corneish_zen/corneish_zen_v2_right_defconfig @@ -35,6 +35,9 @@ CONFIG_FLASH_MAP=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_30PPM=y +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y + # enable display drivers CONFIG_ZMK_DISPLAY_WORK_QUEUE_DEDICATED=y CONFIG_ZMK_DISPLAY_DEDICATED_THREAD_STACK_SIZE=2048 diff --git a/app/boards/arm/dz60rgb/Kconfig.defconfig b/app/boards/arm/dz60rgb/Kconfig.defconfig index 2e30e3d048d..6e0592569de 100644 --- a/app/boards/arm/dz60rgb/Kconfig.defconfig +++ b/app/boards/arm/dz60rgb/Kconfig.defconfig @@ -8,7 +8,4 @@ if BOARD_DZ60RGB_REV1 config ZMK_KEYBOARD_NAME default "DZ60RGB Rev 1" -config ZMK_USB - default y - endif # BOARD_DZ60RGB_REV1 diff --git a/app/boards/arm/dz60rgb/dz60rgb_rev1_defconfig b/app/boards/arm/dz60rgb/dz60rgb_rev1_defconfig index 33840f966ef..53bc0e11034 100644 --- a/app/boards/arm/dz60rgb/dz60rgb_rev1_defconfig +++ b/app/boards/arm/dz60rgb/dz60rgb_rev1_defconfig @@ -25,3 +25,5 @@ CONFIG_CLOCK_STM32_PLL_MULTIPLIER=9 CONFIG_CLOCK_STM32_AHB_PRESCALER=1 CONFIG_CLOCK_STM32_APB1_PRESCALER=2 CONFIG_CLOCK_STM32_APB2_PRESCALER=1 + +CONFIG_ZMK_USB=y \ No newline at end of file diff --git a/app/boards/arm/ferris/Kconfig.defconfig b/app/boards/arm/ferris/Kconfig.defconfig index 7cf43bcb9e6..420ea01fab9 100644 --- a/app/boards/arm/ferris/Kconfig.defconfig +++ b/app/boards/arm/ferris/Kconfig.defconfig @@ -11,9 +11,6 @@ config BOARD config ZMK_KEYBOARD_NAME default "Ferris rev 0.2" -config ZMK_USB - default y - config ZMK_KSCAN_MATRIX_POLLING default y diff --git a/app/boards/arm/kbdfans_tofu65/Kconfig.defconfig b/app/boards/arm/kbdfans_tofu65/Kconfig.defconfig index 993d51423b5..0444f510105 100644 --- a/app/boards/arm/kbdfans_tofu65/Kconfig.defconfig +++ b/app/boards/arm/kbdfans_tofu65/Kconfig.defconfig @@ -9,7 +9,4 @@ config ZMK_KEYBOARD_NAME config RP2_FLASH_W25Q080 default y -config ZMK_USB - default y - endif # BOARD_KBDFANS_TOFU65_V2 diff --git a/app/boards/arm/kbdfans_tofu65/kbdfans_tofu65_v2_defconfig b/app/boards/arm/kbdfans_tofu65/kbdfans_tofu65_v2_defconfig index cf546683452..57014acf37a 100644 --- a/app/boards/arm/kbdfans_tofu65/kbdfans_tofu65_v2_defconfig +++ b/app/boards/arm/kbdfans_tofu65/kbdfans_tofu65_v2_defconfig @@ -18,3 +18,5 @@ CONFIG_USE_DT_CODE_PARTITION=y # Output UF2 by default, native bootloader supports it. CONFIG_BUILD_OUTPUT_UF2=y + +CONFIG_ZMK_USB=y \ No newline at end of file diff --git a/app/boards/arm/mikoto/Kconfig.defconfig b/app/boards/arm/mikoto/Kconfig.defconfig index 8c7746dbb8e..8117cc87329 100644 --- a/app/boards/arm/mikoto/Kconfig.defconfig +++ b/app/boards/arm/mikoto/Kconfig.defconfig @@ -21,12 +21,6 @@ endif # USB config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - config PINMUX default y diff --git a/app/boards/arm/mikoto/mikoto_520_defconfig b/app/boards/arm/mikoto/mikoto_520_defconfig index c755633e4cf..354fa56aa34 100644 --- a/app/boards/arm/mikoto/mikoto_520_defconfig +++ b/app/boards/arm/mikoto/mikoto_520_defconfig @@ -21,3 +21,6 @@ CONFIG_SETTINGS_NVS=y CONFIG_FLASH=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/nice60/Kconfig.defconfig b/app/boards/arm/nice60/Kconfig.defconfig index f3347df9c6e..76818e87b0f 100644 --- a/app/boards/arm/nice60/Kconfig.defconfig +++ b/app/boards/arm/nice60/Kconfig.defconfig @@ -16,10 +16,4 @@ endif # USB_DEVICE_STACK config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - endif # BOARD_NICE60 diff --git a/app/boards/arm/nice60/nice60_defconfig b/app/boards/arm/nice60/nice60_defconfig index 48936a04704..fabcb7eddf4 100644 --- a/app/boards/arm/nice60/nice60_defconfig +++ b/app/boards/arm/nice60/nice60_defconfig @@ -28,3 +28,7 @@ CONFIG_WS2812_STRIP=y CONFIG_ZMK_RGB_UNDERGLOW_HUE_START=160 CONFIG_ZMK_RGB_UNDERGLOW_EFF_START=3 + + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/nice_nano/Kconfig.defconfig b/app/boards/arm/nice_nano/Kconfig.defconfig index ada59dd9f2e..63102a571f9 100644 --- a/app/boards/arm/nice_nano/Kconfig.defconfig +++ b/app/boards/arm/nice_nano/Kconfig.defconfig @@ -16,10 +16,4 @@ endif # USB_DEVICE_STACK config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - endif # BOARD_NICE_NANO || BOARD_NICE_NANO_V2 diff --git a/app/boards/arm/nice_nano/nice_nano_defconfig b/app/boards/arm/nice_nano/nice_nano_defconfig index a837b7d2875..6b7fcab252d 100644 --- a/app/boards/arm/nice_nano/nice_nano_defconfig +++ b/app/boards/arm/nice_nano/nice_nano_defconfig @@ -22,3 +22,6 @@ CONFIG_SETTINGS_NVS=y CONFIG_FLASH=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/nice_nano/nice_nano_v2_defconfig b/app/boards/arm/nice_nano/nice_nano_v2_defconfig index 667cf71aca2..6b5044e5ef0 100644 --- a/app/boards/arm/nice_nano/nice_nano_v2_defconfig +++ b/app/boards/arm/nice_nano/nice_nano_v2_defconfig @@ -22,3 +22,6 @@ CONFIG_SETTINGS_NVS=y CONFIG_FLASH=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y + +CONFIG_ZMK_BLE=y +CONFIG_ZMK_USB=y \ No newline at end of file diff --git a/app/boards/arm/nrf52840_m2/Kconfig.defconfig b/app/boards/arm/nrf52840_m2/Kconfig.defconfig index 50a049bb926..a5227fc0a10 100644 --- a/app/boards/arm/nrf52840_m2/Kconfig.defconfig +++ b/app/boards/arm/nrf52840_m2/Kconfig.defconfig @@ -16,10 +16,4 @@ endif # USB_DEVICE_STACK config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - endif # BOARD_NRF52840_M2 diff --git a/app/boards/arm/nrf52840_m2/nrf52840_m2_defconfig b/app/boards/arm/nrf52840_m2/nrf52840_m2_defconfig index b7671ba9f12..93eef9e6ef8 100644 --- a/app/boards/arm/nrf52840_m2/nrf52840_m2_defconfig +++ b/app/boards/arm/nrf52840_m2/nrf52840_m2_defconfig @@ -20,3 +20,6 @@ CONFIG_SETTINGS_NVS=y CONFIG_FLASH=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/nrfmicro/Kconfig.defconfig b/app/boards/arm/nrfmicro/Kconfig.defconfig index 7d752ac6ee6..659e9c5c173 100644 --- a/app/boards/arm/nrfmicro/Kconfig.defconfig +++ b/app/boards/arm/nrfmicro/Kconfig.defconfig @@ -18,12 +18,6 @@ endif # USB_DEVICE_STACK config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - config PINMUX default y diff --git a/app/boards/arm/nrfmicro/nrfmicro_11_defconfig b/app/boards/arm/nrfmicro/nrfmicro_11_defconfig index b51929b0c8f..5ba4d6e1478 100644 --- a/app/boards/arm/nrfmicro/nrfmicro_11_defconfig +++ b/app/boards/arm/nrfmicro/nrfmicro_11_defconfig @@ -23,3 +23,6 @@ CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y CONFIG_CLOCK_CONTROL_NRF=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/nrfmicro/nrfmicro_11_flipped_defconfig b/app/boards/arm/nrfmicro/nrfmicro_11_flipped_defconfig index 335a2d75b12..31cbfc9ae75 100644 --- a/app/boards/arm/nrfmicro/nrfmicro_11_flipped_defconfig +++ b/app/boards/arm/nrfmicro/nrfmicro_11_flipped_defconfig @@ -23,3 +23,6 @@ CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y CONFIG_CLOCK_CONTROL_NRF=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/nrfmicro/nrfmicro_13_52833_defconfig b/app/boards/arm/nrfmicro/nrfmicro_13_52833_defconfig index 4af1ff86a7c..f459f35636a 100644 --- a/app/boards/arm/nrfmicro/nrfmicro_13_52833_defconfig +++ b/app/boards/arm/nrfmicro/nrfmicro_13_52833_defconfig @@ -23,3 +23,6 @@ CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y CONFIG_CLOCK_CONTROL_NRF=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/nrfmicro/nrfmicro_13_defconfig b/app/boards/arm/nrfmicro/nrfmicro_13_defconfig index 43ba40e2dc3..9ffb2766a66 100644 --- a/app/boards/arm/nrfmicro/nrfmicro_13_defconfig +++ b/app/boards/arm/nrfmicro/nrfmicro_13_defconfig @@ -23,3 +23,6 @@ CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y CONFIG_CLOCK_CONTROL_NRF=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/pillbug/Kconfig.defconfig b/app/boards/arm/pillbug/Kconfig.defconfig index 8657f8aea30..48427ed3ec2 100644 --- a/app/boards/arm/pillbug/Kconfig.defconfig +++ b/app/boards/arm/pillbug/Kconfig.defconfig @@ -16,10 +16,4 @@ endif # USB_DEVICE_STACK config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - endif # BOARD_PILLBUG diff --git a/app/boards/arm/pillbug/pillbug_defconfig b/app/boards/arm/pillbug/pillbug_defconfig index 9781cf995cc..9ec72c417ef 100644 --- a/app/boards/arm/pillbug/pillbug_defconfig +++ b/app/boards/arm/pillbug/pillbug_defconfig @@ -23,3 +23,6 @@ CONFIG_SETTINGS_NVS=y CONFIG_FLASH=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/planck/Kconfig.defconfig b/app/boards/arm/planck/Kconfig.defconfig index d1304da0588..69dea84ce54 100644 --- a/app/boards/arm/planck/Kconfig.defconfig +++ b/app/boards/arm/planck/Kconfig.defconfig @@ -8,9 +8,6 @@ if BOARD_PLANCK_REV6 config ZMK_KEYBOARD_NAME default "Planck V6" -config ZMK_USB - default y - config ZMK_KSCAN_MATRIX_POLLING default y diff --git a/app/boards/arm/planck/planck_rev6_defconfig b/app/boards/arm/planck/planck_rev6_defconfig index a78ea45de88..74050f3d9c1 100644 --- a/app/boards/arm/planck/planck_rev6_defconfig +++ b/app/boards/arm/planck/planck_rev6_defconfig @@ -15,3 +15,5 @@ CONFIG_GPIO=y # clock configuration CONFIG_CLOCK_CONTROL=y + +CONFIG_ZMK_USB=y diff --git a/app/boards/arm/preonic/Kconfig.defconfig b/app/boards/arm/preonic/Kconfig.defconfig index 186c88bc5e3..86b2e3d0ef4 100644 --- a/app/boards/arm/preonic/Kconfig.defconfig +++ b/app/boards/arm/preonic/Kconfig.defconfig @@ -8,9 +8,6 @@ if BOARD_PREONIC_REV3 config ZMK_KEYBOARD_NAME default "Preonic V3" -config ZMK_USB - default y - config ZMK_KSCAN_MATRIX_POLLING default y diff --git a/app/boards/arm/preonic/preonic_rev3_defconfig b/app/boards/arm/preonic/preonic_rev3_defconfig index ab19d10f6f8..e063827a4e2 100644 --- a/app/boards/arm/preonic/preonic_rev3_defconfig +++ b/app/boards/arm/preonic/preonic_rev3_defconfig @@ -13,3 +13,5 @@ CONFIG_GPIO=y # clock configuration CONFIG_CLOCK_CONTROL=y + +CONFIG_ZMK_USB=y \ No newline at end of file diff --git a/app/boards/arm/proton_c/Kconfig.defconfig b/app/boards/arm/proton_c/Kconfig.defconfig index f5089119c13..eed4b830442 100644 --- a/app/boards/arm/proton_c/Kconfig.defconfig +++ b/app/boards/arm/proton_c/Kconfig.defconfig @@ -8,7 +8,4 @@ if BOARD_QMK_PROTON_C config BOARD default "proton_c" -config ZMK_USB - default y - endif # BOARD_QMK_PROTON_C diff --git a/app/boards/arm/proton_c/proton_c_defconfig b/app/boards/arm/proton_c/proton_c_defconfig index 32e1ade9604..c552bf15df9 100644 --- a/app/boards/arm/proton_c/proton_c_defconfig +++ b/app/boards/arm/proton_c/proton_c_defconfig @@ -17,3 +17,4 @@ CONFIG_GPIO=y # clock configuration CONFIG_CLOCK_CONTROL=y +CONFIG_ZMK_USB=y \ No newline at end of file diff --git a/app/boards/arm/puchi_ble/Kconfig.defconfig b/app/boards/arm/puchi_ble/Kconfig.defconfig index c4fca8e1f31..3533104b2a5 100644 --- a/app/boards/arm/puchi_ble/Kconfig.defconfig +++ b/app/boards/arm/puchi_ble/Kconfig.defconfig @@ -16,12 +16,6 @@ endif # USB_DEVICE_STACK config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - config PINMUX default y diff --git a/app/boards/arm/puchi_ble/puchi_ble_v1_defconfig b/app/boards/arm/puchi_ble/puchi_ble_v1_defconfig index 1adb9217c8a..ab197df0a80 100644 --- a/app/boards/arm/puchi_ble/puchi_ble_v1_defconfig +++ b/app/boards/arm/puchi_ble/puchi_ble_v1_defconfig @@ -25,3 +25,6 @@ CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y CONFIG_CLOCK_CONTROL_NRF=y CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/arm/s40nc/Kconfig.defconfig b/app/boards/arm/s40nc/Kconfig.defconfig index 11e62cf54c5..f892f392d5c 100644 --- a/app/boards/arm/s40nc/Kconfig.defconfig +++ b/app/boards/arm/s40nc/Kconfig.defconfig @@ -19,10 +19,4 @@ endif # USB config BT_CTLR default BT -config ZMK_BLE - default y - -config ZMK_USB - default y - endif # BOARD_S40NC diff --git a/app/boards/arm/s40nc/s40nc_defconfig b/app/boards/arm/s40nc/s40nc_defconfig index 31972781db1..b523ceb80b7 100644 --- a/app/boards/arm/s40nc/s40nc_defconfig +++ b/app/boards/arm/s40nc/s40nc_defconfig @@ -20,3 +20,6 @@ CONFIG_SETTINGS_NVS=y CONFIG_FLASH=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y + +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y \ No newline at end of file diff --git a/app/boards/shields/nibble/Kconfig.defconfig b/app/boards/shields/nibble/Kconfig.defconfig index 31ac7cfe7dd..19bddec77b8 100644 --- a/app/boards/shields/nibble/Kconfig.defconfig +++ b/app/boards/shields/nibble/Kconfig.defconfig @@ -6,10 +6,6 @@ if SHIELD_NIBBLE config ZMK_KEYBOARD_NAME default "NIBBLE" -config ZMK_USB - default y - - if ZMK_DISPLAY config I2C diff --git a/app/src/split/bluetooth/Kconfig b/app/src/split/bluetooth/Kconfig index 5919010ba31..858e7308fef 100644 --- a/app/src/split/bluetooth/Kconfig +++ b/app/src/split/bluetooth/Kconfig @@ -62,9 +62,6 @@ config ZMK_SPLIT_BLE_PERIPHERAL_POSITION_QUEUE_SIZE int "Max number of key position state events to queue to send to the central" default 10 -config ZMK_USB - default n - config BT_MAX_PAIRED default 1 From afe65ead9c0f1418fa34bfa93325d0cce33c6c2c Mon Sep 17 00:00:00 2001 From: Pete Johanson Date: Mon, 13 Nov 2023 16:50:00 -0500 Subject: [PATCH 49/49] Revert "feat(build): Add support for artifact-name in build.yaml" This reverts commit c1bf35ce1de4b7d99c034300c9a813a0a3d11669. --- .github/workflows/build-user-config.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-user-config.yml b/.github/workflows/build-user-config.yml index 5aacadca90c..c1a97b4d6e0 100644 --- a/.github/workflows/build-user-config.yml +++ b/.github/workflows/build-user-config.yml @@ -55,14 +55,12 @@ jobs: - name: Prepare variables shell: sh -x {0} env: - board: ${{ matrix.board }} shield: ${{ matrix.shield }} - artifact_name: ${{ matrix.artifact-name }} run: | echo "zephyr_version=${ZEPHYR_VERSION}" >> $GITHUB_ENV echo "extra_cmake_args=${shield:+-DSHIELD=\"$shield\"}" >> $GITHUB_ENV - echo "display_name=${shield:+$shield - }${board}" >> $GITHUB_ENV - echo "artifact_name=${artifact_name:-\"${shield:+$shield-}${board}-zmk\"}" >> $GITHUB_ENV + echo "display_name=${shield:+$shield - }${{ matrix.board }}" >> $GITHUB_ENV + echo "artifact_name=${shield:+$shield-}${{ matrix.board }}-zmk" >> $GITHUB_ENV - name: Checkout uses: actions/checkout@v3