From dc5676229d19f317b97df6a3d3582bbb02df33df Mon Sep 17 00:00:00 2001 From: Tab_theFox Date: Sun, 23 Mar 2025 17:45:19 +0100 Subject: add single touch shourtcuts for touch wheel This allows you to use the touch wheel for quick playback control: - bottom quadrant for play/pause - left and right quadrant for track skip back / forward - top quadrant to seek back 25 seconds (handy when listening to a podcast) --- src/drivers/include/drivers/nvs.hpp | 1 + src/drivers/nvs.cpp | 2 + src/tangara/input/device_factory.cpp | 17 +++++++- src/tangara/input/input_hook_actions.cpp | 6 +++ src/tangara/input/input_hook_actions.hpp | 2 + src/tangara/input/input_touch_wheel.cpp | 66 ++++++++++++++++++++++++-------- src/tangara/input/input_touch_wheel.hpp | 8 +++- src/tangara/input/lvgl_input_driver.cpp | 2 + src/tangara/lua/lua_controls.cpp | 6 ++- src/tangara/ui/ui_events.hpp | 4 ++ src/tangara/ui/ui_fsm.cpp | 25 ++++++++++++ src/tangara/ui/ui_fsm.hpp | 2 + 12 files changed, 123 insertions(+), 18 deletions(-) diff --git a/src/drivers/include/drivers/nvs.hpp b/src/drivers/include/drivers/nvs.hpp index b490ac3d..6fc61131 100644 --- a/src/drivers/include/drivers/nvs.hpp +++ b/src/drivers/include/drivers/nvs.hpp @@ -147,6 +147,7 @@ class NvsStorage { kDisabled = 0, kDirectionalWheel = 1, kRotatingWheel = 2, + kWheelWithButtons = 3, }; auto WheelInput() -> WheelInputModes; diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index 64ed9c1a..d02c80e2 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -628,6 +628,8 @@ auto NvsStorage::WheelInput() -> WheelInputModes { return WheelInputModes::kDirectionalWheel; case static_cast(WheelInputModes::kRotatingWheel): return WheelInputModes::kRotatingWheel; + case static_cast(WheelInputModes::kWheelWithButtons): + return WheelInputModes::kWheelWithButtons; default: return WheelInputModes::kRotatingWheel; } diff --git a/src/tangara/input/device_factory.cpp b/src/tangara/input/device_factory.cpp index ff597556..9ae69e47 100644 --- a/src/tangara/input/device_factory.cpp +++ b/src/tangara/input/device_factory.cpp @@ -25,7 +25,11 @@ DeviceFactory::DeviceFactory( : services_(services) { if (services->touchwheel()) { wheel_ = - std::make_shared(services->nvs(), **services->touchwheel()); + std::make_shared(services->nvs(), **services->touchwheel(), services->track_queue()); + auto wheel_mode = services_->nvs().WheelInput(); + if (wheel_mode == drivers::NvsStorage::WheelInputModes::kWheelWithButtons) { + wheel_->activate_buttons(true); + } } reset_ = std::make_shared(services_->gpios()); } @@ -73,6 +77,17 @@ auto DeviceFactory::createInputs() } break; case drivers::NvsStorage::WheelInputModes::kRotatingWheel: + if (wheel_) { + wheel_->activate_buttons(false); + ret.push_back(wheel_); + } + break; + case drivers::NvsStorage::WheelInputModes::kWheelWithButtons: + if (wheel_) { + wheel_->activate_buttons(true); + ret.push_back(wheel_); + } + break; default: // Don't break input over a bad enum value. if (wheel_) { ret.push_back(wheel_); diff --git a/src/tangara/input/input_hook_actions.cpp b/src/tangara/input/input_hook_actions.cpp index 001d3fb0..587683ba 100644 --- a/src/tangara/input/input_hook_actions.cpp +++ b/src/tangara/input/input_hook_actions.cpp @@ -68,6 +68,12 @@ auto nextTrack(audio::TrackQueue& queue) -> HookCallback { }}; } +auto skipBack() -> HookCallback { + return HookCallback{.name = "skip_back", .fn = [&](lv_indev_data_t* d) { + events::Ui().Dispatch(ui::SeekBack{.seconds = 25}); + }}; +} + auto prevTrack(audio::TrackQueue& queue) -> HookCallback { return HookCallback{.name = "prev_track", .fn = [&](lv_indev_data_t* d) { queue.previous(); diff --git a/src/tangara/input/input_hook_actions.hpp b/src/tangara/input/input_hook_actions.hpp index 2db0b6e7..67902924 100644 --- a/src/tangara/input/input_hook_actions.hpp +++ b/src/tangara/input/input_hook_actions.hpp @@ -27,6 +27,8 @@ auto togglePlayPause() -> HookCallback; auto nextTrack(audio::TrackQueue& queue) -> HookCallback; auto prevTrack(audio::TrackQueue& queue) -> HookCallback; +auto skipBack() -> HookCallback; + auto volumeUp() -> HookCallback; auto volumeDown() -> HookCallback; diff --git a/src/tangara/input/input_touch_wheel.cpp b/src/tangara/input/input_touch_wheel.cpp index ae192e31..0a8bab48 100644 --- a/src/tangara/input/input_touch_wheel.cpp +++ b/src/tangara/input/input_touch_wheel.cpp @@ -25,9 +25,12 @@ namespace input { -TouchWheel::TouchWheel(drivers::NvsStorage& nvs, drivers::TouchWheel& wheel) +TouchWheel::TouchWheel(drivers::NvsStorage& nvs, + drivers::TouchWheel& wheel, + audio::TrackQueue& queue) : nvs_(nvs), wheel_(wheel), + queue_(queue), sensitivity_(static_cast(nvs.ScrollSensitivity()), [&](const lua::LuaValue& val) { if (!std::holds_alternative(val)) { @@ -55,7 +58,23 @@ TouchWheel::TouchWheel(drivers::NvsStorage& nvs, drivers::TouchWheel& wheel) threshold_(calculateThreshold(nvs.ScrollSensitivity())), is_first_read_(true), last_angle_(0), - last_wheel_touch_time_(0) {} + last_wheel_touch_time_(0), + buttons_active_(false) {} + +auto TouchWheel::activate_buttons(bool activate) -> void { + if (activate) { + up_.override(Trigger::State::kClick, {actions::skipBack()}); + left_.override(Trigger::State::kClick, {actions::prevTrack(queue_)}); + right_.override(Trigger::State::kClick, {actions::nextTrack(queue_)}); + down_.override(Trigger::State::kClick, {actions::togglePlayPause()}); + } else { + up_.override(Trigger::State::kClick, std::nullopt); + left_.override(Trigger::State::kClick, std::nullopt); + right_.override(Trigger::State::kClick, std::nullopt); + down_.override(Trigger::State::kClick, std::nullopt); + } + buttons_active_ = activate; +} auto TouchWheel::read(lv_indev_data_t* data, std::vector& events) -> void { if (locked_) { @@ -99,25 +118,42 @@ auto TouchWheel::read(lv_indev_data_t* data, std::vector& events) -> // If the user is touching the wheel but not scrolling, then they may be // clicking on one of the wheel's cardinal directions. + const uint64_t now_us = esp_timer_get_time() / 1000; if (is_scrolling_) { up_.cancel(); right_.cancel(); down_.cancel(); left_.cancel(); + last_scroll_ = now_us; } else { - bool pressing = wheel_data.is_wheel_touched; - up_.update(pressing && drivers::TouchWheel::isAngleWithin( - wheel_data.wheel_position, 0, 32), - data); - right_.update(pressing && drivers::TouchWheel::isAngleWithin( - wheel_data.wheel_position, 192, 32), - data); - down_.update(pressing && drivers::TouchWheel::isAngleWithin( - wheel_data.wheel_position, 128, 32), - data); - left_.update(pressing && drivers::TouchWheel::isAngleWithin( - wheel_data.wheel_position, 64, 32), - data); + const auto time_since_last_scroll = last_scroll_ - now_us; + bool pressing = wheel_data.is_wheel_touched && + (time_since_last_scroll > SCROLL_TIMEOUT_US); + + const auto ustate = + up_.update(pressing && drivers::TouchWheel::isAngleWithin( + wheel_data.wheel_position, 0, 32), + data); + const auto rstate = + right_.update(pressing && drivers::TouchWheel::isAngleWithin( + wheel_data.wheel_position, 192, 32), + data); + const auto dstate = + down_.update(pressing && drivers::TouchWheel::isAngleWithin( + wheel_data.wheel_position, 128, 32), + data); + const auto lstate = + left_.update(pressing && drivers::TouchWheel::isAngleWithin( + wheel_data.wheel_position, 64, 32), + data); + + // This is for haptic feedback. + if (buttons_active_ && + (ustate == Trigger::State::kClick || rstate == Trigger::State::kClick || + dstate == Trigger::State::kClick || + lstate == Trigger::State::kClick)) { + events.push_back(InputEvent::kOnPress); + } } } diff --git a/src/tangara/input/input_touch_wheel.hpp b/src/tangara/input/input_touch_wheel.hpp index 3f2ed49e..9c9135b4 100644 --- a/src/tangara/input/input_touch_wheel.hpp +++ b/src/tangara/input/input_touch_wheel.hpp @@ -23,7 +23,7 @@ namespace input { class TouchWheel : public IInputDevice { public: - TouchWheel(drivers::NvsStorage&, drivers::TouchWheel&); + TouchWheel(drivers::NvsStorage&, drivers::TouchWheel&, audio::TrackQueue&); auto read(lv_indev_data_t* data, std::vector& events) -> void override; @@ -35,6 +35,8 @@ class TouchWheel : public IInputDevice { auto sensitivity() -> lua::Property&; + auto activate_buttons(bool activate) -> void; + private: const int64_t SCROLL_TIMEOUT_US = 250000; // 250ms @@ -44,6 +46,8 @@ class TouchWheel : public IInputDevice { drivers::NvsStorage& nvs_; drivers::TouchWheel& wheel_; + audio::TrackQueue& queue_; + lua::Property sensitivity_; TriggerHooks centre_; @@ -54,10 +58,12 @@ class TouchWheel : public IInputDevice { bool locked_; bool is_scrolling_; + uint64_t last_scroll_; uint8_t threshold_; bool is_first_read_; uint8_t last_angle_; int64_t last_wheel_touch_time_; + bool buttons_active_; }; } // namespace input diff --git a/src/tangara/input/lvgl_input_driver.cpp b/src/tangara/input/lvgl_input_driver.cpp index b4bab365..00cc2b81 100644 --- a/src/tangara/input/lvgl_input_driver.cpp +++ b/src/tangara/input/lvgl_input_driver.cpp @@ -64,6 +64,8 @@ auto intToWheelMode(int raw) return drivers::NvsStorage::WheelInputModes::kDirectionalWheel; case 2: return drivers::NvsStorage::WheelInputModes::kRotatingWheel; + case 3: + return drivers::NvsStorage::WheelInputModes::kWheelWithButtons; default: return {}; } diff --git a/src/tangara/lua/lua_controls.cpp b/src/tangara/lua/lua_controls.cpp index 69053f43..f1305c55 100644 --- a/src/tangara/lua/lua_controls.cpp +++ b/src/tangara/lua/lua_controls.cpp @@ -36,7 +36,11 @@ static auto wheel_schemes(lua_State* L) -> int { lua_pushliteral(L, "Touchwheel"); lua_rawseti( - L, -2, static_cast(drivers::NvsStorage::WheelInputModes::kRotatingWheel)); + L, -2, static_cast(drivers::NvsStorage::WheelInputModes::kRotatingWheel)); + + lua_pushliteral(L, "Wheel with Buttons"); + lua_rawseti(L, -2, + static_cast(drivers::NvsStorage::WheelInputModes::kWheelWithButtons)); return 1; } diff --git a/src/tangara/ui/ui_events.hpp b/src/tangara/ui/ui_events.hpp index 0f371769..ce9320fc 100644 --- a/src/tangara/ui/ui_events.hpp +++ b/src/tangara/ui/ui_events.hpp @@ -34,6 +34,10 @@ struct Screenshot : tinyfsm::Event { std::string filename; }; +struct SeekBack : tinyfsm::Event { + uint32_t seconds; +}; + namespace internal { struct InitDisplay : tinyfsm::Event { diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index aaf35d13..1b8c0984 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -403,6 +403,31 @@ void UiState::react(const Screenshot& ev) { SaveScreenshot(sCurrentScreen->root(), ev.filename); } +void UiState::react(const SeekBack& ev) { + const auto playback_position = sPlaybackPosition.get(); + if (!std::holds_alternative(playback_position)) { + // I don't think this ever happens, but check anyway. + return; + } + + const auto track = sPlaybackTrack.get(); + if (!std::holds_alternative(track)) { + // Nothing is playing + return; + } + + const auto current_position = std::get(playback_position); + int32_t seek_position = current_position - ev.seconds; + if (seek_position < 1) { + seek_position = 0; + } + + events::Audio().Dispatch(audio::SetTrack{ + .new_track = std::get(track).uri, + .seek_to_second = seek_position, + }); +} + void UiState::react(const system_fsm::KeyLockChanged& ev) { sDisplay->SetDisplayOn(!ev.locking); sInput->lock(ev.locking); diff --git a/src/tangara/ui/ui_fsm.hpp b/src/tangara/ui/ui_fsm.hpp index d4354bec..01f0beb1 100644 --- a/src/tangara/ui/ui_fsm.hpp +++ b/src/tangara/ui/ui_fsm.hpp @@ -56,6 +56,8 @@ class UiState : public tinyfsm::Fsm { void react(const tinyfsm::Event& ev) {} void react(const Screenshot&); + void react(const SeekBack&); + virtual void react(const OnLuaError&) {} virtual void react(const DumpLuaStack&) {} virtual void react(const internal::BackPressed&) {} -- cgit v1.2.3