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>
custom
cooljqln 1 year ago
commit dd1ea595a7
  1. 3
      src/drivers/CMakeLists.txt
  2. 52
      src/drivers/include/relative_wheel.hpp
  3. 4
      src/drivers/include/touchwheel.hpp
  4. 92
      src/drivers/relative_wheel.cpp
  5. 7
      src/drivers/touchwheel.cpp
  6. 12
      src/input/CMakeLists.txt
  7. 58
      src/input/device_factory.cpp
  8. 37
      src/input/feedback_haptics.cpp
  9. 39
      src/input/include/device_factory.hpp
  10. 32
      src/input/include/feedback_device.hpp
  11. 26
      src/input/include/feedback_haptics.hpp
  12. 39
      src/input/include/input_device.hpp
  13. 34
      src/input/include/input_nav_buttons.hpp
  14. 36
      src/input/include/input_touch_dpad.hpp
  15. 51
      src/input/include/input_touch_wheel.hpp
  16. 37
      src/input/include/input_trigger.hpp
  17. 34
      src/input/include/input_volume_buttons.hpp
  18. 58
      src/input/include/lvgl_input_driver.hpp
  19. 44
      src/input/input_nav_buttons.cpp
  20. 75
      src/input/input_touch_dpad.cpp
  21. 143
      src/input/input_touch_wheel.cpp
  22. 72
      src/input/input_trigger.cpp
  23. 35
      src/input/input_volume_buttons.cpp
  24. 107
      src/input/lvgl_input_driver.cpp
  25. 1
      src/system_fsm/include/system_fsm.hpp
  26. 1
      src/system_fsm/system_fsm.cpp
  27. 8
      src/ui/CMakeLists.txt
  28. 358
      src/ui/encoder_input.cpp
  29. 107
      src/ui/include/encoder_input.hpp
  30. 7
      src/ui/include/lvgl_task.hpp
  31. 1
      src/ui/include/ui_events.hpp
  32. 14
      src/ui/include/ui_fsm.hpp
  33. 15
      src/ui/lvgl_task.cpp
  34. 100
      src/ui/ui_fsm.cpp

@ -5,8 +5,7 @@
idf_component_register( idf_component_register(
SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "adc.cpp" "storage.cpp" SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "adc.cpp" "storage.cpp"
"i2c.cpp" "bluetooth.cpp" "spi.cpp" "display.cpp" "display_init.cpp" "i2c.cpp" "bluetooth.cpp" "spi.cpp" "display.cpp" "display_init.cpp"
"samd.cpp" "relative_wheel.cpp" "wm8523.cpp" "nvs.cpp" "haptics.cpp" "samd.cpp" "wm8523.cpp" "nvs.cpp" "haptics.cpp" "spiffs.cpp"
"spiffs.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks" "nvs_flash" "spiffs" REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks" "nvs_flash" "spiffs"
"bt" "tinyfsm" "util") "bt" "tinyfsm" "util")

@ -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

