haptics: adds a wrapper for the DRV2605L haptic motor driver

... with facilities to trigger effects via the system fsm.
custom
Robin Howard 1 year ago
parent 3abf599c4f
commit 135185f12b
  1. 2
      src/drivers/CMakeLists.txt
  2. 422
      src/drivers/haptics.cpp
  3. 316
      src/drivers/include/haptics.hpp
  4. 3
      src/system_fsm/booting.cpp
  5. 10
      src/system_fsm/include/service_locator.hpp
  6. 5
      src/system_fsm/include/system_events.hpp
  7. 2
      src/system_fsm/include/system_fsm.hpp
  8. 5
      src/system_fsm/system_fsm.cpp

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

@ -0,0 +1,422 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>, robin <robin@rhoward.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "haptics.hpp"
#include <stdint.h>
#include <cstdint>
#include <initializer_list>
#include <mutex>
#include "assert.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/projdefs.h"
#include "hal/gpio_types.h"
#include "hal/i2c_types.h"
#include "i2c.hpp"
namespace drivers {
static constexpr char kTag[] = "haptics";
static constexpr uint8_t kHapticsAddress = 0x5A;
Haptics::Haptics() {
// TODO(robin): is this needed?
vTaskDelay(pdMS_TO_TICKS(300));
PowerUp();
// Put into ERM Open Loop:
// (§8.5.4.1 Programming for ERM Open-Loop Operation)
// - Turn off N_ERM_LRA first
WriteRegister(Register::kControl1,
static_cast<uint8_t>(RegisterDefaults::kControl1) &
(~ControlMask::kNErmLra));
// - Turn on ERM_OPEN_LOOP
WriteRegister(Register::kControl3,
static_cast<uint8_t>(RegisterDefaults::kControl3) |
ControlMask::kErmOpenLoop);
// Set library
// TODO(robin): try the other libraries and test response. C is marginal, D
// too much?
WriteRegister(Register::kWaveformLibrary, static_cast<uint8_t>(kDefaultLibrary));
// Set mode (internal trigger, on writing 1 to Go register)
WriteRegister(Register::kMode, static_cast<uint8_t>(Mode::kInternalTrigger));
// Set up a default effect (sequence of one effect)
SetWaveformEffect(kStartupEffect);
}
Haptics::~Haptics() {}
void Haptics::WriteRegister(Register reg, uint8_t val) {
uint8_t regRaw = static_cast<uint8_t>(reg);
I2CTransaction transaction;
transaction.start()
.write_addr(kHapticsAddress, I2C_MASTER_WRITE)
.write_ack(regRaw, val)
.stop();
esp_err_t res = transaction.Execute(1);
if (res != ESP_OK) {
ESP_LOGW(kTag, "write failed: %s", esp_err_to_name(res));
}
}
auto Haptics::PlayWaveformEffect(Effect effect) -> void {
const std::lock_guard<std::mutex> lock{playing_effect_}; // locks until freed
SetWaveformEffect(effect);
Go();
}
// Starts the pre-programmed sequence
auto Haptics::Go() -> void {
WriteRegister(Register::kGo,
static_cast<uint8_t>(RegisterDefaults::kGo) | 0b00000001);
}
auto Haptics::SetWaveformEffect(Effect effect) -> void {
if (!current_effect_ || current_effect_.value() != effect) {
WriteRegister(Register::kWaveformSequenceSlot1,
static_cast<uint8_t>(effect));
WriteRegister(Register::kWaveformSequenceSlot2,
static_cast<uint8_t>(Effect::kStop));
}
current_effect_ = effect;
}
auto Haptics::TourEffects() -> void {
TourEffects(Effect::kFirst, Effect::kLast, kDefaultLibrary);
}
auto Haptics::TourEffects(Library lib) -> void {
TourEffects(Effect::kFirst, Effect::kLast, lib);
}
auto Haptics::TourEffects(Effect from, Effect to) -> void {
TourEffects(from, to, kDefaultLibrary);
}
auto Haptics::TourEffects(Effect from, Effect to, Library lib) -> void {
ESP_LOGI(kTag, "With library #%u...", static_cast<uint8_t>(lib));
for (uint8_t e = static_cast<uint8_t>(from);
e <= static_cast<uint8_t>(to) &&
e <= static_cast<uint8_t>(Effect::kLast);
e++) {
auto effect = static_cast<Effect>(e);
auto label = EffectToLabel(effect);
if (effect == Effect::kDontUseThis_Longbuzzforprogrammaticstopping_100Pct) {
ESP_LOGI(kTag, "Ignoring effect '%s'...", label.c_str());
continue;
}
ESP_LOGI(kTag, "Playing effect #%u: %s", e, label.c_str());
PlayWaveformEffect(effect);
Go();
vTaskDelay(pdMS_TO_TICKS(800 /*ms*/));
}
}
auto Haptics::TourLibraries(Effect from, Effect to) -> void {
for (uint8_t lib = 1; lib <= 5; lib++) {
WriteRegister(Register::kWaveformLibrary, lib);
for (auto e = static_cast<uint8_t>(Effect::kFirst);
e <= static_cast<uint8_t>(Effect::kLast); e++) {
auto effect = static_cast<Effect>(e);
ESP_LOGI(kTag, "Library %u, Effect: %s", lib,
EffectToLabel(effect).c_str());
PlayWaveformEffect(effect);
Go();
vTaskDelay(pdMS_TO_TICKS(800 /*ms*/));
}
}
}
auto Haptics::PowerDown() -> void {
WriteRegister(Register::kMode, static_cast<uint8_t>(Mode::kInternalTrigger) |
ModeMask::kStandby);
}
auto Haptics::Reset() -> void {
WriteRegister(Register::kMode, static_cast<uint8_t>(Mode::kInternalTrigger) |
ModeMask::kDevReset);
}
auto Haptics::PowerUp() -> void {
// FIXME: technically overwriting the RESERVED bits of Mode, but eh
uint8_t newMask = static_cast<uint8_t>(Mode::kInternalTrigger) &
(~ModeMask::kStandby) & (~ModeMask::kDevReset);
WriteRegister(Register::kMode,
static_cast<uint8_t>(RegisterDefaults::kMode) | newMask);
}
auto Haptics::EffectToLabel(Effect effect) -> std::string {
switch (static_cast<Effect>(effect)) {
case Effect::kStrongClick_100Pct:
return "Strong Click - 100%";
case Effect::kStrongClick_60Pct:
return "Strong Click (60%)";
case Effect::kStrongClick_30Pct:
return "Strong Click (30%)";
case Effect::kSharpClick_100Pct:
return "Sharp Click (100%)";
case Effect::kSharpClick_60Pct:
return "Sharp Click (60%)";
case Effect::kSharpClick_30Pct:
return "Sharp Click (30%)";
case Effect::kSoftBump_100Pct:
return "Soft Bump (100%)";
case Effect::kSoftBump_60Pct:
return "Soft Bump (60%)";
case Effect::kSoftBump_30Pct:
return "Soft Bump (30%)";
case Effect::kDoubleClick_100Pct:
return "Double Click (100%)";
case Effect::kDoubleClick_60Pct:
return "Double Click (60%)";
case Effect::kTripleClick_100Pct:
return "Triple Click (100%)";
case Effect::kSoftFuzz_60Pct:
return "Soft Fuzz (60%)";
case Effect::kStrongBuzz_100Pct:
return "Strong Buzz (100%)";
case Effect::k750msAlert_100Pct:
return "750ms Alert (100%)";
case Effect::k1000msAlert_100Pct:
return "1000ms Alert (100%)";
case Effect::kStrongClick1_100Pct:
return "Strong Click1 (100%)";
case Effect::kStrongClick2_80Pct:
return "Strong Click2 (80%)";
case Effect::kStrongClick3_60Pct:
return "Strong Click3 (60%)";
case Effect::kStrongClick4_30Pct:
return "Strong Click4 (30%)";
case Effect::kMediumClick1_100Pct:
return "Medium Click1 (100%)";
case Effect::kMediumClick2_80Pct:
return "Medium Click2 (80%)";
case Effect::kMediumClick3_60Pct:
return "Medium Click3 (60%)";
case Effect::kSharpTick1_100Pct:
return "Sharp Tick1 (100%)";
case Effect::kSharpTick2_80Pct:
return "Sharp Tick2 (80%)";
case Effect::kSharpTick3_60Pct:
return "Sharp Tick3 (60%)";
case Effect::kShortDoubleClickStrong1_100Pct:
return "Short Double Click Strong1 (100%)";
case Effect::kShortDoubleClickStrong2_80Pct:
return "Short Double Click Strong2 (80%)";
case Effect::kShortDoubleClickStrong3_60Pct:
return "Short Double Click Strong3 (60%)";
case Effect::kShortDoubleClickStrong4_30Pct:
return "Short Double Click Strong4 (30%)";
case Effect::kShortDoubleClickMedium1_100Pct:
return "Short Double Click Medium1 (100%)";
case Effect::kShortDoubleClickMedium2_80Pct:
return "Short Double Click Medium2 (80%)";
case Effect::kShortDoubleClickMedium3_60Pct:
return "Short Double Click Medium3 (60%)";
case Effect::kShortDoubleSharpTick1_100Pct:
return "Short Double Sharp Tick1 (100%)";
case Effect::kShortDoubleSharpTick2_80Pct:
return "Short Double Sharp Tick2 (80%)";
case Effect::kShortDoubleSharpTick3_60Pct:
return "Short Double Sharp Tick3 (60%)";
case Effect::kLongDoubleSharpClickStrong1_100Pct:
return "Long Double Sharp Click Strong1 (100%)";
case Effect::kLongDoubleSharpClickStrong2_80Pct:
return "Long Double Sharp Click Strong2 (80%)";
case Effect::kLongDoubleSharpClickStrong3_60Pct:
return "Long Double Sharp Click Strong3 (60%)";
case Effect::kLongDoubleSharpClickStrong4_30Pct:
return "Long Double Sharp Click Strong4 (30%)";
case Effect::kLongDoubleSharpClickMedium1_100Pct:
return "Long Double Sharp Click Medium1 (100%)";
case Effect::kLongDoubleSharpClickMedium2_80Pct:
return "Long Double Sharp Click Medium2 (80%)";
case Effect::kLongDoubleSharpClickMedium3_60Pct:
return "Long Double Sharp Click Medium3 (60%)";
case Effect::kLongDoubleSharpTick1_100Pct:
return "Long Double Sharp Tick1 (100%)";
case Effect::kLongDoubleSharpTick2_80Pct:
return "Long Double Sharp Tick2 (80%)";
case Effect::kLongDoubleSharpTick3_60Pct:
return "Long Double Sharp Tick3 (60%)";
case Effect::kBuzz1_100Pct:
return "Buzz1 (100%)";
case Effect::kBuzz2_80Pct:
return "Buzz2 (80%)";
case Effect::kBuzz3_60Pct:
return "Buzz3 (60%)";
case Effect::kBuzz4_40Pct:
return "Buzz4 (40%)";
case Effect::kBuzz5_20Pct:
return "Buzz5 (20%)";
case Effect::kPulsingStrong1_100Pct:
return "Pulsing Strong1 (100%)";
case Effect::kPulsingStrong2_60Pct:
return "Pulsing Strong2 (60%)";
case Effect::kPulsingMedium1_100Pct:
return "Pulsing Medium1 (100%)";
case Effect::kPulsingMedium2_60Pct:
return "Pulsing Medium2 (60%)";
case Effect::kPulsingSharp1_100Pct:
return "Pulsing Sharp1 (100%)";
case Effect::kPulsingSharp2_60Pct:
return "Pulsing Sharp2 (60%)";
case Effect::kTransitionClick1_100Pct:
return "Transition Click1 (100%)";
case Effect::kTransitionClick2_80Pct:
return "Transition Click2 (80%)";
case Effect::kTransitionClick3_60Pct:
return "Transition Click3 (60%)";
case Effect::kTransitionClick4_40Pct:
return "Transition Click4 (40%)";
case Effect::kTransitionClick5_20Pct:
return "Transition Click5 (20%)";
case Effect::kTransitionClick6_10Pct:
return "Transition Click6 (10%)";
case Effect::kTransitionHum1_100Pct:
return "Transition Hum1 (100%)";
case Effect::kTransitionHum2_80Pct:
return "Transition Hum2 (80%)";
case Effect::kTransitionHum3_60Pct:
return "Transition Hum3 (60%)";
case Effect::kTransitionHum4_40Pct:
return "Transition Hum4 (40%)";
case Effect::kTransitionHum5_20Pct:
return "Transition Hum5 (20%)";
case Effect::kTransitionHum6_10Pct:
return "Transition Hum6 (10%)";
// TODO: fix labels for XtoY-style
case Effect::kTransitionRampDownLongSmooth1_100to0Pct:
return "Transition Ramp Down Long Smooth1 (100→0%)";
case Effect::kTransitionRampDownLongSmooth2_100to0Pct:
return "Transition Ramp Down Long Smooth2 (100→0%)";
case Effect::kTransitionRampDownMediumSmooth1_100to0Pct:
return "Transition Ramp Down Medium Smooth1 (100→0%)";
case Effect::kTransitionRampDownMediumSmooth2_100to0Pct:
return "Transition Ramp Down Medium Smooth2 (100→0%)";
case Effect::kTransitionRampDownShortSmooth1_100to0Pct:
return "Transition Ramp Down Short Smooth1 (100→0%)";
case Effect::kTransitionRampDownShortSmooth2_100to0Pct:
return "Transition Ramp Down Short Smooth2 (100→0%)";
case Effect::kTransitionRampDownLongSharp1_100to0Pct:
return "Transition Ramp Down Long Sharp1 (100→0%)";
case Effect::kTransitionRampDownLongSharp2_100to0Pct:
return "Transition Ramp Down Long Sharp2 (100→0%)";
case Effect::kTransitionRampDownMediumSharp1_100to0Pct:
return "Transition Ramp Down Medium Sharp1 (100→0%)";
case Effect::kTransitionRampDownMediumSharp2_100to0Pct:
return "Transition Ramp Down Medium Sharp2 (100→0%)";
case Effect::kTransitionRampDownShortSharp1_100to0Pct:
return "Transition Ramp Down Short Sharp1 (100→0%)";
case Effect::kTransitionRampDownShortSharp2_100to0Pct:
return "Transition Ramp Down Short Sharp2 (100→0%)";
case Effect::kTransitionRampUpLongSmooth1_0to100Pct:
return "Transition Ramp Up Long Smooth1 (0→100%)";
case Effect::kTransitionRampUpLongSmooth2_0to100Pct:
return "Transition Ramp Up Long Smooth2 (0→100%)";
case Effect::kTransitionRampUpMediumSmooth1_0to100Pct:
return "Transition Ramp Up Medium Smooth1 (0→100%)";
case Effect::kTransitionRampUpMediumSmooth2_0to100Pct:
return "Transition Ramp Up Medium Smooth2 (0→100%)";
case Effect::kTransitionRampUpShortSmooth1_0to100Pct:
return "Transition Ramp Up Short Smooth1 (0→100%)";
case Effect::kTransitionRampUpShortSmooth2_0to100Pct:
return "Transition Ramp Up Short Smooth2 (0→100%)";
case Effect::kTransitionRampUpLongSharp1_0to100Pct:
return "Transition Ramp Up Long Sharp1 (0→100%)";
case Effect::kTransitionRampUpLongSharp2_0to100Pct:
return "Transition Ramp Up Long Sharp2 (0→100%)";
case Effect::kTransitionRampUpMediumSharp1_0to100Pct:
return "Transition Ramp Up Medium Sharp1 (0→100%)";
case Effect::kTransitionRampUpMediumSharp2_0to100Pct:
return "Transition Ramp Up Medium Sharp2 (0→100%)";
case Effect::kTransitionRampUpShortSharp1_0to100Pct:
return "Transition Ramp Up Short Sharp1 (0→100%)";
case Effect::kTransitionRampUpShortSharp2_0to100Pct:
return "Transition Ramp Up Short Sharp2 (0→100%)";
case Effect::kTransitionRampDownLongSmooth1_50to0Pct:
return "Transition Ramp Down Long Smooth1 (50→0%)";
case Effect::kTransitionRampDownLongSmooth2_50to0Pct:
return "Transition Ramp Down Long Smooth2 (50→0%)";
case Effect::kTransitionRampDownMediumSmooth1_50to0Pct:
return "Transition Ramp Down Medium Smooth1 (50→0%)";
case Effect::kTransitionRampDownMediumSmooth2_50to0Pct:
return "Transition Ramp Down Medium Smooth2 (50→0%)";
case Effect::kTransitionRampDownShortSmooth1_50to0Pct:
return "Transition Ramp Down Short Smooth1 (50→0%)";
case Effect::kTransitionRampDownShortSmooth2_50to0Pct:
return "Transition Ramp Down Short Smooth2 (50→0%)";
case Effect::kTransitionRampDownLongSharp1_50to0Pct:
return "Transition Ramp Down Long Sharp1 (50→0%)";
case Effect::kTransitionRampDownLongSharp2_50to0Pct:
return "Transition Ramp Down Long Sharp2 (50→0%)";
case Effect::kTransitionRampDownMediumSharp1_50to0Pct:
return "Transition Ramp Down Medium Sharp1 (50→0%)";
case Effect::kTransitionRampDownMediumSharp2_50to0Pct:
return "Transition Ramp Down Medium Sharp2 (50→0%)";
case Effect::kTransitionRampDownShortSharp1_50to0Pct:
return "Transition Ramp Down Short Sharp1 (50→0%)";
case Effect::kTransitionRampDownShortSharp2_50to0Pct:
return "Transition Ramp Down Short Sharp2 (50→0%)";
case Effect::kTransitionRampUpLongSmooth_10to50Pct:
return "Transition Ramp Up Long Smooth10to (10→50%)";
case Effect::kTransitionRampUpLongSmooth_20to50Pct:
return "Transition Ramp Up Long Smooth20to (10→50%)";
case Effect::kTransitionRampUpMediumSmooth_10to50Pct:
return "Transition Ramp Up Medium Smooth (10→50%)";
case Effect::kTransitionRampUpMediumSmooth_20to50Pct:
return "Transition Ramp Up Medium Smooth (20→50%)";
case Effect::kTransitionRampUpShortSmooth_10to50Pct:
return "Transition Ramp Up Short Smooth (10→50%)";
case Effect::kTransitionRampUpShortSmooth_20to50Pct:
return "Transition Ramp Up Short Smooth (20→50%)";
case Effect::kTransitionRampUpLongSharp_10to50Pct:
return "Transition Ramp Up Long Sharp (10→50%)";
case Effect::kTransitionRampUpLongSharp_20to50Pct:
return "Transition Ramp Up Long Sharp (20→50%)";
case Effect::kTransitionRampUpMediumSharp_10to50Pct:
return "Transition Ramp Up Medium Sharp (10→50%)";
case Effect::kTransitionRampUpMediumSharp_20to50Pct:
return "Transition Ramp Up Medium Sharp (20→50%)";
case Effect::kTransitionRampUpShortSharp_10to50Pct:
return "Transition Ramp Up Short Sharp (10→50%)";
case Effect::kTransitionRampUpShortSharp_20to50Pct:
return "Transition Ramp Up Short Sharp (20→50%)";
case Effect::kDontUseThis_Longbuzzforprogrammaticstopping_100Pct:
return "DON'T USE: Long Buzz for Programmatic Stopping (100%)";
case Effect::kSmoothHum1NoKickOrBrakePulse_50Pct:
return "Smooth Hum1 No Kick Or Brake Pulse (50%)";
case Effect::kSmoothHum2NoKickOrBrakePulse_40Pct:
return "Smooth Hum2 No Kick Or Brake Pulse (40%)";
case Effect::kSmoothHum3NoKickOrBrakePulse_30Pct:
return "Smooth Hum3 No Kick Or Brake Pulse (30%)";
case Effect::kSmoothHum4NoKickOrBrakePulse_20Pct:
return "Smooth Hum4 No Kick Or Brake Pulse (20%)";
case Effect::kSmoothHum5NoKickOrBrakePulse_10Pct:
return "Smooth Hum5 No Kick Or Brake Pulse (10%)";
default:
return "UNKNOWN EFFECT";
}
}
} // namespace drivers

