feat(library): Add percent throttle, improve usability & fix bugs

- Adds a new `sendThrottlePercent()` function for more intuitive throttle control.

- Allows initializing the motor magnet count directly in the constructor.

- Updates all examples to use the new features.

- Adds a new `throttle_percent` example sketch.

- Fixes a critical resource leak in `begin()` on partial init failure.

- Hardens web examples with security warnings and improves the UI.

- Updates README to reflect all changes.
This commit is contained in:
Wastl Kraus 2025-09-20 16:47:39 +02:00
parent 7ee5c83133
commit 76896312db
8 changed files with 246 additions and 29 deletions

View File

@ -11,7 +11,7 @@ This library is a rewrite using the modern ESP-IDF 5 RMT encoder API (`rmt_tx.h`
- **Multiple DShot Modes:** Supports DSHOT150, DSHOT300, DSHOT600, and DSHOT1200.
- **Bidirectional DShot:** Full support for RPM telemetry feedback.
- **Hardware-Timed Signals:** Precise signal generation using the ESP32 RMT peripheral, ensuring stable and reliable motor control.
- **Simple API:** Easy-to-use C++ class for sending throttle commands and receiving telemetry data.
- **Simple API:** Easy-to-use C++ class with intuitive methods like `sendThrottlePercent()`.
- **Efficient and Lightweight:** The core library has no external dependencies.
- **Arduino and ESP-IDF Compatible:** Can be used in both Arduino and ESP-IDF projects.
@ -52,16 +52,16 @@ void setup() {
// Initialize the DShot motor
motor.begin();
Serial.println("Motor initialized. Sending low throttle for 5 seconds...");
Serial.println("Motor initialized. Ramping up to 25% throttle...");
// Send a low throttle command for 5 seconds
for (int i = 0; i < 500; i++) {
motor.sendThrottle(100);
delay(10);
// Ramp up to 25% throttle over 2.5 seconds
for (int i = 0; i <= 25; i++) {
motor.sendThrottlePercent(i);
delay(100);
}
Serial.println("Stopping motor.");
motor.sendThrottle(0);
motor.sendThrottlePercent(0);
}
void loop() {
@ -73,7 +73,8 @@ void loop() {
The `examples` folder contains more advanced examples:
- **`dshot300`:** A simple example demonstrating how to send DShot commands and receive telemetry via the serial monitor.
- **`throttle_percent`:** A focused example showing how to control motor speed using percentage values (0-100) via the serial monitor.
- **`dshot300`:** A more advanced example demonstrating how to send raw DShot commands and receive telemetry via the serial monitor.
- **`web_control`:** A full-featured web application for controlling a motor from a web browser. It creates a WiFi access point and serves a web page with a throttle slider and arming switch.
- **`web_client`:** A variation of the `web_control` example that connects to an existing WiFi network instead of creating its own access point.
@ -101,7 +102,8 @@ The main class is `DShotRMT`. Here are the most important methods:
- `DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional = false)`: Constructor to create a new DShotRMT instance.
- `begin()`: Initializes the RMT peripheral and the DShot encoder.
- `sendThrottle(uint16_t throttle)`: Sends a throttle value (48-2047) to the motor.
- `sendThrottlePercent(float percent)`: Sends a throttle value as a percentage (0.0-100.0).
- `sendThrottle(uint16_t throttle)`: Sends a raw throttle value (48-2047) to the motor.
- `sendCommand(uint16_t command)`: Sends a DShot command (0-47) to the motor.
- `getTelemetry(uint16_t magnet_count)`: Receives and parses telemetry data from the motor (for bidirectional DShot).

View File

@ -27,7 +27,7 @@ static constexpr auto IS_BIDIRECTIONAL = true;
static constexpr auto MOTOR01_MAGNET_COUNT = 14;
// Creates the motor instance
DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL);
DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, MOTOR01_MAGNET_COUNT);
//
void setup()
@ -85,7 +85,7 @@ void loop()
// Get Motor RPM if bidirectional
if (IS_BIDIRECTIONAL)
{
dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT);
dshot_result_t telem_result = motor01.getTelemetry();
printDShotResult(telem_result);
}
@ -134,7 +134,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous
}
else if (input == "rpm" && IS_BIDIRECTIONAL)
{
dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT);
dshot_result_t result = motor01.getTelemetry();
printDShotResult(result);
}
else if (input.startsWith("cmd "))

View File

