From 3522aef2a10b8fd1c4d693098cd74dacee26bf51 Mon Sep 17 00:00:00 2001
From: William Emfinger
Date: Thu, 25 May 2023 15:36:57 -0500
Subject: [PATCH 01/30] feat(bldc_haptics): initial bldc_haptics component *
Added bldc_haptics component which templates on a motor concept (works with
bldc_motor right now) - copied with modifications from smartknob as initial
implementation. * Added bldc_haptics_example to showcase new component.
---
components/bldc_haptics/CMakeLists.txt | 5 +
.../bldc_haptics/example/CMakeLists.txt | 21 ++
components/bldc_haptics/example/README.md | 67 ++++
.../bldc_haptics/example/main/CMakeLists.txt | 2 +
.../example/main/bldc_haptics_example.cpp | 175 +++++++++
.../bldc_haptics/example/sdkconfig.defaults | 25 ++
.../bldc_haptics/include/bldc_haptics.hpp | 353 ++++++++++++++++++
.../bldc_haptics/include/detent_config.hpp | 118 ++++++
.../bldc_haptics/include/haptic_config.hpp | 11 +
.../include/smartknob_configs.hpp | 157 ++++++++
10 files changed, 934 insertions(+)
create mode 100644 components/bldc_haptics/CMakeLists.txt
create mode 100644 components/bldc_haptics/example/CMakeLists.txt
create mode 100644 components/bldc_haptics/example/README.md
create mode 100644 components/bldc_haptics/example/main/CMakeLists.txt
create mode 100644 components/bldc_haptics/example/main/bldc_haptics_example.cpp
create mode 100644 components/bldc_haptics/example/sdkconfig.defaults
create mode 100644 components/bldc_haptics/include/bldc_haptics.hpp
create mode 100644 components/bldc_haptics/include/detent_config.hpp
create mode 100644 components/bldc_haptics/include/haptic_config.hpp
create mode 100644 components/bldc_haptics/include/smartknob_configs.hpp
diff --git a/components/bldc_haptics/CMakeLists.txt b/components/bldc_haptics/CMakeLists.txt
new file mode 100644
index 000000000..6d2360613
--- /dev/null
+++ b/components/bldc_haptics/CMakeLists.txt
@@ -0,0 +1,5 @@
+idf_component_register(
+ INCLUDE_DIRS "include"
+ SRC_DIRS "src"
+ REQUIRES logger math pid task bldc_motor
+ )
diff --git a/components/bldc_haptics/example/CMakeLists.txt b/components/bldc_haptics/example/CMakeLists.txt
new file mode 100644
index 000000000..5fc917040
--- /dev/null
+++ b/components/bldc_haptics/example/CMakeLists.txt
@@ -0,0 +1,21 @@
+# The following lines of boilerplate have to be in your project's CMakeLists
+# in this exact order for cmake to work correctly
+cmake_minimum_required(VERSION 3.5)
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+
+# add the component directories that we want to use
+set(EXTRA_COMPONENT_DIRS
+ "../../../components/"
+)
+
+set(
+ COMPONENTS
+ "main esptool_py filters task monitor mt6701 bldc_motor bldc_driver bldc_haptics"
+ CACHE STRING
+ "List of components to include"
+ )
+
+project(bldc_motor_example)
+
+set(CMAKE_CXX_STANDARD 20)
diff --git a/components/bldc_haptics/example/README.md b/components/bldc_haptics/example/README.md
new file mode 100644
index 000000000..ffe0beb9d
--- /dev/null
+++ b/components/bldc_haptics/example/README.md
@@ -0,0 +1,67 @@
+_Note that this is a template for an ESP-IDF example README.md file. When using this template, replace all these emphasised placeholders with example-specific content._
+
+| Supported Targets | _Supported target, e.g. ESP32_ | _Another supported target, e.g. ESP32-S3_ |
+| ----------------- | ------------------------------ | ----------------------------------------- |
+
+_If the example supports all targets supported by ESP-IDF then the table can be omitted_
+# _Example Title_
+
+(See the README.md file in the upper level 'examples' directory for more information about examples.)
+
+_What is this example? What does it do?_
+
+_What features of ESP-IDF does it use?_
+
+_What could someone create based on this example? ie applications/use cases/etc_
+
+_If there are any acronyms or Espressif-only words used here, explain them or mention where in the datasheet/TRM this information can be found._
+
+## How to use example
+
+### Hardware Required
+
+_If possible, example should be able to run on any commonly available ESP32 development board. Otherwise, describe what specific hardware should be used._
+
+_If any other items (server, BLE device, app, second chip, whatever) are needed, mention them here. Include links if applicable. Explain how to set them up._
+
+### Configure the project
+
+```
+idf.py menuconfig
+```
+
+* _If there is any project configuration that the user must set for this example, mention this here._
+
+### Build and Flash
+
+Build the project and flash it to the board, then run monitor tool to view serial output:
+
+```
+idf.py -p PORT flash monitor
+```
+
+(Replace PORT with the name of the serial port to use.)
+
+(To exit the serial monitor, type ``Ctrl-]``.)
+
+See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
+
+## Example Output
+
+_Include an example of the console output from the running example, here:_
+
+```
+Use this style for pasting the log.
+```
+
+_If the user is supposed to interact with the example at this point (read/write GATT attribute, send HTTP request, press button, etc. then mention it here)_
+
+_For examples where ESP32 is connected with some other hardware, include a table or schematics with connection details._
+
+## Troubleshooting
+
+_If there are any likely problems or errors which many users might encounter, mention them here. Remove this section for very simple examples where nothing is likely to go wrong._
+
+## Example Breakdown
+
+_If the example source code is lengthy, complex, or cannot be easily understood, use this section to break down and explain the source code. This can be done by breaking down the execution path step by step, or explaining what each major function/task/source file does. Add sub titles if necessary. Remove this section for very simple examples where the source code is self explanatory._
\ No newline at end of file
diff --git a/components/bldc_haptics/example/main/CMakeLists.txt b/components/bldc_haptics/example/main/CMakeLists.txt
new file mode 100644
index 000000000..a941e22ba
--- /dev/null
+++ b/components/bldc_haptics/example/main/CMakeLists.txt
@@ -0,0 +1,2 @@
+idf_component_register(SRC_DIRS "."
+ INCLUDE_DIRS ".")
diff --git a/components/bldc_haptics/example/main/bldc_haptics_example.cpp b/components/bldc_haptics/example/main/bldc_haptics_example.cpp
new file mode 100644
index 000000000..16bbf9864
--- /dev/null
+++ b/components/bldc_haptics/example/main/bldc_haptics_example.cpp
@@ -0,0 +1,175 @@
+#include
+#include
+
+#include "driver/i2c.h"
+
+#include "bldc_driver.hpp"
+#include "bldc_haptics.hpp"
+#include "bldc_motor.hpp"
+#include "butterworth_filter.hpp"
+#include "lowpass_filter.hpp"
+#include "mt6701.hpp"
+#include "task.hpp"
+
+using namespace std::chrono_literals;
+
+// pins for the bldc motor test stand with the TinyS3
+static constexpr auto I2C_NUM = (I2C_NUM_1);
+static constexpr auto I2C_SCL_IO = (GPIO_NUM_9);
+static constexpr auto I2C_SDA_IO = (GPIO_NUM_8);
+static constexpr int I2C_FREQ_HZ = (400 * 1000);
+static constexpr int I2C_TIMEOUT_MS = (10);
+
+extern "C" void app_main(void) {
+ espp::Logger logger({.tag = "BLDC Haptics Example", .level = espp::Logger::Verbosity::DEBUG});
+ constexpr int num_seconds_to_run = 120;
+ {
+ logger.info("Running BLDC Haptics example for {} seconds!", num_seconds_to_run);
+
+ // make the I2C that we'll use to communicate with the mt6701 (magnetic encoder)
+ i2c_config_t i2c_cfg;
+ logger.info("initializing i2c driver...");
+ memset(&i2c_cfg, 0, sizeof(i2c_cfg));
+ i2c_cfg.sda_io_num = I2C_SDA_IO;
+ i2c_cfg.scl_io_num = I2C_SCL_IO;
+ i2c_cfg.mode = I2C_MODE_MASTER;
+ i2c_cfg.sda_pullup_en = GPIO_PULLUP_ENABLE;
+ i2c_cfg.scl_pullup_en = GPIO_PULLUP_ENABLE;
+ i2c_cfg.master.clk_speed = I2C_FREQ_HZ;
+ auto err = i2c_param_config(I2C_NUM, &i2c_cfg);
+ if (err != ESP_OK)
+ logger.error("config i2c failed");
+ err = i2c_driver_install(I2C_NUM, I2C_MODE_MASTER, 0, 0, 0);
+ if (err != ESP_OK)
+ logger.error("install i2c driver failed");
+ // make some lambda functions we'll use to read/write to the mt6701
+ auto i2c_write = [](uint8_t dev_addr, uint8_t *data, size_t len) {
+ i2c_master_write_to_device(I2C_NUM, dev_addr, data, len, I2C_TIMEOUT_MS / portTICK_PERIOD_MS);
+ };
+
+ auto i2c_read = [](uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, size_t len) {
+ i2c_master_write_read_device(I2C_NUM, dev_addr, ®_addr, 1, data, len,
+ I2C_TIMEOUT_MS / portTICK_PERIOD_MS);
+ };
+
+ // make the velocity filter
+ static constexpr float core_update_period = 0.001f; // seconds
+ static constexpr float filter_cutoff_hz = 4.0f;
+ espp::ButterworthFilter<2, espp::BiquadFilterDf2> bwfilter({
+ .normalized_cutoff_frequency = 2.0f * filter_cutoff_hz * 0.01 // core_update_period
+ });
+ espp::LowpassFilter lpfilter(
+ {.normalized_cutoff_frequency = 2.0f * filter_cutoff_hz * 0.01, // core_update_period,
+ .q_factor = 1.0f});
+ auto filter_fn = [&bwfilter, &lpfilter](float raw) -> float {
+ // return bwfilter.update(raw);
+ // return lpfilter.update(raw);
+
+ // NOTE: right now there seems to be something wrong with the filter
+ // configuration, so we don't filter at all. Either 1) the filtering
+ // is not actually removing the noise we want, 2) it is adding too
+ // much delay for the PID to compensate for, or 3) there is a bug in
+ // the update function which doesn't take previous state into
+ // account?
+ return raw;
+ };
+
+ // now make the mt6701 which decodes the data
+ std::shared_ptr mt6701 = std::make_shared(
+ espp::Mt6701::Config{.write = i2c_write,
+ .read = i2c_read,
+ .velocity_filter = filter_fn,
+ .update_period = std::chrono::duration(core_update_period),
+ .log_level = espp::Logger::Verbosity::WARN});
+
+ // now make the bldc driver
+ std::shared_ptr driver =
+ std::make_shared(espp::BldcDriver::Config{
+ // this pinout is configured for the TinyS3 connected to the
+ // TMC6300-BOB in the BLDC Motor Test Stand
+ .gpio_a_h = 1,
+ .gpio_a_l = 2,
+ .gpio_b_h = 3,
+ .gpio_b_l = 4,
+ .gpio_c_h = 5,
+ .gpio_c_l = 21,
+ .gpio_enable = 34, // connected to the VIO/~Stdby pin of TMC6300-BOB
+ .power_supply_voltage = 5.0f,
+ .limit_voltage = 5.0f,
+ .log_level = espp::Logger::Verbosity::WARN});
+
+ // now make the bldc motor
+ using BldcMotor = espp::BldcMotor;
+ auto motor = BldcMotor(BldcMotor::Config{
+ // measured by setting it into ANGLE_OPENLOOP and then counting how many
+ // spots you feel when rotating it.
+ .num_pole_pairs = 7,
+ .phase_resistance =
+ 5.0f, // tested by running velocity_openloop and seeing if the veloicty is ~correct
+ .kv_rating =
+ 320, // tested by running velocity_openloop and seeing if the velocity is ~correct
+ .current_limit = 0.1f, // Amps
+ .zero_electric_offset = 1.1784807f, // gotten from previously running without providing this
+ // and it will be logged.
+ .sensor_direction = BldcMotor::Direction::CLOCKWISE,
+ .foc_type = BldcMotor::FocType::SPACE_VECTOR_PWM,
+ .driver = driver,
+ .sensor = mt6701,
+ .velocity_pid_config =
+ {
+ .kp = 0.010f,
+ .ki = 1.000f,
+ .kd = 0.000f,
+ .integrator_min = -1.0f, // same scale as output_min (so same scale as current)
+ .integrator_max = 1.0f, // same scale as output_max (so same scale as current)
+ .output_min = -1.0, // velocity pid works on current (if we have phase resistance)
+ .output_max = 1.0, // velocity pid works on current (if we have phase resistance)
+ },
+ .angle_pid_config =
+ {
+ .kp = 7.000f,
+ .ki = 0.300f,
+ .kd = 0.010f,
+ .integrator_min = -10.0f, // same scale as output_min (so same scale as velocity)
+ .integrator_max = 10.0f, // same scale as output_max (so same scale as velocity)
+ .output_min = -20.0, // angle pid works on velocity (rad/s)
+ .output_max = 20.0, // angle pid works on velocity (rad/s)
+ },
+ .log_level = espp::Logger::Verbosity::INFO});
+
+ // set the motion control type to velocity openloop for the haptics
+ static auto motion_control_type = BldcMotor::MotionControlType::VELOCITY_OPENLOOP;
+ motor.set_motion_control_type(motion_control_type);
+
+ using BldcHaptics = espp::BldcHaptics;
+
+ auto haptic_motor = BldcHaptics({.motor = motor, .log_level = espp::Logger::Verbosity::INFO});
+ logger.info("Setting detent config to MAGNETIC_DETENTS");
+ haptic_motor.update_detent_config(espp::detail::MAGNETIC_DETENTS);
+ haptic_motor.start();
+
+ // TODO: test the haptic buzz / click
+ logger.info("Playing haptic buzz for 1 second");
+ haptic_motor.play_haptic(espp::detail::HapticConfig{
+ .strength = 5.0f,
+ .duration = 1s // NOTE: duration is unused for now
+ });
+
+ static auto start = std::chrono::high_resolution_clock::now();
+ auto now = std::chrono::high_resolution_clock::now();
+ auto seconds = std::chrono::duration(now - start).count();
+ while (seconds < num_seconds_to_run) {
+ now = std::chrono::high_resolution_clock::now();
+ seconds = std::chrono::duration(now - start).count();
+ std::this_thread::sleep_for(500ms);
+ }
+ }
+ // now clean up the i2c driver
+ i2c_driver_delete(I2C_NUM);
+
+ logger.info("BLDC Haptics example complete!");
+
+ while (true) {
+ std::this_thread::sleep_for(1s);
+ }
+}
diff --git a/components/bldc_haptics/example/sdkconfig.defaults b/components/bldc_haptics/example/sdkconfig.defaults
new file mode 100644
index 000000000..253c0a196
--- /dev/null
+++ b/components/bldc_haptics/example/sdkconfig.defaults
@@ -0,0 +1,25 @@
+CONFIG_IDF_TARGET="esp32s3"
+
+CONFIG_COMPILER_OPTIMIZATION_PERF=y
+# CONFIG_COMPILER_OPTIMIZATION_SIZE=y
+
+# disable interrupt watchdog
+# CONFIG_ESP_INT_WDT=n
+# CONFIG_ESP_TASK_WDT_EN=n
+
+CONFIG_FREERTOS_HZ=1000
+
+CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
+CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
+
+# ESP32-specific
+#
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
+
+# CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT=y
+
+# Common ESP-related
+#
+CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
+CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
diff --git a/components/bldc_haptics/include/bldc_haptics.hpp b/components/bldc_haptics/include/bldc_haptics.hpp
new file mode 100644
index 000000000..50488d558
--- /dev/null
+++ b/components/bldc_haptics/include/bldc_haptics.hpp
@@ -0,0 +1,353 @@
+#pragma once
+
+#include
+#include
+
+#include "bldc_motor.hpp"
+#include "detent_config.hpp"
+#include "haptic_config.hpp"
+#include "logger.hpp"
+#include "task.hpp"
+
+namespace espp {
+template
+concept MotorConcept = requires {
+ static_cast(&FOO::enable);
+ static_cast(&FOO::disable);
+ static_cast(&FOO::move);
+ static_cast(&FOO::loop_foc);
+ static_cast(&FOO::get_shaft_angle);
+ static_cast(&FOO::get_shaft_velocity);
+ static_cast(&FOO::get_electrical_angle);
+};
+
+/// @brief Class which creates haptic feedback for the user by vibrating the
+/// motor This class is based on the work at
+/// https://github.com/scottbez1/smartknob to use a small BLDC gimbal motor as a
+/// haptic feedback device. It does so by varying the control type, setpoints, and
+/// gains of the motor to create a vibration. The motor is driven using the ESP32's MCPWM
+/// peripheral. The motor is driven in a closed loop using the encoder feedback.
+/// @note The motor is configured to be driven in an open-loop mode, so that the PID contained
+/// in the motor controller does not interfere with the haptic feedback.
+///
+/// The haptics provided by this class enable configuration of:
+/// - Positions (with non-magnetic detents) - evenly spaced across the allowed range of motion
+/// - Number of magnetic detents - where the
+/// - Width of the detents
+/// - Strength of the detents
+/// - Snap point (position at which the motor will snap to the next detent / position)
+/// - Bounds on the rotation of the motor - can be unbounded, bounded within a
+/// single revolution, or bounded within multiple revolutions
+///
+/// The haptics provided by this class provide the following functionality:
+/// - Positions: Evenly spaced positions across the allowed range of motion
+/// (specified by the min and max position) will have a detent.
+/// - Detents: Manually specified detents will be placed at the specified
+/// positions. The detents will have a specified width and strength.
+/// - End Stops: The motor will vibrate when it is at the min or max position.
+/// This is useful for providing feedback when the motor is at the end of its
+/// range of motion. The end stops are configured by specifying the strength
+/// of the end stops.
+/// - Snap point: The snap point is the position at which the motor will snap
+/// to the next detent / position. This is useful for providing feedback when
+/// the motor is at a certain position. The snap point is configured by
+/// specifying the snap point (percentage of the way through the detent) and
+/// the snap point bias (percentage of the way through the detent to bias the
+/// snap point).
+///
+/// Some example configurations are provided as static constexpr members of this
+/// class. They are:
+/// - UNBOUNDED_NO_DETENTS: No detents, no end stops, no snap point, no bounds
+/// - BOUNDED_NO_DETENTS: No detents, no end stops, no snap point, bounded
+/// within a single revolution
+/// - RETURN_TO_CENTER_WITH_DETENTS: 3 detents, end stops, snap point, bounded
+/// within a single revolution
+/// - RETURN_TO_CENTER_WITH_DETENTS_AND_MULTIPLE_REVOLUTIONS: 3 detents, end
+/// stops, snap point, bounded within multiple revolutions
+///
+/// Some haptic behaviors that can be implemented with this library are:
+/// - Unbounded with no detents
+/// - Bounded with no detents
+/// - Multiple revolutions
+/// - On/off with strong detent
+/// - Return to center without detents
+/// - Return to center with detents
+/// - Fine values with no detents
+/// - Fine values with detents
+/// - Coarse values with strong detents
+/// - Coarse values with weak detents
+template class BldcHaptics {
+public:
+ /// @brief Configuration for the haptic motor
+ struct Config {
+ std::reference_wrapper motor; ///< Pointer to the motor to use for haptics
+ Logger::Verbosity log_level; ///< Log level to use for the haptics
+ };
+
+ /// @brief Constructor for the haptic motor
+ /// @param config Configuration for the haptic motor
+ BldcHaptics(const Config &config)
+ : detent_pid_({.kp = 0,
+ .ki = 0,
+ .kd = 0,
+ .integrator_min = 0,
+ .integrator_max = 0,
+ .output_min = 0,
+ .output_max = 0}),
+ motor_(config.motor), logger_({.tag = "BldcHaptics", .level = config.log_level}) {
+ // create the motor task
+ motor_task_ =
+ Task::make_unique({.name = "haptic_motor",
+ .callback = std::bind(&BldcHaptics::motor_task, this,
+ std::placeholders::_1, std::placeholders::_2),
+ .stack_size_bytes = 1024 * 6,
+ .log_level = Logger::Verbosity::WARN});
+ }
+
+ /// @brief Start the haptic motor
+ void start() {
+ motor_.get().set_motion_control_type(M::MotionControlType::VELOCITY_OPENLOOP);
+ motor_task_->start();
+ }
+
+ /// @brief Stop the haptic motor
+ void stop() { motor_task_->stop(); }
+
+ /// @brief Configure the detents for the haptic motor
+ void update_detent_config(const detail::DetentConfig &config) {
+ std::unique_lock lk(detent_mutex_);
+ // update the detent center if the position or width changed
+ if (config.detent_positions.size() != 0 &&
+ config.position_width != detent_config_.position_width) {
+ // update the detent center
+ current_detent_center_ = motor_.get().get_shaft_angle();
+ }
+ // update the detent config
+ detent_config_ = config;
+
+ // Update derivative factor of torque controller based on detent width. If
+ // the D factor is large on coarse detents, the motor ends up making noise
+ // because the P&D factors amplify the noise from the sensor. This is a
+ // piecewise linear function so that fine detents (small width) get a
+ // higher D factor and coarse detents get a small D factor. Fine detents
+ // need a nonzero D factor to artificially create "clicks" each time a new
+ // value is reached (the P factor is small for fine detents due to the
+ // smaller angular errors, and the existing P factor doesn't work well for
+ // very small angle changes (easy to get runaway due to sensor noise &
+ // lag)).
+ // TODO: consider eliminating this D factor entirely and just "play" a
+ // hardcoded haptic "click" (e.g. a quick burst of torque in each
+ // direction) whenever the position changes when the detent width is too
+ // small for the P factor to work well.
+ const float derivative_lower_strength = config.detent_strength * 0.08;
+ const float derivative_upper_strength = config.detent_strength * 0.02;
+ const float derivative_position_width_lower = 3.0f * M_PI / 180.0f; // radians(3);
+ const float derivative_position_width_upper = 8.0f * M_PI / 180.0f; // radians(8);
+ const float raw = derivative_lower_strength +
+ (derivative_upper_strength - derivative_lower_strength) /
+ (derivative_position_width_upper - derivative_position_width_lower) *
+ (config.position_width - derivative_position_width_lower);
+ // When there are intermittent detents (set via detent_positions), disable
+ // derivative factor as this adds extra "clicks" when nearing a detent.
+ float new_kd =
+ config.detent_positions.size() > 0
+ ? 0
+ : std::clamp(
+ raw, std::min(derivative_lower_strength, derivative_upper_strength),
+ std::max(derivative_lower_strength, derivative_upper_strength));
+ // update the PID parameters
+ auto pid_config = detent_pid_.get_config();
+ pid_config.kd = new_kd;
+ // we don't want to clear the PID state when we change the config, so we
+ // pass false
+ detent_pid_.set_config(pid_config, false);
+ }
+
+ /// @brief Play haptic feedback
+ /// @note Plays a somewhat-configurable haptic "buzz" / "click" for the user
+ /// @note This is a blocking call that will wait for the haptic feedback to
+ /// finish before returning. It will also block the motor/detent task
+ /// from running until the haptic feedback is finished.
+ /// @param config Configuration for the haptic feedback
+ void play_haptic(const detail::HapticConfig &config) {
+ std::unique_lock lk(motor_mutex_);
+ // TODO: use the config duration
+ // Play a hardcoded haptic "click"
+ float strength = config.strength; // 5 or 1.5 were used in SmartKnob
+ motor_.get().move(strength);
+ using namespace std::chrono_literals;
+ for (uint8_t i = 0; i < 3; i++) {
+ motor_.get().loop_foc();
+ std::this_thread::sleep_for(1ms);
+ }
+ motor_.get().move(-strength);
+ for (uint8_t i = 0; i < 3; i++) {
+ motor_.get().loop_foc();
+ std::this_thread::sleep_for(1ms);
+ }
+ motor_.get().move(0);
+ motor_.get().loop_foc();
+ }
+
+protected:
+ /// @brief Task which runs the haptic motor
+ /// @param m Mutex to use for the task
+ /// @param cv Condition variable to use for the task
+ /// @return True if the task should be stopped, false otherwise
+ bool motor_task(std::mutex &m, std::condition_variable &cv) {
+ auto start = std::chrono::steady_clock::now();
+ // if we are not moving, and we're close to the center (but not exactly at
+ // the center), slowly move back to the center
+
+ // get the current detent config (copy it so we don't hold the mutex for too long)
+ detail::DetentConfig detent_config;
+ {
+ std::unique_lock lk(detent_mutex_);
+ detent_config = detent_config_;
+ }
+
+ {
+ std::unique_lock lk(motor_mutex_);
+ motor_.get().loop_foc();
+ // manage the haptics (detents, positions, etc.)
+ // check our position vs the nearest detent, and update our position if we're
+ // close enough to snap to another detent
+ float motor_angle = motor_.get().get_shaft_angle();
+ float angle_to_detent_center = motor_angle - current_detent_center_;
+
+ // apply motor torque based on angle to the nearest detent (strength is
+ // handled by the PID parameters)
+
+ // The snap point determines the position at which we snap to the next
+ // detent. If we're close enough to the snap point, we snap to the next
+ // detent. If we're not close enough to the snap point, we apply a torque
+ // to move us towards the snap point.
+ // The snap point is a percentage of the detent width, and is relative to
+ // the detent center. For example, if the snap point is 1.1, and the detent
+ // width is 10 degrees, then the snap point is 1.1 * 10 = 11 degrees away
+ // from the detent center. If the detent center is at 0 degrees, then the
+ // snap point is at 11 degrees. If the detent center is at 5 degrees, then
+ // the snap point is at 16 degrees.
+
+ /*
+ config.position = 0
+ config.min_position = 0
+ config.max_position = 5
+ config.position_width_radians = math.radians(10)
+ config.detent_strength_unit = 1
+ config.endstop_strength_unit = 1
+ config.snap_point = 1.1
+ */
+
+ // Handle the snap point - if we're close enough to the snap point, snap
+ // to the snap point
+ float snap_point_radians = detent_config.position_width * detent_config.snap_point;
+ // the bias is the amount of the detent width that we bias the snap point
+ // by. For example, if the bias is 0.1, and the detent width is 10 degrees,
+ // then the snap point is 11 degrees away from the detent center. If the
+ // bias is 0.2, then the snap point is 12 degrees away from the detent
+ // center.
+ float bias_radians = detent_config.position_width * detent_config.snap_point_bias;
+ float snap_point_radians_decrease =
+ snap_point_radians + (current_position_ <= 0 ? bias_radians : -bias_radians);
+ float snap_point_radians_increase =
+ -snap_point_radians + (current_position_ >= 0 ? -bias_radians : bias_radians);
+
+ int32_t num_positions = detent_config.max_position - detent_config.min_position + 1;
+ // update our position if we're close enough to snap to another detent
+ // (and we're not at the end of the range)
+ if (angle_to_detent_center > snap_point_radians_decrease &&
+ (num_positions <= 0 || current_position_ > detent_config.min_position)) {
+ // we're past the snap point, so snap to the next detent
+ current_detent_center_ += detent_config.position_width;
+ angle_to_detent_center -= detent_config.position_width;
+ current_position_--;
+ } else if (angle_to_detent_center < snap_point_radians_increase &&
+ (num_positions <= 0 || current_position_ < detent_config.max_position)) {
+ // we're past the snap point, so snap to the next detent
+ current_detent_center_ -= detent_config.position_width;
+ angle_to_detent_center += detent_config.position_width;
+ current_position_++;
+ }
+
+ float dead_zone_adjustment =
+ std::clamp(angle_to_detent_center,
+ std::max(-detent_config.position_width * detent_config.dead_zone_percent,
+ -detent_config.dead_zone_abs_max_radians),
+ std::min(detent_config.position_width * detent_config.dead_zone_percent,
+ detent_config.dead_zone_abs_max_radians));
+
+ bool out_of_bounds =
+ num_positions > 0 &&
+ ((angle_to_detent_center > 0 && current_position_ == detent_config.min_position) ||
+ (angle_to_detent_center < 0 && current_position_ == detent_config.max_position));
+
+ // update the PID parameters based on our position
+ auto pid_config = detent_pid_.get_config();
+ pid_config.output_max = 10; // out_of_bounds ? 10 : 3;
+ pid_config.output_min = -10; // out_of_bounds ? 10 : 3;
+ pid_config.kp = out_of_bounds ? detent_config.end_strength *
+ 4 // if we're out of bounds, then we apply end stop force
+ : detent_config.detent_strength *
+ 4; // if we're in bounds, then we apply detent force
+ // we don't want to clear the PID state when we change the config, so we pass false
+ detent_pid_.set_config(pid_config, false);
+
+ // Apply motor torque based on our angle to the nearest detent (detent
+ // strength, etc is handled by the pid parameters)
+ if (std::abs(motor_.get().get_shaft_velocity()) > 60) {
+ // Don't apply torque if velocity is too high (helps avoid positive
+ // feedback loop/runaway)
+ motor_.get().move(0);
+ } else {
+ // apply torque based on our angle to the nearest detent
+ float input = -angle_to_detent_center + dead_zone_adjustment;
+ // if we're out of bounds, then we apply torque regardless of our
+ // position
+ if (!out_of_bounds && detent_config.detent_positions.size() > 0) {
+ // if there are manually specified detents, then we only apply torque
+ // if we're in a detent
+ bool in_detent = false;
+ for (auto &detent : detent_config.detent_positions) {
+ if (detent == current_position_) {
+ in_detent = true;
+ break;
+ }
+ }
+ // if we're not in a detent, then we don't apply any torque
+ if (!in_detent) {
+ input = 0;
+ }
+ }
+ // get the torque from the PID controller
+ float torque = detent_pid_.update(input);
+ // apply the torque to the motor
+ motor_.get().move(torque);
+ } // end if std::abs(motor_.get().get_shaft_velocity()) > 60
+ } // end motor_mutex_
+
+ // now sleep
+ {
+ using namespace std::chrono_literals;
+ std::unique_lock lk(m);
+ cv.wait_until(lk, start + 1ms);
+ }
+
+ // don't want to stop the task
+ return false;
+ }
+
+ Pid detent_pid_; ///< PID controller for the detents
+ std::atomic current_position_{0}; ///< Current position of the motor
+ std::atomic current_detent_center_{0}; ///< Current center of the detent
+ std::mutex detent_mutex_; ///< Mutex for accessing the detents
+ detail::DetentConfig detent_config_; ///< Configuration for the detents
+
+ std::mutex motor_mutex_; ///< Mutex for accessing the motor
+ std::reference_wrapper motor_; ///< Pointer to the motor to use for haptics
+
+ std::unique_ptr motor_task_; ///< Task which runs the haptic motor
+
+ Logger logger_; ///< Logger for the haptics
+};
+} // namespace espp
diff --git a/components/bldc_haptics/include/detent_config.hpp b/components/bldc_haptics/include/detent_config.hpp
new file mode 100644
index 000000000..e256dd67e
--- /dev/null
+++ b/components/bldc_haptics/include/detent_config.hpp
@@ -0,0 +1,118 @@
+#pragma once
+
+#include
+
+namespace espp::detail {
+/// @brief Configuration for the detents
+/// @note max_position < min_position indicates no bounds
+struct DetentConfig {
+ float position_width; ///< Width of the positions, in radians
+ float min_position{0}; ///< Minimum position of motor, there will be an end stop at this position;
+ ///< There will be (max-min+1) positions between min and max (inclusive).
+ float max_position{
+ 5}; ///< Maximum position of motor, there will be an end stop at this position; There will be
+ ///< (max-min+1) positions between min and max (inclusive). max < min indicates no bounds
+ std::vector detent_positions{}; ///< Positions of the detents, an integer value between min
+ ///< and max position. Optional, if empty / not specified, no
+ ///< detents will be used.
+ float detent_strength{1}; ///< Strength of the detents
+ float end_strength{1}; ///< Strength of the end detents
+ float snap_point{1.1}; ///< Position of the snap point, in radians, should be >= 0.5 for stability
+ float snap_point_bias{0}; ///< Bias for the snap point, in radians, should be >= 0 for stability
+ float dead_zone_percent{0.2f}; ///< Percent of the dead zone to use for the detent
+ float dead_zone_abs_max_radians{
+ M_PI / 180.0f}; ///< Absolute maximum of the dead zone to use for the detent in radians
+};
+
+/*
+static const DetentConfig UNBOUNDED_NO_DETENTS;
+static const DetentConfig BOUNDED_NO_DETENTS;
+static const DetentConfig MULTI_REV_NO_DETENTS;
+static const DetentConfig COARSE_VALUES_STRONG_DETENTS;
+static const DetentConfig FINE_VALUES_NO_DETENTS;
+static const DetentConfig FINE_VALUES_WITH_DETENTS;
+static const DetentConfig MAGNETIC_DETENTS;
+static const DetentConfig RETURN_TO_CENTER_WITH_DETENTS;
+*/
+static const DetentConfig UNBOUNDED_NO_DETENTS = {
+ .position_width = 10.0 * M_PI / 180.0,
+ .min_position = 0,
+ .max_position = -1, // max < min indicates no bounds
+ .detent_strength = 0,
+ .end_strength = 1,
+ .snap_point = 1.1,
+ .snap_point_bias = 0,
+};
+
+static const DetentConfig BOUNDED_NO_DETENTS = {
+ .position_width = 10.0 * M_PI / 180.0,
+ .min_position = 0,
+ .max_position = 10,
+ .detent_strength = 0,
+ .end_strength = 1,
+ .snap_point = 1.1,
+ .snap_point_bias = 0,
+};
+
+static const DetentConfig MULTI_REV_NO_DETENTS = {
+ .position_width = 10.0 * M_PI / 180.0,
+ .min_position = 0,
+ .max_position = 72,
+ .detent_strength = 0,
+ .end_strength = 1,
+ .snap_point = 1.1,
+ .snap_point_bias = 0,
+};
+
+static const DetentConfig COARSE_VALUES_STRONG_DETENTS = {
+ .position_width = 8.225f * M_PI / 180.0,
+ .min_position = 0,
+ .max_position = 31,
+ .detent_strength = 2,
+ .end_strength = 1,
+ .snap_point = 1.1,
+ .snap_point_bias = 0,
+};
+
+static const DetentConfig FINE_VALUES_NO_DETENTS = {
+ .position_width = 1.0f * M_PI / 180.0,
+ .min_position = 0,
+ .max_position = 255,
+ .detent_strength = 0,
+ .end_strength = 1,
+ .snap_point = 1.1,
+ .snap_point_bias = 0,
+};
+
+static const DetentConfig FINE_VALUES_WITH_DETENTS = {
+ .position_width = 1.0f * M_PI / 180.0,
+ .min_position = 0,
+ .max_position = 255,
+ .detent_strength = 1,
+ .end_strength = 1,
+ .snap_point = 1.1,
+ .snap_point_bias = 0,
+};
+
+static const DetentConfig MAGNETIC_DETENTS = {
+ .position_width = 7.0 * M_PI / 180.0,
+ .min_position = 0,
+ .max_position = 31,
+ .detent_positions = {2, 10, 21, 22},
+ .detent_strength = 2.5,
+ .end_strength = 1,
+ .snap_point = 0.7,
+ .snap_point_bias = 0,
+};
+
+static const DetentConfig RETURN_TO_CENTER_WITH_DETENTS = {
+ .position_width = 0,
+ .min_position = -6,
+ .max_position = 6,
+ .detent_strength = 1,
+ .end_strength = 1,
+ .snap_point = 0.55,
+ .snap_point_bias = 0.4,
+};
+
+} // namespace espp::detail
diff --git a/components/bldc_haptics/include/haptic_config.hpp b/components/bldc_haptics/include/haptic_config.hpp
new file mode 100644
index 000000000..068dbd2b0
--- /dev/null
+++ b/components/bldc_haptics/include/haptic_config.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#include
+
+namespace espp::detail {
+/// @brief Configuration for the haptic feedback
+struct HapticConfig {
+ float strength; ///< Strength of the haptic feedback
+ std::chrono::duration duration; ///< Duration of the haptic feedback
+};
+} // namespace espp::detail
diff --git a/components/bldc_haptics/include/smartknob_configs.hpp b/components/bldc_haptics/include/smartknob_configs.hpp
new file mode 100644
index 000000000..2f79f2e48
--- /dev/null
+++ b/components/bldc_haptics/include/smartknob_configs.hpp
@@ -0,0 +1,157 @@
+static PB_SmartKnobConfig configs[] = {
+ // int32_t position;
+ // int32_t min_position;
+ // int32_t max_position;
+ // float position_width_radians;
+ // float detent_strength_unit;
+ // float endstop_strength_unit;
+ // float snap_point;
+ // char text[51];
+ // pb_size_t detent_positions_count;
+ // int32_t detent_positions[5];
+ // float snap_point_bias;
+
+ {
+ 0,
+ 0,
+ -1, // max position < min position indicates no bounds
+ 10 * PI / 180,
+ 0,
+ 1,
+ 1.1,
+ "Unbounded\nNo detents",
+ 0,
+ {},
+ 0,
+ },
+ {
+ 0,
+ 0,
+ 10,
+ 10 * PI / 180,
+ 0,
+ 1,
+ 1.1,
+ "Bounded 0-10\nNo detents",
+ 0,
+ {},
+ 0,
+ },
+ {
+ 0,
+ 0,
+ 72,
+ 10 * PI / 180,
+ 0,
+ 1,
+ 1.1,
+ "Multi-rev\nNo detents",
+ 0,
+ {},
+ 0,
+ },
+ {
+ 0,
+ 0,
+ 1,
+ 60 * PI / 180,
+ 1,
+ 1,
+ 0.55, // Note the snap point is slightly past the midpoint (0.5); compare to normal detents
+ // which use a snap point *past* the next value (i.e. > 1)
+ "On/off\nStrong detent",
+ 0,
+ {},
+ 0,
+ },
+ {
+ 0,
+ 0,
+ 0,
+ 60 * PI / 180,
+ 0.01,
+ 0.6,
+ 1.1,
+ "Return-to-center",
+ 0,
+ {},
+ 0,
+ },
+ {
+ 127,
+ 0,
+ 255,
+ 1 * PI / 180,
+ 0,
+ 1,
+ 1.1,
+ "Fine values\nNo detents",
+ 0,
+ {},
+ 0,
+ },
+ {
+ 127,
+ 0,
+ 255,
+ 1 * PI / 180,
+ 1,
+ 1,
+ 1.1,
+ "Fine values\nWith detents",
+ 0,
+ {},
+ 0,
+ },
+ {
+ 0,
+ 0,
+ 31,
+ 8.225806452 * PI / 180,
+ 2,
+ 1,
+ 1.1,
+ "Coarse values\nStrong detents",
+ 0,
+ {},
+ 0,
+ },
+ {
+ 0,
+ 0,
+ 31,
+ 8.225806452 * PI / 180,
+ 0.2,
+ 1,
+ 1.1,
+ "Coarse values\nWeak detents",
+ 0,
+ {},
+ 0,
+ },
+ {
+ 0,
+ 0,
+ 31,
+ 7 * PI / 180,
+ 2.5,
+ 1,
+ 0.7,
+ "Magnetic detents",
+ 4,
+ {2, 10, 21, 22},
+ 0,
+ },
+ // int32_t position;
+ // int32_t min_position;
+ // int32_t max_position;
+ // float position_width_radians;
+ // float detent_strength_unit;
+ // float endstop_strength_unit;
+ // float snap_point;
+ // char text[51];
+ // pb_size_t detent_positions_count;
+ // int32_t detent_positions[5];
+ // float snap_point_bias;
+ {0, -6, 6, 60 * PI / 180, 1, 1, 0.55, "Return-to-center\nwith detents", 0, {}, 0.4},
+};
From 04aa8abfba118c6cebba4186e618d0f12a28da06 Mon Sep 17 00:00:00 2001
From: William Emfinger
Date: Thu, 25 May 2023 15:54:02 -0500
Subject: [PATCH 02/30] feat(bldc_haptics): remove empty src
---
components/bldc_haptics/CMakeLists.txt | 1 -
1 file changed, 1 deletion(-)
diff --git a/components/bldc_haptics/CMakeLists.txt b/components/bldc_haptics/CMakeLists.txt
index 6d2360613..cb71043a1 100644
--- a/components/bldc_haptics/CMakeLists.txt
+++ b/components/bldc_haptics/CMakeLists.txt
@@ -1,5 +1,4 @@
idf_component_register(
INCLUDE_DIRS "include"
- SRC_DIRS "src"
REQUIRES logger math pid task bldc_motor
)
From e6ee866acc6f117a5abcf51fbcf5b2da4fa55414 Mon Sep 17 00:00:00 2001
From: William Emfinger
Date: Fri, 26 May 2023 12:35:17 -0500
Subject: [PATCH 03/30] feat(pid): udpate API * Updated PID config api to allow
user to specify whether they want to clear the state or not (default true) *
Added `set_config` api to mirror `get_config`
---
components/pid/include/pid.hpp | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/components/pid/include/pid.hpp b/components/pid/include/pid.hpp
index b20af9e1b..b42584df8 100644
--- a/components/pid/include/pid.hpp
+++ b/components/pid/include/pid.hpp
@@ -50,12 +50,23 @@ class Pid {
/**
* @brief Change the gains and other configuration for the PID controller.
* @param config Configuration struct with new gains and sampling time.
+ * @param reset_state Reset / clear the PID controller state.
*/
- void change_gains(const Config &config) {
+ void change_gains(const Config &config, bool reset_state = true) {
std::lock_guard lk(mutex_);
logger_.info("Updated config: {}", config);
config_ = config;
- clear();
+ if (reset_state)
+ clear(); // clear the state
+ }
+
+ /**
+ * @brief Change the gains and other configuration for the PID controller.
+ * @param config Configuration struct with new gains and sampling time.
+ * @param reset_state Reset / clear the PID controller state.
+ */
+ void set_config(const Config &config, bool reset_state = true) {
+ change_gains(config, reset_state);
}
/**
From 1c18925f2a3fc3d99b90e8364da255ebc31f2d66 Mon Sep 17 00:00:00 2001
From: William Emfinger
Date: Fri, 26 May 2023 12:36:02 -0500
Subject: [PATCH 04/30] chore(math): add maybe_unused to suppress compiler
warning
---
components/math/include/fast_math.hpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/components/math/include/fast_math.hpp b/components/math/include/fast_math.hpp
index 13b325f65..05e33c077 100644
--- a/components/math/include/fast_math.hpp
+++ b/components/math/include/fast_math.hpp
@@ -70,7 +70,7 @@ float fast_ln(float x) {
// bx = * reinterpret_cast (&x);
uint32_t ex = bx >> 23;
signed int t = (signed int)ex - (signed int)127;
- uint32_t s = (t < 0) ? (-t) : t;
+ [[maybe_unused]] uint32_t s = (t < 0) ? (-t) : t;
bx = 1065353216 | (bx & 8388607);
// x = * reinterpret_cast(&bx);
memcpy(&x, &bx, sizeof(x));
From ae25fb9cc739ef314d2ca8bfc80e50d1f1ab132f Mon Sep 17 00:00:00 2001
From: William Emfinger
Date: Fri, 26 May 2023 13:02:29 -0500
Subject: [PATCH 05/30] doc: update * Updated docs to include bldc haptics info
* Updated bldc_haptics comments for more complete docs and remove unused code
* Updated bldc_motor to point to example usage (was missing)
---
.../example/main/bldc_haptics_example.cpp | 5 +-
.../bldc_haptics/include/bldc_haptics.hpp | 42 +-
.../bldc_haptics/include/detent_config.hpp | 22 +-
.../bldc_haptics/include/haptic_config.hpp | 1 +
components/bldc_motor/include/bldc_motor.hpp | 2 +
doc/Doxyfile | 4 +
doc/en/haptics/bldc_haptics.rst | 34 ++
doc/en/haptics/index.rst | 1 +
docs/_sources/haptics/bldc_haptics.rst.txt | 34 ++
docs/_sources/haptics/index.rst.txt | 1 +
docs/adc/adc_types.html | 2 +-
docs/adc/ads1x15.html | 4 +-
docs/adc/continuous_adc.html | 4 +-
docs/adc/index.html | 2 +-
docs/adc/oneshot_adc.html | 4 +-
docs/bldc/bldc_driver.html | 4 +-
docs/bldc/bldc_motor.html | 109 ++++-
docs/bldc/index.html | 2 +-
docs/cli.html | 6 +-
docs/color.html | 4 +-
docs/controller.html | 4 +-
docs/csv.html | 4 +-
docs/display/display.html | 4 +-
docs/display/display_drivers.html | 6 +-
docs/display/index.html | 2 +-
docs/encoder/abi_encoder.html | 4 +-
docs/encoder/as5600.html | 4 +-
docs/encoder/encoder_types.html | 2 +-
docs/encoder/index.html | 2 +-
docs/encoder/mt6701.html | 4 +-
docs/event_manager.html | 4 +-
docs/file_system.html | 4 +-
docs/filters/biquad.html | 4 +-
docs/filters/butterworth.html | 4 +-
docs/filters/index.html | 2 +-
docs/filters/lowpass.html | 4 +-
docs/filters/sos.html | 4 +-
docs/filters/transfer_function.html | 2 +-
docs/ftp/ftp_server.html | 6 +-
docs/ftp/index.html | 2 +-
docs/genindex.html | 24 +-
docs/haptics/bldc_haptics.html | 384 ++++++++++++++++++
docs/haptics/drv2605.html | 9 +-
docs/haptics/index.html | 8 +-
docs/index.html | 3 +-
docs/input/index.html | 2 +-
docs/input/touchpad_input.html | 4 +-
docs/io_expander/aw9523.html | 4 +-
docs/io_expander/index.html | 2 +-
docs/io_expander/mcp23x17.html | 4 +-
docs/joystick.html | 4 +-
docs/led.html | 4 +-
docs/logger.html | 4 +-
docs/math/bezier.html | 4 +-
docs/math/fast_math.html | 2 +-
docs/math/gaussian.html | 4 +-
docs/math/index.html | 2 +-
docs/math/range_mapper.html | 4 +-
docs/math/vector2d.html | 4 +-
docs/monitor.html | 4 +-
docs/network/index.html | 2 +-
docs/network/socket.html | 4 +-
docs/network/tcp_socket.html | 4 +-
docs/network/udp_socket.html | 4 +-
docs/nfc/index.html | 2 +-
docs/nfc/ndef.html | 4 +-
docs/nfc/st25dv.html | 4 +-
docs/objects.inv | Bin 43175 -> 43565 bytes
docs/pid.html | 4 +-
docs/rmt.html | 106 ++---
docs/rtsp.html | 18 +-
docs/searchindex.js | 2 +-
docs/serialization.html | 4 +-
docs/state_machine.html | 10 +-
docs/task.html | 4 +-
docs/wifi/index.html | 2 +-
docs/wifi/wifi_ap.html | 4 +-
docs/wifi/wifi_sta.html | 4 +-
78 files changed, 810 insertions(+), 211 deletions(-)
create mode 100644 doc/en/haptics/bldc_haptics.rst
create mode 100644 docs/_sources/haptics/bldc_haptics.rst.txt
create mode 100644 docs/haptics/bldc_haptics.html
diff --git a/components/bldc_haptics/example/main/bldc_haptics_example.cpp b/components/bldc_haptics/example/main/bldc_haptics_example.cpp
index 16bbf9864..51cd3c86d 100644
--- a/components/bldc_haptics/example/main/bldc_haptics_example.cpp
+++ b/components/bldc_haptics/example/main/bldc_haptics_example.cpp
@@ -137,6 +137,7 @@ extern "C" void app_main(void) {
},
.log_level = espp::Logger::Verbosity::INFO});
+ //! [bldc_haptics_example_1]
// set the motion control type to velocity openloop for the haptics
static auto motion_control_type = BldcMotor::MotionControlType::VELOCITY_OPENLOOP;
motor.set_motion_control_type(motion_control_type);
@@ -152,8 +153,10 @@ extern "C" void app_main(void) {
logger.info("Playing haptic buzz for 1 second");
haptic_motor.play_haptic(espp::detail::HapticConfig{
.strength = 5.0f,
- .duration = 1s // NOTE: duration is unused for now
+ .frequency = 200.0f, // Hz, NOTE: frequency is unused for now
+ .duration = 1s // NOTE: duration is unused for now
});
+ //! [bldc_haptics_example_1]
static auto start = std::chrono::high_resolution_clock::now();
auto now = std::chrono::high_resolution_clock::now();
diff --git a/components/bldc_haptics/include/bldc_haptics.hpp b/components/bldc_haptics/include/bldc_haptics.hpp
index 50488d558..19fa4d263 100644
--- a/components/bldc_haptics/include/bldc_haptics.hpp
+++ b/components/bldc_haptics/include/bldc_haptics.hpp
@@ -10,15 +10,18 @@
#include "task.hpp"
namespace espp {
+/// @brief Concept for a motor that can be used for haptics
template
concept MotorConcept = requires {
- static_cast(&FOO::enable);
- static_cast(&FOO::disable);
- static_cast(&FOO::move);
- static_cast(&FOO::loop_foc);
- static_cast(&FOO::get_shaft_angle);
- static_cast(&FOO::get_shaft_velocity);
- static_cast(&FOO::get_electrical_angle);
+ static_cast(&FOO::enable); ///< Enable the motor
+ static_cast(&FOO::disable); ///< Disable the motor
+ static_cast(
+ &FOO::move); ///< Move the motor to a new target (position, velocity, or torque depending on
+ ///< the motor control type)
+ static_cast(&FOO::loop_foc); ///< Run the FOC loop
+ static_cast(&FOO::get_shaft_angle); ///< Get the shaft angle
+ static_cast(&FOO::get_shaft_velocity); ///< Get the shaft velocity
+ static_cast(&FOO::get_electrical_angle); ///< Get the electrical angle
};
/// @brief Class which creates haptic feedback for the user by vibrating the
@@ -55,11 +58,17 @@ concept MotorConcept = requires {
/// the snap point bias (percentage of the way through the detent to bias the
/// snap point).
///
-/// Some example configurations are provided as static constexpr members of this
-/// class. They are:
+/// Some example configurations are provided as static constexpr in
+/// espp::detail. They are:
/// - UNBOUNDED_NO_DETENTS: No detents, no end stops, no snap point, no bounds
/// - BOUNDED_NO_DETENTS: No detents, no end stops, no snap point, bounded
/// within a single revolution
+/// - MULTI_REV_NO_DETENTS: No detents, no end stops, no snap point, bounded
+/// within multiple revolutions
+/// - COARSE_VALUES_STRONG_DETENTS: detents, end stops, snap point, bounded
+/// within a single revolution
+/// - FINE_VALUES_NO_DETENTS: No detents, end stops, snap point, bounded
+/// - FINE_VALUES_WITH_DETENTS: detents, end stops, snap point, bounded
/// - RETURN_TO_CENTER_WITH_DETENTS: 3 detents, end stops, snap point, bounded
/// within a single revolution
/// - RETURN_TO_CENTER_WITH_DETENTS_AND_MULTIPLE_REVOLUTIONS: 3 detents, end
@@ -76,6 +85,9 @@ concept MotorConcept = requires {
/// - Fine values with detents
/// - Coarse values with strong detents
/// - Coarse values with weak detents
+///
+/// \section bldc_haptics_ex1 Example 1: Bounded with magnetic detents
+/// \snippet bldc_haptics_example.cpp bldc_haptics_example_1
template class BldcHaptics {
public:
/// @brief Configuration for the haptic motor
@@ -215,7 +227,7 @@ template class BldcHaptics {
float motor_angle = motor_.get().get_shaft_angle();
float angle_to_detent_center = motor_angle - current_detent_center_;
- // apply motor torque based on angle to the nearest detent (strength is
+ // apply motor torque based on angle to the nearest position (strength is
// handled by the PID parameters)
// The snap point determines the position at which we snap to the next
@@ -229,16 +241,6 @@ template class BldcHaptics {
// snap point is at 11 degrees. If the detent center is at 5 degrees, then
// the snap point is at 16 degrees.
- /*
- config.position = 0
- config.min_position = 0
- config.max_position = 5
- config.position_width_radians = math.radians(10)
- config.detent_strength_unit = 1
- config.endstop_strength_unit = 1
- config.snap_point = 1.1
- */
-
// Handle the snap point - if we're close enough to the snap point, snap
// to the snap point
float snap_point_radians = detent_config.position_width * detent_config.snap_point;
diff --git a/components/bldc_haptics/include/detent_config.hpp b/components/bldc_haptics/include/detent_config.hpp
index e256dd67e..6c2a31b70 100644
--- a/components/bldc_haptics/include/detent_config.hpp
+++ b/components/bldc_haptics/include/detent_config.hpp
@@ -24,16 +24,7 @@ struct DetentConfig {
M_PI / 180.0f}; ///< Absolute maximum of the dead zone to use for the detent in radians
};
-/*
-static const DetentConfig UNBOUNDED_NO_DETENTS;
-static const DetentConfig BOUNDED_NO_DETENTS;
-static const DetentConfig MULTI_REV_NO_DETENTS;
-static const DetentConfig COARSE_VALUES_STRONG_DETENTS;
-static const DetentConfig FINE_VALUES_NO_DETENTS;
-static const DetentConfig FINE_VALUES_WITH_DETENTS;
-static const DetentConfig MAGNETIC_DETENTS;
-static const DetentConfig RETURN_TO_CENTER_WITH_DETENTS;
-*/
+/// @brief Unbounded motion, no detents
static const DetentConfig UNBOUNDED_NO_DETENTS = {
.position_width = 10.0 * M_PI / 180.0,
.min_position = 0,
@@ -44,6 +35,7 @@ static const DetentConfig UNBOUNDED_NO_DETENTS = {
.snap_point_bias = 0,
};
+/// @brief Bounded motion, no detents
static const DetentConfig BOUNDED_NO_DETENTS = {
.position_width = 10.0 * M_PI / 180.0,
.min_position = 0,
@@ -54,6 +46,7 @@ static const DetentConfig BOUNDED_NO_DETENTS = {
.snap_point_bias = 0,
};
+/// @brief Bounded motion with multiple revolutions, no detents, with end stops
static const DetentConfig MULTI_REV_NO_DETENTS = {
.position_width = 10.0 * M_PI / 180.0,
.min_position = 0,
@@ -64,6 +57,8 @@ static const DetentConfig MULTI_REV_NO_DETENTS = {
.snap_point_bias = 0,
};
+/// @brief Bounded motion with strong position detents spaced 9 degrees apart
+/// (coarse), with end stops
static const DetentConfig COARSE_VALUES_STRONG_DETENTS = {
.position_width = 8.225f * M_PI / 180.0,
.min_position = 0,
@@ -74,6 +69,8 @@ static const DetentConfig COARSE_VALUES_STRONG_DETENTS = {
.snap_point_bias = 0,
};
+/// @brief Bounded motion with no detents spaced 1 degree apart (fine), with end
+/// stops
static const DetentConfig FINE_VALUES_NO_DETENTS = {
.position_width = 1.0f * M_PI / 180.0,
.min_position = 0,
@@ -84,6 +81,8 @@ static const DetentConfig FINE_VALUES_NO_DETENTS = {
.snap_point_bias = 0,
};
+/// @brief Bounded motion with position detents spaced 1 degree apart (fine),
+/// with end stops
static const DetentConfig FINE_VALUES_WITH_DETENTS = {
.position_width = 1.0f * M_PI / 180.0,
.min_position = 0,
@@ -94,6 +93,8 @@ static const DetentConfig FINE_VALUES_WITH_DETENTS = {
.snap_point_bias = 0,
};
+/// @brief Bounded motion with position detents, end stops, and explicit
+/// magnetic detents.
static const DetentConfig MAGNETIC_DETENTS = {
.position_width = 7.0 * M_PI / 180.0,
.min_position = 0,
@@ -105,6 +106,7 @@ static const DetentConfig MAGNETIC_DETENTS = {
.snap_point_bias = 0,
};
+/// @brief Bounded motion for a return to center rotary encoder with positions
static const DetentConfig RETURN_TO_CENTER_WITH_DETENTS = {
.position_width = 0,
.min_position = -6,
diff --git a/components/bldc_haptics/include/haptic_config.hpp b/components/bldc_haptics/include/haptic_config.hpp
index 068dbd2b0..80956b8d7 100644
--- a/components/bldc_haptics/include/haptic_config.hpp
+++ b/components/bldc_haptics/include/haptic_config.hpp
@@ -6,6 +6,7 @@ namespace espp::detail {
/// @brief Configuration for the haptic feedback
struct HapticConfig {
float strength; ///< Strength of the haptic feedback
+ float frequency; ///< Frequency of the haptic feedback
std::chrono::duration duration; ///< Duration of the haptic feedback
};
} // namespace espp::detail
diff --git a/components/bldc_motor/include/bldc_motor.hpp b/components/bldc_motor/include/bldc_motor.hpp
index 19124aad6..8013ebcbe 100644
--- a/components/bldc_motor/include/bldc_motor.hpp
+++ b/components/bldc_motor/include/bldc_motor.hpp
@@ -59,6 +59,8 @@ struct DummyCurrentSense {
* object/type and optionally a current sensor object / type.
* @note This is a port (with some modifications) of the excellent work by
* SimpleFOC - https://simplefoc.com
+ * @section bldc_motor_usage Example Usage
+ * @snippet bldc_motor_example.cpp bldc_motor example
*/
template
class BldcMotor {
diff --git a/doc/Doxyfile b/doc/Doxyfile
index 4f3930ba1..6eccbd7c4 100644
--- a/doc/Doxyfile
+++ b/doc/Doxyfile
@@ -25,6 +25,7 @@ EXAMPLE_PATH += $(PROJECT_PATH)/components/ads1x15/example/main/ads1x15_example.
EXAMPLE_PATH += $(PROJECT_PATH)/components/as5600/example/main/as5600_example.cpp
EXAMPLE_PATH += $(PROJECT_PATH)/components/aw9523/example/main/aw9523_example.cpp
EXAMPLE_PATH += $(PROJECT_PATH)/components/bldc_motor/example/main/bldc_motor_example.cpp
+EXAMPLE_PATH += $(PROJECT_PATH)/components/bldc_haptics/example/main/bldc_haptics_example.cpp
EXAMPLE_PATH += $(PROJECT_PATH)/components/controller/example/main/controller_example.cpp
EXAMPLE_PATH += $(PROJECT_PATH)/components/cli/example/main/cli_example.cpp
EXAMPLE_PATH += $(PROJECT_PATH)/components/color/example/main/color_example.cpp
@@ -68,6 +69,9 @@ INPUT += $(PROJECT_PATH)/components/ads1x15/include/ads1x15.hpp
INPUT += $(PROJECT_PATH)/components/as5600/include/as5600.hpp
INPUT += $(PROJECT_PATH)/components/aw9523/include/aw9523.hpp
INPUT += $(PROJECT_PATH)/components/bldc_driver/include/bldc_driver.hpp
+INPUT += $(PROJECT_PATH)/components/bldc_haptics/include/bldc_haptics.hpp
+INPUT += $(PROJECT_PATH)/components/bldc_haptics/include/detent_config.hpp
+INPUT += $(PROJECT_PATH)/components/bldc_haptics/include/haptic_config.hpp
INPUT += $(PROJECT_PATH)/components/bldc_motor/include/bldc_motor.hpp
INPUT += $(PROJECT_PATH)/components/controller/include/controller.hpp
INPUT += $(PROJECT_PATH)/components/cli/include/cli.hpp
diff --git a/doc/en/haptics/bldc_haptics.rst b/doc/en/haptics/bldc_haptics.rst
new file mode 100644
index 000000000..d3e6851ba
--- /dev/null
+++ b/doc/en/haptics/bldc_haptics.rst
@@ -0,0 +1,34 @@
+BLDC Haptics
+************
+
+The `BldcHaptics` class is a high-level interface for controlling a BLDC motor
+with a haptic feedback loop. It is designed to be used to provide haptic
+feedback as part of a rotary input device (the BLDC motor). The component
+provides a `DetentConfig` interface for configuring the input / haptic feedback
+profile for the motor dynamically with configuration of:
+
+ - Range of motion (min/max position + width of each position)
+ - Width of each position in the range (which will be used to calculate the
+ actual range of motion based on the number of positions)
+ - Strength of the haptic feedback at each position (detent)
+ - Strength of the haptic feedback at the edges of the range of motion
+ - Specific positions to provide haptic feedback at (detents)
+ - Snap point (percentage of position width which will trigger a snap to the
+ nearest position)
+
+
+The component also provides a `HapticConfig` interface for configuring the
+haptic feedback loop with configuration of:
+
+ - Strength of the haptic feedback
+ - Frequency of the haptic feedback [currently not implemented]
+ - Duration of the haptic feedback [currently not implemented]
+
+.. ---------------------------- API Reference ----------------------------------
+
+API Reference
+-------------
+
+.. include-build-file:: inc/detent_config.inc
+.. include-build-file:: inc/haptic_config.inc
+.. include-build-file:: inc/bldc_haptics.inc
diff --git a/doc/en/haptics/index.rst b/doc/en/haptics/index.rst
index 06acc45d2..d3a00145d 100644
--- a/doc/en/haptics/index.rst
+++ b/doc/en/haptics/index.rst
@@ -4,6 +4,7 @@ Haptics APIs
.. toctree::
:maxdepth: 1
+ bldc_haptics
drv2605
There are multiple components which provide haptic feedback functionality,
diff --git a/docs/_sources/haptics/bldc_haptics.rst.txt b/docs/_sources/haptics/bldc_haptics.rst.txt
new file mode 100644
index 000000000..d3e6851ba
--- /dev/null
+++ b/docs/_sources/haptics/bldc_haptics.rst.txt
@@ -0,0 +1,34 @@
+BLDC Haptics
+************
+
+The `BldcHaptics` class is a high-level interface for controlling a BLDC motor
+with a haptic feedback loop. It is designed to be used to provide haptic
+feedback as part of a rotary input device (the BLDC motor). The component
+provides a `DetentConfig` interface for configuring the input / haptic feedback
+profile for the motor dynamically with configuration of:
+
+ - Range of motion (min/max position + width of each position)
+ - Width of each position in the range (which will be used to calculate the
+ actual range of motion based on the number of positions)
+ - Strength of the haptic feedback at each position (detent)
+ - Strength of the haptic feedback at the edges of the range of motion
+ - Specific positions to provide haptic feedback at (detents)
+ - Snap point (percentage of position width which will trigger a snap to the
+ nearest position)
+
+
+The component also provides a `HapticConfig` interface for configuring the
+haptic feedback loop with configuration of:
+
+ - Strength of the haptic feedback
+ - Frequency of the haptic feedback [currently not implemented]
+ - Duration of the haptic feedback [currently not implemented]
+
+.. ---------------------------- API Reference ----------------------------------
+
+API Reference
+-------------
+
+.. include-build-file:: inc/detent_config.inc
+.. include-build-file:: inc/haptic_config.inc
+.. include-build-file:: inc/bldc_haptics.inc
diff --git a/docs/_sources/haptics/index.rst.txt b/docs/_sources/haptics/index.rst.txt
index 06acc45d2..d3a00145d 100644
--- a/docs/_sources/haptics/index.rst.txt
+++ b/docs/_sources/haptics/index.rst.txt
@@ -4,6 +4,7 @@ Haptics APIs
.. toctree::
:maxdepth: 1
+ bldc_haptics
drv2605
There are multiple components which provide haptic feedback functionality,
diff --git a/docs/adc/adc_types.html b/docs/adc/adc_types.html
index 70fe96f89..cba7513ad 100644
--- a/docs/adc/adc_types.html
+++ b/docs/adc/adc_types.html
@@ -135,7 +135,7 @@
ADC APIs »
ADC Types
- Edit on GitHub
+ Edit on GitHub
diff --git a/docs/adc/ads1x15.html b/docs/adc/ads1x15.html
index dadfe6a29..519c4f389 100644
--- a/docs/adc/ads1x15.html
+++ b/docs/adc/ads1x15.html
@@ -136,7 +136,7 @@
ADC APIs »
ADS1x15 I2C ADC
- Edit on GitHub
+ Edit on GitHub
@@ -153,7 +153,7 @@ API Reference
+
Note
This is a port (with some modifications) of the excellent work by SimpleFOC - https://simplefoc.com
diff --git a/docs/bldc/index.html b/docs/bldc/index.html
index 27caf1977..c6adfe112 100644
--- a/docs/bldc/index.html
+++ b/docs/bldc/index.html
@@ -126,7 +126,7 @@
»
BLDC APIs
- Edit on GitHub
+ Edit on GitHub
diff --git a/docs/cli.html b/docs/cli.html
index 3b8d69ec3..051436ee2 100644
--- a/docs/cli.html
+++ b/docs/cli.html
@@ -132,7 +132,7 @@
»
Command Line Interface (CLI) APIs
- Edit on GitHub
+ Edit on GitHub
@@ -175,7 +175,7 @@
API Reference