parent
							
								
									2e59325c22
								
							
						
					
					
						commit
						ed82063af5
					
				@ -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,11 @@ | 
				
			||||
# 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_volume_buttons.cpp" "lvgl_input_driver.cpp" "feedback_haptics.cpp" | 
				
			||||
  INCLUDE_DIRS "include" | 
				
			||||
  REQUIRES "drivers" "lvgl" "events" "system_fsm") | 
				
			||||
 | 
				
			||||
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) | 
				
			||||
@ -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,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,33 @@ | 
				
			||||
/*
 | 
				
			||||
 * 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" | 
				
			||||
 | 
				
			||||
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?)
 | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
}  // namespace input
 | 
				
			||||
@ -0,0 +1,30 @@ | 
				
			||||
/*
 | 
				
			||||
 * 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 "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_; | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
}  // 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 "hal/lv_hal_indev.h" | 
				
			||||
 | 
				
			||||
#include "haptics.hpp" | 
				
			||||
#include "input_device.hpp" | 
				
			||||
#include "touchwheel.hpp" | 
				
			||||
 | 
				
			||||
namespace input { | 
				
			||||
 | 
				
			||||
class TouchWheel : public IInputDevice { | 
				
			||||
 public: | 
				
			||||
  TouchWheel(drivers::TouchWheel&); | 
				
			||||
 | 
				
			||||
  auto read(lv_indev_data_t* data) -> void override; | 
				
			||||
 | 
				
			||||
 private: | 
				
			||||
  auto calculate_ticks(const drivers::TouchWheelData& data) -> int8_t; | 
				
			||||
 | 
				
			||||
  drivers::TouchWheel& wheel_; | 
				
			||||
 | 
				
			||||
  bool is_scrolling_; | 
				
			||||
  uint8_t sensitivity_; | 
				
			||||
  uint8_t threshold_; | 
				
			||||
  bool is_first_read_; | 
				
			||||
  uint8_t last_angle_; | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
}  // namespace input
 | 
				
			||||
@ -0,0 +1,30 @@ | 
				
			||||
/*
 | 
				
			||||
 * 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 "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_; | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
}  // namespace input
 | 
				
			||||
@ -0,0 +1,53 @@ | 
				
			||||
/*
 | 
				
			||||
 * 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 "feedback_device.hpp" | 
				
			||||
#include "gpios.hpp" | 
				
			||||
#include "hal/lv_hal_indev.h" | 
				
			||||
 | 
				
			||||
#include "input_device.hpp" | 
				
			||||
#include "nvs.hpp" | 
				
			||||
#include "service_locator.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(std::shared_ptr<system_fsm::ServiceLocator>); | 
				
			||||
 | 
				
			||||
  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: | 
				
			||||
  std::shared_ptr<system_fsm::ServiceLocator> services_; | 
				
			||||
 | 
				
			||||
  lv_indev_drv_t driver_; | 
				
			||||
  lv_indev_t* registration_; | 
				
			||||
 | 
				
			||||
  std::vector<std::unique_ptr<IInputDevice>> inputs_; | 
				
			||||
  std::vector<std::unique_ptr<IFeedbackDevice>> feedbacks_; | 
				
			||||
 | 
				
			||||
  bool is_locked_; | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
}  // namespace input
 | 
				
			||||
@ -0,0 +1,35 @@ | 
				
			||||
/*
 | 
				
			||||
 * 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 "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(); | 
				
			||||
 | 
				
			||||
  // TODO: reimplement
 | 
				
			||||
} | 
				
			||||
 | 
				
			||||
}  // namespace input
 | 
				
			||||
@ -0,0 +1,81 @@ | 
				
			||||
/*
 | 
				
			||||
 * 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 "hal/lv_hal_indev.h" | 
				
			||||
 | 
				
			||||
#include "haptics.hpp" | 
				
			||||
#include "input_device.hpp" | 
				
			||||
#include "touchwheel.hpp" | 
				
			||||
 | 
				
			||||
namespace input { | 
				
			||||
 | 
				
			||||
TouchWheel::TouchWheel(drivers::TouchWheel& wheel) | 
				
			||||
    : wheel_(wheel), | 
				
			||||
      is_scrolling_(false), | 
				
			||||
      sensitivity_(128), | 
				
			||||
      threshold_(10), | 
				
			||||
      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 = calculate_ticks(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; | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
auto TouchWheel::calculate_ticks(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; | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
}  // namespace input
 | 
				
			||||
@ -0,0 +1,26 @@ | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2024 jacqueline <me@jacqueline.id.au> | 
				
			||||
 * | 
				
			||||
 * SPDX-License-Identifier: GPL-3.0-only | 
				
			||||
 */ | 
				
			||||
 | 
				
			||||
#include "input_volume_buttons.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); | 
				
			||||
  if (!vol_up) { | 
				
			||||
    ESP_LOGI("volume", "vol up"); | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  bool vol_down = gpios_.Get(drivers::IGpios::Pin::kKeyDown); | 
				
			||||
  if (!vol_down) { | 
				
			||||
    ESP_LOGI("volume", "vol down"); | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
}  // namespace input
 | 
				
			||||
@ -0,0 +1,84 @@ | 
				
			||||
/*
 | 
				
			||||
 * 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 "feedback_haptics.hpp" | 
				
			||||
#include "input_touch_wheel.hpp" | 
				
			||||
#include "input_volume_buttons.hpp" | 
				
			||||
#include "lvgl.h" | 
				
			||||
#include "service_locator.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); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
LvglInputDriver::LvglInputDriver( | 
				
			||||
    std::shared_ptr<system_fsm::ServiceLocator> services) | 
				
			||||
    : services_(services), | 
				
			||||
      driver_(), | 
				
			||||
      registration_(nullptr), | 
				
			||||
      inputs_(), | 
				
			||||
      feedbacks_(), | 
				
			||||
      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; | 
				
			||||
 | 
				
			||||
  registration_ = lv_indev_drv_register(&driver_); | 
				
			||||
 | 
				
			||||
  // TODO: Make these devices configurable. I'm thinking each device gets an id
 | 
				
			||||
  // and then we have:
 | 
				
			||||
  //  - a factory to create instance given an id
 | 
				
			||||
  //  - add/remove device methods on LvglInputDriver that operate on ids
 | 
				
			||||
  //  - the user's enabled devices (+ their configuration) stored in NVS.
 | 
				
			||||
  auto touchwheel = services_->touchwheel(); | 
				
			||||
  if (touchwheel) { | 
				
			||||
    inputs_.push_back(std::make_unique<TouchWheel>(**touchwheel)); | 
				
			||||
  } | 
				
			||||
  inputs_.push_back(std::make_unique<VolumeButtons>(services_->gpios())); | 
				
			||||
  feedbacks_.push_back(std::make_unique<Haptics>(services_->haptics())); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
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