diff --git a/doc/services/input/index.rst b/doc/services/input/index.rst index 111d6a0268a..e42af58ecbc 100644 --- a/doc/services/input/index.rst +++ b/doc/services/input/index.rst @@ -100,6 +100,8 @@ General Purpose Drivers matrix to key events. - :dtcompatible:`zephyr,input-longpress`: listens for key events, emits events for short and long press. +- :dtcompatible:`zephyr,input-double-tap`: listens for key events, emits events + for input double taps - :dtcompatible:`zephyr,lvgl-button-input` :dtcompatible:`zephyr,lvgl-encoder-input` :dtcompatible:`zephyr,lvgl-keypad-input` diff --git a/dts/bindings/input/zephyr,input-double-tap.yaml b/dts/bindings/input/zephyr,input-double-tap.yaml new file mode 100644 index 00000000000..18d297f88f1 --- /dev/null +++ b/dts/bindings/input/zephyr,input-double-tap.yaml @@ -0,0 +1,48 @@ +# Copyright 2024 Kelly Helmut Lord +# SPDX-License-Identifier: Apache-2.0 + +description: | + Input double tap pseudo-device + + Listens for key events as an input and produces key events as output + corresponding to double taps. + + Can be optionally be associated to a specific device to listen for events + only from that device. + + Example configuration: + + #include + + double_tap { + input = <&buttons>; + compatible = "zephyr,input-double-tap"; + input-codes = , ; + double-tap-codes = , ; + double-tap-delay-ms = <300>; + }; + +compatible: "zephyr,input-double-tap" + +properties: + input: + type: phandle + description: | + Input device phandle, if not specified listen for input from all devices. + + input-codes: + type: array + required: true + description: | + Array of input event key codes (INPUT_KEY_* or INPUT_BTN_*). + + double-tap-codes: + type: array + required: true + description: | + Array of key codes to be generated for double taps (INPUT_KEY_* or INPUT_BTN_*). + + double-tap-delay-ms: + type: int + required: true + description: Maximum time delay between taps to register a double tap, in milliseconds. diff --git a/subsys/input/CMakeLists.txt b/subsys/input/CMakeLists.txt index f536787d1ea..9c8be141b31 100644 --- a/subsys/input/CMakeLists.txt +++ b/subsys/input/CMakeLists.txt @@ -8,3 +8,4 @@ zephyr_library_sources(input_utils.c) zephyr_library_sources_ifdef(CONFIG_INPUT_KEYMAP input_keymap.c) zephyr_library_sources_ifdef(CONFIG_INPUT_LONGPRESS input_longpress.c) +zephyr_library_sources_ifdef(CONFIG_INPUT_DOUBLE_TAP input_double_tap.c) diff --git a/subsys/input/Kconfig b/subsys/input/Kconfig index 3678f6da28b..c9789c3680a 100644 --- a/subsys/input/Kconfig +++ b/subsys/input/Kconfig @@ -95,4 +95,11 @@ config INPUT_KEYMAP help Enable the input keymap driver. +config INPUT_DOUBLE_TAP + bool "Input double tap" + default y + depends on DT_HAS_ZEPHYR_INPUT_DOUBLE_TAP_ENABLED + help + Enable the input double tap driver. + endif # INPUT diff --git a/subsys/input/input_double_tap.c b/subsys/input/input_double_tap.c new file mode 100644 index 00000000000..1928393a637 --- /dev/null +++ b/subsys/input/input_double_tap.c @@ -0,0 +1,125 @@ +/* + * Copyright 2024 Kelly Helmut Lord + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT zephyr_input_double_tap + +#include +#include +#include + +#include +LOG_MODULE_REGISTER(input_double_tap, CONFIG_INPUT_LOG_LEVEL); + +struct double_tap_config { + const struct device *input_dev; + struct double_tap_data_entry *entries; + const uint16_t *input_codes; + const uint16_t *double_tap_codes; + uint32_t double_tap_delay_ms; + uint8_t num_codes; +}; + +struct double_tap_data_entry { + const struct device *dev; + struct k_work_delayable work; + uint8_t index; + bool first_tap; +}; + +static void double_tap_deferred(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct double_tap_data_entry *entry = + CONTAINER_OF(dwork, struct double_tap_data_entry, work); + + entry->first_tap = false; +} + +static void double_tap_cb(struct input_event *evt, void *user_data) +{ + const struct device *dev = user_data; + const struct double_tap_config *cfg = dev->config; + struct double_tap_data_entry *entry; + int i; + + if (evt->type != INPUT_EV_KEY) { + return; + } + + for (i = 0; i < cfg->num_codes; i++) { + if (evt->code == cfg->input_codes[i]) { + break; + } + } + if (i == cfg->num_codes) { + LOG_DBG("ignored code %d", evt->code); + return; + } + + entry = &cfg->entries[i]; + + if (evt->value) { + if (entry->first_tap) { + k_work_cancel_delayable(&entry->work); + input_report_key(dev, cfg->double_tap_codes[i], 1, true, K_FOREVER); + input_report_key(dev, cfg->double_tap_codes[i], 0, true, K_FOREVER); + entry->first_tap = false; + } else { + entry->first_tap = true; + k_work_schedule(&entry->work, K_MSEC(cfg->double_tap_delay_ms)); + } + } +} + +static int double_tap_init(const struct device *dev) +{ + const struct double_tap_config *cfg = dev->config; + + if (cfg->input_dev && !device_is_ready(cfg->input_dev)) { + LOG_ERR("input device not ready"); + return -ENODEV; + } + + for (int i = 0; i < cfg->num_codes; i++) { + struct double_tap_data_entry *entry = &cfg->entries[i]; + + entry->dev = dev; + entry->index = i; + entry->first_tap = false; + k_work_init_delayable(&entry->work, double_tap_deferred); + } + + return 0; +} + +#define INPUT_DOUBLE_TAP_DEFINE(inst) \ + BUILD_ASSERT(DT_INST_PROP_LEN(inst, input_codes) == \ + DT_INST_PROP_LEN(inst, double_tap_codes)); \ + \ + INPUT_CALLBACK_DEFINE_NAMED(DEVICE_DT_GET_OR_NULL(DT_INST_PHANDLE(inst, input)), \ + double_tap_cb, (void *)DEVICE_DT_INST_GET(inst), \ + double_tap_cb_##inst); \ + \ + static const uint16_t double_tap_input_codes_##inst[] = DT_INST_PROP(inst, input_codes); \ + \ + static const uint16_t double_tap_codes_##inst[] = DT_INST_PROP(inst, double_tap_codes); \ + \ + static struct double_tap_data_entry \ + double_tap_data_entries_##inst[DT_INST_PROP_LEN(inst, input_codes)]; \ + \ + static const struct double_tap_config double_tap_config_##inst = { \ + .input_dev = DEVICE_DT_GET_OR_NULL(DT_INST_PHANDLE(inst, input)), \ + .entries = double_tap_data_entries_##inst, \ + .input_codes = double_tap_input_codes_##inst, \ + .double_tap_codes = double_tap_codes_##inst, \ + .num_codes = DT_INST_PROP_LEN(inst, input_codes), \ + .double_tap_delay_ms = DT_INST_PROP(inst, double_tap_delay_ms), \ + }; \ + \ + DEVICE_DT_INST_DEFINE(inst, double_tap_init, NULL, NULL, &double_tap_config_##inst, \ + POST_KERNEL, CONFIG_INPUT_INIT_PRIORITY, NULL); + +DT_INST_FOREACH_STATUS_OKAY(INPUT_DOUBLE_TAP_DEFINE) diff --git a/tests/drivers/build_all/input/app.overlay b/tests/drivers/build_all/input/app.overlay index a0216c0d789..06e91145fa1 100644 --- a/tests/drivers/build_all/input/app.overlay +++ b/tests/drivers/build_all/input/app.overlay @@ -154,6 +154,14 @@ long-delay-ms = <100>; }; + double_tap: doubletap { + input = <&double_tap>; + compatible = "zephyr,input-double-tap"; + input-codes = <0>; + double-tap-codes = <0>; + double-tap-delay-ms = <0>; + }; + i2c@1 { #address-cells = <1>; #size-cells = <0>; diff --git a/tests/subsys/input/double_tap/CMakeLists.txt b/tests/subsys/input/double_tap/CMakeLists.txt new file mode 100644 index 00000000000..55f5841e093 --- /dev/null +++ b/tests/subsys/input/double_tap/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +project(input_double_tap) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/subsys/input/double_tap/boards/native_sim.overlay b/tests/subsys/input/double_tap/boards/native_sim.overlay new file mode 100644 index 00000000000..fa74ae609a7 --- /dev/null +++ b/tests/subsys/input/double_tap/boards/native_sim.overlay @@ -0,0 +1,21 @@ +/* + * Copyright 2024 Kelly Helmut Lord + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +/ { + fake_input_device: fake-device { + compatible = "vnd,input-device"; + }; + + double_tap: doubletap { + input = <&fake_input_device>; + compatible = "zephyr,input-double-tap"; + input-codes = , ; + double-tap-codes = , ; + double-tap-delay-ms = <300>; + }; +}; diff --git a/tests/subsys/input/double_tap/boards/native_sim_native_64.overlay b/tests/subsys/input/double_tap/boards/native_sim_native_64.overlay new file mode 100644 index 00000000000..091e5d6abd6 --- /dev/null +++ b/tests/subsys/input/double_tap/boards/native_sim_native_64.overlay @@ -0,0 +1,6 @@ +/* + * Copyright 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "native_sim.overlay" diff --git a/tests/subsys/input/double_tap/prj.conf b/tests/subsys/input/double_tap/prj.conf new file mode 100644 index 00000000000..bc713698bbf --- /dev/null +++ b/tests/subsys/input/double_tap/prj.conf @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_ZTEST=y +CONFIG_INPUT=y +CONFIG_INPUT_MODE_SYNCHRONOUS=y diff --git a/tests/subsys/input/double_tap/src/main.c b/tests/subsys/input/double_tap/src/main.c new file mode 100644 index 00000000000..34c30d62444 --- /dev/null +++ b/tests/subsys/input/double_tap/src/main.c @@ -0,0 +1,78 @@ +/* + * Copyright 2024 Kelly Helmut Lord + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +static const struct device *const fake_dev = DEVICE_DT_GET( + DT_NODELABEL(fake_input_device)); +static const struct device *const double_tap_dev = DEVICE_DT_GET( + DT_NODELABEL(double_tap)); + +DEVICE_DT_DEFINE(DT_INST(0, vnd_input_device), NULL, NULL, NULL, NULL, + PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEVICE, NULL); + +static int event_count; +static struct input_event last_events[2]; + +static void test_cb(struct input_event *evt, void *user_data) +{ + TC_PRINT("%s: %d %x %d\n", __func__, event_count, evt->code, evt->value); + + event_count++; + memcpy(&last_events[1], &last_events[0], sizeof(struct input_event)); + memcpy(&last_events[0], evt, sizeof(struct input_event)); +} +INPUT_CALLBACK_DEFINE(double_tap_dev, test_cb, NULL); + +ZTEST(double_tap, test_double_tap_test) +{ + zassert_equal(event_count, 0); + + /* ignored */ + input_report_key(fake_dev, INPUT_KEY_3, 1, true, K_FOREVER); + input_report_key(fake_dev, INPUT_KEY_3, 0, true, K_FOREVER); + zassert_equal(event_count, 0); + input_report_abs(fake_dev, INPUT_KEY_0, 1, true, K_FOREVER); + input_report_abs(fake_dev, INPUT_KEY_0, 0, true, K_FOREVER); + zassert_equal(event_count, 0); + + /* double tap*/ + input_report_key(fake_dev, INPUT_KEY_0, 1, true, K_FOREVER); + k_sleep(K_MSEC(50)); + input_report_key(fake_dev, INPUT_KEY_0, 0, true, K_FOREVER); + k_sleep(K_MSEC(50)); + input_report_key(fake_dev, INPUT_KEY_0, 1, true, K_FOREVER); + k_sleep(K_MSEC(50)); + input_report_key(fake_dev, INPUT_KEY_0, 0, true, K_FOREVER); + zassert_equal(event_count, 2); + zassert_equal(last_events[1].type, INPUT_EV_KEY); + zassert_equal(last_events[1].code, INPUT_KEY_X); + zassert_equal(last_events[1].value, 1); + zassert_equal(last_events[0].type, INPUT_EV_KEY); + zassert_equal(last_events[0].code, INPUT_KEY_X); + zassert_equal(last_events[0].value, 0); + + /* double tap - other key */ + input_report_key(fake_dev, INPUT_KEY_1, 1, true, K_FOREVER); + k_sleep(K_MSEC(50)); + input_report_key(fake_dev, INPUT_KEY_1, 0, true, K_FOREVER); + k_sleep(K_MSEC(50)); + input_report_key(fake_dev, INPUT_KEY_1, 1, true, K_FOREVER); + k_sleep(K_MSEC(50)); + input_report_key(fake_dev, INPUT_KEY_1, 0, true, K_FOREVER); + zassert_equal(event_count, 4); + zassert_equal(last_events[1].type, INPUT_EV_KEY); + zassert_equal(last_events[1].code, INPUT_KEY_Y); + zassert_equal(last_events[1].value, 1); + zassert_equal(last_events[0].type, INPUT_EV_KEY); + zassert_equal(last_events[0].code, INPUT_KEY_Y); + zassert_equal(last_events[0].value, 0); +} + +ZTEST_SUITE(double_tap, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/subsys/input/double_tap/testcase.yaml b/tests/subsys/input/double_tap/testcase.yaml new file mode 100644 index 00000000000..7460cd6ec7b --- /dev/null +++ b/tests/subsys/input/double_tap/testcase.yaml @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 + +tests: + input.input_double_tap: + platform_allow: + - native_sim + - native_sim/native/64 + tags: + - drivers + - input + integration_platforms: + - native_sim