@ -0,0 +1,142 @@
/**
* @file throttle_percent.ino
* @brief Demo sketch for DShotRMT library using percentage throttle.
* @author Wastl Kraus
* @date 2025-09-20
* @license MIT
*/
#include <Arduino.h>
#include <DShotRMT.h>
// USB serial port settings
static constexpr auto &USB_SERIAL = Serial0;
static constexpr auto USB_SERIAL_BAUD = 115200;
// Motor configuration - Pin number or GPIO_PIN
static constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_27;
// Supported: DSHOT150, DSHOT300, DSHOT600, (DSHOT1200)
static constexpr dshot_mode_t DSHOT_MODE = DSHOT300;
// BiDirectional DShot Support (default: false)
static constexpr auto IS_BIDIRECTIONAL = true;
// Motor magnet count for RPM calculation
static constexpr auto MOTOR01_MAGNET_COUNT = 14;
// Creates the motor instance
DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, MOTOR01_MAGNET_COUNT);
// Forward declaration
void handleSerialInput(const String &input);
void printMenu();
//
void setup()
{
// Starts the USB Serial Port
USB_SERIAL.begin(USB_SERIAL_BAUD);
// Initialize DShot Signal
motor01.begin();
// Print CPU Info
motor01.printCpuInfo();
//
printMenu();
}
//
void loop()
{
// Handle serial input
if (USB_SERIAL.available() > 0)
{
String input = USB_SERIAL.readStringUntil('\n');
input.trim();
if (input.length() > 0)
{
handleSerialInput(input);
}
}
}
//
void printMenu()
{
USB_SERIAL.println(" ");
USB_SERIAL.println("*******************************************");
USB_SERIAL.println(" DShotRMT Percent Demo ");
USB_SERIAL.println("*******************************************");
USB_SERIAL.println(" <value> - Set throttle (0 - 100)");
USB_SERIAL.println(" 0 - Stop motor");
USB_SERIAL.println("*******************************************");
USB_SERIAL.println(" cmd <number> - Send DShot command (0 - 47)");
USB_SERIAL.println(" info - Show motor info");
if (IS_BIDIRECTIONAL)
{
USB_SERIAL.println(" rpm - Get telemetry data");
}
USB_SERIAL.println("*******************************************");
USB_SERIAL.println(" h / help - Show this Menu");
USB_SERIAL.println("*******************************************");
}
//
void handleSerialInput(const String &input)
{
if (input == "0")
{
// Stop motor
dshot_result_t result = motor01.sendThrottlePercent(0.0f);
printDShotResult(result);
}
else if (input == "info")
{
motor01.printDShotInfo();
}
else if (input == "rpm" && IS_BIDIRECTIONAL)
{
dshot_result_t result = motor01.getTelemetry();
printDShotResult(result);
}
else if (input.startsWith("cmd "))
{
// Send DShot command
int cmd_num = input.substring(4).toInt();
if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX)
{
dshot_result_t result = motor01.sendCommand(cmd_num);
printDShotResult(result);
}
else
{
USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX);
}
}
else if (input == "h" || input == "help")
{
printMenu();
}
else
{
// Parse input throttle value as a percentage
float throttle_percent = input.toFloat();
if (throttle_percent >= 0.0f && throttle_percent <= 100.0f)
{
dshot_result_t result = motor01.sendThrottlePercent(throttle_percent);
printDShotResult(result);
}
else
{
USB_SERIAL.println(" ");
USB_SERIAL.printf("Invalid input: '%s'\n", input.c_str());
USB_SERIAL.printf("Valid throttle range: 0.0 - 100.0\n");
}
}
}

View File

@ -6,6 +6,15 @@
* @license MIT
*/
/******************************************************************
* SECURITY WARNING
* This example provides a web interface to control a motor
* without any authentication. It is intended for use on a
* trusted local network only.
*
* DO NOT EXPOSE THIS DEVICE DIRECTLY TO THE INTERNET.
******************************************************************/
#include <Arduino.h>
#include <Update.h>
#include <WiFi.h>

View File

@ -6,6 +6,15 @@
* @license MIT
*/
/******************************************************************
* SECURITY WARNING
* This example provides a web interface to control a motor
* without any authentication. It is intended for use on a
* trusted local network only.
*
* DO NOT EXPOSE THIS DEVICE DIRECTLY TO THE INTERNET.
******************************************************************/
#include <Arduino.h>
#include <WiFi.h>

View File

