summaryrefslogtreecommitdiff
path: root/src/input
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 /src/input
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>
Diffstat (limited to 'src/input')
-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
19 files changed, 969 insertions, 0 deletions
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