diff --git a/app/include/zmk/keymap.h b/app/include/zmk/keymap.h index a47cd505643..9ce140bfa27 100644 --- a/app/include/zmk/keymap.h +++ b/app/include/zmk/keymap.h @@ -8,6 +8,10 @@ #include +#define ZMK_LAYER_CHILD_LEN_PLUS_ONE(node) 1 + +#define ZMK_KEYMAP_LAYERS_LEN \ + (DT_FOREACH_CHILD(DT_INST(0, zmk_keymap), ZMK_LAYER_CHILD_LEN_PLUS_ONE) 0) + typedef uint32_t zmk_keymap_layers_state_t; uint8_t zmk_keymap_layer_default(); diff --git a/app/include/zmk/sensors.h b/app/include/zmk/sensors.h index 8ac1c283cb6..1919d4ce647 100644 --- a/app/include/zmk/sensors.h +++ b/app/include/zmk/sensors.h @@ -24,7 +24,9 @@ struct zmk_sensor_config { uint16_t triggers_per_rotation; }; +// This struct is also used for data transfer for splits, so any changes to the size, layout, etc +// is a breaking change for the split GATT service protocol. struct zmk_sensor_channel_data { - enum sensor_channel channel; struct sensor_value value; -}; + enum sensor_channel channel; +} __packed; diff --git a/app/include/zmk/split/bluetooth/service.h b/app/include/zmk/split/bluetooth/service.h index f0c1d79ff7d..112cd552942 100644 --- a/app/include/zmk/split/bluetooth/service.h +++ b/app/include/zmk/split/bluetooth/service.h @@ -6,8 +6,18 @@ #pragma once +#include +#include + #define ZMK_SPLIT_RUN_BEHAVIOR_DEV_LEN 9 +struct sensor_event { + uint8_t sensor_index; + + uint8_t channel_data_size; + struct zmk_sensor_channel_data channel_data[ZMK_SENSOR_EVENT_MAX_CHANNELS]; +} __packed; + struct zmk_split_run_behavior_data { uint8_t position; uint8_t state; @@ -21,4 +31,7 @@ struct zmk_split_run_behavior_payload { } __packed; int zmk_split_bt_position_pressed(uint8_t position); -int zmk_split_bt_position_released(uint8_t position); \ No newline at end of file +int zmk_split_bt_position_released(uint8_t position); +int zmk_split_bt_sensor_triggered(uint8_t sensor_index, + const struct zmk_sensor_channel_data channel_data[], + size_t channel_data_size); diff --git a/app/include/zmk/split/bluetooth/uuid.h b/app/include/zmk/split/bluetooth/uuid.h index cbdb17723cd..c38131dd83e 100644 --- a/app/include/zmk/split/bluetooth/uuid.h +++ b/app/include/zmk/split/bluetooth/uuid.h @@ -16,3 +16,4 @@ #define ZMK_SPLIT_BT_SERVICE_UUID ZMK_BT_SPLIT_UUID(0x00000000) #define ZMK_SPLIT_BT_CHAR_POSITION_STATE_UUID ZMK_BT_SPLIT_UUID(0x00000001) #define ZMK_SPLIT_BT_CHAR_RUN_BEHAVIOR_UUID ZMK_BT_SPLIT_UUID(0x00000002) +#define ZMK_SPLIT_BT_CHAR_SENSOR_STATE_UUID ZMK_BT_SPLIT_UUID(0x00000003) diff --git a/app/src/behaviors/behavior_sensor_rotate_common.c b/app/src/behaviors/behavior_sensor_rotate_common.c index 586cac3fdf2..98b4aec1267 100644 --- a/app/src/behaviors/behavior_sensor_rotate_common.c +++ b/app/src/behaviors/behavior_sensor_rotate_common.c @@ -28,7 +28,7 @@ int zmk_behavior_sensor_rotate_common_accept_data( if (value.val1 == 0) { triggers = value.val2; } else { - struct sensor_value remainder = data->remainder[sensor_index]; + struct sensor_value remainder = data->remainder[sensor_index][event.layer]; remainder.val1 += value.val1; remainder.val2 += value.val2; @@ -42,15 +42,16 @@ int zmk_behavior_sensor_rotate_common_accept_data( triggers = remainder.val1 / trigger_degrees; remainder.val1 %= trigger_degrees; - data->remainder[sensor_index] = remainder; + data->remainder[sensor_index][event.layer] = remainder; } LOG_DBG( "val1: %d, val2: %d, remainder: %d/%d triggers: %d inc keycode 0x%02X dec keycode 0x%02X", - value.val1, value.val2, data->remainder[sensor_index].val1, - data->remainder[sensor_index].val2, triggers, binding->param1, binding->param2); + value.val1, value.val2, data->remainder[sensor_index][event.layer].val1, + data->remainder[sensor_index][event.layer].val2, triggers, binding->param1, + binding->param2); - data->triggers[sensor_index] = triggers; + data->triggers[sensor_index][event.layer] = triggers; return 0; } @@ -64,11 +65,11 @@ int zmk_behavior_sensor_rotate_common_process(struct zmk_behavior_binding *bindi const int sensor_index = ZMK_SENSOR_POSITION_FROM_VIRTUAL_KEY_POSITION(event.position); if (mode != BEHAVIOR_SENSOR_BINDING_PROCESS_MODE_TRIGGER) { - data->triggers[sensor_index] = 0; + data->triggers[sensor_index][event.layer] = 0; return ZMK_BEHAVIOR_TRANSPARENT; } - int triggers = data->triggers[sensor_index]; + int triggers = data->triggers[sensor_index][event.layer]; struct zmk_behavior_binding triggered_binding; if (triggers > 0) { diff --git a/app/src/behaviors/behavior_sensor_rotate_common.h b/app/src/behaviors/behavior_sensor_rotate_common.h index d354b67937c..c92ac3d5e5f 100644 --- a/app/src/behaviors/behavior_sensor_rotate_common.h +++ b/app/src/behaviors/behavior_sensor_rotate_common.h @@ -6,6 +6,7 @@ #include #include +#include #include struct behavior_sensor_rotate_config { @@ -16,8 +17,8 @@ struct behavior_sensor_rotate_config { }; struct behavior_sensor_rotate_data { - struct sensor_value remainder[ZMK_KEYMAP_SENSORS_LEN]; - int triggers[ZMK_KEYMAP_SENSORS_LEN]; + struct sensor_value remainder[ZMK_KEYMAP_SENSORS_LEN][ZMK_KEYMAP_LAYERS_LEN]; + int triggers[ZMK_KEYMAP_SENSORS_LEN][ZMK_KEYMAP_LAYERS_LEN]; }; int zmk_behavior_sensor_rotate_common_accept_data( diff --git a/app/src/keymap.c b/app/src/keymap.c index 020faf3f2be..bda694276c8 100644 --- a/app/src/keymap.c +++ b/app/src/keymap.c @@ -31,10 +31,6 @@ static uint8_t _zmk_keymap_layer_default = 0; #define DT_DRV_COMPAT zmk_keymap -#define LAYER_CHILD_LEN(node) 1 + -#define ZMK_KEYMAP_NODE DT_DRV_INST(0) -#define ZMK_KEYMAP_LAYERS_LEN (DT_INST_FOREACH_CHILD(0, LAYER_CHILD_LEN) 0) - #define BINDING_WITH_COMMA(idx, drv_inst) ZMK_KEYMAP_EXTRACT_BINDING(idx, drv_inst) #define TRANSFORMED_LAYER(node) \ diff --git a/app/src/sensors.c b/app/src/sensors.c index e339afe0437..60f2bd2a33f 100644 --- a/app/src/sensors.c +++ b/app/src/sensors.c @@ -29,7 +29,7 @@ struct sensors_item_cfg { { \ .dev = DEVICE_DT_GET_OR_NULL(node), \ .trigger = {.type = SENSOR_TRIG_DATA_READY, .chan = SENSOR_CHAN_ROTATION}, \ - .config = &configs[idx] \ + .config = &configs[idx], .sensor_index = idx \ } #define SENSOR_ITEM(idx, _i) _SENSOR_ITEM(idx, ZMK_KEYMAP_SENSORS_BY_IDX(idx)) @@ -112,7 +112,7 @@ static void zmk_sensors_trigger_handler(const struct device *dev, int sensor_index = test_item - sensors; if (sensor_index < 0 || sensor_index >= ARRAY_SIZE(sensors)) { - LOG_ERR("Invalid sensor item triggered our callback"); + LOG_ERR("Invalid sensor item triggered our callback (%d)", sensor_index); return; } @@ -127,8 +127,6 @@ static void zmk_sensors_trigger_handler(const struct device *dev, static void zmk_sensors_init_item(uint8_t i) { LOG_DBG("Init sensor at index %d", i); - sensors[i].sensor_index = i; - if (!sensors[i].dev) { LOG_DBG("No local device for %d", i); return; diff --git a/app/src/split/bluetooth/central.c b/app/src/split/bluetooth/central.c index 8a5e9d35c5a..b70d79e3a3a 100644 --- a/app/src/split/bluetooth/central.c +++ b/app/src/split/bluetooth/central.c @@ -21,10 +21,12 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include #include #include #include #include +#include static int start_scanning(void); @@ -41,6 +43,7 @@ struct peripheral_slot { struct bt_conn *conn; struct bt_gatt_discover_params discover_params; struct bt_gatt_subscribe_params subscribe_params; + struct bt_gatt_subscribe_params sensor_subscribe_params; struct bt_gatt_discover_params sub_discover_params; uint16_t run_behavior_handle; uint8_t position_state[POSITION_STATE_DATA_LEN]; @@ -165,6 +168,52 @@ int confirm_peripheral_slot_conn(struct bt_conn *conn) { return 0; } +#if ZMK_KEYMAP_HAS_SENSORS +K_MSGQ_DEFINE(peripheral_sensor_event_msgq, sizeof(struct zmk_sensor_event), + CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE, 4); + +void peripheral_sensor_event_work_callback(struct k_work *work) { + struct zmk_sensor_event ev; + while (k_msgq_get(&peripheral_sensor_event_msgq, &ev, K_NO_WAIT) == 0) { + LOG_DBG("Trigger sensor change for %d", ev.sensor_index); + ZMK_EVENT_RAISE(new_zmk_sensor_event(ev)); + } +} + +K_WORK_DEFINE(peripheral_sensor_event_work, peripheral_sensor_event_work_callback); + +static uint8_t split_central_sensor_notify_func(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length) { + if (!data) { + LOG_DBG("[UNSUBSCRIBED]"); + params->value_handle = 0U; + return BT_GATT_ITER_STOP; + } + + LOG_DBG("[SENSOR NOTIFICATION] data %p length %u", data, length); + + if (length < offsetof(struct sensor_event, channel_data)) { + LOG_WRN("Ignoring sensor notify with insufficient data length (%d)", length); + return BT_GATT_ITER_STOP; + } + + struct sensor_event sensor_event; + memcpy(&sensor_event, data, MIN(length, sizeof(sensor_event))); + struct zmk_sensor_event ev = { + .sensor_index = sensor_event.sensor_index, + .channel_data_size = MIN(sensor_event.channel_data_size, ZMK_SENSOR_EVENT_MAX_CHANNELS), + .timestamp = k_uptime_get()}; + + memcpy(ev.channel_data, sensor_event.channel_data, + sizeof(struct zmk_sensor_channel_data) * sensor_event.channel_data_size); + k_msgq_put(&peripheral_sensor_event_msgq, &ev, K_NO_WAIT); + k_work_submit(&peripheral_sensor_event_work); + + return BT_GATT_ITER_CONTINUE; +} +#endif /* ZMK_KEYMAP_HAS_SENSORS */ + static uint8_t split_central_notify_func(struct bt_conn *conn, struct bt_gatt_subscribe_params *params, const void *data, uint16_t length) { @@ -209,14 +258,8 @@ static uint8_t split_central_notify_func(struct bt_conn *conn, return BT_GATT_ITER_CONTINUE; } -static void split_central_subscribe(struct bt_conn *conn) { - struct peripheral_slot *slot = peripheral_slot_for_conn(conn); - if (slot == NULL) { - LOG_ERR("No peripheral state found for connection"); - return; - } - - int err = bt_gatt_subscribe(conn, &slot->subscribe_params); +static int split_central_subscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *params) { + int err = bt_gatt_subscribe(conn, params); switch (err) { case -EALREADY: LOG_DBG("[ALREADY SUBSCRIBED]"); @@ -228,6 +271,8 @@ static void split_central_subscribe(struct bt_conn *conn) { LOG_ERR("Subscribe failed (err %d)", err); break; } + + return err; } static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn, @@ -250,9 +295,9 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn, } LOG_DBG("[ATTRIBUTE] handle %u", attr->handle); + const struct bt_uuid *chrc_uuid = ((struct bt_gatt_chrc *)attr->user_data)->uuid; - if (bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid, - BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_POSITION_STATE_UUID)) == 0) { + if (bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_POSITION_STATE_UUID)) == 0) { LOG_DBG("Found position state characteristic"); slot->discover_params.uuid = NULL; slot->discover_params.start_handle = attr->handle + 2; @@ -263,14 +308,33 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn, slot->subscribe_params.value_handle = bt_gatt_attr_value_handle(attr); slot->subscribe_params.notify = split_central_notify_func; slot->subscribe_params.value = BT_GATT_CCC_NOTIFY; - split_central_subscribe(conn); - } else if (bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid, - BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_RUN_BEHAVIOR_UUID)) == 0) { + split_central_subscribe(conn, &slot->subscribe_params); +#if ZMK_KEYMAP_HAS_SENSORS + } else if (bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_SENSOR_STATE_UUID)) == + 0) { + slot->discover_params.uuid = NULL; + slot->discover_params.start_handle = attr->handle + 2; + slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; + + slot->sensor_subscribe_params.disc_params = &slot->sub_discover_params; + slot->sensor_subscribe_params.end_handle = slot->discover_params.end_handle; + slot->sensor_subscribe_params.value_handle = bt_gatt_attr_value_handle(attr); + slot->sensor_subscribe_params.notify = split_central_sensor_notify_func; + slot->sensor_subscribe_params.value = BT_GATT_CCC_NOTIFY; + split_central_subscribe(conn, &slot->sensor_subscribe_params); +#endif /* ZMK_KEYMAP_HAS_SENSORS */ + } else if (bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_RUN_BEHAVIOR_UUID)) == + 0) { LOG_DBG("Found run behavior handle"); + slot->discover_params.uuid = NULL; + slot->discover_params.start_handle = attr->handle + 2; slot->run_behavior_handle = bt_gatt_attr_value_handle(attr); } - bool subscribed = (slot->run_behavior_handle && slot->subscribe_params.value_handle); + bool subscribed = slot->run_behavior_handle && slot->subscribe_params.value_handle; +#if ZMK_KEYMAP_HAS_SENSORS + subscribed = subscribed && slot->sensor_subscribe_params.value_handle; +#endif /* ZMK_KEYMAP_HAS_SENSORS */ return subscribed ? BT_GATT_ITER_STOP : BT_GATT_ITER_CONTINUE; } diff --git a/app/src/split/bluetooth/service.c b/app/src/split/bluetooth/service.c index f7b0d587b26..620df53e11c 100644 --- a/app/src/split/bluetooth/service.c +++ b/app/src/split/bluetooth/service.c @@ -4,6 +4,7 @@ * SPDX-License-Identifier: MIT */ +#include #include #include #include @@ -20,6 +21,22 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include +#include + +#if ZMK_KEYMAP_HAS_SENSORS +static struct sensor_event last_sensor_event; + +static ssize_t split_svc_sensor_state(struct bt_conn *conn, const struct bt_gatt_attr *attrs, + void *buf, uint16_t len, uint16_t offset) { + return bt_gatt_attr_read(conn, attrs, buf, len, offset, &last_sensor_event, + sizeof(last_sensor_event)); +} + +static void split_svc_sensor_state_ccc(const struct bt_gatt_attr *attr, uint16_t value) { + LOG_DBG("value %d", value); +} +#endif /* ZMK_KEYMAP_HAS_SENSORS */ #define POS_STATE_LEN 16 @@ -98,7 +115,14 @@ BT_GATT_SERVICE_DEFINE( BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE_ENCRYPT, NULL, split_svc_run_behavior, &behavior_run_payload), BT_GATT_DESCRIPTOR(BT_UUID_NUM_OF_DIGITALS, BT_GATT_PERM_READ, split_svc_num_of_positions, NULL, - &num_of_positions), ); + &num_of_positions), +#if ZMK_KEYMAP_HAS_SENSORS + BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_SENSOR_STATE_UUID), + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_READ_ENCRYPT, + split_svc_sensor_state, NULL, &last_sensor_event), + BT_GATT_CCC(split_svc_sensor_state_ccc, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), +#endif /* ZMK_KEYMAP_HAS_SENSORS */ +); K_THREAD_STACK_DEFINE(service_q_stack, CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE); @@ -151,6 +175,58 @@ int zmk_split_bt_position_released(uint8_t position) { return send_position_state(); } +#if ZMK_KEYMAP_HAS_SENSORS +K_MSGQ_DEFINE(sensor_state_msgq, sizeof(struct sensor_event), + CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_POSITION_QUEUE_SIZE, 4); + +void send_sensor_state_callback(struct k_work *work) { + while (k_msgq_get(&sensor_state_msgq, &last_sensor_event, K_NO_WAIT) == 0) { + int err = bt_gatt_notify(NULL, &split_svc.attrs[8], &last_sensor_event, + sizeof(last_sensor_event)); + if (err) { + LOG_DBG("Error notifying %d", err); + } + } +}; + +K_WORK_DEFINE(service_sensor_notify_work, send_sensor_state_callback); + +int send_sensor_state(struct sensor_event ev) { + int err = k_msgq_put(&sensor_state_msgq, &ev, K_MSEC(100)); + if (err) { + // retry... + switch (err) { + case -EAGAIN: { + LOG_WRN("Sensor state message queue full, popping first message and queueing again"); + struct sensor_event discarded_state; + k_msgq_get(&sensor_state_msgq, &discarded_state, K_NO_WAIT); + return send_sensor_state(ev); + } + default: + LOG_WRN("Failed to queue sensor state to send (%d)", err); + return err; + } + } + + k_work_submit_to_queue(&service_work_q, &service_sensor_notify_work); + return 0; +} + +int zmk_split_bt_sensor_triggered(uint8_t sensor_index, + const struct zmk_sensor_channel_data channel_data[], + size_t channel_data_size) { + if (channel_data_size > ZMK_SENSOR_EVENT_MAX_CHANNELS) { + return -EINVAL; + } + + struct sensor_event ev = + (struct sensor_event){.sensor_index = sensor_index, .channel_data_size = channel_data_size}; + memcpy(ev.channel_data, channel_data, + channel_data_size * sizeof(struct zmk_sensor_channel_data)); + return send_sensor_state(ev); +} +#endif /* ZMK_KEYMAP_HAS_SENSORS */ + int service_init(const struct device *_arg) { static const struct k_work_queue_config queue_config = { .name = "Split Peripheral Notification Queue"}; diff --git a/app/src/split/bluetooth/split_listener.c b/app/src/split/bluetooth/split_listener.c index eb5398c42d6..9b680d2c89c 100644 --- a/app/src/split/bluetooth/split_listener.c +++ b/app/src/split/bluetooth/split_listener.c @@ -13,21 +13,35 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include +#include #include +#include #include int split_listener(const zmk_event_t *eh) { LOG_DBG(""); - const struct zmk_position_state_changed *ev = as_zmk_position_state_changed(eh); - if (ev != NULL) { - if (ev->state) { - return zmk_split_bt_position_pressed(ev->position); + const struct zmk_position_state_changed *pos_ev; + if ((pos_ev = as_zmk_position_state_changed(eh)) != NULL) { + if (pos_ev->state) { + return zmk_split_bt_position_pressed(pos_ev->position); } else { - return zmk_split_bt_position_released(ev->position); + return zmk_split_bt_position_released(pos_ev->position); } } + +#if ZMK_KEYMAP_HAS_SENSORS + const struct zmk_sensor_event *sensor_ev; + if ((sensor_ev = as_zmk_sensor_event(eh)) != NULL) { + return zmk_split_bt_sensor_triggered(sensor_ev->sensor_index, sensor_ev->channel_data, + sensor_ev->channel_data_size); + } +#endif /* ZMK_KEYMAP_HAS_SENSORS */ return ZMK_EV_EVENT_BUBBLE; } ZMK_LISTENER(split_listener, split_listener); -ZMK_SUBSCRIPTION(split_listener, zmk_position_state_changed); \ No newline at end of file +ZMK_SUBSCRIPTION(split_listener, zmk_position_state_changed); + +#if ZMK_KEYMAP_HAS_SENSORS +ZMK_SUBSCRIPTION(split_listener, zmk_sensor_event); +#endif /* ZMK_KEYMAP_HAS_SENSORS */ diff --git a/docs/docs/features/encoders.md b/docs/docs/features/encoders.md index 29906c909de..0c493330cc6 100644 --- a/docs/docs/features/encoders.md +++ b/docs/docs/features/encoders.md @@ -5,10 +5,6 @@ sidebar_label: Encoders Existing support for encoders in ZMK is focused around the five pin EC11 rotary encoder with push button design used in the majority of current keyboard and macropad designs. -:::note -Encoders are currently only support on the left/central sides of splits. For progress on this, see [#728](https://github.com/zmkfirmware/zmk/pull/728). -::: - ## Enabling EC11 Encoders To enable encoders for boards that have existing encoder support, uncomment the `CONFIG_EC11=y` and `CONFIG_EC11_TRIGGER_GLOBAL_THREAD=y` lines in your board's .conf file in your `zmk-config/config` folder. Save and push your changes, then download and flash the new firmware. diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 142dcafc97e..d65ac46e405 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -23,11 +23,11 @@ ZMK is currently missing some features found in other popular firmware. This tab | Split Keyboard Support | ✅ | ✅ | ✅ | | [Keymaps and Layers](behaviors/layers.md) | ✅ | ✅ | ✅ | | [Hold-Tap](behaviors/hold-tap.md) (which includes [Mod-Tap](behaviors/mod-tap.md) and [Layer-Tap](behaviors/layers.md/#layer-tap)) | ✅ | ✅ | ✅ | -| [Tap-Dance](behaviors/tap-dance.md) | ✅ | ✅[^3] | ✅ | +| [Tap-Dance](behaviors/tap-dance.md) | ✅ | ✅[^2] | ✅ | | [Keyboard Codes](codes/index.mdx#keyboard) | ✅ | ✅ | ✅ | | [Media](codes/index.mdx#media-controls) & [Consumer](codes/index.mdx#consumer-controls) Codes | ✅ | ✅ | ✅ | -| [Encoders](features/encoders.md)[^1] | ✅ | ✅ | ✅ | -| [Display Support](features/displays.md)[^2] | 🚧 | 🚧 | ✅ | +| [Encoders](features/encoders.md) | ✅ | ✅ | ✅ | +| [Display Support](features/displays.md)[^1] | 🚧 | 🚧 | ✅ | | [RGB Underglow](features/underglow.md) | ✅ | ✅ | ✅ | | [Backlight](features/backlight.md) | ✅ | ✅ | ✅ | | One Shot Keys | ✅ | ✅ | ✅ | @@ -43,8 +43,7 @@ ZMK is currently missing some features found in other popular firmware. This tab | AVR/8 Bit | | | ✅ | | [Wide Range of ARM Chips Supported](https://docs.zephyrproject.org/latest/boards/index.html) | ✅ | | | -[^3]: Tap-Dances are limited to single and double-tap on BlueMicro -[^2]: Encoders are not currently supported on peripheral side splits. +[^2]: Tap-Dances are limited to single and double-tap on BlueMicro [^1]: OLEDs are currently proof of concept in ZMK. ## Code Of Conduct