From 135185f12ba07dea8568b06c0a65a00a8af7deb7 Mon Sep 17 00:00:00 2001 From: Robin Howard Date: Tue, 7 Nov 2023 15:46:07 +1100 Subject: [PATCH] haptics: adds a wrapper for the DRV2605L haptic motor driver ... with facilities to trigger effects via the system fsm. --- src/drivers/CMakeLists.txt | 2 +- src/drivers/haptics.cpp | 422 +++++++++++++++++++++ src/drivers/include/haptics.hpp | 316 +++++++++++++++ src/system_fsm/booting.cpp | 3 + src/system_fsm/include/service_locator.hpp | 10 + src/system_fsm/include/system_events.hpp | 5 + src/system_fsm/include/system_fsm.hpp | 2 + src/system_fsm/system_fsm.cpp | 5 + 8 files changed, 764 insertions(+), 1 deletion(-) create mode 100644 src/drivers/haptics.cpp create mode 100644 src/drivers/include/haptics.hpp 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 , robin + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "haptics.hpp" +#include + +#include +#include +#include + +#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(RegisterDefaults::kControl1) & + (~ControlMask::kNErmLra)); + // - Turn on ERM_OPEN_LOOP + WriteRegister(Register::kControl3, + static_cast(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(kDefaultLibrary)); + + // Set mode (internal trigger, on writing 1 to Go register) + WriteRegister(Register::kMode, static_cast(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(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 lock{playing_effect_}; // locks until freed + + SetWaveformEffect(effect); + Go(); +} + +// Starts the pre-programmed sequence +auto Haptics::Go() -> void { + WriteRegister(Register::kGo, + static_cast(RegisterDefaults::kGo) | 0b00000001); +} + +auto Haptics::SetWaveformEffect(Effect effect) -> void { + if (!current_effect_ || current_effect_.value() != effect) { + WriteRegister(Register::kWaveformSequenceSlot1, + static_cast(effect)); + WriteRegister(Register::kWaveformSequenceSlot2, + static_cast(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(lib)); + + for (uint8_t e = static_cast(from); + e <= static_cast(to) && + e <= static_cast(Effect::kLast); + e++) { + auto effect = static_cast(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(Effect::kFirst); + e <= static_cast(Effect::kLast); e++) { + auto effect = static_cast(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(Mode::kInternalTrigger) | + ModeMask::kStandby); +} + +auto Haptics::Reset() -> void { + WriteRegister(Register::kMode, static_cast(Mode::kInternalTrigger) | + ModeMask::kDevReset); +} + +auto Haptics::PowerUp() -> void { + // FIXME: technically overwriting the RESERVED bits of Mode, but eh + uint8_t newMask = static_cast(Mode::kInternalTrigger) & + (~ModeMask::kStandby) & (~ModeMask::kDevReset); + WriteRegister(Register::kMode, + static_cast(RegisterDefaults::kMode) | newMask); +} + +auto Haptics::EffectToLabel(Effect effect) -> std::string { + switch (static_cast(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 , robin + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include +#include + +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 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 +#include #include "assert.h" #include "esp_err.h" @@ -75,6 +77,7 @@ auto Booting::entry() -> void { std::unique_ptr(drivers::NvsStorage::OpenSync())); sServices->touchwheel( std::unique_ptr{drivers::TouchWheel::Create()}); + sServices->haptics(std::make_unique()); auto adc = drivers::AdcBattery::Create(); sServices->battery(std::make_unique( 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 i) { + haptics_ = std::move(i); + } + auto database() -> std::weak_ptr { return database_; } auto database(std::unique_ptr i) { @@ -130,6 +139,7 @@ class ServiceLocator { std::unique_ptr samd_; std::unique_ptr nvs_; std::unique_ptr touchwheel_; + std::unique_ptr haptics_; std::unique_ptr bluetooth_; std::unique_ptr 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 { 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);