@ -33,10 +33,11 @@ void printDShotResult(dshot_result_t &result, Stream &output)
// Constructors & Destructor
// Constructor with GPIO number
DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional)
DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count)
: _gpio(gpio),
_mode(mode),
_is_bidirectional(is_bidirectional),
_motor_magnet_count(magnet_count),
_dshot_timing(DSHOT_TIMING_US[mode]),
_frame_timer_us(0),
_rmt_ticks{0},
@ -68,8 +69,8 @@ DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional)
}
// Constructor using pin number
DShotRMT::DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional)
: DShotRMT(static_cast<gpio_num_t>(pin_nr), mode, is_bidirectional)
DShotRMT::DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count)
: DShotRMT(static_cast<gpio_num_t>(pin_nr), mode, is_bidirectional, magnet_count)
{
// Delegates to primary constructor with type cast
}
@ -118,12 +119,25 @@ dshot_result_t DShotRMT::begin()
{
if (!_initRXChannel().success)
{
// Cleanup previously allocated TX channel on failure
rmt_disable(_rmt_tx_channel);
rmt_del_channel(_rmt_tx_channel);
_rmt_tx_channel = nullptr;
return {false, RX_INIT_FAILED};
}
}
if (!_initDShotEncoder().success)
{
// Cleanup previously allocated channels on failure
rmt_disable(_rmt_tx_channel);
rmt_del_channel(_rmt_tx_channel);
_rmt_tx_channel = nullptr;
if (_rmt_rx_channel) {
rmt_disable(_rmt_rx_channel);
rmt_del_channel(_rmt_rx_channel);
_rmt_rx_channel = nullptr;
}
return {false, ENCODER_INIT_FAILED};
}
@ -146,6 +160,20 @@ dshot_result_t DShotRMT::sendThrottle(uint16_t throttle)
return _sendDShotFrame(_packet);
}
// Send throttle value as a percentage
dshot_result_t DShotRMT::sendThrottlePercent(float percent)
{
if (percent < 0.0f || percent > 100.0f)
{
return {false, PERCENT_NOT_IN_RANGE};
}
// Map percent to DShot throttle range
uint16_t throttle = static_cast<uint16_t>(DSHOT_THROTTLE_MIN + ((DSHOT_THROTTLE_MAX - DSHOT_THROTTLE_MIN) / 100.0f) * percent);
return sendThrottle(throttle);
}
// Send DShot command to ESC
dshot_result_t DShotRMT::sendCommand(uint16_t command)
{
@ -212,16 +240,19 @@ dshot_result_t DShotRMT::getTelemetry(uint16_t magnet_count)
return result;
}
// Use stored magnet count if parameter is 0 (default)
uint16_t final_magnet_count = (magnet_count == 0) ? _motor_magnet_count : magnet_count;
// Check if the callback has set the flag for new data
if (_telemetry_ready_flag_atomic)
{
_telemetry_ready_flag_atomic = false; // Reset the flag
uint16_t erpm = _last_erpm_atomic; // Read the atomic variable
if (erpm != DSHOT_NULL_PACKET && magnet_count >= MAGNETS_PER_POLE_PAIR)
if (erpm != DSHOT_NULL_PACKET && final_magnet_count >= MAGNETS_PER_POLE_PAIR)
{
// Calculate motor RPM from eRPM and magnet count
uint8_t pole_pairs = magnet_count / MAGNETS_PER_POLE_PAIR;
uint8_t pole_pairs = final_magnet_count / MAGNETS_PER_POLE_PAIR;
uint32_t motor_rpm = (erpm / pole_pairs);
result.success = true;
@ -255,6 +286,11 @@ dshot_result_t DShotRMT::saveESCSettings()
}
// Public Info & Debug Functions
void DShotRMT::setMotorMagnetCount(uint16_t magnet_count)
{
_motor_magnet_count = magnet_count;
}
void DShotRMT::printDShotInfo(Stream &output) const
{
output.println("\n === DShot Signal Info === ");

View File

@ -82,22 +82,29 @@ class DShotRMT
{
public:
// Constructors & Destructor
explicit DShotRMT(gpio_num_t gpio = GPIO_NUM_16, dshot_mode_t mode = DSHOT300, bool is_bidirectional = false);
DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional);
explicit DShotRMT(gpio_num_t gpio = GPIO_NUM_16, dshot_mode_t mode = DSHOT300, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT);
DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT);
~DShotRMT();
// Public Core Functions
dshot_result_t begin();
dshot_result_t sendThrottle(uint16_t throttle);
dshot_result_t sendThrottlePercent(float percent);
dshot_result_t sendCommand(uint16_t command);
dshot_result_t sendCommand(dshot_commands_t dshot_command, uint16_t repeat_count = DEFAULT_CMD_REPEAT_COUNT, uint16_t delay_us = DEFAULT_CMD_DELAY_US);
dshot_result_t getTelemetry(uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT);
/**
* @brief Gets telemetry data from the ESC.
* @param magnet_count Optional. Number of motor magnets. If 0 or omitted, uses the value set by setMotorMagnetCount().
* @return dshot_result_t Result containing success status, message, and telemetry data.
*/
dshot_result_t getTelemetry(uint16_t magnet_count = 0);
dshot_result_t getESCInfo();
dshot_result_t setMotorSpinDirection(bool reversed);
dshot_result_t saveESCSettings();
// Public Utility & Info Functions
void setMotorMagnetCount(uint16_t magnet_count);
void printDShotInfo(Stream &output = Serial) const;
void printCpuInfo(Stream &output = Serial) const;
@ -159,6 +166,7 @@ private:
static constexpr char const *TRANSMISSION_FAILED = "Transmission failed!";
static constexpr char const *RECEIVER_FAILED = "RMT receiver failed!";
static constexpr char const *THROTTLE_NOT_IN_RANGE = "Throttle not in range! (48 - 2047)";
static constexpr char const *PERCENT_NOT_IN_RANGE = "Percent not in range! (0.0 - 100.0)";
static constexpr char const *COMMAND_NOT_VALID = "Command not valid! (0 - 47)";
static constexpr char const *BIDIR_NOT_ENABLED = "Bidirectional DShot not enabled!";
static constexpr char const *TELEMETRY_SUCCESS = "Valid Telemetric Frame received!";
@ -177,6 +185,7 @@ private:
gpio_num_t _gpio;
dshot_mode_t _mode;
bool _is_bidirectional;
uint16_t _motor_magnet_count;
const dshot_timing_us_t &_dshot_timing;
uint64_t _frame_timer_us;

View File

@ -217,6 +217,7 @@ static constexpr char index_html[] = R"rawliteral(
<!-- Throttle Section -->
<div class="throttle-section" id="throttleSection">
<div id="throttleValue">0</div>
<div id="throttlePercent" style="font-size: 1.2rem; color: #bdc3c7; margin-top: -15px; margin-bottom: 15px;">0%</div>
<input type="range" min="48" max="2047" value="0" id="throttleSlider" disabled>
</div>
@ -253,6 +254,19 @@ static constexpr char index_html[] = R"rawliteral(
setTimeout(initWebSocket, 2000);
}
function updateThrottleDisplays(rawValue) {
const DSHOT_MIN = 48;
const DSHOT_MAX = 2047;
const clampedValue = Math.max(0, Math.min(DSHOT_MAX, rawValue));
document.getElementById('throttleValue').innerText = clampedValue;
let percent = 0;
if (clampedValue > 0) {
percent = (clampedValue - DSHOT_MIN) / (DSHOT_MAX - DSHOT_MIN) * 100;
}
document.getElementById('throttlePercent').innerText = Math.round(percent) + '%';
document.getElementById('throttleSlider').value = clampedValue;
}
// Getting data from sketch
function onMessage(event) {
try {
@ -265,8 +279,7 @@ static constexpr char index_html[] = R"rawliteral(
// Sync web and serial throttle inputs
if (data.throttle !== undefined) {
if (isArmed) {
document.getElementById('throttleSlider').value = data.throttle;
document.getElementById('throttleValue').innerText = data.throttle;
updateThrottleDisplays(data.throttle);
}
}
@ -304,8 +317,7 @@ static constexpr char index_html[] = R"rawliteral(
// If disarmed, set throttle to 0
if (!isArmed) {
slider.value = 0;
sliderValue.innerText = 0;
updateThrottleDisplays(0);
}
});
@ -325,8 +337,7 @@ static constexpr char index_html[] = R"rawliteral(
armingStatus.className = 'status-disarmed';
throttleSection.classList.remove('armed');
slider.disabled = true;
slider.value = 0;
sliderValue.innerText = 0;
updateThrottleDisplays(0);
}
}
@ -334,13 +345,12 @@ static constexpr char index_html[] = R"rawliteral(
slider.addEventListener('input', () => {
if (!isArmed) {
slider.disabled = true;
slider.value = 0;
sliderValue.innerText = 0;
updateThrottleDisplays(0);
return;
}
const throttle = slider.value;
sliderValue.innerText = throttle;
updateThrottleDisplays(throttle);
const message = JSON.stringify({
"throttle": parseInt(throttle),