@ -0,0 +1,316 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>, robin <robin@rhoward.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <stdint.h>
#include <initializer_list>
#include <mutex>
#include <optional>
namespace drivers {
class Haptics {
public:
static auto Create() -> Haptics* { return new Haptics(); }
Haptics();
~Haptics();
// Not copyable or movable.
Haptics(const Haptics&) = delete;
Haptics& operator=(const Haptics&) = delete;
// See the datasheet for section references in the below comments:
// https://www.ti.com/lit/ds/symlink/drv2605l.pdf
// §12.1.2 Waveform Library Effects List
enum class Effect {
kStop = 0, // Sentinel/terminator Effect for the Waveform Sequence Slots
kStrongClick_100Pct = 1,
kStrongClick_60Pct = 2,
kStrongClick_30Pct = 3,
kSharpClick_100Pct = 4,
kSharpClick_60Pct = 5,
kSharpClick_30Pct = 6,
kSoftBump_100Pct = 7,
kSoftBump_60Pct = 8,
kSoftBump_30Pct = 9,
kDoubleClick_100Pct = 10,
kDoubleClick_60Pct = 11,
kTripleClick_100Pct = 12,
kSoftFuzz_60Pct = 13,
kStrongBuzz_100Pct = 14,
k750msAlert_100Pct = 15,
k1000msAlert_100Pct = 16,
kStrongClick1_100Pct = 17,
kStrongClick2_80Pct = 18,
kStrongClick3_60Pct = 19,
kStrongClick4_30Pct = 20,
kMediumClick1_100Pct = 21,
kMediumClick2_80Pct = 22,
kMediumClick3_60Pct = 23,
kSharpTick1_100Pct = 24,
kSharpTick2_80Pct = 25,
kSharpTick3_60Pct = 26,
kShortDoubleClickStrong1_100Pct = 27,
kShortDoubleClickStrong2_80Pct = 28,
kShortDoubleClickStrong3_60Pct = 29,
kShortDoubleClickStrong4_30Pct = 30,
kShortDoubleClickMedium1_100Pct = 31,
kShortDoubleClickMedium2_80Pct = 32,
kShortDoubleClickMedium3_60Pct = 33,
kShortDoubleSharpTick1_100Pct = 34,
kShortDoubleSharpTick2_80Pct = 35,
kShortDoubleSharpTick3_60Pct = 36,
kLongDoubleSharpClickStrong1_100Pct = 37,
kLongDoubleSharpClickStrong2_80Pct = 38,
kLongDoubleSharpClickStrong3_60Pct = 39,
kLongDoubleSharpClickStrong4_30Pct = 40,
kLongDoubleSharpClickMedium1_100Pct = 41,
kLongDoubleSharpClickMedium2_80Pct = 42,
kLongDoubleSharpClickMedium3_60Pct = 43,
kLongDoubleSharpTick1_100Pct = 44,
kLongDoubleSharpTick2_80Pct = 45,
kLongDoubleSharpTick3_60Pct = 46,
kBuzz1_100Pct = 47,
kBuzz2_80Pct = 48,
kBuzz3_60Pct = 49,
kBuzz4_40Pct = 50,
kBuzz5_20Pct = 51,
kPulsingStrong1_100Pct = 52,
kPulsingStrong2_60Pct = 53,
kPulsingMedium1_100Pct = 54,
kPulsingMedium2_60Pct = 55,
kPulsingSharp1_100Pct = 56,
kPulsingSharp2_60Pct = 57,
kTransitionClick1_100Pct = 58,
kTransitionClick2_80Pct = 59,
kTransitionClick3_60Pct = 60,
kTransitionClick4_40Pct = 61,
kTransitionClick5_20Pct = 62,
kTransitionClick6_10Pct = 63,
kTransitionHum1_100Pct = 64,
kTransitionHum2_80Pct = 65,
kTransitionHum3_60Pct = 66,
kTransitionHum4_40Pct = 67,
kTransitionHum5_20Pct = 68,
kTransitionHum6_10Pct = 69,
kTransitionRampDownLongSmooth1_100to0Pct = 70,
kTransitionRampDownLongSmooth2_100to0Pct = 71,
kTransitionRampDownMediumSmooth1_100to0Pct = 72,
kTransitionRampDownMediumSmooth2_100to0Pct = 73,
kTransitionRampDownShortSmooth1_100to0Pct = 74,
kTransitionRampDownShortSmooth2_100to0Pct = 75,
kTransitionRampDownLongSharp1_100to0Pct = 76,
kTransitionRampDownLongSharp2_100to0Pct = 77,
kTransitionRampDownMediumSharp1_100to0Pct = 78,
kTransitionRampDownMediumSharp2_100to0Pct = 79,
kTransitionRampDownShortSharp1_100to0Pct = 80,
kTransitionRampDownShortSharp2_100to0Pct = 81,
kTransitionRampUpLongSmooth1_0to100Pct = 82,
kTransitionRampUpLongSmooth2_0to100Pct = 83,
kTransitionRampUpMediumSmooth1_0to100Pct = 84,
kTransitionRampUpMediumSmooth2_0to100Pct = 85,
kTransitionRampUpShortSmooth1_0to100Pct = 86,
kTransitionRampUpShortSmooth2_0to100Pct = 87,
kTransitionRampUpLongSharp1_0to100Pct = 88,
kTransitionRampUpLongSharp2_0to100Pct = 89,
kTransitionRampUpMediumSharp1_0to100Pct = 90,
kTransitionRampUpMediumSharp2_0to100Pct = 91,
kTransitionRampUpShortSharp1_0to100Pct = 92,
kTransitionRampUpShortSharp2_0to100Pct = 93,
kTransitionRampDownLongSmooth1_50to0Pct = 94,
kTransitionRampDownLongSmooth2_50to0Pct = 95,
kTransitionRampDownMediumSmooth1_50to0Pct = 96,
kTransitionRampDownMediumSmooth2_50to0Pct = 97,
kTransitionRampDownShortSmooth1_50to0Pct = 98,
kTransitionRampDownShortSmooth2_50to0Pct = 99,
kTransitionRampDownLongSharp1_50to0Pct = 100,
kTransitionRampDownLongSharp2_50to0Pct = 101,
kTransitionRampDownMediumSharp1_50to0Pct = 102,
kTransitionRampDownMediumSharp2_50to0Pct = 103,
kTransitionRampDownShortSharp1_50to0Pct = 104,
kTransitionRampDownShortSharp2_50to0Pct = 105,
kTransitionRampUpLongSmooth_10to50Pct = 106,
kTransitionRampUpLongSmooth_20to50Pct = 107,
kTransitionRampUpMediumSmooth_10to50Pct = 108,
kTransitionRampUpMediumSmooth_20to50Pct = 109,
kTransitionRampUpShortSmooth_10to50Pct = 110,
kTransitionRampUpShortSmooth_20to50Pct = 111,
kTransitionRampUpLongSharp_10to50Pct = 112,
kTransitionRampUpLongSharp_20to50Pct = 113,
kTransitionRampUpMediumSharp_10to50Pct = 114,
kTransitionRampUpMediumSharp_20to50Pct = 115,
kTransitionRampUpShortSharp_10to50Pct = 116,
kTransitionRampUpShortSharp_20to50Pct = 117,
kSmoothHum1NoKickOrBrakePulse_50Pct = 119,
kSmoothHum2NoKickOrBrakePulse_40Pct = 120,
kSmoothHum3NoKickOrBrakePulse_30Pct = 121,
kSmoothHum4NoKickOrBrakePulse_20Pct = 122,
kSmoothHum5NoKickOrBrakePulse_10Pct = 123,
// We can't use this one; need to have the EN pin hooked up.
kDontUseThis_Longbuzzforprogrammaticstopping_100Pct = 118,
kFirst = kStrongClick_100Pct,
kLast = kSmoothHum5NoKickOrBrakePulse_10Pct,
};
static constexpr Effect kStartupEffect = Effect::kLongDoubleSharpTick1_100Pct;
// §8.3.5.2 Internal Memory Interface
// Pick the ERM Library matching the motor.
enum class Library : uint8_t {
A = 1, // 1.3V-3V, Rise: 40-60ms, Brake: 20-40ms
B = 2, // 3V, Rise: 40-60ms, Brake: 5-15ms
C = 3, // 3V, Rise: 60-80ms, Brake: 10-20ms
D = 4, // 3V, Rise: 100-140ms, Brake: 15-25ms
E = 5 // 3V, Rise: >140ms, Brake: >30ms
// 6 is LRA-only, 7 is 4.5V+
};
static constexpr Library kDefaultLibrary = Library::C;
auto PowerDown() -> void;
auto Reset() -> void;
auto PlayWaveformEffect(Effect effect) -> void;
// Play a range of Effects
auto TourEffects() -> void;
auto TourEffects(Effect from, Effect to) -> void;
auto TourEffects(Library lib) -> void;
auto TourEffects(Effect from, Effect to, Library lib) -> void;
// Play a range of Effects to all the Libraries we support.
// TODO(robin): remove; I'm leaving this around for temporary testing
auto TourLibraries(Effect from, Effect to) -> void;
private:
std::optional<Effect> current_effect_;
std::mutex playing_effect_;
// §8.4.2 Changing Modes of Operation
enum class Mode : uint8_t {
kInternalTrigger = 0,
kExternalTriggerEdge = 1,
kExternalTriggerLevel = 2,
kPwmAnalog = 3,
kAudioToVibe = 4,
kRealtimePlayback = 5,
kDiagnostics = 6,
kAutoCalibrate = 7,
};
struct ModeMask {
// §8.4.1.4 Operation With STANDBY Control
static constexpr uint8_t kStandby = 0b01000000;
// §8.4.1.5 Operation With DEV_RESET Control
static constexpr uint8_t kDevReset = 0b10000000;
};
struct ControlMask {
// Control1
static constexpr uint8_t kNErmLra = 0b10000000;
// Control3
static constexpr uint8_t kErmOpenLoop = 0b00100000;
};
// §8.6 Register Map
enum class Register {
kStatus = 0x00,
kMode = 0x01,
kRealtimePlaybackInput = 0x02,
kWaveformLibrary = 0x03, // see Library enum
kWaveformSequenceSlot1 = 0x04,
kWaveformSequenceSlot2 = 0x05,
kWaveformSequenceSlot3 = 0x06,
kWaveformSequenceSlot4 = 0x07,
kWaveformSequenceSlot5 = 0x08,
kWaveformSequenceSlot6 = 0x09,
kWaveformSequenceSlot7 = 0x0a,
kWaveformSequenceSlot8 = 0x0b,
kGo = 0x0C,
// §8.3.5.2.2 Library Parameterization
kOverdriveTimeOffset = 0x0D,
kSustainTimeOffsetPositive = 0x0E,
kSustainTimeOffsetNegative = 0x0F,
kBrakeTimeOffset = 0x10,
kAudioToVibeControl = 0x11,
kAudioToVibeInputLevelMin = 0x12,
kAudioToVibeInputLevelMax = 0x13,
kAudioToVibeOutputLevelMin = 0x14,
kAudioToVibeOutputLevelMax = 0x15,
kRatedVoltage = 0x16,
kOverdriveClampVoltage = 0x17,
kAutoCalibrationCompensationResult = 0x18,
kAutoCalibrationBackEmfResult = 0x19,
// A bunch of different options, not grouped
// in any particular sensible way
kControl1 = 0x1A,
kControl2 = 0x1B,
kControl3 = 0x1C,
kControl4 = 0x1D,
kControl5 = 0x1E,
kControl6 = 0x1F,
kSupplyVoltageMonitor = 0x21, // "VBAT"
kLraResonancePeriod = 0x22,
};
enum class RegisterDefaults : uint8_t {
kStatus = 0xE0,
kMode = 0x40,
kRealtimePlaybackInput = 0,
kWaveformLibrary = 0x01,
kWaveformSequenceSlot1 = 0x01,
kWaveformSequenceSlot2 = 0,
kWaveformSequenceSlot3 = 0,
kWaveformSequenceSlot4 = 0,
kWaveformSequenceSlot5 = 0,
kWaveformSequenceSlot6 = 0,
kWaveformSequenceSlot7 = 0,
kWaveformSequenceSlot8 = 0,
kGo = 0,
kOverdriveTimeOffset = 0,
kSustainTimeOffsetPositive = 0,
kSustainTimeOffsetNegative = 0,
kBrakeTimeOffset = 0,
kAudioToVibeControl = 0x05,
kAudioToVibeInputLevelMin = 0x19,
kAudioToVibeInputLevelMax = 0xFF,
kAudioToVibeOutputLevelMin = 0x19,
kAudioToVibeOutputLevelMax = 0xFF,
kRatedVoltage = 0x3E,
kOverdriveClampVoltage = 0x8C,
kAutoCalibrationCompensationResult = 0x0C,
kAutoCalibrationBackEmfResult = 0x6C,
kControl1 = 0x36,
kControl2 = 0x93,
kControl3 = 0xF5,
kControl4 = 0xA0,
kControl5 = 0x20,
kControl6 = 0x80,
kSupplyVoltageMonitor = 0,
kLraResonancePeriod = 0,
};
auto PowerUp() -> void;
auto WriteRegister(Register reg, uint8_t val) -> void;
auto SetWaveformEffect(Effect effect) -> void;
auto Go() -> void;
auto EffectToLabel(Effect effect) -> std::string;
};
} // namespace drivers

