diff --git a/src/drivers/include/nvs.hpp b/src/drivers/include/nvs.hpp index 3e37c49e..46a3d154 100644 --- a/src/drivers/include/nvs.hpp +++ b/src/drivers/include/nvs.hpp @@ -44,6 +44,16 @@ class NvsStorage { auto HasShownOnboarding() -> bool; auto HasShownOnboarding(bool) -> bool; + enum class InputModes : uint8_t { + kButtonsOnly = 0, + kButtonsWithWheel = 1, + kDirectionalWheel = 2, + kRotatingWheel = 3, + }; + + auto PrimaryInput() -> InputModes; + auto PrimaryInput(InputModes) -> bool; + explicit NvsStorage(nvs_handle_t); ~NvsStorage(); diff --git a/src/drivers/relative_wheel.cpp b/src/drivers/relative_wheel.cpp index 859f69e3..b944b47b 100644 --- a/src/drivers/relative_wheel.cpp +++ b/src/drivers/relative_wheel.cpp @@ -23,7 +23,6 @@ RelativeWheel::RelativeWheel(TouchWheel& touch) last_angle_(0) {} auto RelativeWheel::Update() -> void { - touch_.Update(); TouchWheelData d = touch_.GetTouchWheelData(); is_clicking_ = d.is_button_touched; diff --git a/src/drivers/touchwheel.cpp b/src/drivers/touchwheel.cpp index aabc5787..cad3433d 100644 --- a/src/drivers/touchwheel.cpp +++ b/src/drivers/touchwheel.cpp @@ -99,10 +99,10 @@ uint8_t TouchWheel::ReadRegister(uint8_t reg) { void TouchWheel::Update() { // Read data from device into member struct - // bool has_data = !gpio_get_level(kIntPin); - // if (!has_data) { - // return; - // } + bool has_data = !gpio_get_level(kIntPin); + if (!has_data) { + return; + } uint8_t status = ReadRegister(Register::DETECTION_STATUS); if (status & 0b10000000) { // Still calibrating. diff --git a/src/ui/encoder_input.cpp b/src/ui/encoder_input.cpp index 62cb4c8b..f6f74aaf 100644 --- a/src/ui/encoder_input.cpp +++ b/src/ui/encoder_input.cpp @@ -10,11 +10,24 @@ #include #include "core/lv_group.h" +#include "esp_timer.h" #include "gpios.hpp" #include "hal/lv_hal_indev.h" +#include "nvs.hpp" #include "relative_wheel.hpp" #include "touchwheel.hpp" +constexpr int kDPadAngleThreshold = 20; +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) { @@ -25,7 +38,9 @@ static void encoder_read(lv_indev_drv_t* drv, lv_indev_data_t* data) { EncoderInput::EncoderInput(drivers::IGpios& gpios, drivers::TouchWheel& wheel) : gpios_(gpios), raw_wheel_(wheel), - relative_wheel_(std::make_unique(wheel)) { + relative_wheel_(std::make_unique(wheel)), + scroller_(std::make_unique()), + mode_(drivers::NvsStorage::InputModes::kRotatingWheel) { lv_indev_drv_init(&driver_); driver_.type = LV_INDEV_TYPE_ENCODER; driver_.read_cb = encoder_read; @@ -37,10 +52,178 @@ EncoderInput::EncoderInput(drivers::IGpios& gpios, drivers::TouchWheel& wheel) auto EncoderInput::Read(lv_indev_data_t* data) -> void { raw_wheel_.Update(); relative_wheel_->Update(); + // GPIOs 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. + HandleKey(Keys::kVolumeUp, now_ms, !gpios_.Get(drivers::IGpios::Pin::kKeyUp)); + HandleKey(Keys::kVolumeDown, now_ms, + !gpios_.Get(drivers::IGpios::Pin::kKeyDown)); + + drivers::TouchWheelData wheel_data = raw_wheel_.GetTouchWheelData(); + HandleKey(Keys::kTouchWheel, now_ms, wheel_data.is_wheel_touched); + HandleKey(Keys::kTouchWheelCenter, now_ms, wheel_data.is_button_touched); + + HandleKey( + Keys::kDirectionalUp, now_ms, + wheel_data.is_wheel_touched && + IsAngleWithin(wheel_data.wheel_position, 0, kDPadAngleThreshold)); + HandleKey( + Keys::kDirectionalLeft, now_ms, + wheel_data.is_wheel_touched && + IsAngleWithin(wheel_data.wheel_position, 63, kDPadAngleThreshold)); + HandleKey( + Keys::kDirectionalDown, now_ms, + wheel_data.is_wheel_touched && + IsAngleWithin(wheel_data.wheel_position, 127, kDPadAngleThreshold)); + HandleKey( + Keys::kDirectionalRight, now_ms, + wheel_data.is_wheel_touched && + IsAngleWithin(wheel_data.wheel_position, 189, kDPadAngleThreshold)); + + // We now have enough information to give LVGL its update. + switch (mode_) { + case drivers::NvsStorage::InputModes::kButtonsOnly: + data->state = LV_INDEV_STATE_RELEASED; + if (ShortPressTrigger(Keys::kVolumeUp)) { + data->enc_diff = -1; + } else if (ShortPressTrigger(Keys::kVolumeDown)) { + data->enc_diff = 1; + } else if (LongPressTrigger(Keys::kVolumeDown, now_ms)) { + data->state = LV_INDEV_STATE_PRESSED; + } else if (LongPressTrigger(Keys::kVolumeUp, now_ms)) { + // TODO: Back button event + } + break; + case drivers::NvsStorage::InputModes::kButtonsWithWheel: + data->state = ShortPressTrigger(Keys::kTouchWheel) + ? LV_INDEV_STATE_PRESSED + : LV_INDEV_STATE_RELEASED; + if (ShortPressTriggerRepeating(Keys::kVolumeUp, now_ms)) { + data->enc_diff = scroller_->AddInput(now_ms, -1); + } else if (ShortPressTriggerRepeating(Keys::kVolumeDown, now_ms)) { + data->enc_diff = scroller_->AddInput(now_ms, 1); + } + + if (!touch_time_ms_.contains(Keys::kVolumeDown) && + !touch_time_ms_.contains(Keys::kVolumeUp)) { + data->enc_diff = scroller_->AddInput(now_ms, 0); + } + // TODO: Long-press events. + break; + case drivers::NvsStorage::InputModes::kDirectionalWheel: + data->state = ShortPressTrigger(Keys::kTouchWheelCenter) + ? LV_INDEV_STATE_PRESSED + : LV_INDEV_STATE_RELEASED; + if (!ShortPressTriggerRepeating(Keys::kTouchWheel, now_ms)) { + break; + } + if (ShortPressTriggerRepeating(Keys::kDirectionalUp, now_ms)) { + data->enc_diff = scroller_->AddInput(now_ms, -1); + } else if (ShortPressTriggerRepeating(Keys::kDirectionalDown, now_ms)) { + data->enc_diff = scroller_->AddInput(now_ms, 1); + } else if (ShortPressTrigger(Keys::kDirectionalRight)) { + // TODO: ??? + } else if (ShortPressTrigger(Keys::kDirectionalLeft)) { + // TODO: Back button event. + } + + if (!touch_time_ms_.contains(Keys::kDirectionalUp) && + !touch_time_ms_.contains(Keys::kDirectionalDown)) { + data->enc_diff = scroller_->AddInput(now_ms, 0); + } + // TODO: Long-press events. + break; + case drivers::NvsStorage::InputModes::kRotatingWheel: + if (!raw_wheel_.GetTouchWheelData().is_wheel_touched) { + data->enc_diff = scroller_->AddInput(now_ms, 0); + } else if (relative_wheel_->ticks() != 0) { + data->enc_diff = scroller_->AddInput(now_ms, relative_wheel_->ticks()); + } else { + data->enc_diff = 0; + } + data->state = relative_wheel_->is_clicking() ? LV_INDEV_STATE_PRESSED + : LV_INDEV_STATE_RELEASED; + // TODO: Long-press events. + break; + } - data->enc_diff = relative_wheel_->ticks(); - data->state = relative_wheel_->is_clicking() ? LV_INDEV_STATE_PRESSED - : LV_INDEV_STATE_RELEASED; + // TODO: Apply inertia / acceleration. } +auto EncoderInput::HandleKey(Keys key, uint64_t ms, bool clicked) -> void { + if (!clicked) { + touch_time_ms_.erase(key); + short_press_fired_.erase(key); + long_press_fired_.erase(key); + return; + } + if (!touch_time_ms_.contains(key)) { + touch_time_ms_[key] = ms; + } +} + +auto EncoderInput::ShortPressTrigger(Keys key) -> bool { + if (touch_time_ms_.contains(key) && !short_press_fired_.contains(key)) { + short_press_fired_[key] = true; + return true; + } + return false; +} + +auto EncoderInput::ShortPressTriggerRepeating(Keys key, uint64_t ms) -> bool { + if (touch_time_ms_.contains(key) && + (!short_press_fired_.contains(key) || + ms - touch_time_ms_[key] >= kRepeatDelayMs)) { + touch_time_ms_[key] = ms; + short_press_fired_[key] = true; + return true; + } + return false; +} + +auto EncoderInput::LongPressTrigger(Keys key, uint64_t ms) -> bool { + if (touch_time_ms_.contains(key) && !long_press_fired_.contains(key) && + ms - touch_time_ms_[key] >= kLongPressDelayMs) { + long_press_fired_[key] = true; + return true; + } + return false; +} + +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 index 9c114e80..e685a7a2 100644 --- a/src/ui/include/encoder_input.hpp +++ b/src/ui/include/encoder_input.hpp @@ -6,17 +6,22 @@ #pragma once +#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. @@ -31,6 +36,7 @@ class EncoderInput { auto Read(lv_indev_data_t* data) -> void; auto registration() -> lv_indev_t* { return registration_; } + auto mode(drivers::NvsStorage::InputModes mode) { mode_ = mode; } auto lock(bool l) -> void { is_locked_ = l; } private: @@ -40,8 +46,41 @@ class EncoderInput { drivers::IGpios& gpios_; drivers::TouchWheel& raw_wheel_; std::unique_ptr relative_wheel_; + std::unique_ptr scroller_; + drivers::NvsStorage::InputModes mode_; bool is_locked_; + + enum class Keys { + kVolumeUp, + kVolumeDown, + kTouchWheel, + kTouchWheelCenter, + kDirectionalUp, + kDirectionalRight, + kDirectionalDown, + kDirectionalLeft, + }; + + std::unordered_map touch_time_ms_; + std::unordered_map short_press_fired_; + std::unordered_map long_press_fired_; + + auto HandleKey(Keys key, uint64_t ms, bool clicked) -> void; + auto ShortPressTrigger(Keys key) -> bool; + auto ShortPressTriggerRepeating(Keys key, uint64_t ms) -> bool; + auto LongPressTrigger(Keys key, uint64_t ms) -> bool; +}; + +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