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
commit
dd1ea595a7
@ -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
|
|
@ -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
|
|
@ -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
|
@ -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
|
|
Loading…
Reference in new issue