@ -24,6 +24,10 @@ struct TouchWheelData {
class TouchWheel { class TouchWheel {
public: public:
static auto isAngleWithin(int16_t wheel_angle,
int16_t target_angle,
int threshold) -> bool;
static auto Create() -> TouchWheel* { return new TouchWheel(); } static auto Create() -> TouchWheel* { return new TouchWheel(); }
TouchWheel(); TouchWheel();
~TouchWheel(); ~TouchWheel();

@ -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

@ -28,6 +28,13 @@ namespace drivers {
static const uint8_t kTouchWheelAddress = 0x1C; static const uint8_t kTouchWheelAddress = 0x1C;
static const gpio_num_t kIntPin = GPIO_NUM_25; 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() { TouchWheel::TouchWheel() {
gpio_config_t int_config{ gpio_config_t int_config{
.pin_bit_mask = 1ULL << kIntPin, .pin_bit_mask = 1ULL << kIntPin,

@ -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})

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -17,7 +17,6 @@
#include "display.hpp" #include "display.hpp"
#include "gpios.hpp" #include "gpios.hpp"
#include "nvs.hpp" #include "nvs.hpp"
#include "relative_wheel.hpp"
#include "samd.hpp" #include "samd.hpp"
#include "service_locator.hpp" #include "service_locator.hpp"
#include "storage.hpp" #include "storage.hpp"

@ -9,7 +9,6 @@
#include "driver/gpio.h" #include "driver/gpio.h"
#include "event_queue.hpp" #include "event_queue.hpp"
#include "gpios.hpp" #include "gpios.hpp"
#include "relative_wheel.hpp"
#include "service_locator.hpp" #include "service_locator.hpp"
#include "system_events.hpp" #include "system_events.hpp"
#include "tag_parser.hpp" #include "tag_parser.hpp"

@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-only # SPDX-License-Identifier: GPL-3.0-only
idf_component_register( idf_component_register(
SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "encoder_input.cpp" SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "themes.cpp"
"themes.cpp" "screen.cpp" "modal.cpp" "screen_lua.cpp" "screen.cpp" "modal.cpp" "screen_lua.cpp" "splash.c" "font_fusion_12.c"
"splash.c" "font_fusion_12.c" "font_fusion_10.c" "font_fusion_10.c"
INCLUDE_DIRS "include" 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}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -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

@ -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

@ -15,8 +15,7 @@
#include "freertos/timers.h" #include "freertos/timers.h"
#include "display.hpp" #include "display.hpp"
#include "encoder_input.hpp" #include "lvgl_input_driver.hpp"
#include "relative_wheel.hpp"
#include "screen.hpp" #include "screen.hpp"
#include "themes.hpp" #include "themes.hpp"
#include "touchwheel.hpp" #include "touchwheel.hpp"
@ -28,14 +27,14 @@ class UiTask {
static auto Start() -> UiTask*; static auto Start() -> UiTask*;
~UiTask(); ~UiTask();
auto input(std::shared_ptr<EncoderInput> input) -> void; auto input(std::shared_ptr<input::LvglInputDriver> input) -> void;
private: private:
UiTask(); UiTask();
auto Main() -> void; auto Main() -> void;
std::shared_ptr<EncoderInput> input_; std::shared_ptr<input::LvglInputDriver> input_;
std::shared_ptr<Screen> current_screen_; std::shared_ptr<Screen> current_screen_;
}; };

@ -32,7 +32,6 @@ struct DumpLuaStack : tinyfsm::Event {};
namespace internal { namespace internal {
struct ControlSchemeChanged : tinyfsm::Event {};
struct ReindexDatabase : tinyfsm::Event {}; struct ReindexDatabase : tinyfsm::Event {};
struct BackPressed : tinyfsm::Event {}; struct BackPressed : tinyfsm::Event {};

@ -13,15 +13,18 @@
#include "audio_events.hpp" #include "audio_events.hpp"
#include "battery.hpp" #include "battery.hpp"
#include "db_events.hpp" #include "db_events.hpp"
#include "device_factory.hpp"
#include "display.hpp" #include "display.hpp"
#include "encoder_input.hpp" #include "feedback_haptics.hpp"
#include "gpios.hpp" #include "gpios.hpp"
#include "input_touch_wheel.hpp"
#include "input_volume_buttons.hpp"
#include "lua_thread.hpp" #include "lua_thread.hpp"
#include "lvgl_input_driver.hpp"
#include "lvgl_task.hpp" #include "lvgl_task.hpp"
#include "modal.hpp" #include "modal.hpp"
#include "nvs.hpp" #include "nvs.hpp"
#include "property.hpp" #include "property.hpp"
#include "relative_wheel.hpp"
#include "screen.hpp" #include "screen.hpp"
#include "service_locator.hpp" #include "service_locator.hpp"
#include "storage.hpp" #include "storage.hpp"
@ -68,7 +71,6 @@ class UiState : public tinyfsm::Fsm<UiState> {
void react(const system_fsm::SamdUsbStatusChanged&); void react(const system_fsm::SamdUsbStatusChanged&);
void react(const internal::DismissAlerts&); void react(const internal::DismissAlerts&);
void react(const internal::ControlSchemeChanged&);
void react(const database::event::UpdateStarted&); void react(const database::event::UpdateStarted&);
void react(const database::event::UpdateProgress&){}; void react(const database::event::UpdateProgress&){};
@ -92,7 +94,9 @@ class UiState : public tinyfsm::Fsm<UiState> {
static std::unique_ptr<UiTask> sTask; static std::unique_ptr<UiTask> sTask;
static std::shared_ptr<system_fsm::ServiceLocator> sServices; static std::shared_ptr<system_fsm::ServiceLocator> sServices;
static std::unique_ptr<drivers::Display> sDisplay; 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::stack<std::shared_ptr<Screen>> sScreens;
static std::shared_ptr<Screen> sCurrentScreen; static std::shared_ptr<Screen> sCurrentScreen;
@ -126,8 +130,6 @@ class UiState : public tinyfsm::Fsm<UiState> {
static lua::Property sDisplayBrightness; static lua::Property sDisplayBrightness;
static lua::Property sControlsScheme;
static lua::Property sScrollSensitivity;
static lua::Property sLockSwitch; static lua::Property sLockSwitch;
static lua::Property sDatabaseUpdating; static lua::Property sDatabaseUpdating;

@ -33,11 +33,11 @@
#include "lua.h" #include "lua.h"
#include "lv_api_map.h" #include "lv_api_map.h"
#include "lvgl/lvgl.h" #include "lvgl/lvgl.h"
#include "lvgl_input_driver.hpp"
#include "misc/lv_color.h" #include "misc/lv_color.h"
#include "misc/lv_style.h" #include "misc/lv_style.h"
#include "misc/lv_timer.h" #include "misc/lv_timer.h"
#include "modal.hpp" #include "modal.hpp"
#include "relative_wheel.hpp"
#include "tasks.hpp" #include "tasks.hpp"
#include "touchwheel.hpp" #include "touchwheel.hpp"
#include "ui_fsm.hpp" #include "ui_fsm.hpp"
@ -50,8 +50,6 @@ namespace ui {
[[maybe_unused]] static const char* kTag = "ui_task"; [[maybe_unused]] static const char* kTag = "ui_task";
static auto group_focus_cb(lv_group_t *group) -> void;
UiTask::UiTask() {} UiTask::UiTask() {}
UiTask::~UiTask() { UiTask::~UiTask() {
@ -78,7 +76,6 @@ auto UiTask::Main() -> void {
if (input_ && current_screen_->group() != current_group) { if (input_ && current_screen_->group() != current_group) {
current_group = current_screen_->group(); current_group = current_screen_->group();
lv_indev_set_group(input_->registration(), current_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(); 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_); assert(current_screen_);
input_ = input; input_ = input;
lv_indev_set_group(input_->registration(), current_screen_->group());
} }
auto UiTask::Start() -> UiTask* { auto UiTask::Start() -> UiTask* {
@ -98,11 +94,4 @@ auto UiTask::Start() -> UiTask* {
return ret; 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 } // namespace ui

@ -12,9 +12,14 @@
#include "bluetooth_types.hpp" #include "bluetooth_types.hpp"
#include "db_events.hpp" #include "db_events.hpp"
#include "device_factory.hpp"
#include "display_init.hpp" #include "display_init.hpp"
#include "feedback_haptics.hpp"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "freertos/projdefs.h" #include "freertos/projdefs.h"
#include "input_device.hpp"
#include "input_touch_wheel.hpp"
#include "input_volume_buttons.hpp"
#include "lua.h" #include "lua.h"
#include "lua.hpp" #include "lua.hpp"
@ -30,19 +35,18 @@
#include "lauxlib.h" #include "lauxlib.h"
#include "lua_thread.hpp" #include "lua_thread.hpp"
#include "luavgl.h" #include "luavgl.h"
#include "lvgl_input_driver.hpp"
#include "memory_resource.hpp" #include "memory_resource.hpp"
#include "misc/lv_gc.h" #include "misc/lv_gc.h"
#include "audio_events.hpp" #include "audio_events.hpp"
#include "display.hpp" #include "display.hpp"
#include "encoder_input.hpp"
#include "event_queue.hpp" #include "event_queue.hpp"
#include "gpios.hpp" #include "gpios.hpp"
#include "lua_registry.hpp" #include "lua_registry.hpp"
#include "lvgl_task.hpp" #include "lvgl_task.hpp"
#include "nvs.hpp" #include "nvs.hpp"
#include "property.hpp" #include "property.hpp"
#include "relative_wheel.hpp"
#include "samd.hpp" #include "samd.hpp"
#include "screen.hpp" #include "screen.hpp"
#include "screen_lua.hpp" #include "screen_lua.hpp"
@ -63,7 +67,9 @@ namespace ui {
std::unique_ptr<UiTask> UiState::sTask; std::unique_ptr<UiTask> UiState::sTask;
std::shared_ptr<system_fsm::ServiceLocator> UiState::sServices; std::shared_ptr<system_fsm::ServiceLocator> UiState::sServices;
std::unique_ptr<drivers::Display> UiState::sDisplay; 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::stack<std::shared_ptr<Screen>> UiState::sScreens;
std::shared_ptr<Screen> UiState::sCurrentScreen; std::shared_ptr<Screen> UiState::sCurrentScreen;
@ -234,52 +240,6 @@ lua::Property UiState::sDisplayBrightness{
return true; 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::sLockSwitch{false};
lua::Property UiState::sDatabaseUpdating{false}; lua::Property UiState::sDatabaseUpdating{false};
@ -368,13 +328,6 @@ void UiState::react(const system_fsm::SamdUsbStatusChanged& ev) {
drivers::Samd::UsbStatus::kAttachedBusy); 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&) { void UiState::react(const database::event::UpdateStarted&) {
sDatabaseUpdating.Update(true); sDatabaseUpdating.Update(true);
} }
@ -478,22 +431,11 @@ void Splash::react(const system_fsm::BootComplete& ev) {
sDisplayBrightness.Update(brightness); sDisplayBrightness.Update(brightness);
sDisplay->SetBrightness(brightness); sDisplay->SetBrightness(brightness);
auto touchwheel = sServices->touchwheel(); sDeviceFactory = std::make_unique<input::DeviceFactory>(sServices);
if (touchwheel) { sInput = std::make_shared<input::LvglInputDriver>(sServices->nvs(),
sInput = std::make_shared<EncoderInput>(sServices->gpios(), **touchwheel); *sDeviceFactory);
auto mode = sServices->nvs().PrimaryInput(); sTask->input(sInput);
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!");
}
} }
void Splash::react(const system_fsm::StorageMounted&) { void Splash::react(const system_fsm::StorageMounted&) {
@ -550,12 +492,16 @@ void Lua::entry() {
{"brightness", &sDisplayBrightness}, {"brightness", &sDisplayBrightness},
}); });
registry.AddPropertyModule("controls", registry.AddPropertyModule("controls", {
{ {"scheme", &sInput->mode()},
{"scheme", &sControlsScheme}, {"lock_switch", &sLockSwitch},
{"scroll_sensitivity", &sScrollSensitivity}, });
{"lock_switch", &sLockSwitch},
}); if (sDeviceFactory->touch_wheel()) {
registry.AddPropertyModule(
"controls", {{"scroll_sensitivity",
&sDeviceFactory->touch_wheel()->sensitivity()}});
}
registry.AddPropertyModule( registry.AddPropertyModule(
"backstack", "backstack",

Loading…
Cancel
Save