From 76896312db590635c7847e95a3baa498baa29ada Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Sat, 20 Sep 2025 16:47:39 +0200 Subject: [PATCH] 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. --- README.md | 20 +-- examples/dshot300/dshot300.ino | 6 +- .../throttle_percent/throttle_percent.ino | 142 ++++++++++++++++++ examples/web_client/web_client.ino | 9 ++ examples/web_control/web_control.ino | 9 ++ src/DShotRMT.cpp | 46 +++++- src/DShotRMT.h | 15 +- src/web_content.h | 28 ++-- 8 files changed, 246 insertions(+), 29 deletions(-) create mode 100644 examples/throttle_percent/throttle_percent.ino diff --git a/README.md b/README.md index 7e96ee0..8e131dd 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index 3a804d3..0de9d39 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -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 ")) diff --git a/examples/throttle_percent/throttle_percent.ino b/examples/throttle_percent/throttle_percent.ino new file mode 100644 index 0000000..3d74d14 --- /dev/null +++ b/examples/throttle_percent/throttle_percent.ino @@ -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 +#include + +// 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(" - Set throttle (0 - 100)"); + USB_SERIAL.println(" 0 - Stop motor"); + USB_SERIAL.println("*******************************************"); + USB_SERIAL.println(" cmd - 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"); + } + } +} diff --git a/examples/web_client/web_client.ino b/examples/web_client/web_client.ino index 635b644..dcbad8b 100644 --- a/examples/web_client/web_client.ino +++ b/examples/web_client/web_client.ino @@ -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 #include #include diff --git a/examples/web_control/web_control.ino b/examples/web_control/web_control.ino index 5db9332..07a8141 100644 --- a/examples/web_control/web_control.ino +++ b/examples/web_control/web_control.ino @@ -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 #include diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 15df4e9..a5c2796 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -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(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(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(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 === "); diff --git a/src/DShotRMT.h b/src/DShotRMT.h index 457d648..e857943 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -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; diff --git a/src/web_content.h b/src/web_content.h index 2f7459d..104adcb 100644 --- a/src/web_content.h +++ b/src/web_content.h @@ -217,6 +217,7 @@ static constexpr char index_html[] = R"rawliteral(
0
+
0%
@@ -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),