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