summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRobin Howard <robin@rhoward.id.au>2023-11-07 15:46:07 +1100
committerRobin Howard <robin@rhoward.id.au>2023-11-07 16:31:55 +1100
commit135185f12ba07dea8568b06c0a65a00a8af7deb7 (patch)
tree19724b4caf02c6127766be4e9bfd33d006cfdd25 /src
parent3abf599c4f8b04bed4ed67084588204f65a644d0 (diff)
downloadtangara-fw-135185f12ba07dea8568b06c0a65a00a8af7deb7.tar.gz
haptics: adds a wrapper for the DRV2605L haptic motor driver
... with facilities to trigger effects via the system fsm.
Diffstat (limited to 'src')
-rw-r--r--src/drivers/CMakeLists.txt2
-rw-r--r--src/drivers/haptics.cpp422
-rw-r--r--src/drivers/include/haptics.hpp316
-rw-r--r--src/system_fsm/booting.cpp3
-rw-r--r--src/system_fsm/include/service_locator.hpp10
-rw-r--r--src/system_fsm/include/system_events.hpp5
-rw-r--r--src/system_fsm/include/system_fsm.hpp2
-rw-r--r--src/system_fsm/system_fsm.cpp5
8 files changed, 764 insertions, 1 deletions
diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt
index 1df58f72..b2b5bfdd 100644
--- a/src/drivers/CMakeLists.txt
+++ b/src/drivers/CMakeLists.txt
@@ -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})
diff --git a/src/drivers/haptics.cpp b/src/drivers/haptics.cpp
new file mode 100644
index 00000000..542c85bc
--- /dev/null
+++ b/src/drivers/haptics.cpp
@@ -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
diff --git a/src/drivers/include/haptics.hpp b/src/drivers/include/haptics.hpp
new file mode 100644
index 00000000..dfafa2eb
--- /dev/null
+++ b/src/drivers/include/haptics.hpp
@@ -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
diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp
index 940e10db..affd3ebc 100644
--- a/src/system_fsm/booting.cpp
+++ b/src/system_fsm/booting.cpp
@@ -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>(
diff --git a/src/system_fsm/include/service_locator.hpp b/src/system_fsm/include/service_locator.hpp
index 327d0c50..4aa57df0 100644
--- a/src/system_fsm/include/service_locator.hpp
+++ b/src/system_fsm/include/service_locator.hpp
@@ -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_;
diff --git a/src/system_fsm/include/system_events.hpp b/src/system_fsm/include/system_events.hpp
index 4db9beb0..2722fa80 100644
--- a/src/system_fsm/include/system_events.hpp
+++ b/src/system_fsm/include/system_events.hpp
@@ -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 {};
diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp
index 28448e5a..c9803bef 100644
--- a/src/system_fsm/include/system_fsm.hpp
+++ b/src/system_fsm/include/system_fsm.hpp
@@ -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&) {}
diff --git a/src/system_fsm/system_fsm.cpp b/src/system_fsm/system_fsm.cpp
index 1e92cd62..31aec789 100644
--- a/src/system_fsm/system_fsm.cpp
+++ b/src/system_fsm/system_fsm.cpp
@@ -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);