From ed82063af5f83530afa5cfb5bf5bd516f3d05f2a Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 10 Apr 2024 16:56:10 +1000 Subject: [PATCH] WIP decompose our giant LVGL driver into smaller classes --- src/drivers/CMakeLists.txt | 3 +- src/drivers/include/relative_wheel.hpp | 52 --- src/drivers/relative_wheel.cpp | 92 ------ src/input/CMakeLists.txt | 11 + src/input/feedback_haptics.cpp | 37 +++ src/input/include/feedback_device.hpp | 32 ++ src/input/include/feedback_haptics.hpp | 26 ++ src/input/include/input_device.hpp | 33 ++ src/input/include/input_touch_dpad.hpp | 30 ++ src/input/include/input_touch_wheel.hpp | 37 +++ src/input/include/input_volume_buttons.hpp | 30 ++ src/input/include/lvgl_input_driver.hpp | 53 +++ src/input/input_touch_dpad.cpp | 35 ++ src/input/input_touch_wheel.cpp | 81 +++++ src/input/input_volume_buttons.cpp | 26 ++ src/input/lvgl_input_driver.cpp | 84 +++++ src/system_fsm/include/system_fsm.hpp | 1 - src/system_fsm/system_fsm.cpp | 1 - src/ui/CMakeLists.txt | 8 +- src/ui/encoder_input.cpp | 358 --------------------- src/ui/include/encoder_input.hpp | 107 ------ src/ui/include/lvgl_task.hpp | 7 +- src/ui/include/ui_fsm.hpp | 5 +- src/ui/lvgl_task.cpp | 15 +- src/ui/ui_fsm.cpp | 122 ++++--- 25 files changed, 584 insertions(+), 702 deletions(-) delete mode 100644 src/drivers/include/relative_wheel.hpp delete mode 100644 src/drivers/relative_wheel.cpp create mode 100644 src/input/CMakeLists.txt create mode 100644 src/input/feedback_haptics.cpp create mode 100644 src/input/include/feedback_device.hpp create mode 100644 src/input/include/feedback_haptics.hpp create mode 100644 src/input/include/input_device.hpp create mode 100644 src/input/include/input_touch_dpad.hpp create mode 100644 src/input/include/input_touch_wheel.hpp create mode 100644 src/input/include/input_volume_buttons.hpp create mode 100644 src/input/include/lvgl_input_driver.hpp create mode 100644 src/input/input_touch_dpad.cpp create mode 100644 src/input/input_touch_wheel.cpp create mode 100644 src/input/input_volume_buttons.cpp create mode 100644 src/input/lvgl_input_driver.cpp delete mode 100644 src/ui/encoder_input.cpp delete mode 100644 src/ui/include/encoder_input.hpp diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index 0b7ead94..891115d4 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -5,8 +5,7 @@ idf_component_register( SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "adc.cpp" "storage.cpp" "i2c.cpp" "bluetooth.cpp" "spi.cpp" "display.cpp" "display_init.cpp" - "samd.cpp" "relative_wheel.cpp" "wm8523.cpp" "nvs.cpp" "haptics.cpp" - "spiffs.cpp" + "samd.cpp" "wm8523.cpp" "nvs.cpp" "haptics.cpp" "spiffs.cpp" INCLUDE_DIRS "include" REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks" "nvs_flash" "spiffs" "bt" "tinyfsm" "util") diff --git a/src/drivers/include/relative_wheel.hpp b/src/drivers/include/relative_wheel.hpp deleted file mode 100644 index e1106143..00000000 --- a/src/drivers/include/relative_wheel.hpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#pragma once - -#include -#include -#include - -#include "esp_err.h" -#include "result.hpp" - -#include "gpios.hpp" -#include "touchwheel.hpp" - -namespace drivers { - -class RelativeWheel { - public: - explicit RelativeWheel(TouchWheel& touch); - - auto Update() -> void; - auto SetEnabled(bool) -> void; - - auto SetSensitivity(uint8_t) -> void; - auto GetSensitivity() -> uint8_t; - - auto is_clicking() const -> bool; - auto ticks() const -> std::int_fast16_t; - - // Not copyable or movable. - RelativeWheel(const RelativeWheel&) = delete; - RelativeWheel& operator=(const RelativeWheel&) = delete; - - private: - TouchWheel& touch_; - - bool is_enabled_; - uint8_t sensitivity_; - uint8_t threshold_; - - bool is_clicking_; - bool was_clicking_; - bool is_first_read_; - std::int_fast16_t ticks_; - uint8_t last_angle_; -}; - -} // namespace drivers diff --git a/src/drivers/relative_wheel.cpp b/src/drivers/relative_wheel.cpp deleted file mode 100644 index e90143ae..00000000 --- a/src/drivers/relative_wheel.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#include "relative_wheel.hpp" - -#include -#include - -#include "esp_log.h" - -namespace drivers { - -RelativeWheel::RelativeWheel(TouchWheel& touch) - : touch_(touch), - is_enabled_(true), - sensitivity_(128), - threshold_(10), - is_clicking_(false), - was_clicking_(false), - is_first_read_(true), - ticks_(0), - last_angle_(0) {} - -auto RelativeWheel::Update() -> void { - TouchWheelData d = touch_.GetTouchWheelData(); - - is_clicking_ = d.is_button_touched; - - if (is_clicking_) { - ticks_ = 0; - return; - } - - if (!d.is_wheel_touched) { - ticks_ = 0; - is_first_read_ = true; - return; - } - - uint8_t new_angle = d.wheel_position; - if (is_first_read_) { - is_first_read_ = false; - last_angle_ = new_angle; - return; - } - - int delta = 128 - last_angle_; - uint8_t rotated_angle = new_angle + delta; - if (rotated_angle < 128 - threshold_) { - ticks_ = 1; - last_angle_ = new_angle; - } else if (rotated_angle > 128 + threshold_) { - ticks_ = -1; - last_angle_ = new_angle; - } else { - ticks_ = 0; - } -} - -auto RelativeWheel::SetEnabled(bool en) -> void { - is_enabled_ = en; -} - -auto RelativeWheel::SetSensitivity(uint8_t val) -> void { - sensitivity_ = val; - int tmax = 35; - int tmin = 5; - threshold_ = (((255. - sensitivity_)/255.)*(tmax - tmin) + tmin); -} - -auto RelativeWheel::GetSensitivity() -> uint8_t { - return sensitivity_; -} - -auto RelativeWheel::is_clicking() const -> bool { - if (!is_enabled_) { - return false; - } - return is_clicking_; -} - -auto RelativeWheel::ticks() const -> std::int_fast16_t { - if (!is_enabled_) { - return 0; - } - return ticks_; -} - -} // namespace drivers diff --git a/src/input/CMakeLists.txt b/src/input/CMakeLists.txt new file mode 100644 index 00000000..92ed3d6c --- /dev/null +++ b/src/input/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright 2023 jacqueline +# +# SPDX-License-Identifier: GPL-3.0-only + +idf_component_register( + SRCS "input_touch_wheel.cpp" "input_touch_dpad.cpp" + "input_volume_buttons.cpp" "lvgl_input_driver.cpp" "feedback_haptics.cpp" + INCLUDE_DIRS "include" + REQUIRES "drivers" "lvgl" "events" "system_fsm") + +target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/input/feedback_haptics.cpp b/src/input/feedback_haptics.cpp new file mode 100644 index 00000000..5e83d0d6 --- /dev/null +++ b/src/input/feedback_haptics.cpp @@ -0,0 +1,37 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "feedback_haptics.hpp" + +#include + +#include "lvgl/lvgl.h" + +#include "core/lv_event.h" +#include "esp_log.h" + +#include "haptics.hpp" + +namespace input { + +using Effect = drivers::Haptics::Effect; + +Haptics::Haptics(drivers::Haptics& haptics_) : haptics_(haptics_) {} + +auto Haptics::feedback(uint8_t event_type) -> void { + switch (event_type) { + case LV_EVENT_FOCUSED: + haptics_.PlayWaveformEffect(Effect::kMediumClick1_100Pct); + break; + case LV_EVENT_CLICKED: + haptics_.PlayWaveformEffect(Effect::kSharpClick_100Pct); + break; + default: + break; + } +} + +} // namespace input diff --git a/src/input/include/feedback_device.hpp b/src/input/include/feedback_device.hpp new file mode 100644 index 00000000..4faeeafd --- /dev/null +++ b/src/input/include/feedback_device.hpp @@ -0,0 +1,32 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include + +namespace input { + +/* + * Interface for providing non-visual feedback to the user as a result of LVGL + * events. 'Feedback Devices' are able to observe all events that are generated + * by LVGL as a result of Input Devices. + * + * Implementations of this interface are a mix of hardware features (e.g. a + * haptic motor buzzing when your selection changes) and firmware features + * (e.g. playing audio feedback that describes the selected element). + */ +class IFeedbackDevice { + public: + virtual ~IFeedbackDevice() {} + + virtual auto feedback(uint8_t event_type) -> void = 0; + + // TODO: Add configuration; likely the same shape of interface that + // IInputDevice uses. +}; + +} // namespace input diff --git a/src/input/include/feedback_haptics.hpp b/src/input/include/feedback_haptics.hpp new file mode 100644 index 00000000..a307a429 --- /dev/null +++ b/src/input/include/feedback_haptics.hpp @@ -0,0 +1,26 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include + +#include "feedback_device.hpp" +#include "haptics.hpp" + +namespace input { + +class Haptics : public IFeedbackDevice { + public: + Haptics(drivers::Haptics& haptics_); + + auto feedback(uint8_t event_type) -> void override; + + private: + drivers::Haptics& haptics_; +}; + +} // namespace input diff --git a/src/input/include/input_device.hpp b/src/input/include/input_device.hpp new file mode 100644 index 00000000..09cf9193 --- /dev/null +++ b/src/input/include/input_device.hpp @@ -0,0 +1,33 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include + +#include "hal/lv_hal_indev.h" + +namespace input { + +/* + * Interface for all device input methods. Each 'Input Device' is polled by + * LVGL at regular intervals, and can effect the device either via LVGL's input + * device driver API, or by emitting events for other parts of the system to + * react to (e.g. issuing a play/pause event, or altering the volume). + */ +class IInputDevice { + public: + virtual ~IInputDevice() {} + + virtual auto read(lv_indev_data_t* data) -> void = 0; + + // TODO: Add hooks and configuration (or are hooks just one kind of + // configuration?) +}; + +} // namespace input diff --git a/src/input/include/input_touch_dpad.hpp b/src/input/include/input_touch_dpad.hpp new file mode 100644 index 00000000..fdb52db9 --- /dev/null +++ b/src/input/include/input_touch_dpad.hpp @@ -0,0 +1,30 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include + +#include "hal/lv_hal_indev.h" + +#include "haptics.hpp" +#include "input_device.hpp" +#include "touchwheel.hpp" + +namespace input { + +class TouchDPad : public IInputDevice { + public: + TouchDPad(drivers::TouchWheel&); + + auto read(lv_indev_data_t* data) -> void override; + + private: + drivers::TouchWheel& wheel_; +}; + +} // namespace input diff --git a/src/input/include/input_touch_wheel.hpp b/src/input/include/input_touch_wheel.hpp new file mode 100644 index 00000000..a4923f21 --- /dev/null +++ b/src/input/include/input_touch_wheel.hpp @@ -0,0 +1,37 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include + +#include "hal/lv_hal_indev.h" + +#include "haptics.hpp" +#include "input_device.hpp" +#include "touchwheel.hpp" + +namespace input { + +class TouchWheel : public IInputDevice { + public: + TouchWheel(drivers::TouchWheel&); + + auto read(lv_indev_data_t* data) -> void override; + + private: + auto calculate_ticks(const drivers::TouchWheelData& data) -> int8_t; + + drivers::TouchWheel& wheel_; + + bool is_scrolling_; + uint8_t sensitivity_; + uint8_t threshold_; + bool is_first_read_; + uint8_t last_angle_; +}; + +} // namespace input diff --git a/src/input/include/input_volume_buttons.hpp b/src/input/include/input_volume_buttons.hpp new file mode 100644 index 00000000..4ca8c16b --- /dev/null +++ b/src/input/include/input_volume_buttons.hpp @@ -0,0 +1,30 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include + +#include "gpios.hpp" +#include "hal/lv_hal_indev.h" + +#include "haptics.hpp" +#include "input_device.hpp" +#include "touchwheel.hpp" + +namespace input { + +class VolumeButtons : public IInputDevice { + public: + VolumeButtons(drivers::IGpios&); + + auto read(lv_indev_data_t* data) -> void override; + + private: + drivers::IGpios& gpios_; +}; + +} // namespace input diff --git a/src/input/include/lvgl_input_driver.hpp b/src/input/include/lvgl_input_driver.hpp new file mode 100644 index 00000000..be452368 --- /dev/null +++ b/src/input/include/lvgl_input_driver.hpp @@ -0,0 +1,53 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include +#include + +#include "core/lv_group.h" +#include "feedback_device.hpp" +#include "gpios.hpp" +#include "hal/lv_hal_indev.h" + +#include "input_device.hpp" +#include "nvs.hpp" +#include "service_locator.hpp" +#include "touchwheel.hpp" + +namespace input { + +/* + * Implementation of an LVGL input device. This class composes multiple + * IInputDevice and IFeedbackDevice instances together into a single LVGL + * device. + */ +class LvglInputDriver { + public: + LvglInputDriver(std::shared_ptr); + + auto read(lv_indev_data_t* data) -> void; + auto feedback(uint8_t) -> void; + + auto registration() -> lv_indev_t* { return registration_; } + auto lock(bool l) -> void { is_locked_ = l; } + + private: + std::shared_ptr services_; + + lv_indev_drv_t driver_; + lv_indev_t* registration_; + + std::vector> inputs_; + std::vector> feedbacks_; + + bool is_locked_; +}; + +} // namespace input diff --git a/src/input/input_touch_dpad.cpp b/src/input/input_touch_dpad.cpp new file mode 100644 index 00000000..a5774ef6 --- /dev/null +++ b/src/input/input_touch_dpad.cpp @@ -0,0 +1,35 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "input_touch_dpad.hpp" + +#include + +#include "hal/lv_hal_indev.h" + +#include "haptics.hpp" +#include "input_device.hpp" +#include "input_touch_dpad.hpp" +#include "touchwheel.hpp" + +namespace input { + +static inline auto IsAngleWithin(int16_t wheel_angle, + int16_t target_angle, + int threshold) -> bool { + int16_t difference = (wheel_angle - target_angle + 127 + 255) % 255 - 127; + return difference <= threshold && difference >= -threshold; +} + +TouchDPad::TouchDPad(drivers::TouchWheel& wheel) : wheel_(wheel) {} + +auto TouchDPad::read(lv_indev_data_t* data) -> void { + wheel_.Update(); + + // TODO: reimplement +} + +} // namespace input diff --git a/src/input/input_touch_wheel.cpp b/src/input/input_touch_wheel.cpp new file mode 100644 index 00000000..302a17aa --- /dev/null +++ b/src/input/input_touch_wheel.cpp @@ -0,0 +1,81 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "input_touch_wheel.hpp" +#include + +#include + +#include "hal/lv_hal_indev.h" + +#include "haptics.hpp" +#include "input_device.hpp" +#include "touchwheel.hpp" + +namespace input { + +TouchWheel::TouchWheel(drivers::TouchWheel& wheel) + : wheel_(wheel), + is_scrolling_(false), + sensitivity_(128), + threshold_(10), + is_first_read_(true), + last_angle_(0) {} + +auto TouchWheel::read(lv_indev_data_t* data) -> void { + wheel_.Update(); + auto wheel_data = wheel_.GetTouchWheelData(); + int8_t ticks = calculate_ticks(wheel_data); + + if (!wheel_data.is_wheel_touched) { + // User has released the wheel. + is_scrolling_ = false; + data->enc_diff = 0; + } else if (ticks != 0) { + // User is touching the wheel, and has just passed the sensitivity + // threshold for a scroll tick. + is_scrolling_ = true; + data->enc_diff = ticks; + } else { + // User is touching the wheel, but hasn't moved. + data->enc_diff = 0; + } + + if (!is_scrolling_ && wheel_data.is_button_touched) { + data->state = LV_INDEV_STATE_PRESSED; + } else { + data->state = LV_INDEV_STATE_RELEASED; + } +} + +auto TouchWheel::calculate_ticks(const drivers::TouchWheelData& data) + -> int8_t { + if (!data.is_wheel_touched) { + is_first_read_ = true; + return 0; + } + + uint8_t new_angle = data.wheel_position; + if (is_first_read_) { + is_first_read_ = false; + last_angle_ = new_angle; + return 0; + } + + int delta = 128 - last_angle_; + uint8_t rotated_angle = new_angle + delta; + if (rotated_angle < 128 - threshold_) { + last_angle_ = new_angle; + return 1; + } else if (rotated_angle > 128 + threshold_) { + last_angle_ = new_angle; + return -1; + } else { + return 0; + } +} + +} // namespace input diff --git a/src/input/input_volume_buttons.cpp b/src/input/input_volume_buttons.cpp new file mode 100644 index 00000000..bf79ed55 --- /dev/null +++ b/src/input/input_volume_buttons.cpp @@ -0,0 +1,26 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "input_volume_buttons.hpp" +#include "gpios.hpp" + +namespace input { + +VolumeButtons::VolumeButtons(drivers::IGpios& gpios) : gpios_(gpios) {} + +auto VolumeButtons::read(lv_indev_data_t* data) -> void { + bool vol_up = gpios_.Get(drivers::IGpios::Pin::kKeyUp); + if (!vol_up) { + ESP_LOGI("volume", "vol up"); + } + + bool vol_down = gpios_.Get(drivers::IGpios::Pin::kKeyDown); + if (!vol_down) { + ESP_LOGI("volume", "vol down"); + } +} + +} // namespace input diff --git a/src/input/lvgl_input_driver.cpp b/src/input/lvgl_input_driver.cpp new file mode 100644 index 00000000..70b6eb5d --- /dev/null +++ b/src/input/lvgl_input_driver.cpp @@ -0,0 +1,84 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lvgl_input_driver.hpp" +#include + +#include +#include + +#include "feedback_haptics.hpp" +#include "input_touch_wheel.hpp" +#include "input_volume_buttons.hpp" +#include "lvgl.h" +#include "service_locator.hpp" + +[[maybe_unused]] static constexpr char kTag[] = "input"; + +namespace input { + +static void read_cb(lv_indev_drv_t* drv, lv_indev_data_t* data) { + LvglInputDriver* instance = + reinterpret_cast(drv->user_data); + instance->read(data); +} + +static void feedback_cb(lv_indev_drv_t* drv, uint8_t event) { + LvglInputDriver* instance = + reinterpret_cast(drv->user_data); + instance->feedback(event); +} + +LvglInputDriver::LvglInputDriver( + std::shared_ptr services) + : services_(services), + driver_(), + registration_(nullptr), + inputs_(), + feedbacks_(), + is_locked_(false) { + lv_indev_drv_init(&driver_); + driver_.type = LV_INDEV_TYPE_ENCODER; + driver_.read_cb = read_cb; + driver_.feedback_cb = feedback_cb; + driver_.user_data = this; + + registration_ = lv_indev_drv_register(&driver_); + + // TODO: Make these devices configurable. I'm thinking each device gets an id + // and then we have: + // - a factory to create instance given an id + // - add/remove device methods on LvglInputDriver that operate on ids + // - the user's enabled devices (+ their configuration) stored in NVS. + auto touchwheel = services_->touchwheel(); + if (touchwheel) { + inputs_.push_back(std::make_unique(**touchwheel)); + } + inputs_.push_back(std::make_unique(services_->gpios())); + feedbacks_.push_back(std::make_unique(services_->haptics())); +} + +auto LvglInputDriver::read(lv_indev_data_t* data) -> void { + // TODO: we should pass lock state on to the individual devices, since they + // may wish to either ignore the lock state, or power down until unlock. + if (is_locked_) { + return; + } + for (auto&& device : inputs_) { + device->read(data); + } +} + +auto LvglInputDriver::feedback(uint8_t event) -> void { + if (is_locked_) { + return; + } + for (auto&& device : feedbacks_) { + device->feedback(event); + } +} + +} // namespace input diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp index e0a0ac7a..f01afb3f 100644 --- a/src/system_fsm/include/system_fsm.hpp +++ b/src/system_fsm/include/system_fsm.hpp @@ -17,7 +17,6 @@ #include "display.hpp" #include "gpios.hpp" #include "nvs.hpp" -#include "relative_wheel.hpp" #include "samd.hpp" #include "service_locator.hpp" #include "storage.hpp" diff --git a/src/system_fsm/system_fsm.cpp b/src/system_fsm/system_fsm.cpp index f502b49a..59d41c73 100644 --- a/src/system_fsm/system_fsm.cpp +++ b/src/system_fsm/system_fsm.cpp @@ -9,7 +9,6 @@ #include "driver/gpio.h" #include "event_queue.hpp" #include "gpios.hpp" -#include "relative_wheel.hpp" #include "service_locator.hpp" #include "system_events.hpp" #include "tag_parser.hpp" diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 6d45fc9f..3814e9d5 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -3,9 +3,9 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "encoder_input.cpp" - "themes.cpp" "screen.cpp" "modal.cpp" "screen_lua.cpp" - "splash.c" "font_fusion_12.c" "font_fusion_10.c" + SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "themes.cpp" + "screen.cpp" "modal.cpp" "screen_lua.cpp" "splash.c" "font_fusion_12.c" + "font_fusion_10.c" INCLUDE_DIRS "include" - REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "lua" "luavgl" "esp_app_format") + REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "lua" "luavgl" "esp_app_format" "input") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/ui/encoder_input.cpp b/src/ui/encoder_input.cpp deleted file mode 100644 index 3b5af2c1..00000000 --- a/src/ui/encoder_input.cpp +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#include "encoder_input.hpp" - -#include -#include - -#include "lvgl.h" - -#include "audio_events.hpp" -#include "core/lv_event.h" -#include "core/lv_group.h" -#include "esp_timer.h" -#include "event_queue.hpp" -#include "gpios.hpp" -#include "hal/lv_hal_indev.h" -#include "nvs.hpp" -#include "relative_wheel.hpp" -#include "touchwheel.hpp" -#include "ui_events.hpp" - -[[maybe_unused]] static constexpr char kTag[] = "input"; - -constexpr int kDPadAngleThreshold = 10; -constexpr int kLongPressDelayMs = 500; -constexpr int kRepeatDelayMs = 250; - -static inline auto IsAngleWithin(int16_t wheel_angle, - int16_t target_angle, - int threshold) -> bool { - int16_t difference = (wheel_angle - target_angle + 127 + 255) % 255 - 127; - return difference <= threshold && difference >= -threshold; -} - -namespace ui { - -static void encoder_read(lv_indev_drv_t* drv, lv_indev_data_t* data) { - EncoderInput* instance = reinterpret_cast(drv->user_data); - instance->Read(data); -} - -EncoderInput::EncoderInput(drivers::IGpios& gpios, drivers::TouchWheel& wheel) - : gpios_(gpios), - raw_wheel_(wheel), - relative_wheel_(std::make_unique(wheel)), - scroller_(std::make_unique()), - mode_(drivers::NvsStorage::InputModes::kRotatingWheel), - is_locked_(false), - scroll_sensitivity_(10), - is_scrolling_wheel_(false) { - lv_indev_drv_init(&driver_); - driver_.type = LV_INDEV_TYPE_ENCODER; - driver_.read_cb = encoder_read; - driver_.user_data = this; - - registration_ = lv_indev_drv_register(&driver_); -} - -auto EncoderInput::Read(lv_indev_data_t* data) -> void { - if (is_locked_) { - return; - } - - lv_obj_t* active_object = nullptr; - if (registration_ && registration_->group) { - active_object = lv_group_get_focused(registration_->group); - } - - raw_wheel_.Update(); - relative_wheel_->Update(); - // GPIO (for volume buttons) updating is handled by system_fsm. - - uint64_t now_ms = esp_timer_get_time() / 1000; - - // Deal with the potential overflow of our timer. - for (auto& it : touch_time_ms_) { - if (it.second > now_ms) { - // esp_timer overflowed. - it.second = 0; - } - } - - // Check each button. - UpdateKeyState(Keys::kVolumeUp, now_ms, - !gpios_.Get(drivers::IGpios::Pin::kKeyUp)); - UpdateKeyState(Keys::kVolumeDown, now_ms, - !gpios_.Get(drivers::IGpios::Pin::kKeyDown)); - - drivers::TouchWheelData wheel_data = raw_wheel_.GetTouchWheelData(); - UpdateKeyState(Keys::kTouchWheel, now_ms, wheel_data.is_wheel_touched); - UpdateKeyState(Keys::kTouchWheelCenter, now_ms, wheel_data.is_button_touched); - - UpdateKeyState( - Keys::kDirectionalUp, now_ms, - wheel_data.is_wheel_touched && - IsAngleWithin(wheel_data.wheel_position, 0, kDPadAngleThreshold)); - UpdateKeyState( - Keys::kDirectionalLeft, now_ms, - wheel_data.is_wheel_touched && - IsAngleWithin(wheel_data.wheel_position, 63, kDPadAngleThreshold)); - UpdateKeyState( - Keys::kDirectionalDown, now_ms, - wheel_data.is_wheel_touched && - IsAngleWithin(wheel_data.wheel_position, 127, kDPadAngleThreshold)); - UpdateKeyState( - Keys::kDirectionalRight, now_ms, - wheel_data.is_wheel_touched && - IsAngleWithin(wheel_data.wheel_position, 189, kDPadAngleThreshold)); - - // When the wheel is being scrolled, we want to ensure that other inputs - // involving the touchwheel don't trigger. This guards again two main issues: - // - hesitating when your thumb is on a cardinal direction, causing an - // unintentional long-press, - // - drifting from the outside of the wheel in a way that causes the centre - // key to be triggered. - if (is_scrolling_wheel_) { - UpdateKeyState(Keys::kTouchWheelCenter, now_ms, false); - UpdateKeyState(Keys::kDirectionalUp, now_ms, false); - UpdateKeyState(Keys::kDirectionalLeft, now_ms, false); - UpdateKeyState(Keys::kDirectionalDown, now_ms, false); - UpdateKeyState(Keys::kDirectionalRight, now_ms, false); - } - - // Now that we've determined the correct state for all keys, we can start - // mapping key states into actions, depending on the current control scheme. - if (mode_ == drivers::NvsStorage::InputModes::kButtonsOnly) { - Trigger trigger; - data->state = LV_INDEV_STATE_RELEASED; - - trigger = TriggerKey(Keys::kVolumeUp, KeyStyle::kLongPress, now_ms); - switch (trigger) { - case Trigger::kNone: - break; - case Trigger::kClick: - data->enc_diff = -1; - break; - case Trigger::kLongPress: - events::Ui().Dispatch(internal::BackPressed{}); - break; - } - - trigger = TriggerKey(Keys::kVolumeDown, KeyStyle::kLongPress, now_ms); - switch (trigger) { - case Trigger::kNone: - break; - case Trigger::kClick: - data->enc_diff = 1; - break; - case Trigger::kLongPress: - data->state = LV_INDEV_STATE_PRESSED; - break; - } - } else if (mode_ == drivers::NvsStorage::InputModes::kDirectionalWheel) { - Trigger trigger; - trigger = TriggerKey(Keys::kTouchWheelCenter, KeyStyle::kLongPress, now_ms); - data->state = trigger == Trigger::kClick ? LV_INDEV_STATE_PRESSED - : LV_INDEV_STATE_RELEASED; - - trigger = TriggerKey(Keys::kDirectionalUp, KeyStyle::kRepeat, now_ms); - if (trigger == Trigger::kClick) { - data->enc_diff = scroller_->AddInput(now_ms, -1); - } - - trigger = TriggerKey(Keys::kDirectionalDown, KeyStyle::kRepeat, now_ms); - if (trigger == Trigger::kClick) { - data->enc_diff = scroller_->AddInput(now_ms, 1); - } - - trigger = TriggerKey(Keys::kDirectionalLeft, KeyStyle::kRepeat, now_ms); - if (trigger == Trigger::kClick) { - events::Ui().Dispatch(internal::BackPressed{}); - } - - trigger = TriggerKey(Keys::kDirectionalRight, KeyStyle::kRepeat, now_ms); - if (trigger == Trigger::kClick) { - // TODO: ??? - } - - // Cancel scrolling if the touchpad is released. - if (!touch_time_ms_.contains(Keys::kDirectionalUp) && - !touch_time_ms_.contains(Keys::kDirectionalDown)) { - data->enc_diff = scroller_->AddInput(now_ms, 0); - } - - trigger = TriggerKey(Keys::kVolumeUp, KeyStyle::kRepeat, now_ms); - switch (trigger) { - case Trigger::kNone: - break; - case Trigger::kClick: - events::Audio().Dispatch(audio::StepUpVolume{}); - break; - case Trigger::kLongPress: - break; - } - - trigger = TriggerKey(Keys::kVolumeDown, KeyStyle::kRepeat, now_ms); - switch (trigger) { - case Trigger::kNone: - break; - case Trigger::kClick: - events::Audio().Dispatch(audio::StepDownVolume{}); - break; - case Trigger::kLongPress: - break; - } - } else if (mode_ == drivers::NvsStorage::InputModes::kRotatingWheel) { - if (!raw_wheel_.GetTouchWheelData().is_wheel_touched) { - // User has released the wheel. - is_scrolling_wheel_ = false; - data->enc_diff = scroller_->AddInput(now_ms, 0); - } else if (relative_wheel_->ticks() != 0) { - // User is touching the wheel, and has just passed the sensitivity - // threshold for a scroll tick. - is_scrolling_wheel_ = true; - data->enc_diff = scroller_->AddInput(now_ms, relative_wheel_->ticks()); - } else { - // User is touching the wheel, but hasn't moved. - data->enc_diff = 0; - } - - Trigger trigger = - TriggerKey(Keys::kTouchWheelCenter, KeyStyle::kLongPress, now_ms); - switch (trigger) { - case Trigger::kNone: - data->state = LV_INDEV_STATE_RELEASED; - break; - case Trigger::kClick: - data->state = LV_INDEV_STATE_PRESSED; - break; - case Trigger::kLongPress: - if (active_object) { - lv_event_send(active_object, LV_EVENT_LONG_PRESSED, NULL); - } - break; - } - - trigger = TriggerKey(Keys::kVolumeUp, KeyStyle::kRepeat, now_ms); - switch (trigger) { - case Trigger::kNone: - break; - case Trigger::kClick: - events::Audio().Dispatch(audio::StepUpVolume{}); - break; - case Trigger::kLongPress: - break; - } - - trigger = TriggerKey(Keys::kVolumeDown, KeyStyle::kRepeat, now_ms); - switch (trigger) { - case Trigger::kNone: - break; - case Trigger::kClick: - events::Audio().Dispatch(audio::StepDownVolume{}); - break; - case Trigger::kLongPress: - break; - } - - trigger = TriggerKey(Keys::kDirectionalLeft, KeyStyle::kLongPress, now_ms); - switch (trigger) { - case Trigger::kNone: - break; - case Trigger::kClick: - break; - case Trigger::kLongPress: - events::Ui().Dispatch(internal::BackPressed{}); - break; - } - } -} - -auto EncoderInput::scroll_sensitivity(uint8_t val) -> void { - scroll_sensitivity_ = val; - relative_wheel_->SetSensitivity(scroll_sensitivity_); -} - -auto EncoderInput::UpdateKeyState(Keys key, uint64_t ms, bool clicked) -> void { - if (clicked) { - if (!touch_time_ms_.contains(key)) { - // Key was just clicked. - touch_time_ms_[key] = ms; - just_released_.erase(key); - fired_.erase(key); - } - return; - } - - // Key is not clicked. - if (touch_time_ms_.contains(key)) { - // Key was just released. - just_released_.insert(key); - touch_time_ms_.erase(key); - } -} - -auto EncoderInput::TriggerKey(Keys key, KeyStyle s, uint64_t ms) -> Trigger { - if (s == KeyStyle::kRepeat) { - bool may_repeat = fired_.contains(key) && touch_time_ms_.contains(key) && - ms - touch_time_ms_[key] >= kRepeatDelayMs; - - // Repeatable keys trigger on press. - if (touch_time_ms_.contains(key) && (!fired_.contains(key) || may_repeat)) { - fired_.insert(key); - return Trigger::kClick; - } else { - return Trigger::kNone; - } - } else if (s == KeyStyle::kLongPress) { - // Long press keys trigger on release, or after holding for a delay. - if (just_released_.contains(key)) { - just_released_.erase(key); - if (!fired_.contains(key)) { - fired_.insert(key); - return Trigger::kClick; - } - } - if (touch_time_ms_.contains(key) && - ms - touch_time_ms_[key] >= kLongPressDelayMs && - !fired_.contains(key)) { - fired_.insert(key); - return Trigger::kLongPress; - } - } - - return Trigger::kNone; -} - -auto Scroller::AddInput(uint64_t ms, int direction) -> int { - bool dir_changed = - ((velocity_ < 0 && direction > 0) || (velocity_ > 0 && direction < 0)); - if (direction == 0 || dir_changed) { - last_input_ms_ = ms; - velocity_ = 0; - return 0; - } - // Decay with time - if (last_input_ms_ > ms) { - last_input_ms_ = 0; - } - uint diff = ms - last_input_ms_; - uint diff_steps = diff / 25; - last_input_ms_ = ms + (last_input_ms_ % 50); - // Use powers of two for our exponential decay so we can implement decay - // trivially via bit shifting. - velocity_ >>= diff_steps; - - velocity_ += direction * 1000; - if (velocity_ > 0) { - return (velocity_ + 500) / 1000; - } else { - return (velocity_ - 500) / 1000; - } -} -} // namespace ui diff --git a/src/ui/include/encoder_input.hpp b/src/ui/include/encoder_input.hpp deleted file mode 100644 index 7dfac071..00000000 --- a/src/ui/include/encoder_input.hpp +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#pragma once - -#include -#include -#include -#include - -#include "core/lv_group.h" -#include "gpios.hpp" -#include "hal/lv_hal_indev.h" - -#include "nvs.hpp" -#include "relative_wheel.hpp" -#include "touchwheel.hpp" - -namespace ui { - -class Scroller; - -/* - * Main input device abstracting that handles turning lower-level input device - * drivers into events and LVGL inputs. - * - * As far as LVGL is concerned, this class represents an ordinary rotary - * encoder, supporting only left and right ticks, and clicking. - */ -class EncoderInput { - public: - EncoderInput(drivers::IGpios& gpios, drivers::TouchWheel& wheel); - - auto Read(lv_indev_data_t* data) -> void; - auto registration() -> lv_indev_t* { return registration_; } - - auto mode(drivers::NvsStorage::InputModes mode) { mode_ = mode; } - auto scroll_sensitivity(uint8_t val) -> void; // Value between 0-255, used to scale the threshold - auto lock(bool l) -> void { is_locked_ = l; } - - private: - lv_indev_drv_t driver_; - lv_indev_t* registration_; - - drivers::IGpios& gpios_; - drivers::TouchWheel& raw_wheel_; - std::unique_ptr relative_wheel_; - std::unique_ptr scroller_; - - drivers::NvsStorage::InputModes mode_; - bool is_locked_; - uint8_t scroll_sensitivity_; - - // Every kind of distinct input that we could map to an action. - enum class Keys { - kVolumeUp, - kVolumeDown, - kTouchWheel, - kTouchWheelCenter, - kDirectionalUp, - kDirectionalRight, - kDirectionalDown, - kDirectionalLeft, - }; - - // Map from a Key, to the time that it was first touched in ms. If the key is - // currently released, where will be no entry. - std::unordered_map touch_time_ms_; - // Set of keys that were released during the current update. - std::set just_released_; - // Set of keys that have had an event fired for them since being pressed. - std::set fired_; - - bool is_scrolling_wheel_; - - enum class Trigger { - kNone, - // Regular short-click. Triggered on release for long-pressable keys, - // triggered on the initial press for repeatable keys. - kClick, - kLongPress, - }; - - enum class KeyStyle { - kRepeat, - kLongPress, - }; - - auto UpdateKeyState(Keys key, uint64_t ms, bool clicked) -> void; - auto TriggerKey(Keys key, KeyStyle t, uint64_t ms) -> Trigger; -}; - -class Scroller { - public: - Scroller() : last_input_ms_(0), velocity_(0) {} - - auto AddInput(uint64_t, int) -> int; - - private: - uint64_t last_input_ms_; - int velocity_; -}; - -} // namespace ui diff --git a/src/ui/include/lvgl_task.hpp b/src/ui/include/lvgl_task.hpp index f212ab9d..8efcbf35 100644 --- a/src/ui/include/lvgl_task.hpp +++ b/src/ui/include/lvgl_task.hpp @@ -15,8 +15,7 @@ #include "freertos/timers.h" #include "display.hpp" -#include "encoder_input.hpp" -#include "relative_wheel.hpp" +#include "lvgl_input_driver.hpp" #include "screen.hpp" #include "themes.hpp" #include "touchwheel.hpp" @@ -28,14 +27,14 @@ class UiTask { static auto Start() -> UiTask*; ~UiTask(); - auto input(std::shared_ptr input) -> void; + auto input(std::shared_ptr input) -> void; private: UiTask(); auto Main() -> void; - std::shared_ptr input_; + std::shared_ptr input_; std::shared_ptr current_screen_; }; diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 2bab487d..c238a447 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -14,14 +14,13 @@ #include "battery.hpp" #include "db_events.hpp" #include "display.hpp" -#include "encoder_input.hpp" #include "gpios.hpp" #include "lua_thread.hpp" +#include "lvgl_input_driver.hpp" #include "lvgl_task.hpp" #include "modal.hpp" #include "nvs.hpp" #include "property.hpp" -#include "relative_wheel.hpp" #include "screen.hpp" #include "service_locator.hpp" #include "storage.hpp" @@ -92,7 +91,7 @@ class UiState : public tinyfsm::Fsm { static std::unique_ptr sTask; static std::shared_ptr sServices; static std::unique_ptr sDisplay; - static std::shared_ptr sInput; + static std::shared_ptr sInput; static std::stack> sScreens; static std::shared_ptr sCurrentScreen; diff --git a/src/ui/lvgl_task.cpp b/src/ui/lvgl_task.cpp index f0184766..4cf25c15 100644 --- a/src/ui/lvgl_task.cpp +++ b/src/ui/lvgl_task.cpp @@ -33,11 +33,11 @@ #include "lua.h" #include "lv_api_map.h" #include "lvgl/lvgl.h" +#include "lvgl_input_driver.hpp" #include "misc/lv_color.h" #include "misc/lv_style.h" #include "misc/lv_timer.h" #include "modal.hpp" -#include "relative_wheel.hpp" #include "tasks.hpp" #include "touchwheel.hpp" #include "ui_fsm.hpp" @@ -50,8 +50,6 @@ namespace ui { [[maybe_unused]] static const char* kTag = "ui_task"; -static auto group_focus_cb(lv_group_t *group) -> void; - UiTask::UiTask() {} UiTask::~UiTask() { @@ -78,7 +76,6 @@ auto UiTask::Main() -> void { if (input_ && current_screen_->group() != current_group) { current_group = current_screen_->group(); lv_indev_set_group(input_->registration(), current_group); - lv_group_set_focus_cb(current_group, &group_focus_cb); } TickType_t delay = lv_timer_handler(); @@ -86,10 +83,9 @@ auto UiTask::Main() -> void { } } -auto UiTask::input(std::shared_ptr input) -> void { +auto UiTask::input(std::shared_ptr input) -> void { assert(current_screen_); input_ = input; - lv_indev_set_group(input_->registration(), current_screen_->group()); } auto UiTask::Start() -> UiTask* { @@ -98,11 +94,4 @@ auto UiTask::Start() -> UiTask* { return ret; } -static auto group_focus_cb(lv_group_t *group) -> void { - // TODO(robin): we probably want to vary this, configure this, etc - events::System().Dispatch(system_fsm::HapticTrigger{ - .effect = drivers::Haptics::Effect::kMediumClick1_100Pct, - }); -} - } // namespace ui diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 28733123..a9c1d3a3 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -30,19 +30,18 @@ #include "lauxlib.h" #include "lua_thread.hpp" #include "luavgl.h" +#include "lvgl_input_driver.hpp" #include "memory_resource.hpp" #include "misc/lv_gc.h" #include "audio_events.hpp" #include "display.hpp" -#include "encoder_input.hpp" #include "event_queue.hpp" #include "gpios.hpp" #include "lua_registry.hpp" #include "lvgl_task.hpp" #include "nvs.hpp" #include "property.hpp" -#include "relative_wheel.hpp" #include "samd.hpp" #include "screen.hpp" #include "screen_lua.hpp" @@ -63,7 +62,7 @@ namespace ui { std::unique_ptr UiState::sTask; std::shared_ptr UiState::sServices; std::unique_ptr UiState::sDisplay; -std::shared_ptr UiState::sInput; +std::shared_ptr UiState::sInput; std::stack> UiState::sScreens; std::shared_ptr UiState::sCurrentScreen; @@ -234,51 +233,58 @@ lua::Property UiState::sDisplayBrightness{ return true; }}; -lua::Property UiState::sControlsScheme{ - 0, [](const lua::LuaValue& val) { - if (!std::holds_alternative(val)) { - return false; - } - drivers::NvsStorage::InputModes mode; - switch (std::get(val)) { - case 0: - mode = drivers::NvsStorage::InputModes::kButtonsOnly; - break; - case 1: - mode = drivers::NvsStorage::InputModes::kButtonsWithWheel; - break; - case 2: - mode = drivers::NvsStorage::InputModes::kDirectionalWheel; - break; - case 3: - mode = drivers::NvsStorage::InputModes::kRotatingWheel; - break; - default: - return false; - } - sServices->nvs().PrimaryInput(mode); - sInput->mode(mode); - return true; - }}; - -lua::Property UiState::sScrollSensitivity{ - 0, [](const lua::LuaValue& val) { - std::optional sensitivity = 0; - std::visit( - [&](auto&& v) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - sensitivity = v; - } - }, - val); - if (!sensitivity) { - return false; - } - sInput->scroll_sensitivity(*sensitivity); - sServices->nvs().ScrollSensitivity(*sensitivity); - return true; - }}; +lua::Property UiState::sControlsScheme{0, [](const lua::LuaValue& val) { + /* + if (!std::holds_alternative(val)) + { return false; + } + drivers::NvsStorage::InputModes mode; + switch (std::get(val)) { + case 0: + mode = + drivers::NvsStorage::InputModes::kButtonsOnly; + break; + case 1: + mode = + drivers::NvsStorage::InputModes::kButtonsWithWheel; + break; + case 2: + mode = + drivers::NvsStorage::InputModes::kDirectionalWheel; + break; + case 3: + mode = + drivers::NvsStorage::InputModes::kRotatingWheel; + break; + default: + return false; + } + sServices->nvs().PrimaryInput(mode); + sInput->mode(mode); + */ + return true; + }}; + +lua::Property UiState::sScrollSensitivity{0, [](const lua::LuaValue& val) { + /* + std::optional sensitivity = 0; + std::visit( + [&](auto&& v) { + using T = + std::decay_t; if + constexpr (std::is_same_v) { + sensitivity = v; + } + }, + val); + if (!sensitivity) { + return false; + } + sInput->scroll_sensitivity(*sensitivity); + sServices->nvs().ScrollSensitivity(*sensitivity); + */ + return true; + }}; lua::Property UiState::sLockSwitch{false}; @@ -372,7 +378,7 @@ void UiState::react(const internal::ControlSchemeChanged&) { if (!sInput) { return; } - sInput->mode(sServices->nvs().PrimaryInput()); + // sInput->mode(sServices->nvs().PrimaryInput()); } void UiState::react(const database::event::UpdateStarted&) { @@ -478,22 +484,8 @@ void Splash::react(const system_fsm::BootComplete& ev) { sDisplayBrightness.Update(brightness); sDisplay->SetBrightness(brightness); - auto touchwheel = sServices->touchwheel(); - if (touchwheel) { - sInput = std::make_shared(sServices->gpios(), **touchwheel); - - auto mode = sServices->nvs().PrimaryInput(); - sInput->mode(mode); - sControlsScheme.Update(static_cast(mode)); - - auto sensitivity = sServices->nvs().ScrollSensitivity(); - sInput->scroll_sensitivity(sensitivity); - sScrollSensitivity.Update(static_cast(sensitivity)); - - sTask->input(sInput); - } else { - ESP_LOGE(kTag, "no input devices initialised!"); - } + sInput = std::make_shared(sServices); + sTask->input(sInput); } void Splash::react(const system_fsm::StorageMounted&) {