@ -5,9 +5,11 @@
*/
#include "collation.hpp"
#include "haptics.hpp"
#include "system_fsm.hpp"
#include <stdint.h>
#include <memory>
#include "assert.h"
#include "esp_err.h"
@ -75,6 +77,7 @@ auto Booting::entry() -> void {
std::unique_ptr<drivers::NvsStorage>(drivers::NvsStorage::OpenSync()));
sServices->touchwheel(
std::unique_ptr<drivers::TouchWheel>{drivers::TouchWheel::Create()});
sServices->haptics(std::make_unique<drivers::Haptics>());
auto adc = drivers::AdcBattery::Create();
sServices->battery(std::make_unique<battery::Battery>(

@ -13,6 +13,7 @@
#include "collation.hpp"
#include "database.hpp"
#include "gpios.hpp"
#include "haptics.hpp"
#include "nvs.hpp"
#include "samd.hpp"
#include "storage.hpp"
@ -79,6 +80,14 @@ class ServiceLocator {
touchwheel_ = std::move(i);
}
auto haptics() -> drivers::Haptics& {
return *haptics_;
}
auto haptics(std::unique_ptr<drivers::Haptics> i) {
haptics_ = std::move(i);
}
auto database() -> std::weak_ptr<database::Database> { return database_; }
auto database(std::unique_ptr<database::Database> i) {
@ -130,6 +139,7 @@ class ServiceLocator {
std::unique_ptr<drivers::Samd> samd_;
std::unique_ptr<drivers::NvsStorage> nvs_;
std::unique_ptr<drivers::TouchWheel> touchwheel_;
std::unique_ptr<drivers::Haptics> haptics_;
std::unique_ptr<drivers::Bluetooth> bluetooth_;
std::unique_ptr<audio::TrackQueue> queue_;

@ -10,6 +10,7 @@
#include "battery.hpp"
#include "database.hpp"
#include "haptics.hpp"
#include "service_locator.hpp"
#include "tinyfsm.hpp"
@ -55,6 +56,10 @@ struct BatteryStateChanged : tinyfsm::Event {
struct BluetoothDevicesChanged : tinyfsm::Event {};
struct HapticTrigger : tinyfsm::Event {
drivers::Haptics::Effect effect;
};
namespace internal {
struct GpioInterrupt : tinyfsm::Event {};

@ -50,6 +50,8 @@ class SystemState : public tinyfsm::Fsm<SystemState> {
void react(const internal::GpioInterrupt&);
void react(const internal::SamdInterrupt&);
void react(const HapticTrigger&);
virtual void react(const DisplayReady&) {}
virtual void react(const BootComplete&) {}
virtual void react(const StorageMounted&) {}

@ -29,6 +29,11 @@ void SystemState::react(const FatalError& err) {
}
}
void SystemState::react(const HapticTrigger& trigger) {
auto& haptics = sServices->haptics();
haptics.PlayWaveformEffect(trigger.effect);
}
void SystemState::react(const internal::GpioInterrupt&) {
auto& gpios = sServices->gpios();
bool prev_key_lock = gpios.Get(drivers::Gpios::Pin::kKeyLock);

Loading…
Cancel
Save