diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 83cf1bb8..e283487f 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -28,6 +28,7 @@ target_sources(app PRIVATE src/kscan.c) target_sources(app PRIVATE src/matrix_transform.c) target_sources(app PRIVATE src/hid.c) target_sources(app PRIVATE src/sensors.c) +target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/wpm.c) target_sources(app PRIVATE src/event_manager.c) target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/ext_power_generic.c) target_sources(app PRIVATE src/events/activity_state_changed.c) @@ -36,6 +37,7 @@ target_sources(app PRIVATE src/events/layer_state_changed.c) target_sources(app PRIVATE src/events/keycode_state_changed.c) target_sources(app PRIVATE src/events/modifiers_state_changed.c) target_sources(app PRIVATE src/events/sensor_event.c) +target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/events/wpm_state_changed.c) target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/events/ble_active_profile_changed.c) target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/events/battery_state_changed.c) target_sources_ifdef(CONFIG_USB app PRIVATE src/events/usb_conn_state_changed.c) diff --git a/app/Kconfig b/app/Kconfig index df00f1db..737895c7 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -417,6 +417,10 @@ config REBOOT config USB default y if HAS_HW_NRF_USBD +config ZMK_WPM + bool "Calculate WPM" + default n + module = ZMK module-str = zmk source "subsys/logging/Kconfig.template.log_config" diff --git a/app/include/zmk/display/widgets/wpm_status.h b/app/include/zmk/display/widgets/wpm_status.h new file mode 100644 index 00000000..0592299e --- /dev/null +++ b/app/include/zmk/display/widgets/wpm_status.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +struct zmk_widget_wpm_status { + sys_snode_t node; + lv_obj_t *obj; +}; + +int zmk_widget_wpm_status_init(struct zmk_widget_wpm_status *widget, lv_obj_t *parent); +lv_obj_t *zmk_widget_wpm_status_obj(struct zmk_widget_wpm_status *widget); \ No newline at end of file diff --git a/app/include/zmk/events/wpm_state_changed.h b/app/include/zmk/events/wpm_state_changed.h new file mode 100644 index 00000000..3d1a3695 --- /dev/null +++ b/app/include/zmk/events/wpm_state_changed.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include + +struct zmk_wpm_state_changed { + int state; +}; + +ZMK_EVENT_DECLARE(zmk_wpm_state_changed); diff --git a/app/include/zmk/wpm.h b/app/include/zmk/wpm.h new file mode 100644 index 00000000..6db100a0 --- /dev/null +++ b/app/include/zmk/wpm.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +int zmk_wpm_get_state(); \ No newline at end of file diff --git a/app/src/display/status_screen.c b/app/src/display/status_screen.c index 0c88717a..6f32b283 100644 --- a/app/src/display/status_screen.c +++ b/app/src/display/status_screen.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -24,6 +25,10 @@ static struct zmk_widget_output_status output_status_widget; static struct zmk_widget_layer_status layer_status_widget; #endif +#if IS_ENABLED(CONFIG_ZMK_WIDGET_WPM_STATUS) +static struct zmk_widget_wpm_status wpm_status_widget; +#endif + lv_obj_t *zmk_display_status_screen() { lv_obj_t *screen; @@ -47,5 +52,10 @@ lv_obj_t *zmk_display_status_screen() { 0, 0); #endif +#if IS_ENABLED(CONFIG_ZMK_WIDGET_WPM_STATUS) + zmk_widget_wpm_status_init(&wpm_status_widget, screen); + lv_obj_align(zmk_widget_wpm_status_obj(&wpm_status_widget), NULL, LV_ALIGN_IN_BOTTOM_RIGHT, -12, + 0); +#endif return screen; } diff --git a/app/src/display/widgets/CMakeLists.txt b/app/src/display/widgets/CMakeLists.txt index ad7e132a..1d115dcc 100644 --- a/app/src/display/widgets/CMakeLists.txt +++ b/app/src/display/widgets/CMakeLists.txt @@ -4,3 +4,4 @@ target_sources_ifdef(CONFIG_ZMK_WIDGET_BATTERY_STATUS app PRIVATE battery_status.c) target_sources_ifdef(CONFIG_ZMK_WIDGET_OUTPUT_STATUS app PRIVATE output_status.c) target_sources_ifdef(CONFIG_ZMK_WIDGET_LAYER_STATUS app PRIVATE layer_status.c) +target_sources_ifdef(CONFIG_ZMK_WIDGET_WPM_STATUS app PRIVATE wpm_status.c) diff --git a/app/src/display/widgets/Kconfig b/app/src/display/widgets/Kconfig index bdb3024e..f12d99a3 100644 --- a/app/src/display/widgets/Kconfig +++ b/app/src/display/widgets/Kconfig @@ -22,5 +22,12 @@ config ZMK_WIDGET_OUTPUT_STATUS default y if BT select LVGL_USE_LABEL select LVGL_FONT_MONTSERRAT_16 + +config ZMK_WIDGET_WPM_STATUS + bool "Widget for displaying typed words per minute" + depends on !ZMK_SPLIT || ZMK_SPLIT_BLE_ROLE_CENTRAL + select LVGL_USE_LABEL + select LVGL_FONT_MONTSERRAT_16 + select ZMK_WPM endmenu diff --git a/app/src/display/widgets/wpm_status.c b/app/src/display/widgets/wpm_status.c new file mode 100644 index 00000000..41ee3685 --- /dev/null +++ b/app/src/display/widgets/wpm_status.c @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include +#include +#include +#include + +static sys_slist_t widgets = SYS_SLIST_STATIC_INIT(&widgets); +static lv_style_t label_style; + +static bool style_initialized = false; + +void wpm_status_init() { + if (style_initialized) { + return; + } + + style_initialized = true; + lv_style_init(&label_style); + lv_style_set_text_color(&label_style, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_style_set_text_font(&label_style, LV_STATE_DEFAULT, &lv_font_montserrat_12); + lv_style_set_text_letter_space(&label_style, LV_STATE_DEFAULT, 1); + lv_style_set_text_line_space(&label_style, LV_STATE_DEFAULT, 1); +} + +void set_wpm_symbol(lv_obj_t *label, int wpm) { + char text[4] = {}; + + LOG_DBG("WPM changed to %i", wpm); + sprintf(text, "%i ", wpm); + + lv_label_set_text(label, text); +} + +int zmk_widget_wpm_status_init(struct zmk_widget_wpm_status *widget, lv_obj_t *parent) { + wpm_status_init(); + widget->obj = lv_label_create(parent, NULL); + lv_obj_add_style(widget->obj, LV_LABEL_PART_MAIN, &label_style); + lv_label_set_align(widget->obj, LV_LABEL_ALIGN_RIGHT); + + lv_obj_set_size(widget->obj, 40, 15); + set_wpm_symbol(widget->obj, 0); + + sys_slist_append(&widgets, &widget->node); + + return 0; +} + +lv_obj_t *zmk_widget_wpm_status_obj(struct zmk_widget_wpm_status *widget) { return widget->obj; } + +int wpm_status_listener(const zmk_event_t *eh) { + struct zmk_wpm_state_changed *ev = as_zmk_wpm_state_changed(eh); + struct zmk_widget_wpm_status *widget; + SYS_SLIST_FOR_EACH_CONTAINER(&widgets, widget, node) { set_wpm_symbol(widget->obj, ev->state); } + return 0; +} + +ZMK_LISTENER(widget_wpm_status, wpm_status_listener) +ZMK_SUBSCRIPTION(widget_wpm_status, zmk_wpm_state_changed); \ No newline at end of file diff --git a/app/src/events/wpm_state_changed.c b/app/src/events/wpm_state_changed.c new file mode 100644 index 00000000..3d9830bf --- /dev/null +++ b/app/src/events/wpm_state_changed.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_wpm_state_changed); \ No newline at end of file diff --git a/app/src/wpm.c b/app/src/wpm.c new file mode 100644 index 00000000..bcabf377 --- /dev/null +++ b/app/src/wpm.c @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include +#include + +#include + +#define WPM_UPDATE_INTERVAL_SECONDS 1 +#define WPM_RESET_INTERVAL_SECONDS 5 + +// See https://en.wikipedia.org/wiki/Words_per_minute +// "Since the length or duration of words is clearly variable, for the purpose of measurement of +// text entry, the definition of each "word" is often standardized to be five characters or +// keystrokes long in English" +#define CHARS_PER_WORD 5.0 + +static uint8_t wpm_state = -1; +static uint8_t last_wpm_state; +static uint8_t wpm_update_counter; +static uint32_t key_pressed_count; + +int zmk_wpm_get_state() { return wpm_state; } + +int wpm_event_listener(const zmk_event_t *eh) { + const struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh); + if (ev) { + // count only key up events + if (!ev->state) { + key_pressed_count++; + LOG_DBG("key_pressed_count %d keycode %d", key_pressed_count, ev->keycode); + } + } + return 0; +} + +void wpm_work_handler(struct k_work *work) { + wpm_update_counter++; + wpm_state = (key_pressed_count / CHARS_PER_WORD) / + (wpm_update_counter * WPM_UPDATE_INTERVAL_SECONDS / 60.0); + + if (last_wpm_state != wpm_state) { + LOG_DBG("Raised WPM state changed %d wpm_update_counter %d", wpm_state, wpm_update_counter); + + ZMK_EVENT_RAISE( + new_zmk_wpm_state_changed((struct zmk_wpm_state_changed){.state = wpm_state})); + + last_wpm_state = wpm_state; + } + + if (wpm_update_counter >= WPM_RESET_INTERVAL_SECONDS) { + wpm_update_counter = 0; + key_pressed_count = 0; + } +} + +K_WORK_DEFINE(wpm_work, wpm_work_handler); + +void wpm_expiry_function() { k_work_submit(&wpm_work); } + +K_TIMER_DEFINE(wpm_timer, wpm_expiry_function, NULL); + +int wpm_init() { + wpm_state = 0; + wpm_update_counter = 0; + k_timer_start(&wpm_timer, K_SECONDS(WPM_UPDATE_INTERVAL_SECONDS), + K_SECONDS(WPM_UPDATE_INTERVAL_SECONDS)); + return 0; +} + +ZMK_LISTENER(wpm, wpm_event_listener); +ZMK_SUBSCRIPTION(wpm, zmk_keycode_state_changed); + +SYS_INIT(wpm_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/tests/wpm/1-single_keypress/events.patterns b/app/tests/wpm/1-single_keypress/events.patterns new file mode 100644 index 00000000..c49e6c00 --- /dev/null +++ b/app/tests/wpm/1-single_keypress/events.patterns @@ -0,0 +1,2 @@ +s/.*wpm_work_handler: //p +s/.*wpm_event_listener: //p \ No newline at end of file diff --git a/app/tests/wpm/1-single_keypress/keycode_events.snapshot b/app/tests/wpm/1-single_keypress/keycode_events.snapshot new file mode 100644 index 00000000..c86f323b --- /dev/null +++ b/app/tests/wpm/1-single_keypress/keycode_events.snapshot @@ -0,0 +1,7 @@ +key_pressed_count 1 keycode 5 +Raised WPM state changed 12 wpm_update_counter 1 +Raised WPM state changed 6 wpm_update_counter 2 +Raised WPM state changed 4 wpm_update_counter 3 +Raised WPM state changed 3 wpm_update_counter 4 +Raised WPM state changed 2 wpm_update_counter 5 +Raised WPM state changed 0 wpm_update_counter 1 diff --git a/app/tests/wpm/1-single_keypress/native_posix.conf b/app/tests/wpm/1-single_keypress/native_posix.conf new file mode 100644 index 00000000..360e77d5 --- /dev/null +++ b/app/tests/wpm/1-single_keypress/native_posix.conf @@ -0,0 +1,9 @@ +CONFIG_KSCAN=n +CONFIG_ZMK_KSCAN_MOCK_DRIVER=y +CONFIG_ZMK_KSCAN_GPIO_DRIVER=n +CONFIG_GPIO=n +CONFIG_ZMK_BLE=n +CONFIG_LOG=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_ZMK_LOG_LEVEL_DBG=y +CONFIG_ZMK_WPM=y \ No newline at end of file diff --git a/app/tests/wpm/1-single_keypress/native_posix.keymap b/app/tests/wpm/1-single_keypress/native_posix.keymap new file mode 100644 index 00000000..ec12a286 --- /dev/null +++ b/app/tests/wpm/1-single_keypress/native_posix.keymap @@ -0,0 +1,10 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + /* Wait for the worker to trigger and reset after 5 seconds, followed by a 0 at 6 seconds */ + ZMK_MOCK_PRESS(0,0,6000) + >; +}; \ No newline at end of file diff --git a/app/tests/wpm/2-multiple_keypress/events.patterns b/app/tests/wpm/2-multiple_keypress/events.patterns new file mode 100644 index 00000000..c49e6c00 --- /dev/null +++ b/app/tests/wpm/2-multiple_keypress/events.patterns @@ -0,0 +1,2 @@ +s/.*wpm_work_handler: //p +s/.*wpm_event_listener: //p \ No newline at end of file diff --git a/app/tests/wpm/2-multiple_keypress/keycode_events.snapshot b/app/tests/wpm/2-multiple_keypress/keycode_events.snapshot new file mode 100644 index 00000000..a0e8f7a8 --- /dev/null +++ b/app/tests/wpm/2-multiple_keypress/keycode_events.snapshot @@ -0,0 +1,4 @@ +key_pressed_count 1 keycode 5 +Raised WPM state changed 12 wpm_update_counter 1 +key_pressed_count 2 keycode 5 +Raised WPM state changed 8 wpm_update_counter 3 diff --git a/app/tests/wpm/2-multiple_keypress/native_posix.conf b/app/tests/wpm/2-multiple_keypress/native_posix.conf new file mode 100644 index 00000000..f0e1a480 --- /dev/null +++ b/app/tests/wpm/2-multiple_keypress/native_posix.conf @@ -0,0 +1,9 @@ +CONFIG_KSCAN=n +CONFIG_ZMK_KSCAN_MOCK_DRIVER=y +CONFIG_ZMK_KSCAN_GPIO_DRIVER=n +CONFIG_GPIO=n +CONFIG_ZMK_BLE=n +CONFIG_LOG=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_ZMK_LOG_LEVEL_DBG=y +CONFIG_ZMK_WPM=y diff --git a/app/tests/wpm/2-multiple_keypress/native_posix.keymap b/app/tests/wpm/2-multiple_keypress/native_posix.keymap new file mode 100644 index 00000000..f4ba2dfe --- /dev/null +++ b/app/tests/wpm/2-multiple_keypress/native_posix.keymap @@ -0,0 +1,15 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + //1st WPM worker call - 12wpm - 1 key press in 1 second + ZMK_MOCK_PRESS(0,0,1000) + ZMK_MOCK_RELEASE(0,0,10) + // 2nd WPM worker call - 12wpm - 2 key press in 2 second + // note there is no event for this as WPM hasn't changed + // 3rd WPM worker call - 8wpm - 2 key press in 3 seconds + ZMK_MOCK_PRESS(0,0,2000) + >; +}; \ No newline at end of file diff --git a/app/tests/wpm/behavior_keymap.dtsi b/app/tests/wpm/behavior_keymap.dtsi new file mode 100644 index 00000000..f0c5d0c2 --- /dev/null +++ b/app/tests/wpm/behavior_keymap.dtsi @@ -0,0 +1,17 @@ +#include +#include +#include + +/ { + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp B &none + &none &none + >; + }; + }; +};