BLDC Haptics Example (USB / WebUSB controlled)
This example shows the use of the BldcHaptics component to drive a BLDC motor
(such as a tiny gimbal motor) as a user input / output device that provides
haptic feedback (such as might be used as a rotary encoder input).
On top of the haptic engine, the example exposes a USB vendor-specific
(WebUSB) interface (via the espp usb_device component) so the knob can be
controlled and visualized live from a Chromium browser — no driver, no app
install:
Live telemetry + dial visualization — position / detent index, continuous knob value, shaft angle and velocity, streamed at a configurable rate and rendered on an animated dial (detents, end stops and the current detent marked).
Control — enable/disable the haptics, move to a detent, play a haptic “click” with adjustable strength.
Mode switching — select any of the built-in
espp::detaildetent presets (unbounded, bounded, multi-rev, on/off, coarse/fine, magnetic detents, return-to-center) from a dropdown.Firmware update (OTA) — upload a new
.binover the same USB interface (via the esppotacomponent), with progress, image validation (SHA-256) and bootloader rollback support.
The wire protocol is documented in PROTOCOL.md; the browser console lives in webapp/index.html.
How to use example
Hardware Required
This example targets ESP32-S3 hardware (the native USB-OTG peripheral is
required for the vendor / WebUSB interface). Select the hardware via
idf.py menuconfig → Example Configuration:
MotorGo Mini or MotorGo Axis — everything on-board (motor driver + SSI magnetic encoder); just connect a gimbal motor.
BLDC Motor Test Stand (TinyS3) (default) — discrete wiring:
Magnetic encoder chip (this example uses
Mt6701) over I2CBLDC Motor Driver chip (tested with the
TMC6300 BOBdev board)Some mounting hardware to mount the motor, magnet, encoder, etc.
Motor powered via a benchtop power supply at 5V
:warning:
NOTE: you MUST make sure that you run the example with the
zero_electrical_offsetvalue set to 0 (or not provided) at least once otherwise the sample will not work and could potentially damage your motor.
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-].)
The example uses an OTA-capable partition table (partitions.csv: otadata +
two 3 MB app slots on an 8 MB flash), so the very first flash must be a full
idf.py flash (not just app-flash) to lay down the partition table and
otadata.
On boards whose only USB connector is the ESP32-S3’s native USB (e.g. MotorGo Mini), TinyUSB takes over the connector once the app starts, so the USB-Serial-JTAG console (
idf.py monitor) goes away. Runtime logs are still available over the same cable: the example exposes a CDC-ACM serial port and routes the system console to it, so attach any serial terminal (e.g.screen /dev/tty.usbmodem*) for live logs. Flashing also still works over the same connector via the ROM bootloader (hold BOOT while resetting, or just usewebapp/index.htmlfor OTA updates after the first flash).
Web console
Flash and start the example, then connect the board’s native USB-OTG port to your computer (on an S3 devkit this is the “USB” connector, not “UART”).
Open
webapp/index.htmlin a Chromium browser (Chrome / Edge / Opera). It is a single self-contained file and works fromfile://,http://localhostor anyhttpsorigin. (Chrome may also offer the hosted console automatically via the WebUSB landing-page notification.)Click Connect and pick “espp BLDC Haptics” (VID
0x1209, PID0x0d34).The dial starts animating from the telemetry stream. Use the controls to switch detent presets, enable/disable the motor, move to a detent, or play a haptic click.
Firmware update (OTA) flow
Make a change and
idf.py build(do not flash).In the web console’s Firmware update panel, pick
build/bldc_haptics_example.bin(the app image — NOT the merged / bootloader image) and click Upload.The device streams the image into the inactive OTA slot (progress + rate are shown), validates it (structure + SHA-256), switches the boot partition and reboots. Expect a USB disconnect; reconnect after the device re-enumerates.
Rollback: the freshly-booted image starts in
PENDING_VERIFY; this example marks itself valid after its self-check (motor + haptics up). If the new image crashes before that, the bootloader automatically rolls back to the previous slot on the next reset.
The OTA subset is part of the haptics protocol on dispatcher module 2, so it
is not interchangeable with the generic espp ota example (which is module 0)
— use this example’s own web console for OTA here.
Example Behaviors
The detent presets can be switched at runtime from the web console (or by editing the default in the code). Some examples:
coarse values strong detents (best with sound)
https://github.com/esp-cpp/espp/assets/213467/a256b401-6e45-4284-89c7-2dec9a49daa7
magnetic detents (best with sound)
https://github.com/esp-cpp/espp/assets/213467/ab1ace5c-f967-4cfc-b304-7736fdb35bcb
On / Off Strong Detents (best with sound)
https://github.com/esp-cpp/espp/assets/213467/038d79b1-7cd9-4af9-b7e8-1b4daf6a363a
Multi-rev no detents
https://github.com/esp-cpp/espp/assets/213467/2af81edb-67b8-488b-ae7a-3549be36b8cc
For more information, see the documentation or the original PR: https://github.com/esp-cpp/espp/pull/60
Troubleshooting
Make sure to run the example once with zero_electrical_offset set to 0 so that
the motor will go through a calibration / zero offset routine. At the end of
this startup routine it will print the measured zero electrical offset that you
can then provide within the code, at which point it will not need to run the
calibration routine.
You must run this calibration any time you change your hardware configuration (such as by remounting your motor, magnet, encoder chip).
If the web console cannot see the device:
WebUSB needs a Chromium-based browser and a secure context (
https,http://localhostorfile://).Make sure you connected the native USB-OTG port (not a UART bridge port).
On Linux you may need a udev rule granting access to VID
0x1209.Tick “show all USB devices” in the console to bypass the VID/PID filter.
Example Breakdown
This example builds complex haptic behavior + connectivity using the following components:
espp::Mt6701(I2C on the test stand, SSI on the MotorGo boards)espp::BldcDriver/ the MotorGo board componentsespp::BldcMotorespp::BldcHapticsespp::UsbDevice— native USB vendor interface with WebUSB + MS OS 2.0 descriptors (driverless browser access)espp::Ota— transport-agnostic OTA engine fed from the USB protocolThe
stream_framecodec (components/stream_frame/include/stream_frame.hpp) as the framing layer for the haptics protocol (module 2) (see PROTOCOL.md)
You combine the Mt6701 and BldcDriver together when creating the BldcMotor
and then simply pass the BldcMotor to the BldcHaptics component. At that
point, you only have to interface to the BldcHaptics to read the input
position or reconfigure the haptics — which is exactly what the USB protocol
handlers do.