summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcooljqln <cooljqln@noreply.codeberg.org>2024-04-11 07:02:53 +0000
committercooljqln <cooljqln@noreply.codeberg.org>2024-04-11 07:02:53 +0000
commitdd1ea595a7753706d4fa5f19b66f3dc1cbd56a02 (patch)
tree60bfa4569af0f9506ccffe85f19e89bbe2a83332
parentf580928cbab797e4e8a3eae5ae1c0b18b8066066 (diff)
parent33919e9e3f419e13318fa6b8217d8c8dcd86c1eb (diff)
downloadtangara-fw-dd1ea595a7753706d4fa5f19b66f3dc1cbd56a02.tar.gz
Merge pull request 'jqln/input-devices' (#62) from jqln/input-devices into main
Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/62 Reviewed-by: ailurux <ailurux@noreply.codeberg.org>
-rw-r--r--src/drivers/CMakeLists.txt3
-rw-r--r--src/drivers/include/relative_wheel.hpp52
-rw-r--r--src/drivers/include/touchwheel.hpp4
-rw-r--r--src/drivers/relative_wheel.cpp92
-rw-r--r--src/drivers/touchwheel.cpp7
-rw-r--r--src/input/CMakeLists.txt12
-rw-r--r--src/input/device_factory.cpp58
-rw-r--r--src/input/feedback_haptics.cpp37
-rw-r--r--src/input/include/device_factory.hpp39
-rw-r--r--src/input/include/feedback_device.hpp32
-rw-r--r--src/input/include/feedback_haptics.hpp26
-rw-r--r--src/input/include/input_device.hpp39
-rw-r--r--src/input/include/input_nav_buttons.hpp34
-rw-r--r--src/input/include/input_touch_dpad.hpp36
-rw-r--r--src/input/include/input_touch_wheel.hpp51
-rw-r--r--src/input/include/input_trigger.hpp37
-rw-r--r--src/input/include/input_volume_buttons.hpp34
-rw-r--r--src/input/include/lvgl_input_driver.hpp58
-rw-r--r--src/input/input_nav_buttons.cpp44
-rw-r--r--src/input/input_touch_dpad.cpp75
-rw-r--r--src/input/input_touch_wheel.cpp143
-rw-r--r--src/input/input_trigger.cpp72
-rw-r--r--src/input/input_volume_buttons.cpp35
-rw-r--r--src/input/lvgl_input_driver.cpp107
-rw-r--r--src/system_fsm/include/system_fsm.hpp1
-rw-r--r--src/system_fsm/system_fsm.cpp1
-rw-r--r--src/ui/CMakeLists.txt8
-rw-r--r--src/ui/encoder_input.cpp358
-rw-r--r--src/ui/include/encoder_input.hpp107
-rw-r--r--src/ui/include/lvgl_task.hpp7
-rw-r--r--src/ui/include/ui_events.hpp1
-rw-r--r--src/ui/include/ui_fsm.hpp14
-rw-r--r--src/ui/lvgl_task.cpp15
-rw-r--r--src/ui/ui_fsm.cpp100
34 files changed, 1021 insertions, 718 deletions
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 <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <stdint.h>
-#include <cstdint>
-#include <functional>
-
-#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/include/touchwheel.hpp b/src/drivers/include/touchwheel.hpp
index 9d002156..18ace2b4 100644
--- a/src/drivers/include/touchwheel.hpp
+++ b/src/drivers/include/touchwheel.hpp
@@ -24,6 +24,10 @@ struct TouchWheelData {
class TouchWheel {
public:
+ static auto isAngleWithin(int16_t wheel_angle,
+ int16_t target_angle,
+ int threshold) -> bool;
+
static auto Create() -> TouchWheel* { return new TouchWheel(); }
TouchWheel();
~TouchWheel();
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 <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "relative_wheel.hpp"
-
-#include <stdint.h>
-#include <cstdint>
-
-#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/drivers/touchwheel.cpp b/src/drivers/touchwheel.cpp
index a20f434b..41b9a6af 100644
--- a/src/drivers/touchwheel.cpp
+++ b/src/drivers/touchwheel.cpp
@@ -28,6 +28,13 @@ namespace drivers {
static const uint8_t kTouchWheelAddress = 0x1C;
static const gpio_num_t kIntPin = GPIO_NUM_25;
+auto TouchWheel::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;
+}
+
TouchWheel::TouchWheel() {
gpio_config_t int_config{
.pin_bit_mask = 1ULL << kIntPin,
diff --git a/src/input/CMakeLists.txt b/src/input/CMakeLists.txt
new file mode 100644
index 00000000..3bc3e39d
--- /dev/null
+++ b/src/input/CMakeLists.txt
@@ -0,0 +1,12 @@
+# Copyright 2023 jacqueline <me@jacqueline.id.au>
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+idf_component_register(
+ SRCS "input_touch_wheel.cpp" "input_touch_dpad.cpp" "input_trigger.cpp"
+ "input_volume_buttons.cpp" "lvgl_input_driver.cpp" "feedback_haptics.cpp"
+ "device_factory.cpp" "input_nav_buttons.cpp"
+ INCLUDE_DIRS "include"
+ REQUIRES "drivers" "lvgl" "events" "system_fsm")
+
+target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})
diff --git a/src/input/device_factory.cpp b/src/input/device_factory.cpp
new file mode 100644
index 00000000..65f4d785
--- /dev/null
+++ b/src/input/device_factory.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "device_factory.hpp"
+
+#include <memory>
+
+#include "feedback_haptics.hpp"
+#include "input_device.hpp"
+#include "input_nav_buttons.hpp"
+#include "input_touch_dpad.hpp"
+#include "input_touch_wheel.hpp"
+#include "input_volume_buttons.hpp"
+
+namespace input {
+
+DeviceFactory::DeviceFactory(
+ std::shared_ptr<system_fsm::ServiceLocator> services)
+ : services_(services) {
+ if (services->touchwheel()) {
+ wheel_ =
+ std::make_shared<TouchWheel>(services->nvs(), **services->touchwheel());
+ }
+}
+
+auto DeviceFactory::createInputs(drivers::NvsStorage::InputModes mode)
+ -> std::vector<std::shared_ptr<IInputDevice>> {
+ std::vector<std::shared_ptr<IInputDevice>> ret;
+ switch (mode) {
+ case drivers::NvsStorage::InputModes::kButtonsOnly:
+ ret.push_back(std::make_shared<NavButtons>(services_->gpios()));
+ break;
+ case drivers::NvsStorage::InputModes::kDirectionalWheel:
+ ret.push_back(std::make_shared<VolumeButtons>(services_->gpios()));
+ if (services_->touchwheel()) {
+ ret.push_back(std::make_shared<TouchDPad>(**services_->touchwheel()));
+ }
+ break;
+ case drivers::NvsStorage::InputModes::kRotatingWheel:
+ default: // Don't break input over a bad enum value.
+ ret.push_back(std::make_shared<VolumeButtons>(services_->gpios()));
+ if (wheel_) {
+ ret.push_back(wheel_);
+ }
+ break;
+ }
+ return ret;
+}
+
+auto DeviceFactory::createFeedbacks()
+ -> std::vector<std::shared_ptr<IFeedbackDevice>> {
+ return {std::make_shared<Haptics>(services_->haptics())};
+}
+
+} // namespace input
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 <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "feedback_haptics.hpp"
+
+#include <cstdint>
+
+#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/device_factory.hpp b/src/input/include/device_factory.hpp
new file mode 100644
index 00000000..dd9c7133
--- /dev/null
+++ b/src/input/include/device_factory.hpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+
+#include "feedback_device.hpp"
+#include "input_device.hpp"
+#include "input_touch_wheel.hpp"
+#include "nvs.hpp"
+#include "service_locator.hpp"
+
+namespace input {
+
+class DeviceFactory {
+ public:
+ DeviceFactory(std::shared_ptr<system_fsm::ServiceLocator>);
+
+ auto createInputs(drivers::NvsStorage::InputModes mode)
+ -> std::vector<std::shared_ptr<IInputDevice>>;
+
+ auto createFeedbacks() -> std::vector<std::shared_ptr<IFeedbackDevice>>;
+
+ auto touch_wheel() -> std::shared_ptr<TouchWheel> { return wheel_; }
+
+ private:
+ std::shared_ptr<system_fsm::ServiceLocator> services_;
+
+ // HACK: the touchwheel is current a special case, since it's the only input
+ // device that has some kind of setting/configuration; scroll sensitivity.
+ std::shared_ptr<TouchWheel> wheel_;
+};
+
+} // 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 <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <cstdint>
+
+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 <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <cstdint>
+
+#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..5fc3e066
--- /dev/null
+++ b/src/input/include/input_device.hpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <functional>
+#include <string>
+#include <vector>
+
+#include "hal/lv_hal_indev.h"
+#include "property.hpp"
+
+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?)
+
+ virtual auto settings()
+ -> std::vector<std::pair<std::string, lua::Property>> {
+ return {};
+ }
+};
+
+} // namespace input
diff --git a/src/input/include/input_nav_buttons.hpp b/src/input/include/input_nav_buttons.hpp
new file mode 100644
index 00000000..29a19a16
--- /dev/null
+++ b/src/input/include/input_nav_buttons.hpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <cstdint>
+
+#include "gpios.hpp"
+#include "hal/lv_hal_indev.h"
+
+#include "haptics.hpp"
+#include "input_device.hpp"
+#include "input_trigger.hpp"
+#include "touchwheel.hpp"
+
+namespace input {
+
+class NavButtons : public IInputDevice {
+ public:
+ NavButtons(drivers::IGpios&);
+
+ auto read(lv_indev_data_t* data) -> void override;
+
+ private:
+ drivers::IGpios& gpios_;
+
+ Trigger up_;
+ Trigger down_;
+};
+
+} // 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..03936acb
--- /dev/null
+++ b/src/input/include/input_touch_dpad.hpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <cstdint>
+
+#include "hal/lv_hal_indev.h"
+
+#include "haptics.hpp"
+#include "input_device.hpp"
+#include "input_trigger.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_;
+
+ Trigger up_;
+ Trigger right_;
+ Trigger down_;
+ Trigger left_;
+};
+
+} // 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..c81cbb1a
--- /dev/null
+++ b/src/input/include/input_touch_wheel.hpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <sys/_stdint.h>
+#include <cstdint>
+
+#include "hal/lv_hal_indev.h"
+
+#include "haptics.hpp"
+#include "input_device.hpp"
+#include "input_trigger.hpp"
+#include "nvs.hpp"
+#include "property.hpp"
+#include "touchwheel.hpp"
+
+namespace input {
+
+class TouchWheel : public IInputDevice {
+ public:
+ TouchWheel(drivers::NvsStorage&, drivers::TouchWheel&);
+
+ auto read(lv_indev_data_t* data) -> void override;
+
+ auto sensitivity() -> lua::Property&;
+
+ private:
+ auto calculateTicks(const drivers::TouchWheelData& data) -> int8_t;
+ auto calculateThreshold(uint8_t sensitivity) -> uint8_t;
+
+ drivers::NvsStorage& nvs_;
+ drivers::TouchWheel& wheel_;
+
+ lua::Property sensitivity_;
+
+ Trigger up_;
+ Trigger right_;
+ Trigger down_;
+ Trigger left_;
+
+ bool is_scrolling_;
+ uint8_t threshold_;
+ bool is_first_read_;
+ uint8_t last_angle_;
+};
+
+} // namespace input
diff --git a/src/input/include/input_trigger.hpp b/src/input/include/input_trigger.hpp
new file mode 100644
index 00000000..599b796b
--- /dev/null
+++ b/src/input/include/input_trigger.hpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <optional>
+
+#include "hal/lv_hal_indev.h"
+
+namespace input {
+
+const uint16_t kLongPressDelayMs = LV_INDEV_DEF_LONG_PRESS_TIME;
+const uint16_t kRepeatDelayMs = LV_INDEV_DEF_LONG_PRESS_REP_TIME;
+
+class Trigger {
+ public:
+ enum class State {
+ kNone,
+ kClick,
+ kLongPress,
+ kRepeatPress,
+ };
+
+ Trigger();
+
+ auto update(bool is_pressed) -> State;
+
+ private:
+ std::optional<uint64_t> touch_time_ms_;
+ uint16_t times_fired_;
+};
+
+} // 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..162ca8d9
--- /dev/null
+++ b/src/input/include/input_volume_buttons.hpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <cstdint>
+
+#include "gpios.hpp"
+#include "hal/lv_hal_indev.h"
+
+#include "haptics.hpp"
+#include "input_device.hpp"
+#include "input_trigger.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_;
+
+ Trigger up_;
+ Trigger down_;
+};
+
+} // 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..257ccb28
--- /dev/null
+++ b/src/input/include/lvgl_input_driver.hpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <deque>
+#include <memory>
+#include <set>
+
+#include "core/lv_group.h"
+#include "device_factory.hpp"
+#include "feedback_device.hpp"
+#include "gpios.hpp"
+#include "hal/lv_hal_indev.h"
+
+#include "input_device.hpp"
+#include "nvs.hpp"
+#include "property.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(drivers::NvsStorage& nvs, DeviceFactory&);
+
+ auto mode() -> lua::Property& { return mode_; }
+
+ 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:
+ drivers::NvsStorage& nvs_;
+ DeviceFactory& factory_;
+
+ lua::Property mode_;
+ lv_indev_drv_t driver_;
+ lv_indev_t* registration_;
+
+ std::vector<std::shared_ptr<IInputDevice>> inputs_;
+ std::vector<std::shared_ptr<IFeedbackDevice>> feedbacks_;
+
+ bool is_locked_;
+};
+
+} // namespace input
diff --git a/src/input/input_nav_buttons.cpp b/src/input/input_nav_buttons.cpp
new file mode 100644
index 00000000..d83568c8
--- /dev/null
+++ b/src/input/input_nav_buttons.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input_nav_buttons.hpp"
+
+#include "event_queue.hpp"
+#include "gpios.hpp"
+#include "hal/lv_hal_indev.h"
+
+namespace input {
+
+NavButtons::NavButtons(drivers::IGpios& gpios) : gpios_(gpios) {}
+
+auto NavButtons::read(lv_indev_data_t* data) -> void {
+ bool vol_up = gpios_.Get(drivers::IGpios::Pin::kKeyUp);
+ switch (up_.update(!vol_up)) {
+ case Trigger::State::kClick:
+ data->enc_diff = -1;
+ break;
+ case Trigger::State::kLongPress:
+ events::Ui().Dispatch(ui::internal::BackPressed{});
+ break;
+ default:
+ break;
+ }
+
+ bool vol_down = gpios_.Get(drivers::IGpios::Pin::kKeyDown);
+ switch (down_.update(!vol_down)) {
+ case Trigger::State::kClick:
+ data->enc_diff = 1;
+ break;
+ case Trigger::State::kLongPress:
+ data->state = LV_INDEV_STATE_PRESSED;
+ break;
+ default:
+ data->state = LV_INDEV_STATE_RELEASED;
+ break;
+ }
+}
+
+} // namespace input
diff --git a/src/input/input_touch_dpad.cpp b/src/input/input_touch_dpad.cpp
new file mode 100644
index 00000000..828d6b59
--- /dev/null
+++ b/src/input/input_touch_dpad.cpp
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input_touch_dpad.hpp"
+
+#include <cstdint>
+
+#include "hal/lv_hal_indev.h"
+
+#include "event_queue.hpp"
+#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();
+ auto wheel_data = wheel_.GetTouchWheelData();
+
+ if (wheel_data.is_button_touched) {
+ data->state = LV_INDEV_STATE_PRESSED;
+ } else {
+ data->state = LV_INDEV_STATE_RELEASED;
+ }
+
+ switch (up_.update(
+ wheel_data.is_wheel_touched &&
+ drivers::TouchWheel::isAngleWithin(wheel_data.wheel_position, 0, 32))) {
+ case Trigger::State::kNone:
+ break;
+ default:
+ data->enc_diff = -1;
+ break;
+ }
+ switch (right_.update(
+ wheel_data.is_wheel_touched &&
+ drivers::TouchWheel::isAngleWithin(wheel_data.wheel_position, 192, 32))) {
+ default:
+ break;
+ }
+ switch (down_.update(
+ wheel_data.is_wheel_touched &&
+ drivers::TouchWheel::isAngleWithin(wheel_data.wheel_position, 128, 32))) {
+ case Trigger::State::kNone:
+ break;
+ default:
+ data->enc_diff = 1;
+ break;
+ }
+ switch (left_.update(
+ wheel_data.is_wheel_touched &&
+ drivers::TouchWheel::isAngleWithin(wheel_data.wheel_position, 64, 32))) {
+ case Trigger::State::kLongPress:
+ events::Ui().Dispatch(ui::internal::BackPressed{});
+ break;
+ default:
+ break;
+ }
+}
+
+} // namespace input
diff --git a/src/input/input_touch_wheel.cpp b/src/input/input_touch_wheel.cpp
new file mode 100644
index 00000000..7670e342
--- /dev/null
+++ b/src/input/input_touch_wheel.cpp
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input_touch_wheel.hpp"
+#include <stdint.h>
+
+#include <cstdint>
+#include <variant>
+
+#include "event_queue.hpp"
+#include "hal/lv_hal_indev.h"
+
+#include "haptics.hpp"
+#include "input_device.hpp"
+#include "input_trigger.hpp"
+#include "nvs.hpp"
+#include "property.hpp"
+#include "touchwheel.hpp"
+#include "ui_events.hpp"
+
+namespace input {
+
+TouchWheel::TouchWheel(drivers::NvsStorage& nvs, drivers::TouchWheel& wheel)
+ : nvs_(nvs),
+ wheel_(wheel),
+ sensitivity_(static_cast<int>(nvs.ScrollSensitivity()),
+ [&](const lua::LuaValue& val) {
+ if (!std::holds_alternative<int>(val)) {
+ return false;
+ }
+ int int_val = std::get<int>(val);
+ if (int_val < 0 || int_val > UINT8_MAX) {
+ return false;
+ }
+ nvs.ScrollSensitivity(int_val);
+ threshold_ = calculateThreshold(int_val);
+ return true;
+ }),
+ is_scrolling_(false),
+ threshold_(calculateThreshold(nvs.ScrollSensitivity())),
+ 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 = calculateTicks(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;
+ }
+
+ // If the user is touching the wheel but not scrolling, then they may be
+ // clicking on one of the wheel's cardinal directions.
+ bool pressing = wheel_data.is_wheel_touched && !is_scrolling_;
+
+ switch (up_.update(pressing && drivers::TouchWheel::isAngleWithin(
+ wheel_data.wheel_position, 0, 32))) {
+ case Trigger::State::kLongPress:
+ data->enc_diff = INT16_MIN;
+ break;
+ default:
+ break;
+ }
+ switch (right_.update(pressing && drivers::TouchWheel::isAngleWithin(
+ wheel_data.wheel_position, 192, 32))) {
+ default:
+ break;
+ }
+ switch (down_.update(pressing && drivers::TouchWheel::isAngleWithin(
+ wheel_data.wheel_position, 128, 32))) {
+ case Trigger::State::kLongPress:
+ data->enc_diff = INT16_MAX;
+ break;
+ default:
+ break;
+ }
+ switch (left_.update(pressing && drivers::TouchWheel::isAngleWithin(
+ wheel_data.wheel_position, 64, 32))) {
+ case Trigger::State::kLongPress:
+ events::Ui().Dispatch(ui::internal::BackPressed{});
+ break;
+ default:
+ break;
+ }
+}
+
+auto TouchWheel::sensitivity() -> lua::Property& {
+ return sensitivity_;
+}
+
+auto TouchWheel::calculateTicks(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;
+ }
+}
+
+auto TouchWheel::calculateThreshold(uint8_t sensitivity) -> uint8_t {
+ int tmax = 35;
+ int tmin = 5;
+ return (((255. - sensitivity) / 255.) * (tmax - tmin) + tmin);
+}
+
+} // namespace input
diff --git a/src/input/input_trigger.cpp b/src/input/input_trigger.cpp
new file mode 100644
index 00000000..9485ecb4
--- /dev/null
+++ b/src/input/input_trigger.cpp
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input_trigger.hpp"
+#include <sys/_stdint.h>
+
+#include <cstdint>
+#include "esp_log.h"
+#include "esp_timer.h"
+
+namespace input {
+
+Trigger::Trigger() : touch_time_ms_(), times_fired_(0) {}
+
+auto Trigger::update(bool is_pressed) -> State {
+ // Bail out early if we're in a steady-state of not pressed.
+ if (!is_pressed && !touch_time_ms_) {
+ return State::kNone;
+ }
+
+ uint64_t now_ms = esp_timer_get_time() / 1000;
+
+ // Initial press of this key: record the current time, and report that we
+ // haven't triggered yet.
+ if (is_pressed && !touch_time_ms_) {
+ touch_time_ms_ = now_ms;
+ times_fired_ = 0;
+ return State::kNone;
+ }
+
+ // The key was released. If there were no long-press events fired during the
+ // press, then this was a standard click.
+ if (!is_pressed && touch_time_ms_) {
+ touch_time_ms_.reset();
+ if (times_fired_ == 0) {
+ return State::kClick;
+ } else {
+ return State::kNone;
+ }
+ }
+
+ // Now the more complicated case: the user is continuing to press the button.
+ if (times_fired_ == 0) {
+ // We haven't fired yet, so we wait for the long-press event.
+ if (now_ms - *touch_time_ms_ >= kLongPressDelayMs) {
+ times_fired_++;
+ return State::kLongPress;
+ }
+ } else {
+ // We've already fired at least once. How long has the user been holding
+ // the key for?
+ uint64_t time_since_long_press =
+ now_ms - (*touch_time_ms_ + kLongPressDelayMs);
+
+ // How many times should we have fired?
+ // 1 initial fire (for the long-press), plus one additional fire every
+ // kRepeatDelayMs since the long-press event.
+ uint16_t expected_times_fired =
+ 1 + (time_since_long_press / kRepeatDelayMs);
+ if (times_fired_ < expected_times_fired) {
+ times_fired_++;
+ return State::kRepeatPress;
+ }
+ }
+
+ return State::kNone;
+}
+
+} // namespace input
diff --git a/src/input/input_volume_buttons.cpp b/src/input/input_volume_buttons.cpp
new file mode 100644
index 00000000..0413222c
--- /dev/null
+++ b/src/input/input_volume_buttons.cpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input_volume_buttons.hpp"
+#include "event_queue.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);
+ switch (up_.update(!vol_up)) {
+ case Trigger::State::kNone:
+ break;
+ default:
+ events::Audio().Dispatch(audio::StepUpVolume{});
+ break;
+ }
+
+ bool vol_down = gpios_.Get(drivers::IGpios::Pin::kKeyDown);
+ switch (down_.update(!vol_down)) {
+ case Trigger::State::kNone:
+ break;
+ default:
+ events::Audio().Dispatch(audio::StepDownVolume{});
+ break;
+ }
+}
+
+} // namespace input
diff --git a/src/input/lvgl_input_driver.cpp b/src/input/lvgl_input_driver.cpp
new file mode 100644
index 00000000..9698aa79
--- /dev/null
+++ b/src/input/lvgl_input_driver.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lvgl_input_driver.hpp"
+#include <stdint.h>
+
+#include <cstdint>
+#include <memory>
+#include <variant>
+
+#include "device_factory.hpp"
+#include "feedback_haptics.hpp"
+#include "input_touch_wheel.hpp"
+#include "input_trigger.hpp"
+#include "input_volume_buttons.hpp"
+#include "lvgl.h"
+#include "nvs.hpp"
+#include "property.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<LvglInputDriver*>(drv->user_data);
+ instance->read(data);
+}
+
+static void feedback_cb(lv_indev_drv_t* drv, uint8_t event) {
+ LvglInputDriver* instance =
+ reinterpret_cast<LvglInputDriver*>(drv->user_data);
+ instance->feedback(event);
+}
+
+auto intToMode(int raw) -> std::optional<drivers::NvsStorage::InputModes> {
+ switch (raw) {
+ case 0:
+ return drivers::NvsStorage::InputModes::kButtonsOnly;
+ case 1:
+ return drivers::NvsStorage::InputModes::kButtonsWithWheel;
+ case 2:
+ return drivers::NvsStorage::InputModes::kDirectionalWheel;
+ case 3:
+ return drivers::NvsStorage::InputModes::kRotatingWheel;
+ default:
+ return {};
+ }
+}
+
+LvglInputDriver::LvglInputDriver(drivers::NvsStorage& nvs,
+ DeviceFactory& factory)
+ : nvs_(nvs),
+ factory_(factory),
+ mode_(static_cast<int>(nvs.PrimaryInput()),
+ [&](const lua::LuaValue& val) {
+ if (!std::holds_alternative<int>(val)) {
+ return false;
+ }
+ auto mode = intToMode(std::get<int>(val));
+ if (!mode) {
+ return false;
+ }
+ nvs.PrimaryInput(*mode);
+ inputs_ = factory.createInputs(*mode);
+ return true;
+ }),
+ driver_(),
+ registration_(nullptr),
+ inputs_(factory.createInputs(nvs.PrimaryInput())),
+ feedbacks_(factory.createFeedbacks()),
+ 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;
+ driver_.long_press_time = kLongPressDelayMs;
+ driver_.long_press_repeat_time = kRepeatDelayMs;
+
+ registration_ = lv_indev_drv_register(&driver_);
+}
+
+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 <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "encoder_input.hpp"
-
-#include <sys/_stdint.h>
-#include <memory>
-
-#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<EncoderInput*>(drv->user_data);
- instance->Read(data);
-}
-
-EncoderInput::EncoderInput(drivers::IGpios& gpios, drivers::TouchWheel& wheel)
- : gpios_(gpios),
- raw_wheel_(wheel),
- relative_wheel_(std::make_unique<drivers::RelativeWheel>(wheel)),
- scroller_(std::make_unique<Scroller>()),
- 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 <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <stdint.h>
-#include <deque>
-#include <memory>
-#include <set>
-
-#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<drivers::RelativeWheel> relative_wheel_;
- std::unique_ptr<Scroller> 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<Keys, uint64_t> touch_time_ms_;
- // Set of keys that were released during the current update.
- std::set<Keys> just_released_;
- // Set of keys that have had an event fired for them since being pressed.
- std::set<Keys> 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<EncoderInput> input) -> void;
+ auto input(std::shared_ptr<input::LvglInputDriver> input) -> void;
private:
UiTask();
auto Main() -> void;
- std::shared_ptr<EncoderInput> input_;
+ std::shared_ptr<input::LvglInputDriver> input_;
std::shared_ptr<Screen> current_screen_;
};
diff --git a/src/ui/include/ui_events.hpp b/src/ui/include/ui_events.hpp
index 5c033b0c..81e0543a 100644
--- a/src/ui/include/ui_events.hpp
+++ b/src/ui/include/ui_events.hpp
@@ -32,7 +32,6 @@ struct DumpLuaStack : tinyfsm::Event {};
namespace internal {
-struct ControlSchemeChanged : tinyfsm::Event {};
struct ReindexDatabase : tinyfsm::Event {};
struct BackPressed : tinyfsm::Event {};
diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp
index 2bab487d..8eafc6e0 100644
--- a/src/ui/include/ui_fsm.hpp
+++ b/src/ui/include/ui_fsm.hpp
@@ -13,15 +13,18 @@
#include "audio_events.hpp"
#include "battery.hpp"
#include "db_events.hpp"
+#include "device_factory.hpp"
#include "display.hpp"
-#include "encoder_input.hpp"
+#include "feedback_haptics.hpp"
#include "gpios.hpp"
+#include "input_touch_wheel.hpp"
+#include "input_volume_buttons.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"
@@ -68,7 +71,6 @@ class UiState : public tinyfsm::Fsm<UiState> {
void react(const system_fsm::SamdUsbStatusChanged&);
void react(const internal::DismissAlerts&);
- void react(const internal::ControlSchemeChanged&);
void react(const database::event::UpdateStarted&);
void react(const database::event::UpdateProgress&){};
@@ -92,7 +94,9 @@ class UiState : public tinyfsm::Fsm<UiState> {
static std::unique_ptr<UiTask> sTask;
static std::shared_ptr<system_fsm::ServiceLocator> sServices;
static std::unique_ptr<drivers::Display> sDisplay;
- static std::shared_ptr<EncoderInput> sInput;
+
+ static std::shared_ptr<input::LvglInputDriver> sInput;
+ static std::unique_ptr<input::DeviceFactory> sDeviceFactory;
static std::stack<std::shared_ptr<Screen>> sScreens;
static std::shared_ptr<Screen> sCurrentScreen;
@@ -126,8 +130,6 @@ class UiState : public tinyfsm::Fsm<UiState> {
static lua::Property sDisplayBrightness;
- static lua::Property sControlsScheme;
- static lua::Property sScrollSensitivity;
static lua::Property sLockSwitch;
static lua::Property sDatabaseUpdating;
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<EncoderInput> input) -> void {
+auto UiTask::input(std::shared_ptr<input::LvglInputDriver> 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 739c4598..1c296ac7 100644
--- a/src/ui/ui_fsm.cpp
+++ b/src/ui/ui_fsm.cpp
@@ -12,9 +12,14 @@
#include "bluetooth_types.hpp"
#include "db_events.hpp"
+#include "device_factory.hpp"
#include "display_init.hpp"
+#include "feedback_haptics.hpp"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
+#include "input_device.hpp"
+#include "input_touch_wheel.hpp"
+#include "input_volume_buttons.hpp"
#include "lua.h"
#include "lua.hpp"
@@ -30,19 +35,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 +67,9 @@ namespace ui {
std::unique_ptr<UiTask> UiState::sTask;
std::shared_ptr<system_fsm::ServiceLocator> UiState::sServices;
std::unique_ptr<drivers::Display> UiState::sDisplay;
-std::shared_ptr<EncoderInput> UiState::sInput;
+
+std::shared_ptr<input::LvglInputDriver> UiState::sInput;
+std::unique_ptr<input::DeviceFactory> UiState::sDeviceFactory;
std::stack<std::shared_ptr<Screen>> UiState::sScreens;
std::shared_ptr<Screen> UiState::sCurrentScreen;
@@ -234,52 +240,6 @@ lua::Property UiState::sDisplayBrightness{
return true;
}};
-lua::Property UiState::sControlsScheme{
- 0, [](const lua::LuaValue& val) {
- if (!std::holds_alternative<int>(val)) {
- return false;
- }
- drivers::NvsStorage::InputModes mode;
- switch (std::get<int>(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<int> sensitivity = 0;
- std::visit(
- [&](auto&& v) {
- using T = std::decay_t<decltype(v)>;
- if constexpr (std::is_same_v<T, int>) {
- sensitivity = v;
- }
- },
- val);
- if (!sensitivity) {
- return false;
- }
- sInput->scroll_sensitivity(*sensitivity);
- sServices->nvs().ScrollSensitivity(*sensitivity);
- return true;
- }};
-
lua::Property UiState::sLockSwitch{false};
lua::Property UiState::sDatabaseUpdating{false};
@@ -368,13 +328,6 @@ void UiState::react(const system_fsm::SamdUsbStatusChanged& ev) {
drivers::Samd::UsbStatus::kAttachedBusy);
}
-void UiState::react(const internal::ControlSchemeChanged&) {
- if (!sInput) {
- return;
- }
- sInput->mode(sServices->nvs().PrimaryInput());
-}
-
void UiState::react(const database::event::UpdateStarted&) {
sDatabaseUpdating.Update(true);
}
@@ -478,22 +431,11 @@ void Splash::react(const system_fsm::BootComplete& ev) {
sDisplayBrightness.Update(brightness);
sDisplay->SetBrightness(brightness);
- auto touchwheel = sServices->touchwheel();
- if (touchwheel) {
- sInput = std::make_shared<EncoderInput>(sServices->gpios(), **touchwheel);
+ sDeviceFactory = std::make_unique<input::DeviceFactory>(sServices);
+ sInput = std::make_shared<input::LvglInputDriver>(sServices->nvs(),
+ *sDeviceFactory);
- auto mode = sServices->nvs().PrimaryInput();
- sInput->mode(mode);
- sControlsScheme.Update(static_cast<int>(mode));
-
- auto sensitivity = sServices->nvs().ScrollSensitivity();
- sInput->scroll_sensitivity(sensitivity);
- sScrollSensitivity.Update(static_cast<int>(sensitivity));
-
- sTask->input(sInput);
- } else {
- ESP_LOGE(kTag, "no input devices initialised!");
- }
+ sTask->input(sInput);
}
void Splash::react(const system_fsm::StorageMounted&) {
@@ -550,12 +492,16 @@ void Lua::entry() {
{"brightness", &sDisplayBrightness},
});
- registry.AddPropertyModule("controls",
- {
- {"scheme", &sControlsScheme},
- {"scroll_sensitivity", &sScrollSensitivity},
- {"lock_switch", &sLockSwitch},
- });
+ registry.AddPropertyModule("controls", {
+ {"scheme", &sInput->mode()},
+ {"lock_switch", &sLockSwitch},
+ });
+
+ if (sDeviceFactory->touch_wheel()) {
+ registry.AddPropertyModule(
+ "controls", {{"scroll_sensitivity",
+ &sDeviceFactory->touch_wheel()->sensitivity()}});
+ }
registry.AddPropertyModule(
"backstack",