summaryrefslogtreecommitdiff
path: root/src/tangara/input
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2024-05-02 19:12:26 +1000
committerjacqueline <me@jacqueline.id.au>2024-05-02 19:12:26 +1000
commit1573a8c4cde1cd9528b422b2dcc598e37ffe94a7 (patch)
treed162822b8fd7054f81bace0c7a65ab4d5e6f93ef /src/tangara/input
parenta231fd1c8afedbeb14b0bc77d76bad61db986059 (diff)
downloadtangara-fw-1573a8c4cde1cd9528b422b2dcc598e37ffe94a7.tar.gz
WIP merge cyclically dependent components into one big component
Diffstat (limited to 'src/tangara/input')
-rw-r--r--src/tangara/input/device_factory.cpp58
-rw-r--r--src/tangara/input/device_factory.hpp39
-rw-r--r--src/tangara/input/feedback_device.hpp32
-rw-r--r--src/tangara/input/feedback_haptics.cpp37
-rw-r--r--src/tangara/input/feedback_haptics.hpp26
-rw-r--r--src/tangara/input/input_device.hpp37
-rw-r--r--src/tangara/input/input_hook.cpp99
-rw-r--r--src/tangara/input/input_hook.hpp75
-rw-r--r--src/tangara/input/input_hook_actions.cpp73
-rw-r--r--src/tangara/input/input_hook_actions.hpp31
-rw-r--r--src/tangara/input/input_nav_buttons.cpp35
-rw-r--r--src/tangara/input/input_nav_buttons.hpp38
-rw-r--r--src/tangara/input/input_touch_dpad.cpp64
-rw-r--r--src/tangara/input/input_touch_dpad.hpp40
-rw-r--r--src/tangara/input/input_touch_wheel.cpp137
-rw-r--r--src/tangara/input/input_touch_wheel.hpp56
-rw-r--r--src/tangara/input/input_trigger.cpp88
-rw-r--r--src/tangara/input/input_trigger.hpp42
-rw-r--r--src/tangara/input/input_volume_buttons.cpp33
-rw-r--r--src/tangara/input/input_volume_buttons.hpp37
-rw-r--r--src/tangara/input/lvgl_input_driver.cpp258
-rw-r--r--src/tangara/input/lvgl_input_driver.hpp112
22 files changed, 1447 insertions, 0 deletions
diff --git a/src/tangara/input/device_factory.cpp b/src/tangara/input/device_factory.cpp
new file mode 100644
index 00000000..65f4d785
--- /dev/null
+++ b/src/tangara/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/tangara/input/device_factory.hpp b/src/tangara/input/device_factory.hpp
new file mode 100644
index 00000000..dd9c7133
--- /dev/null
+++ b/src/tangara/input/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/tangara/input/feedback_device.hpp b/src/tangara/input/feedback_device.hpp
new file mode 100644
index 00000000..4faeeafd
--- /dev/null
+++ b/src/tangara/input/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/tangara/input/feedback_haptics.cpp b/src/tangara/input/feedback_haptics.cpp
new file mode 100644
index 00000000..90a912ec
--- /dev/null
+++ b/src/tangara/input/feedback_haptics.cpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input/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/tangara/input/feedback_haptics.hpp b/src/tangara/input/feedback_haptics.hpp
new file mode 100644
index 00000000..a307a429
--- /dev/null
+++ b/src/tangara/input/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/tangara/input/input_device.hpp b/src/tangara/input/input_device.hpp
new file mode 100644
index 00000000..d944c3bf
--- /dev/null
+++ b/src/tangara/input/input_device.hpp
@@ -0,0 +1,37 @@
+/*
+ * 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 "input_hook.hpp"
+#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;
+
+ virtual auto name() -> std::string = 0;
+ virtual auto triggers() -> std::vector<std::reference_wrapper<TriggerHooks>> {
+ return {};
+ }
+};
+
+} // namespace input
diff --git a/src/tangara/input/input_hook.cpp b/src/tangara/input/input_hook.cpp
new file mode 100644
index 00000000..d346b863
--- /dev/null
+++ b/src/tangara/input/input_hook.cpp
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input/input_hook.hpp"
+
+#include <functional>
+#include <optional>
+
+#include "hal/lv_hal_indev.h"
+#include "lua.hpp"
+
+#include "input/input_trigger.hpp"
+
+namespace input {
+
+Hook::Hook(std::string name, std::optional<HookCallback> cb)
+ : name_(name), default_(cb), override_() {}
+
+auto Hook::invoke(lv_indev_data_t* d) -> void {
+ auto cb = callback();
+ if (cb) {
+ std::invoke(cb->fn, d);
+ }
+}
+
+auto Hook::override(std::optional<HookCallback> cb) -> void {
+ override_ = cb;
+}
+
+auto Hook::callback() -> std::optional<HookCallback> {
+ if (override_) {
+ return override_;
+ } else if (default_) {
+ return default_;
+ }
+ return {};
+}
+
+TriggerHooks::TriggerHooks(std::string name,
+ std::optional<HookCallback> click,
+ std::optional<HookCallback> double_click,
+ std::optional<HookCallback> long_press,
+ std::optional<HookCallback> repeat)
+ : name_(name),
+ click_("click", click),
+ double_click_("double_click", double_click),
+ long_press_("long_press", long_press),
+ repeat_("repeat", repeat) {}
+
+auto TriggerHooks::update(bool pressed, lv_indev_data_t* d) -> void {
+ switch (trigger_.update(pressed)) {
+ case Trigger::State::kClick:
+ click_.invoke(d);
+ break;
+ case Trigger::State::kDoubleClick:
+ double_click_.invoke(d);
+ break;
+ case Trigger::State::kLongPress:
+ long_press_.invoke(d);
+ break;
+ case Trigger::State::kRepeatPress:
+ repeat_.invoke(d);
+ break;
+ case Trigger::State::kNone:
+ default:
+ break;
+ }
+}
+
+auto TriggerHooks::override(Trigger::State s, std::optional<HookCallback> cb)
+ -> void {
+ switch (s) {
+ case Trigger::State::kClick:
+ click_.override(cb);
+ break;
+ case Trigger::State::kLongPress:
+ long_press_.override(cb);
+ break;
+ case Trigger::State::kRepeatPress:
+ repeat_.override(cb);
+ break;
+ case Trigger::State::kNone:
+ default:
+ break;
+ }
+}
+
+auto TriggerHooks::name() const -> const std::string& {
+ return name_;
+}
+
+auto TriggerHooks::hooks() -> std::vector<std::reference_wrapper<Hook>> {
+ return {click_, long_press_, repeat_};
+}
+
+} // namespace input
diff --git a/src/tangara/input/input_hook.hpp b/src/tangara/input/input_hook.hpp
new file mode 100644
index 00000000..a8705210
--- /dev/null
+++ b/src/tangara/input/input_hook.hpp
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <functional>
+#include <optional>
+#include <string>
+
+#include "hal/lv_hal_indev.h"
+#include "lua.hpp"
+
+#include "input_trigger.hpp"
+
+namespace input {
+
+struct HookCallback {
+ std::string name;
+ std::function<void(lv_indev_data_t*)> fn;
+};
+
+class Hook {
+ public:
+ Hook(std::string name, std::optional<HookCallback> cb);
+
+ auto invoke(lv_indev_data_t*) -> void;
+ auto override(std::optional<HookCallback>) -> void;
+
+ auto name() const -> const std::string& { return name_; }
+ auto callback() -> std::optional<HookCallback>;
+
+ // Not copyable or movable.
+ Hook(const Hook&) = delete;
+ Hook& operator=(const Hook&) = delete;
+
+ private:
+ std::string name_;
+ std::optional<HookCallback> default_;
+ std::optional<HookCallback> override_;
+};
+
+class TriggerHooks {
+ public:
+ TriggerHooks(std::string name, std::optional<HookCallback> cb)
+ : TriggerHooks(name, cb, cb, cb, cb) {}
+ TriggerHooks(std::string name,
+ std::optional<HookCallback> click,
+ std::optional<HookCallback> double_click,
+ std::optional<HookCallback> long_press,
+ std::optional<HookCallback> repeat);
+
+ auto update(bool, lv_indev_data_t*) -> void;
+ auto override(Trigger::State, std::optional<HookCallback>) -> void;
+
+ auto name() const -> const std::string&;
+ auto hooks() -> std::vector<std::reference_wrapper<Hook>>;
+
+ // Not copyable or movable.
+ TriggerHooks(const TriggerHooks&) = delete;
+ TriggerHooks& operator=(const TriggerHooks&) = delete;
+
+ private:
+ std::string name_;
+ Trigger trigger_;
+
+ Hook click_;
+ Hook double_click_;
+ Hook long_press_;
+ Hook repeat_;
+};
+
+} // namespace input
diff --git a/src/tangara/input/input_hook_actions.cpp b/src/tangara/input/input_hook_actions.cpp
new file mode 100644
index 00000000..bc3760ac
--- /dev/null
+++ b/src/tangara/input/input_hook_actions.cpp
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input/input_hook_actions.hpp"
+
+#include <cstdint>
+
+#include "hal/lv_hal_indev.h"
+
+#include "events/event_queue.hpp"
+#include "ui/ui_events.hpp"
+
+namespace input {
+namespace actions {
+
+auto select() -> HookCallback {
+ return HookCallback{.name = "select", .fn = [&](lv_indev_data_t* d) {
+ d->state = LV_INDEV_STATE_PRESSED;
+ }};
+}
+
+auto scrollUp() -> HookCallback {
+ return HookCallback{.name = "scroll_up",
+ .fn = [&](lv_indev_data_t* d) { d->enc_diff = -1; }};
+}
+
+auto scrollDown() -> HookCallback {
+ return HookCallback{.name = "scroll_down",
+ .fn = [&](lv_indev_data_t* d) { d->enc_diff = 1; }};
+}
+
+auto scrollToTop() -> HookCallback {
+ return HookCallback{.name = "scroll_to_top", .fn = [&](lv_indev_data_t* d) {
+ d->enc_diff = INT16_MIN;
+ }};
+}
+
+auto scrollToBottom() -> HookCallback {
+ return HookCallback{
+ .name = "scroll_to_bottom",
+ .fn = [&](lv_indev_data_t* d) { d->enc_diff = INT16_MAX; }};
+}
+
+auto goBack() -> HookCallback {
+ return HookCallback{.name = "back", .fn = [&](lv_indev_data_t* d) {
+ events::Ui().Dispatch(ui::internal::BackPressed{});
+ }};
+}
+
+auto volumeUp() -> HookCallback {
+ return HookCallback{.name = "volume_up", .fn = [&](lv_indev_data_t* d) {
+ events::Audio().Dispatch(audio::StepUpVolume{});
+ }};
+}
+
+auto volumeDown() -> HookCallback {
+ return HookCallback{.name = "volume_down", .fn = [&](lv_indev_data_t* d) {
+ events::Audio().Dispatch(audio::StepDownVolume{});
+ }};
+}
+
+auto allActions() -> std::vector<HookCallback> {
+ return {
+ select(), scrollUp(), scrollDown(), scrollToTop(),
+ scrollToBottom(), goBack(), volumeUp(), volumeDown(),
+ };
+}
+
+} // namespace actions
+} // namespace input
diff --git a/src/tangara/input/input_hook_actions.hpp b/src/tangara/input/input_hook_actions.hpp
new file mode 100644
index 00000000..105bd10d
--- /dev/null
+++ b/src/tangara/input/input_hook_actions.hpp
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include "hal/lv_hal_indev.h"
+#include "input_hook.hpp"
+
+namespace input {
+namespace actions {
+
+auto select() -> HookCallback;
+
+auto scrollUp() -> HookCallback;
+auto scrollDown() -> HookCallback;
+
+auto scrollToTop() -> HookCallback;
+auto scrollToBottom() -> HookCallback;
+
+auto goBack() -> HookCallback;
+
+auto volumeUp() -> HookCallback;
+auto volumeDown() -> HookCallback;
+
+auto allActions() -> std::vector<HookCallback>;
+
+} // namespace actions
+} // namespace input
diff --git a/src/tangara/input/input_nav_buttons.cpp b/src/tangara/input/input_nav_buttons.cpp
new file mode 100644
index 00000000..61d80075
--- /dev/null
+++ b/src/tangara/input/input_nav_buttons.cpp
@@ -0,0 +1,35 @@
+/*
+ * 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"
+#include "input_hook_actions.hpp"
+
+namespace input {
+
+NavButtons::NavButtons(drivers::IGpios& gpios)
+ : gpios_(gpios),
+ up_("upper", {}, actions::scrollUp(), actions::select(), {}),
+ down_("lower", {}, actions::scrollDown(), actions::select(), {}) {}
+
+auto NavButtons::read(lv_indev_data_t* data) -> void {
+ up_.update(!gpios_.Get(drivers::IGpios::Pin::kKeyUp), data);
+ down_.update(!gpios_.Get(drivers::IGpios::Pin::kKeyDown), data);
+}
+
+auto NavButtons::name() -> std::string {
+ return "buttons";
+}
+
+auto NavButtons::triggers()
+ -> std::vector<std::reference_wrapper<TriggerHooks>> {
+ return {up_, down_};
+}
+
+} // namespace input
diff --git a/src/tangara/input/input_nav_buttons.hpp b/src/tangara/input/input_nav_buttons.hpp
new file mode 100644
index 00000000..9feeb375
--- /dev/null
+++ b/src/tangara/input/input_nav_buttons.hpp
@@ -0,0 +1,38 @@
+/*
+ * 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_hook.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;
+
+ auto name() -> std::string override;
+ auto triggers() -> std::vector<std::reference_wrapper<TriggerHooks>> override;
+
+ private:
+ drivers::IGpios& gpios_;
+
+ TriggerHooks up_;
+ TriggerHooks down_;
+};
+
+} // namespace input
diff --git a/src/tangara/input/input_touch_dpad.cpp b/src/tangara/input/input_touch_dpad.cpp
new file mode 100644
index 00000000..f7e12fc5
--- /dev/null
+++ b/src/tangara/input/input_touch_dpad.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input/input_touch_dpad.hpp"
+
+#include <cstdint>
+
+#include "hal/lv_hal_indev.h"
+
+#include "haptics.hpp"
+#include "touchwheel.hpp"
+
+#include "events/event_queue.hpp"
+#include "input/input_device.hpp"
+#include "input/input_hook_actions.hpp"
+#include "input/input_touch_dpad.hpp"
+
+namespace input {
+
+TouchDPad::TouchDPad(drivers::TouchWheel& wheel)
+ : wheel_(wheel),
+ centre_("centre", actions::select(), {}, {}, {}),
+ up_("up", actions::scrollUp()),
+ right_("right", {}),
+ down_("down", actions::scrollDown()),
+ left_("left", actions::goBack()) {}
+
+auto TouchDPad::read(lv_indev_data_t* data) -> void {
+ wheel_.Update();
+ auto wheel_data = wheel_.GetTouchWheelData();
+
+ centre_.update(wheel_data.is_button_touched, data);
+
+ up_.update(
+ wheel_data.is_wheel_touched &&
+ drivers::TouchWheel::isAngleWithin(wheel_data.wheel_position, 0, 32),
+ data);
+ right_.update(
+ wheel_data.is_wheel_touched && drivers::TouchWheel::isAngleWithin(
+ wheel_data.wheel_position, 192, 32),
+ data);
+ down_.update(
+ wheel_data.is_wheel_touched && drivers::TouchWheel::isAngleWithin(
+ wheel_data.wheel_position, 128, 32),
+ data);
+ left_.update(
+ wheel_data.is_wheel_touched &&
+ drivers::TouchWheel::isAngleWithin(wheel_data.wheel_position, 64, 32),
+ data);
+}
+
+auto TouchDPad::name() -> std::string {
+ return "dpad";
+}
+
+auto TouchDPad::triggers()
+ -> std::vector<std::reference_wrapper<TriggerHooks>> {
+ return {centre_, up_, right_, down_, left_};
+}
+
+} // namespace input
diff --git a/src/tangara/input/input_touch_dpad.hpp b/src/tangara/input/input_touch_dpad.hpp
new file mode 100644
index 00000000..0c45b2d9
--- /dev/null
+++ b/src/tangara/input/input_touch_dpad.hpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <cstdint>
+
+#include "hal/lv_hal_indev.h"
+
+#include "haptics.hpp"
+#include "input_device.hpp"
+#include "input_hook.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;
+
+ auto name() -> std::string override;
+ auto triggers() -> std::vector<std::reference_wrapper<TriggerHooks>> override;
+
+ private:
+ drivers::TouchWheel& wheel_;
+
+ TriggerHooks centre_;
+ TriggerHooks up_;
+ TriggerHooks right_;
+ TriggerHooks down_;
+ TriggerHooks left_;
+};
+
+} // namespace input
diff --git a/src/tangara/input/input_touch_wheel.cpp b/src/tangara/input/input_touch_wheel.cpp
new file mode 100644
index 00000000..e6a3b880
--- /dev/null
+++ b/src/tangara/input/input_touch_wheel.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input/input_touch_wheel.hpp"
+
+#include <cstdint>
+#include <variant>
+
+#include "hal/lv_hal_indev.h"
+
+#include "events/event_queue.hpp"
+#include "haptics.hpp"
+#include "input_device.hpp"
+#include "input_hook_actions.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;
+ }),
+ centre_("centre", actions::select(), {}, {}, {}),
+ up_("up", {}, actions::scrollToTop(), {}, {}),
+ right_("right", {}),
+ down_("down", {}, actions::scrollToBottom(), {}, {}),
+ left_("left", {}, actions::goBack(), {}, {}),
+ 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;
+ }
+
+ centre_.update(wheel_data.is_button_touched && !wheel_data.is_wheel_touched,
+ data);
+
+ // 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_;
+
+ 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);
+}
+
+auto TouchWheel::name() -> std::string {
+ return "wheel";
+}
+
+auto TouchWheel::triggers()
+ -> std::vector<std::reference_wrapper<TriggerHooks>> {
+ return {centre_, up_, right_, down_, left_};
+}
+
+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/tangara/input/input_touch_wheel.hpp b/src/tangara/input/input_touch_wheel.hpp
new file mode 100644
index 00000000..764cc68d
--- /dev/null
+++ b/src/tangara/input/input_touch_wheel.hpp
@@ -0,0 +1,56 @@
+/*
+ * 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_hook.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 name() -> std::string override;
+ auto triggers() -> std::vector<std::reference_wrapper<TriggerHooks>> 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_;
+
+ TriggerHooks centre_;
+ TriggerHooks up_;
+ TriggerHooks right_;
+ TriggerHooks down_;
+ TriggerHooks left_;
+
+ bool is_scrolling_;
+ uint8_t threshold_;
+ bool is_first_read_;
+ uint8_t last_angle_;
+};
+
+} // namespace input
diff --git a/src/tangara/input/input_trigger.cpp b/src/tangara/input/input_trigger.cpp
new file mode 100644
index 00000000..11b4dbe9
--- /dev/null
+++ b/src/tangara/input/input_trigger.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input/input_trigger.hpp"
+
+#include <cstdint>
+
+#include "esp_timer.h"
+
+namespace input {
+
+Trigger::Trigger()
+ : touch_time_ms_(),
+ was_pressed_(false),
+ was_double_click_(false),
+ times_long_pressed_(0) {}
+
+auto Trigger::update(bool is_pressed) -> State {
+ // Bail out early if we're in a steady-state of not pressed.
+ if (!is_pressed && !was_pressed_) {
+ was_double_click_ = false;
+ times_long_pressed_ = 0;
+ return State::kNone;
+ }
+
+ uint64_t now_ms = esp_timer_get_time() / 1000;
+
+ // This key wasn't being pressed, but now it is.
+ if (is_pressed && !was_pressed_) {
+ // Is this a double click?
+ if (now_ms - *touch_time_ms_ < kDoubleClickDelayMs) {
+ // Don't update touch_time_ms_, since we don't want triple clicks to
+ // register as double clicks.
+ was_double_click_ = true;
+ was_pressed_ = true;
+ return State::kDoubleClick;
+ }
+ // Not a double click; update our accounting info and wait for the next
+ // call.
+ touch_time_ms_ = now_ms;
+ was_double_click_ = false;
+ times_long_pressed_ = 0;
+ was_pressed_ = true;
+ 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 && was_pressed_) {
+ was_pressed_ = false;
+ if (!was_double_click_ && times_long_pressed_ == 0) {
+ return State::kClick;
+ } else {
+ return State::kNone;
+ }
+ }
+
+ // Now the more complicated case: the user is continuing to press the button.
+ if (times_long_pressed_ == 0) {
+ // We haven't fired yet, so we wait for the long-press event.
+ if (now_ms - *touch_time_ms_ >= kLongPressDelayMs) {
+ times_long_pressed_++;
+ 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_long_pressed_ < expected_times_fired) {
+ times_long_pressed_++;
+ return State::kRepeatPress;
+ }
+ }
+
+ return State::kNone;
+}
+
+} // namespace input
diff --git a/src/tangara/input/input_trigger.hpp b/src/tangara/input/input_trigger.hpp
new file mode 100644
index 00000000..bcafa8ad
--- /dev/null
+++ b/src/tangara/input/input_trigger.hpp
@@ -0,0 +1,42 @@
+/*
+ * 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 kDoubleClickDelayMs = 500;
+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,
+ kDoubleClick,
+ kLongPress,
+ kRepeatPress,
+ };
+
+ Trigger();
+
+ auto update(bool is_pressed) -> State;
+
+ private:
+ std::optional<uint64_t> touch_time_ms_;
+ bool was_pressed_;
+
+ bool was_double_click_;
+ uint16_t times_long_pressed_;
+};
+
+} // namespace input
diff --git a/src/tangara/input/input_volume_buttons.cpp b/src/tangara/input/input_volume_buttons.cpp
new file mode 100644
index 00000000..37cf90e5
--- /dev/null
+++ b/src/tangara/input/input_volume_buttons.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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"
+#include "input_hook_actions.hpp"
+
+namespace input {
+
+VolumeButtons::VolumeButtons(drivers::IGpios& gpios)
+ : gpios_(gpios),
+ up_("upper", actions::volumeUp()),
+ down_("lower", actions::volumeDown()) {}
+
+auto VolumeButtons::read(lv_indev_data_t* data) -> void {
+ up_.update(!gpios_.Get(drivers::IGpios::Pin::kKeyUp), data);
+ down_.update(!gpios_.Get(drivers::IGpios::Pin::kKeyDown), data);
+}
+
+auto VolumeButtons::name() -> std::string {
+ return "buttons";
+}
+
+auto VolumeButtons::triggers()
+ -> std::vector<std::reference_wrapper<TriggerHooks>> {
+ return {up_, down_};
+}
+
+} // namespace input
diff --git a/src/tangara/input/input_volume_buttons.hpp b/src/tangara/input/input_volume_buttons.hpp
new file mode 100644
index 00000000..e3246df4
--- /dev/null
+++ b/src/tangara/input/input_volume_buttons.hpp
@@ -0,0 +1,37 @@
+/*
+ * 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_hook.hpp"
+#include "touchwheel.hpp"
+
+namespace input {
+
+class VolumeButtons : public IInputDevice {
+ public:
+ VolumeButtons(drivers::IGpios&);
+
+ auto read(lv_indev_data_t* data) -> void override;
+
+ auto name() -> std::string override;
+ auto triggers() -> std::vector<std::reference_wrapper<TriggerHooks>> override;
+
+ private:
+ drivers::IGpios& gpios_;
+
+ TriggerHooks up_;
+ TriggerHooks down_;
+};
+
+} // namespace input
diff --git a/src/tangara/input/lvgl_input_driver.cpp b/src/tangara/input/lvgl_input_driver.cpp
new file mode 100644
index 00000000..1cd7167b
--- /dev/null
+++ b/src/tangara/input/lvgl_input_driver.cpp
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "input/lvgl_input_driver.hpp"
+
+#include <cstdint>
+#include <memory>
+#include <variant>
+
+#include "lua.hpp"
+#include "lvgl.h"
+
+#include "nvs.hpp"
+
+#include "input/device_factory.hpp"
+#include "input/feedback_haptics.hpp"
+#include "input/input_hook.hpp"
+#include "input/input_touch_wheel.hpp"
+#include "input/input_trigger.hpp"
+#include "input/input_volume_buttons.hpp"
+#include "lua/lua_thread.hpp"
+#include "lua/property.hpp"
+
+[[maybe_unused]] static constexpr char kTag[] = "input";
+
+static constexpr char kLuaTriggerMetatableName[] = "input_trigger";
+static constexpr char kLuaOverrideText[] = "lua_callback";
+
+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);
+ }
+}
+
+LvglInputDriver::LuaTrigger::LuaTrigger(LvglInputDriver& driver,
+ IInputDevice& dev,
+ TriggerHooks& trigger)
+ : driver_(&driver), device_(dev.name()), trigger_(trigger.name()) {
+ for (auto& hook : trigger.hooks()) {
+ auto cb = hook.get().callback();
+ if (cb) {
+ hooks_[hook.get().name()] = hook.get().callback()->name;
+ } else {
+ hooks_[hook.get().name()] = "";
+ }
+ }
+}
+
+auto LvglInputDriver::LuaTrigger::get(lua_State* L, int idx) -> LuaTrigger& {
+ return **reinterpret_cast<LuaTrigger**>(
+ luaL_checkudata(L, idx, kLuaTriggerMetatableName));
+}
+
+auto LvglInputDriver::LuaTrigger::luaGc(lua_State* L) -> int {
+ LuaTrigger& trigger = LuaTrigger::get(L, 1);
+ delete &trigger;
+ return 0;
+}
+
+auto LvglInputDriver::LuaTrigger::luaToString(lua_State* L) -> int {
+ LuaTrigger& trigger = LuaTrigger::get(L, 1);
+ std::stringstream out;
+ out << "{ ";
+ for (const auto& hook : trigger.hooks_) {
+ if (!hook.second.empty()) {
+ out << hook.first << "=" << hook.second << " ";
+ }
+ }
+ out << "}";
+ lua_pushlstring(L, out.str().data(), out.str().size());
+ return 1;
+}
+
+auto LvglInputDriver::LuaTrigger::luaNewIndex(lua_State* L) -> int {
+ LuaTrigger& trigger = LuaTrigger::get(L, 1);
+ luaL_checktype(L, 3, LUA_TFUNCTION);
+
+ size_t len = 0;
+ const char* str = luaL_checklstring(L, 2, &len);
+ if (!str) {
+ return 0;
+ }
+ OverrideSelector selector{
+ .device_name = trigger.device_,
+ .trigger_name = trigger.trigger_,
+ .hook_name = std::string{str, len},
+ };
+ for (const auto& hook : trigger.hooks_) {
+ if (hook.first == selector.hook_name) {
+ trigger.driver_->setOverride(L, selector);
+ trigger.hooks_[hook.first] = kLuaOverrideText;
+ return 0;
+ }
+ }
+ return 0;
+}
+
+auto LvglInputDriver::pushHooks(lua_State* L) -> int {
+ if (luaL_getmetatable(L, kLuaTriggerMetatableName) == LUA_TNIL) {
+ luaL_newmetatable(L, kLuaTriggerMetatableName);
+ luaL_setfuncs(L, LuaTrigger::kFuncs, 0);
+ lua_pop(L, 1);
+ }
+ lua_pop(L, 1);
+
+ lua_newtable(L);
+
+ for (auto& dev : inputs_) {
+ lua_pushlstring(L, dev->name().data(), dev->name().size());
+ lua_newtable(L);
+
+ for (auto& trigger : dev->triggers()) {
+ lua_pushlstring(L, trigger.get().name().data(),
+ trigger.get().name().size());
+ LuaTrigger** lua_obj = reinterpret_cast<LuaTrigger**>(
+ lua_newuserdatauv(L, sizeof(LuaTrigger*), 0));
+ *lua_obj = new LuaTrigger(*this, *dev, trigger);
+ luaL_setmetatable(L, kLuaTriggerMetatableName);
+ lua_rawset(L, -3);
+ }
+
+ lua_rawset(L, -3);
+ }
+
+ return 1;
+}
+
+auto LvglInputDriver::setOverride(lua_State* L,
+ const OverrideSelector& selector) -> void {
+ if (overrides_.contains(selector)) {
+ LuaOverride& prev = overrides_[selector];
+ luaL_unref(prev.L, LUA_REGISTRYINDEX, prev.ref);
+ }
+
+ int ref = luaL_ref(L, LUA_REGISTRYINDEX);
+ LuaOverride override{
+ .L = L,
+ .ref = ref,
+ };
+ overrides_[selector] = override;
+ applyOverride(selector, override);
+}
+
+auto LvglInputDriver::applyOverride(const OverrideSelector& selector,
+ LuaOverride& override) -> void {
+ // In general, this algorithm is a very slow approach. We could do better
+ // by maintaing maps from [device|trigger|hook]_name to the relevant
+ // trigger, but in practice I expect maybe like 5 overrides total ever,
+ // spread across 2 devices with 2 or 5 hooks each. So it's not that big a
+ // deal. Don't worry about it!!
+
+ // Look for a matching device.
+ for (auto& device : inputs_) {
+ if (device->name() != selector.device_name) {
+ continue;
+ }
+ // Look for a matching trigger
+ for (auto& trigger : device->triggers()) {
+ if (trigger.get().name() != selector.trigger_name) {
+ continue;
+ }
+ // Look for a matching hook
+ for (auto& hook : trigger.get().hooks()) {
+ if (hook.get().name() != selector.hook_name) {
+ continue;
+ }
+ // We found the target! Apply the override.
+ auto lua_callback = [=](lv_indev_data_t* d) {
+ lua_rawgeti(override.L, LUA_REGISTRYINDEX, override.ref);
+ lua::CallProtected(override.L, 0, 0);
+ };
+ hook.get().override(
+ HookCallback{.name = kLuaOverrideText, .fn = lua_callback});
+ }
+ }
+ }
+}
+
+} // namespace input
diff --git a/src/tangara/input/lvgl_input_driver.hpp b/src/tangara/input/lvgl_input_driver.hpp
new file mode 100644
index 00000000..9adaf143
--- /dev/null
+++ b/src/tangara/input/lvgl_input_driver.hpp
@@ -0,0 +1,112 @@
+/*
+ * 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 "input_hook.hpp"
+#include "lua_thread.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; }
+
+ auto pushHooks(lua_State* L) -> int;
+
+ 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_;
+
+ /*
+ * Key for identifying which device, trigger, and specific hook are being
+ * overriden by Lua.
+ */
+ struct OverrideSelector {
+ std::string device_name;
+ std::string trigger_name;
+ std::string hook_name;
+
+ friend bool operator<(const OverrideSelector& l,
+ const OverrideSelector& r) {
+ return std::tie(l.device_name, l.trigger_name, l.hook_name) <
+ std::tie(r.device_name, r.trigger_name, r.hook_name);
+ }
+ };
+
+ /* Userdata object for tracking the Lua mirror of a TriggerHooks object. */
+ class LuaTrigger {
+ public:
+ LuaTrigger(LvglInputDriver&, IInputDevice&, TriggerHooks&);
+
+ static auto get(lua_State*, int idx) -> LuaTrigger&;
+ static auto luaGc(lua_State*) -> int;
+ static auto luaToString(lua_State*) -> int;
+ static auto luaNewIndex(lua_State*) -> int;
+
+ static constexpr struct luaL_Reg kFuncs[] = {{"__gc", luaGc},
+ {"__tostring", luaToString},
+ {"__newindex", luaNewIndex},
+ {NULL, NULL}};
+
+ private:
+ LvglInputDriver* driver_;
+
+ std::string device_;
+ std::string trigger_;
+ std::map<std::string, std::string> hooks_;
+ };
+
+ /* A hook override implemented as a lua callback */
+ struct LuaOverride {
+ lua_State* L;
+ int ref;
+ };
+
+ std::map<OverrideSelector, LuaOverride> overrides_;
+
+ auto setOverride(lua_State* L, const OverrideSelector&) -> void;
+ auto applyOverride(const OverrideSelector&, LuaOverride&) -> void;
+
+ bool is_locked_;
+};
+
+} // namespace input