diff --git a/README.md b/README.md index 952d0df..b324cea 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,11 @@ An Arduino IDE library for generating DShot signals on ESP32 microcontrollers using the **latest ESP-IDF 5.5 RMT Encoder API** (`rmt_tx.h` / `rmt_rx.h`). This library specifically leverages the official `rmt_bytes_encoder` API for an efficient, hardware-timed and maintainable implementation. It provides a simple way to control BLHeli ESCs in both Arduino and ESP-IDF projects. -### Enhanced Bidirectional DShot with Full Telemetry Support. +### ✨ Experimental Bidirectional DShot Support Activated! ✨ + +> [!CAUTION] +> **This feature is currently EXPERIMENTAL and under active development.** +> If you enable bidirectional DShot, you **MUST** connect an external pull-up resistor (e.g., 2k Ohm to 3.3V) to the DShot GPIO pin. This resistor is absolutely crucial for the ESC to properly send telemetry data back to the ESP32. Without it, bidirectional telemetry will **NOT** function correctly. Use at your own risk. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. @@ -34,7 +38,7 @@ The library is architected around a single C++ class, `DShotRMT`. It abstracts t 1. **Signal Generation (TX):** The library uses an RMT 'bytes_encoder'. This encoder is configured with the specific pulse durations for DShot '0' and '1' bits based on the selected speed (e.g., DSHOT300, DSHOT600). When a user calls `sendThrottle()`, the library constructs a 16-bit DShot frame (11-bit throttle, 1-bit telemetry request, 4-bit CRC) and hands it to the RMT encoder. The RMT hardware then autonomously generates the correct electrical signal on the specified GPIO pin. -2. **Bidirectional Telemetry (RX) - Now with Full GCR Telemetry:** For bidirectional communication, the library configures a second RMT channel in receive mode on the same GPIO. An interrupt service routine (`_on_rx_done`) is registered. When the ESC sends back a telemetry signal, the RMT peripheral captures it. The interrupt code intelligently differentiates between eRPM-only frames (21 GCR bits) and full telemetry frames (110 GCR bits). It then decodes the GCR-encoded signal (including 5B/4B GCR decoding for full telemetry), validates its CRC, and stores the resulting eRPM value or full telemetry data (temperature, voltage, current, consumption, RPM) in thread-safe `atomic` variables. The main application can then poll for this data using the `getTelemetry()` method, which now returns a comprehensive `dshot_result_t` with all available telemetry fields. +2. **Bidirectional Telemetry (RX) - Now with Full GCR Telemetry:** **Note: For bidirectional DShot, an external pull-up resistor (e.g., 2k Ohm to 3.3V) is required on the DShot GPIO pin for proper telemetry reception.** For bidirectional communication, the library configures a second RMT channel in receive mode on the same GPIO. An interrupt service routine (`_on_rx_done`) is registered. When the ESC sends back a telemetry signal, the RMT peripheral captures it. The interrupt code intelligently differentiates between eRPM-only frames (21 GCR bits) and full telemetry frames (110 GCR bits). It then decodes the GCR-encoded signal (including 5B/4B GCR decoding for full telemetry), validates its CRC, and stores the resulting eRPM value or full telemetry data (temperature, voltage, current, consumption, RPM) in thread-safe `atomic` variables. The main application can then poll for this data using the `getTelemetry()` method, which now returns a comprehensive `dshot_result_t` with all available telemetry fields. ## ⏱️ DShot Timing Information diff --git a/examples/web_client/web_client.ino b/examples/web_client/web_client.ino index 7bae523..8b414f6 100644 --- a/examples/web_client/web_client.ino +++ b/examples/web_client/web_client.ino @@ -19,8 +19,9 @@ #include #include #include -#include "web_utilities/ota_update.h" -#include "web_utilities/web_content.h" +#include "../web_utilities/ota_update.h" +#include "../web_utilities/web_content.h" +#include "../web_utilities/web_constants.h" #include #include @@ -30,31 +31,12 @@ static constexpr auto *WIFI_SSID = "YOUR_SSID"; // Enter your WiFi SSID static constexpr auto *WIFI_PASSWORD = "YOUR_PASSWORD"; // Enter your WiFi password -// Connection timeout in milliseconds -static constexpr auto WIFI_CONNECT_TIMEOUT = 20000; - -// USB serial port settings -static constexpr auto &USB_SERIAL = Serial; -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 = false; // Note: Bidirectional DShot is currently not officially supported due to instability and external hardware requirements. - -// 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); // Web Server Configuration -AsyncWebServer server(80); -AsyncWebSocket ws("/ws"); +AsyncWebServer server(WEBSERVER_PORT); +AsyncWebSocket ws(WEBSOCKET_PATH); // Global variables static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; @@ -130,7 +112,7 @@ void loop() static uint64_t last_wifi_check = 0; // Check WiFi connection every 30 seconds - if (esp_timer_get_time() - last_wifi_check >= 30000000) + if (esp_timer_get_time() - last_wifi_check >= WIFI_RECONNECT_CHECK_INTERVAL_US) { if (WiFi.status() != WL_CONNECTED && wifi_connected) { @@ -169,7 +151,7 @@ void loop() } // Print motor stats every 3 seconds in continuous mode - if ((esp_timer_get_time() - last_serial_update >= 3000000)) + if ((esp_timer_get_time() - last_serial_update >= MOTOR_STATS_UPDATE_INTERVAL_US)) { printDShotInfo(motor01, USB_SERIAL); @@ -204,9 +186,9 @@ void loop() { // Generate JSON for Webserver JsonDocument doc; - doc["throttle"] = isArmed ? throttle : 0; - doc["armed"] = isArmed; - doc["rpm"] = current_rpm; + doc[JsonKey::THROTTLE] = isArmed ? throttle : 0; + doc[JsonKey::ARMED] = isArmed; + doc[JsonKey::RPM] = current_rpm; String json_output; json_output.reserve(256); @@ -234,19 +216,16 @@ void setupOTA() // Serve OTA update page server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send_P(200, "text/html", ota_html); }); + { request->send_P(200, "text/html", ota_html); }); // Handle OTA update upload - server.on("/update", HTTP_POST, [](AsyncWebServerRequest *request) - { + server.on( + "/update", HTTP_POST, [](AsyncWebServerRequest *request) + { bool shouldReboot = !Update.hasError(); - - AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", - shouldReboot ? "OK" : "FAIL"); - + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", shouldReboot ? "OK" : "FAIL"); response->addHeader("Connection", "close"); request->send(response); - if (shouldReboot) { USB_SERIAL.println("OTA Update successful! Rebooting..."); delay(1000); @@ -254,7 +233,8 @@ void setupOTA() } else { USB_SERIAL.println("OTA Update failed!"); } - }, handleOTAUpload); + }, + handleOTAUpload); USB_SERIAL.println("OTA Update ready at: /update"); } @@ -266,8 +246,9 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind if (!index) { - // Safety: Ensure motor is stopped during update - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); setArmingStatus(false); + // Safety: Ensure motor is stopped during update + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + setArmingStatus(false); USB_SERIAL.printf("OTA Update Start: %s\n", filename.c_str()); @@ -287,7 +268,7 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind } // Print progress every 2 seconds to avoid spam - if (millis() - ota_progress_millis > 2000) + if (millis() - ota_progress_millis > OTA_PROGRESS_UPDATE_INTERVAL_MS) { size_t progress = index + len; USB_SERIAL.printf("OTA Progress: %zu bytes\n", progress); @@ -318,7 +299,7 @@ bool connectToWiFi() uint32_t start_time = millis(); - while (WiFi.status() != WL_CONNECTED && (millis() - start_time) < WIFI_CONNECT_TIMEOUT) + while (WiFi.status() != WL_CONNECTED && (millis() - start_time) < WIFI_CONNECT_TIMEOUT_MS) { delay(500); USB_SERIAL.print("."); @@ -408,22 +389,22 @@ void printMenu() } USB_SERIAL.println("***********************************************"); - USB_SERIAL.println(" arm - Arm motor"); - USB_SERIAL.println(" disarm - Disarm motor (safety)"); - USB_SERIAL.println(" - Set throttle (48 – 2047)"); - USB_SERIAL.println(" 0 - Stop motor"); + USB_SERIAL.printf(" %s\t\t- Arm motor\n", SerialCmd::ARM); + USB_SERIAL.printf(" %s\t\t- Disarm motor (safety)\n", SerialCmd::DISARM); + USB_SERIAL.println(" \t\t- Set throttle (48 – 2047)"); + USB_SERIAL.printf(" %s\t\t\t- Stop motor\n", SerialCmd::STOP); USB_SERIAL.println("***********************************************"); - USB_SERIAL.println(" cmd - Send DShot command (0 - 47)"); - USB_SERIAL.println(" info - Show motor info"); - USB_SERIAL.println(" wifi - Show WiFi status"); - USB_SERIAL.println(" reconnect - Reconnect to WiFi"); - USB_SERIAL.println(" ota - Show OTA info"); + USB_SERIAL.printf(" %s \t- Send DShot command (0 - 47)\n", SerialCmd::CMD_PREFIX); + USB_SERIAL.printf(" %s\t\t- Show motor info\n", SerialCmd::INFO); + USB_SERIAL.printf(" %s\t\t- Show WiFi status\n", SerialCmd::WIFI_STATUS); + USB_SERIAL.printf(" %s\t- Reconnect to WiFi\n", SerialCmd::RECONNECT); + USB_SERIAL.printf(" %s\t\t- Show OTA info\n", SerialCmd::OTA_INFO); if (IS_BIDIRECTIONAL) { - USB_SERIAL.println(" rpm - Get telemetry data"); + USB_SERIAL.printf(" %s\t\t- Get telemetry data\n", SerialCmd::RPM); } USB_SERIAL.println("***********************************************"); - USB_SERIAL.println(" h / help - Show this Menu"); + USB_SERIAL.printf(" %s / %s\t- Show this Menu\n", SerialCmd::HELP, SerialCmd::HELP_ALT); USB_SERIAL.println("***********************************************"); USB_SERIAL.printf(" Current Status: %s\n", isArmed ? "ARMED" : "DISARMED"); USB_SERIAL.printf(" WiFi Status: %s\n", wifi_connected ? "CONNECTED" : "DISCONNECTED"); @@ -433,19 +414,19 @@ void printMenu() // Handle serial inputs and updates global variables void handleSerialInput(const String &input) { - if (input == "arm") + if (input == SerialCmd::ARM) { setArmingStatus(true); return; } - if (input == "0" || input == "disarm") + if (input == SerialCmd::STOP || input == SerialCmd::DISARM) { setArmingStatus(false); return; } - if (input == "info") + if (input == SerialCmd::INFO) { printDShotInfo(motor01, USB_SERIAL); USB_SERIAL.println(" "); @@ -453,13 +434,13 @@ void handleSerialInput(const String &input) return; } - if (input == "wifi") + if (input == SerialCmd::WIFI_STATUS) { printWiFiStatus(); return; } - if (input == "ota") + if (input == SerialCmd::OTA_INFO) { if (wifi_connected) { @@ -477,7 +458,7 @@ void handleSerialInput(const String &input) return; } - if (input == "reconnect") + if (input == SerialCmd::RECONNECT) { USB_SERIAL.println("Reconnecting to WiFi..."); WiFi.disconnect(); @@ -490,7 +471,7 @@ void handleSerialInput(const String &input) return; } - if (input == "rpm" && IS_BIDIRECTIONAL) + if (input == SerialCmd::RPM && IS_BIDIRECTIONAL) { if (isArmed) { @@ -505,7 +486,7 @@ void handleSerialInput(const String &input) return; } - if (input.startsWith("cmd ")) + if (input.startsWith(SerialCmd::CMD_PREFIX)) { if (!isArmed) { @@ -515,7 +496,7 @@ void handleSerialInput(const String &input) } continuous_throttle = false; - int cmd_num = input.substring(4).toInt(); + int cmd_num = input.substring(strlen(SerialCmd::CMD_PREFIX)).toInt(); if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { @@ -530,13 +511,13 @@ void handleSerialInput(const String &input) return; } - if (input == "h" || input == "help") + if (input == SerialCmd::HELP || input == SerialCmd::HELP_ALT) { printMenu(); return; } - if (input == "status") + if (input == SerialCmd::STATUS) { USB_SERIAL.println(" "); USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); @@ -608,15 +589,15 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) bool armedFromWeb = false; // Handle arming status - if (doc.containsKey("armed")) + if (doc.containsKey(JsonKey::ARMED)) { - bool armed = doc["armed"]; + bool armed = doc[JsonKey::ARMED]; setArmingStatus(armed); armedFromWeb = true; } // Handle throttle value (only if armed) - if (doc.containsKey("throttle")) + if (doc.containsKey(JsonKey::THROTTLE)) { if (!isArmed) { @@ -628,7 +609,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) return; } - uint16_t web_throttle = doc["throttle"]; + uint16_t web_throttle = doc[JsonKey::THROTTLE]; if (web_throttle == 0) { @@ -664,8 +645,8 @@ void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventTyp // Send current arming status to new client { JsonDocument doc; - doc["armed"] = isArmed; - doc["throttle"] = isArmed ? throttle : 0; + doc[JsonKey::ARMED] = isArmed; + doc[JsonKey::THROTTLE] = isArmed ? throttle : 0; String json_output; serializeJson(doc, json_output); client->text(json_output); diff --git a/examples/web_control/web_control.ino b/examples/web_control/web_control.ino index 0a88df2..b13ca28 100644 --- a/examples/web_control/web_control.ino +++ b/examples/web_control/web_control.ino @@ -19,42 +19,27 @@ #include #include #include -#include "web_utilities/web_content.h" +#include "../web_utilities/web_content.h" +#include "../web_utilities/web_constants.h" #include #include #include -// Wifi Configuration -static constexpr auto *ssid = "DShotRMT Control"; -static constexpr auto *password = "12345678"; +// Wifi AP Configuration +static constexpr auto *WIFI_SSID_AP = "DShotRMT Control"; +static constexpr auto *WIFI_PASSWORD_AP = "12345678"; IPAddress local_IP(10, 10, 10, 1); IPAddress gateway(0, 0, 0, 0); IPAddress subnet(255, 255, 255, 0); -// USB serial port settings -static constexpr auto &USB_SERIAL = Serial; -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 = false; // Note: Bidirectional DShot is currently not officially supported due to instability and external hardware requirements. - -// 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); // Web Server Configuration -AsyncWebServer server(80); -AsyncWebSocket ws("/ws"); +AsyncWebServer server(WEBSERVER_PORT); +AsyncWebSocket ws(WEBSOCKET_PATH); // Global variables static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; @@ -81,7 +66,7 @@ void setup() // Start Wifi Access Point USB_SERIAL.println("\nStarting Access Point..."); - WiFi.softAP(ssid, password); + WiFi.softAP(WIFI_SSID_AP, WIFI_PASSWORD_AP); IPAddress IP = WiFi.softAPIP(); @@ -136,7 +121,7 @@ void loop() } // Print motor stats every 3 seconds in continuous mode - if ((esp_timer_get_time() - last_serial_update >= 3000000)) + if ((esp_timer_get_time() - last_serial_update >= MOTOR_STATS_UPDATE_INTERVAL_US)) { printDShotInfo(motor01, USB_SERIAL); @@ -171,9 +156,9 @@ void loop() { // Generate JSON for Webserver JsonDocument doc; - doc["throttle"] = isArmed ? throttle : 0; - doc["armed"] = isArmed; - doc["rpm"] = current_rpm; + doc[JsonKey::THROTTLE] = isArmed ? throttle : 0; + doc[JsonKey::ARMED] = isArmed; + doc[JsonKey::RPM] = current_rpm; String json_output; json_output.reserve(256); @@ -221,19 +206,19 @@ void printMenu() USB_SERIAL.println("***********************************************"); USB_SERIAL.println(" Web Config: http://10.10.10.1 "); USB_SERIAL.println("***********************************************"); - USB_SERIAL.println(" arm - Arm motor"); - USB_SERIAL.println(" disarm - Disarm motor (safety)"); - USB_SERIAL.println(" - Set throttle (48 – 2047)"); - USB_SERIAL.println(" 0 - Stop motor"); + USB_SERIAL.printf(" %s\t\t- Arm motor\n", SerialCmd::ARM); + USB_SERIAL.printf(" %s\t\t- Disarm motor (safety)\n", SerialCmd::DISARM); + USB_SERIAL.println(" \t\t- Set throttle (48 – 2047)"); + USB_SERIAL.printf(" %s\t\t\t- Stop motor\n", SerialCmd::STOP); USB_SERIAL.println("***********************************************"); - USB_SERIAL.println(" cmd - Send DShot command (0 - 47)"); - USB_SERIAL.println(" info - Show motor info"); + USB_SERIAL.printf(" %s \t- Send DShot command (0 - 47)\n", SerialCmd::CMD_PREFIX); + USB_SERIAL.printf(" %s\t\t- Show motor info\n", SerialCmd::INFO); if (IS_BIDIRECTIONAL) { - USB_SERIAL.println(" rpm - Get telemetry data"); + USB_SERIAL.printf(" %s\t\t- Get telemetry data\n", SerialCmd::RPM); } USB_SERIAL.println("***********************************************"); - USB_SERIAL.println(" h / help - Show this Menu"); + USB_SERIAL.printf(" %s / %s\t- Show this Menu\n", SerialCmd::HELP, SerialCmd::HELP_ALT); USB_SERIAL.println("***********************************************"); USB_SERIAL.printf(" Current Status: %s\n", isArmed ? "ARMED" : "DISARMED"); USB_SERIAL.println("***********************************************"); @@ -242,24 +227,24 @@ void printMenu() // Handle serial inputs and updates global variables void handleSerialInput(const String &input) { - if (input == "arm") + if (input == SerialCmd::ARM) { setArmingStatus(true); return; } - if (input == "0" || input == "disarm") + if (input == SerialCmd::STOP || input == SerialCmd::DISARM) { setArmingStatus(false); return; } - if (input == "info") + if (input == SerialCmd::INFO) { printDShotInfo(motor01, USB_SERIAL); USB_SERIAL.println(" "); USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); return; } - if (input == "rpm" && IS_BIDIRECTIONAL) + if (input == SerialCmd::RPM && IS_BIDIRECTIONAL) { if (isArmed) { @@ -273,7 +258,7 @@ void handleSerialInput(const String &input) } return; } - if (input.startsWith("cmd ")) + if (input.startsWith(SerialCmd::CMD_PREFIX)) { if (!isArmed) { @@ -283,7 +268,7 @@ void handleSerialInput(const String &input) } continuous_throttle = false; - int cmd_num = input.substring(4).toInt(); + int cmd_num = input.substring(strlen(SerialCmd::CMD_PREFIX)).toInt(); if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { @@ -297,12 +282,12 @@ void handleSerialInput(const String &input) } return; } - if (input == "h" || input == "help") + if (input == SerialCmd::HELP || input == SerialCmd::HELP_ALT) { printMenu(); return; } - if (input == "status") + if (input == SerialCmd::STATUS) { USB_SERIAL.println(" "); USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); @@ -368,15 +353,15 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) bool armedFromWeb = false; // Handle arming status - if (doc.containsKey("armed")) + if (doc.containsKey(JsonKey::ARMED)) { - bool armed = doc["armed"]; + bool armed = doc[JsonKey::ARMED]; setArmingStatus(armed); armedFromWeb = true; } // Handle throttle value (only if armed) - if (doc.containsKey("throttle")) + if (doc.containsKey(JsonKey::THROTTLE)) { if (!isArmed) { @@ -388,7 +373,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) return; } - uint16_t web_throttle = doc["throttle"]; + uint16_t web_throttle = doc[JsonKey::THROTTLE]; if (web_throttle == 0) { @@ -424,8 +409,8 @@ void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventTyp // Send current arming status to new client { JsonDocument doc; - doc["armed"] = isArmed; - doc["throttle"] = isArmed ? throttle : 0; + doc[JsonKey::ARMED] = isArmed; + doc[JsonKey::THROTTLE] = isArmed ? throttle : 0; String json_output; serializeJson(doc, json_output); client->text(json_output); diff --git a/examples/web_utilities/web_constants.h b/examples/web_utilities/web_constants.h new file mode 100644 index 0000000..3876b6b --- /dev/null +++ b/examples/web_utilities/web_constants.h @@ -0,0 +1,62 @@ +/** + * @file web_constants.h + * @brief Shared constants for DShotRMT web examples. + * @author Wastl Kraus + * @date 2025-11-29 + * @license MIT + */ + +#pragma once + +// ============================================================================ +// Serial Configuration & Commands +// ============================================================================ +static constexpr auto& USB_SERIAL = Serial; +static constexpr auto USB_SERIAL_BAUD = 115200; + +// Serial command strings +namespace SerialCmd { + constexpr char ARM[] = "arm"; + constexpr char DISARM[] = "disarm"; + constexpr char STOP[] = "0"; + constexpr char INFO[] = "info"; + constexpr char RPM[] = "rpm"; + constexpr char CMD_PREFIX[] = "cmd "; + constexpr char HELP[] = "h"; + constexpr char HELP_ALT[] = "help"; + constexpr char STATUS[] = "status"; + constexpr char WIFI_STATUS[] = "wifi"; + constexpr char RECONNECT[] = "reconnect"; + constexpr char OTA_INFO[] = "ota"; +} + +// ============================================================================ +// Motor & DShot Configuration +// ============================================================================ +static constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_27; +static constexpr dshot_mode_t DSHOT_MODE = DSHOT300; +static constexpr bool IS_BIDIRECTIONAL = false; +static constexpr uint16_t MOTOR01_MAGNET_COUNT = 14; + +// ============================================================================ +// Web Server & WebSocket Configuration +// ============================================================================ +static constexpr uint16_t WEBSERVER_PORT = 80; +static constexpr char WEBSOCKET_PATH[] = "/ws"; + +// Keys for JSON communication +namespace JsonKey { + constexpr char THROTTLE[] = "throttle"; + constexpr char ARMED[] = "armed"; + constexpr char RPM[] = "rpm"; +} + +// ============================================================================ +// Timing Intervals +// ============================================================================ +static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 20000; // 20 seconds +static constexpr uint64_t MOTOR_STATS_UPDATE_INTERVAL_US = 3000000; // 3 seconds +static constexpr uint64_t WIFI_RECONNECT_CHECK_INTERVAL_US = 30000000; // 30 seconds +static constexpr uint32_t WEBSOCKET_RECONNECT_DELAY_MS = 2000; // 2 seconds +static constexpr uint32_t SERIAL_RAMP_DELAY_MS = 200; // 200 ms +static constexpr uint32_t OTA_PROGRESS_UPDATE_INTERVAL_MS = 2000; // 2 seconds diff --git a/library.properties b/library.properties index 6db6e5e..1c1edf0 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=DShotRMT -version=0.9.2 +version=0.9.5 author=Wastl Kraus maintainer=Wastl Kraus license=MIT diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 2fcbde5..aca0667 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -9,27 +9,6 @@ #include "DShotRMT.h" #include -// Configuration Constants -static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000; -static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111; -static constexpr auto DSHOT_RX_TIMEOUT_MS = 2; -static constexpr auto DSHOT_PADDING_US = 20; // Pause between frames - -static constexpr auto POLE_PAIRS_MIN = 1; -static constexpr auto MAGNETS_PER_POLE_PAIR = 2; -static constexpr auto NO_DSHOT_TELEMETRY = 0; -static constexpr auto DSHOT_PULSE_MIN_NS = 800; // 0.8us minimum pulse -static constexpr auto DSHOT_PULSE_MAX_NS = 8000; // 8.0us maximum pulse -static constexpr auto DSHOT_TELEMETRY_INVALID = DSHOT_THROTTLE_MAX; -static constexpr auto DSHOT_TELEMETRY_BIT_POSITION = 11; -static constexpr auto DSHOT_CRC_BIT_SHIFT = 4; - -// Command Constants -static constexpr auto DEFAULT_CMD_DELAY_US = 10; -static constexpr auto DEFAULT_CMD_REPEAT_COUNT = 1; -static constexpr auto SETTINGS_COMMAND_REPEATS = 10; -static constexpr auto SETTINGS_COMMAND_DELAY_US = 5; - // Constructor with GPIO number DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count) : _gpio(gpio), @@ -38,8 +17,9 @@ DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, ui _motor_magnet_count(magnet_count), _dshot_timing(DSHOT_TIMING_US[_mode]) { - // Pre-calculate timing and bit positions for performance + // Pre-calculate timing and ratios for performance _preCalculateRMTTicks(); + _percent_to_throttle_ratio = (static_cast(DSHOT_THROTTLE_MAX - DSHOT_THROTTLE_MIN)) / DSHOT_PERCENT_MAX; } // Constructor using pin number @@ -90,15 +70,14 @@ dshot_result_t DShotRMT::begin() // Send throttle value dshot_result_t DShotRMT::sendThrottle(uint16_t throttle) { - // A throttle value of 0 is a disarm command + // Per DShot specification, a throttle value of 0 is a disarm command. if (throttle == 0) { - // just to be sure _last_throttle = 0; return sendCommand(DSHOT_CMD_MOTOR_STOP); } - // Constrain throttle to the valid DShot range + // Constrain throttle to the valid DShot range. _last_throttle = constrain(throttle, DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX); _packet = _buildDShotPacket(_last_throttle); @@ -113,15 +92,15 @@ dshot_result_t DShotRMT::sendThrottlePercent(float percent) return {false, DSHOT_PERCENT_NOT_IN_RANGE}; } - // Map percent to DShot throttle range - uint16_t throttle = static_cast(DSHOT_THROTTLE_MIN + ((DSHOT_THROTTLE_MAX - DSHOT_THROTTLE_MIN) / DSHOT_PERCENT_MAX) * percent); + // Map percent to DShot throttle range using pre-calculated ratio. + uint16_t throttle = static_cast(DSHOT_THROTTLE_MIN + _percent_to_throttle_ratio * percent); return sendThrottle(throttle); } // Sends a DShot command (0-47) to the ESC by accepting an integer value. dshot_result_t DShotRMT::sendCommand(uint16_t command_value) { - // Validate the integer command value before casting + // Validate the integer command value before casting. if (command_value < DSHOT_CMD_MOTOR_STOP || command_value > DSHOT_CMD_MAX_VALUE) { return {false, DSHOT_COMMAND_NOT_VALID}; @@ -135,6 +114,7 @@ dshot_result_t DShotRMT::sendCommand(dshotCommands_e command) uint16_t repeat_count = DEFAULT_CMD_REPEAT_COUNT; uint16_t delay_us = DEFAULT_CMD_DELAY_US; + // Certain commands require more repetitions to be reliably accepted by the ESC. switch (command) { case DSHOT_CMD_MOTOR_STOP: @@ -145,7 +125,7 @@ dshot_result_t DShotRMT::sendCommand(dshotCommands_e command) delay_us = SETTINGS_COMMAND_DELAY_US; break; default: - // For other commands, use default repeat and delay + // For other commands, use default repeat and delay. break; } @@ -155,49 +135,17 @@ dshot_result_t DShotRMT::sendCommand(dshotCommands_e command) // Sends a DShot command (0-47) to the ESC with a specified repeat count and delay. dshot_result_t DShotRMT::sendCommand(dshotCommands_e command, uint16_t repeat_count, uint16_t delay_us) { - dshot_result_t result = {false, DSHOT_UNKNOWN, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; - if (!_isValidCommand(command)) { - result.result_code = DSHOT_INVALID_COMMAND; - return result; + return {false, DSHOT_INVALID_COMMAND}; } - - bool all_successful = true; - - // Send command multiple times with delay - for (uint16_t i = 0; i < repeat_count; i++) - { - dshot_result_t single_result = _executeCommand(command); - - if (!single_result.success) - { - all_successful = false; - result.result_code = single_result.result_code; - break; - } - - // Add delay between repetitions (except for last repetition) - if (i < repeat_count - 1) - { - delayMicroseconds(delay_us); - } - } - - result.success = all_successful; - - if (result.success) - { - result.result_code = DSHOT_COMMAND_SUCCESS; - } - - return result; + return _sendRepeatedCommand(static_cast(command), repeat_count, delay_us); } // Get telemetry data dshot_result_t DShotRMT::getTelemetry() { - dshot_result_t result = {false, DSHOT_TELEMETRY_FAILED, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; + dshot_result_t result = {false, DSHOT_TELEMETRY_FAILED}; if (!_is_bidirectional) { @@ -205,24 +153,38 @@ dshot_result_t DShotRMT::getTelemetry() return result; } - // Use stored magnet count if parameter is 0 (default) - uint16_t final_magnet_count = _motor_magnet_count; + // Prioritize checking for full telemetry data, as it is richer. + if (_full_telemetry_ready_flag_atomic) + { + _full_telemetry_ready_flag_atomic = false; // Reset the flag + result.telemetry_data = _last_telemetry_data_atomic; // Read the atomic variable + result.telemetry_available = true; - // Check if the callback has set the flag for new data + // Also populate eRPM fields from the full telemetry data for consistency. + result.erpm = result.telemetry_data.rpm; + if (_motor_magnet_count >= MAGNETS_PER_POLE_PAIR) { + uint8_t pole_pairs = _motor_magnet_count / MAGNETS_PER_POLE_PAIR; + result.motor_rpm = result.telemetry_data.rpm / pole_pairs; + } + + result.success = true; + result.result_code = DSHOT_TELEMETRY_DATA_AVAILABLE; + return result; + } + + // If no full telemetry, check for eRPM-only 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 && final_magnet_count >= MAGNETS_PER_POLE_PAIR) + if (erpm != DSHOT_NULL_PACKET && _motor_magnet_count >= MAGNETS_PER_POLE_PAIR) { // Calculate motor RPM from eRPM and magnet count - uint8_t pole_pairs = final_magnet_count / MAGNETS_PER_POLE_PAIR; - uint32_t motor_rpm = (erpm / pole_pairs); - - result.success = true; + uint8_t pole_pairs = _motor_magnet_count / MAGNETS_PER_POLE_PAIR; result.erpm = erpm; - result.motor_rpm = motor_rpm; + result.motor_rpm = (erpm / pole_pairs); + result.success = true; result.result_code = DSHOT_TELEMETRY_SUCCESS; } } @@ -233,60 +195,60 @@ dshot_result_t DShotRMT::getTelemetry() // Reverse motor direction directly dshot_result_t DShotRMT::setMotorSpinDirection(bool reversed) { - // Use command as a yes / no switch dshotCommands_e command = reversed ? dshotCommands_e::DSHOT_CMD_SPIN_DIRECTION_REVERSED : dshotCommands_e::DSHOT_CMD_SPIN_DIRECTION_NORMAL; - return sendCommand(command, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); } dshot_result_t DShotRMT::sendCustomCommand(uint16_t command_value, uint16_t repeat_count, uint16_t delay_us) { - // Validate the integer command value before casting - if (command_value < DSHOT_CMD_MOTOR_STOP || command_value > DSHOT_CMD_MAX_VALUE) + // Validate the integer command value. + if (command_value < DSHOT_CMD_MIN || command_value > DSHOT_CMD_MAX) { return {false, DSHOT_COMMAND_NOT_VALID}; } + return _sendRepeatedCommand(command_value, repeat_count, delay_us); +} - dshot_result_t result = {false, DSHOT_UNKNOWN, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; +// Writes settings to the ESC's non-volatile memory; use with caution. +dshot_result_t DShotRMT::saveESCSettings() +{ + return sendCommand(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); +} +// Private helper to send a command value multiple times. +dshot_result_t DShotRMT::_sendRepeatedCommand(uint16_t value, uint16_t repeat_count, uint16_t delay_us) +{ bool all_successful = true; + dshot_result_t last_result = {true, DSHOT_COMMAND_SUCCESS}; - // Send command multiple times with delay for (uint16_t i = 0; i < repeat_count; i++) { - dshot_result_t single_result = _sendRawDshotFrame(command_value); + last_result = _sendRawDshotFrame(value); - if (!single_result.success) + if (!last_result.success) { all_successful = false; - result.result_code = single_result.result_code; break; } - // Add delay between repetitions (except for last repetition) if (i < repeat_count - 1) { delayMicroseconds(delay_us); } } - result.success = all_successful; - - if (result.success) + if (all_successful) { - result.result_code = DSHOT_COMMAND_SUCCESS; + return {true, DSHOT_COMMAND_SUCCESS}; + } + else + { + // Return the result from the failed transmission. + return last_result; } - - return result; } -// Use with caution -dshot_result_t DShotRMT::saveESCSettings() -{ - return sendCommand(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); -} - -// Simple check +// Simple check for valid command range. bool DShotRMT::_isValidCommand(dshotCommands_e command) const { return (command >= dshotCommands_e::DSHOT_CMD_MOTOR_STOP && command <= DSHOT_CMD_MAX); @@ -298,20 +260,6 @@ dshot_result_t DShotRMT::_sendRawDshotFrame(uint16_t value) return _sendPacket(_packet); } -// Executes a single DShot command by building and sending a DShot frame. -dshot_result_t DShotRMT::_executeCommand(dshotCommands_e command) -{ - uint64_t start_time = esp_timer_get_time(); - - _packet = _buildDShotPacket(static_cast(command)); - dshot_result_t result = _sendPacket(_packet); - - uint64_t end_time = esp_timer_get_time(); - _last_command_timestamp = end_time; - - return result; -} - // Private Packet Management Functions dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value) const { @@ -320,7 +268,7 @@ dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value) const packet.throttle_value = value & DSHOT_THROTTLE_MAX; packet.telemetric_request = _is_bidirectional ? 1 : 0; - // The data for CRC calculation includes the 11-bit value and the 1-bit telemetry flag + // The data for CRC calculation includes the 11-bit value and the 1-bit telemetry flag. uint16_t data_for_crc = (packet.throttle_value << 1) | packet.telemetric_request; packet.checksum = _calculateCRC(data_for_crc); @@ -329,17 +277,17 @@ dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value) const uint16_t DShotRMT::_buildDShotFrameValue(const dshot_packet_t &packet) const { - // Combine throttle, telemetry bit, and CRC into a single 16-bit frame + // Combine throttle, telemetry bit, and CRC into a single 16-bit frame. uint16_t data_and_telemetry = (packet.throttle_value << 1) | packet.telemetric_request; return (data_and_telemetry << 4) | packet.checksum; } uint16_t DShotRMT::_calculateCRC(const uint16_t &data) const { - // Standard DShot CRC calculation using XOR + // Standard DShot CRC calculation using XOR. uint16_t crc = (data ^ (data >> 4) ^ (data >> 8)) & DSHOT_CRC_MASK; - // For bidirectional DShot, the CRC is inverted + // For bidirectional DShot, the CRC is inverted per specification. if (_is_bidirectional) { crc = (~crc) & DSHOT_CRC_MASK; @@ -357,7 +305,7 @@ uint8_t DShotRMT::_calculateTelemetryCRC(const uint8_t *data, size_t len) const { if (crc & 0x80) { - crc = (crc << 1) ^ 0x07; // DSHOT telemetry uses CRC-8 with polynomial 0x07 + crc = (crc << 1) ^ 0x07; // DSHOT telemetry uses CRC-8 with polynomial 0x07. } else { @@ -370,7 +318,7 @@ uint8_t DShotRMT::_calculateTelemetryCRC(const uint8_t *data, size_t len) const void DShotRMT::_extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_telemetry_data_t &telemetry_data) const { - // Ensure the telemetry_data struct is cleared before filling + // Ensure the telemetry_data struct is cleared before filling. memset(&telemetry_data, 0, sizeof(dshot_telemetry_data_t)); // Telemetry data is typically ordered as: @@ -386,26 +334,20 @@ void DShotRMT::_extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_t telemetry_data.current = (static_cast(raw_telemetry_bytes[3]) << 8) | raw_telemetry_bytes[4]; telemetry_data.consumption = (static_cast(raw_telemetry_bytes[5]) << 8) | raw_telemetry_bytes[6]; telemetry_data.rpm = (static_cast(raw_telemetry_bytes[7]) << 8) | raw_telemetry_bytes[8]; - - // Error flags/count can be derived from other parts of the telemetry data if available, - // or set based on CRC check result. For now, we leave telemetry_data.errors to 0 - // or handle it implicitly through the success/failure of the CRC check. } void DShotRMT::_preCalculateRMTTicks() { - // Pre-calculate all timing values in RMT ticks to save CPU cycles later. + // Pre-calculate all timing values in RMT ticks to save CPU cycles during operation. _rmt_ticks.bit_length_ticks = static_cast(_dshot_timing.bit_length_us * RMT_TICKS_PER_US); _rmt_ticks.t1h_ticks = static_cast(_dshot_timing.t1h_lenght_us * RMT_TICKS_PER_US); _rmt_ticks.t0h_ticks = _rmt_ticks.t1h_ticks >> 1; // High time for a '0' bit is half of a '1' bit. _rmt_ticks.t1l_ticks = _rmt_ticks.bit_length_ticks - _rmt_ticks.t1h_ticks; _rmt_ticks.t0l_ticks = _rmt_ticks.bit_length_ticks - _rmt_ticks.t0h_ticks; - // Calculate the minimum time required between frames. - // Pause between frames is frame time in us, some padding and about 30 us is added by hardware. + // Calculate the minimum time required between frames to prevent signal collision. _frame_timer_us = (static_cast(_dshot_timing.bit_length_us * DSHOT_BITS_PER_FRAME) << 1) + DSHOT_PADDING_US; - // For bidirectional, double up. if (_is_bidirectional) { _frame_timer_us = (_frame_timer_us << 1); @@ -415,7 +357,7 @@ void DShotRMT::_preCalculateRMTTicks() // Private Frame Processing Functions dshot_result_t DShotRMT::_sendPacket(const dshot_packet_t &packet) { - // Ensure enough time has passed since the last transmission + // Ensure enough time has passed since the last transmission. if (!_isFrameIntervalElapsed()) { return {true, DSHOT_NONE}; @@ -423,7 +365,7 @@ dshot_result_t DShotRMT::_sendPacket(const dshot_packet_t &packet) if (_is_bidirectional) { - // Start the receiver to wait for incoming telemetry data + // Start the RMT receiver to wait for the ESC's telemetry response. rmt_symbol_word_t rx_symbols[DSHOT_TELEMETRY_FULL_GCR_BITS]; size_t rx_size_bytes = DSHOT_TELEMETRY_FULL_GCR_BITS * sizeof(rmt_symbol_word_t); @@ -438,23 +380,21 @@ dshot_result_t DShotRMT::_sendPacket(const dshot_packet_t &packet) } } - // Now let's prepare the actual frame _encoded_frame_value = _buildDShotFrameValue(packet); - // Byte-swap the 16-bit value for correct transmission order (ESP32 is little-endian, DShot is MSB first) + // Byte-swap the 16-bit value for correct transmission order. + // The RMT bytes encoder sends MSB of each byte first. uint16_t swapped_value = __builtin_bswap16(_encoded_frame_value); - // The DShot frame is 16 bits, which is 2 bytes + // The DShot frame is 16 bits, which is 2 bytes. size_t tx_size_bytes = sizeof(swapped_value); - rmt_transmit_config_t tx_config = {.loop_count = 0}; // No automatic loops - real-time calculation + rmt_transmit_config_t tx_config = {.loop_count = 0}; // No automatic loops. - // In bidirectional mode, the RMT RX channel must be disabled before transmitting. - // This is to prevent the receiver from picking up the transmitted signal, which would cause a loopback issue. - // The ESP32's RMT peripheral TX and RX channels on the same pin are not fully independent. + // In bidirectional mode, the RMT RX channel must be disabled before transmitting + // to prevent the receiver from picking up the outgoing signal (loopback). if (_is_bidirectional) { - // Disable RMT RX for sending if (rmt_disable(_rmt_rx_channel) != DSHOT_OK) { return {false, DSHOT_RECEIVER_FAILED}; @@ -466,7 +406,7 @@ dshot_result_t DShotRMT::_sendPacket(const dshot_packet_t &packet) return {false, DSHOT_TRANSMISSION_FAILED}; } - // Re-enable RMT RX + // Re-enable RMT RX immediately after transmission to catch the response. if (_is_bidirectional) { if (rmt_enable(_rmt_rx_channel) != DSHOT_OK) @@ -475,19 +415,19 @@ dshot_result_t DShotRMT::_sendPacket(const dshot_packet_t &packet) } } - _recordFrameTransmissionTime(); // Reset the timer for the next frame + _recordFrameTransmissionTime(); // Reset the timer for the next frame. return {true, DSHOT_TRANSMISSION_SUCCESS}; } -// This function needs to be fast, as it generates the RMT symbols just before sending -// Placed in IRAM for high performance, as it's called from an ISR context. +// This function is placed in IRAM for high performance, as it may be +// called from an ISR context depending on RMT driver implementation. uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) const { uint32_t gcr_value = 0; // Decode RMT symbols into a 21-bit GCR (Group Code Recording) value. - // The ESC sends back a signal where the duration determines the bit value. + // The ESC sends back a signal where the duration of high vs. low determines the bit value. for (size_t i = 0; i < DSHOT_ERPM_FRAME_GCR_BITS; ++i) { bool bit_is_one = symbols[i].duration0 > symbols[i].duration1; @@ -497,34 +437,28 @@ uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) // Perform GCR decoding (GCR = Value ^ (Value >> 1)). uint32_t decoded_frame = gcr_value ^ (gcr_value >> 1); - // Extract the 16-bit DShot frame from the decoded data. + // Extract the 16-bit DShot-like frame from the decoded data. uint16_t data_and_crc = (decoded_frame & DSHOT_FULL_PACKET); - // Extract data and CRC from the 16-bit frame. + // The eRPM telemetry frame consists of 12 data bits and 4 CRC bits. uint16_t received_data = data_and_crc >> DSHOT_CRC_BIT_SHIFT; uint16_t received_crc = data_and_crc & DSHOT_CRC_MASK; - // A valid response must have the telemetry request bit set to 1. This is a sanity check. - if (!((received_data >> DSHOT_TELEMETRY_BIT_POSITION) & 1)) - { - return DSHOT_NULL_PACKET; - } - - // Calculate and validate CRC. + // Calculate and validate the CRC for the received data. uint16_t calculated_crc = _calculateCRC(received_data); if (received_crc != calculated_crc) { return DSHOT_NULL_PACKET; } - // Return the eRPM value (first 11 bits). + // Return the eRPM value (the first 11 bits of the data). return received_data & DSHOT_THROTTLE_MAX; } // Timing Control Functions bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() const { - // Check if the minimum interval between frames has passed + // Check if the minimum interval between frames has passed. uint64_t current_time = esp_timer_get_time(); uint64_t elapsed = current_time - _last_transmission_time_us; return elapsed >= _frame_timer_us; @@ -532,20 +466,20 @@ bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() const void DShotRMT::_recordFrameTransmissionTime() { - // Record the time of the current transmission + // Record the time of the current transmission. _last_transmission_time_us = esp_timer_get_time(); } // Static Callback Functions -// Processes a full telemetry frame +// Processes a full telemetry frame from the RMT RX ISR. void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *symbols, size_t num_symbols) { if (num_symbols != DSHOT_TELEMETRY_FULL_GCR_BITS) { - return; // Incorrect number of symbols for full telemetry + return; // Incorrect number of symbols for a full telemetry frame. } - uint8_t gcr_decoded_bytes[DSHOT_TELEMETRY_FRAME_LENGTH_BYTES + 1]; // 10 data bytes + 1 CRC byte + uint8_t gcr_decoded_bytes[DSHOT_TELEMETRY_FRAME_LENGTH_BYTES + 1]; // 10 data bytes + 1 CRC byte. memset(gcr_decoded_bytes, 0, sizeof(gcr_decoded_bytes)); uint8_t data_bit_idx = 0; @@ -560,62 +494,29 @@ void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *sym } } - uint8_t decoded_nibble; // 4 data bits + uint8_t decoded_nibble; // 4 data bits. switch (gcr_group_5bits) { - case 0b11110: - decoded_nibble = 0b0000; - break; - case 0b01001: - decoded_nibble = 0b0001; - break; - case 0b10100: - decoded_nibble = 0b0010; - break; - case 0b10101: - decoded_nibble = 0b0011; - break; - case 0b01010: - decoded_nibble = 0b0100; - break; - case 0b01011: - decoded_nibble = 0b0101; - break; - case 0b01110: - decoded_nibble = 0b0110; - break; - case 0b01111: - decoded_nibble = 0b0111; - break; - case 0b10010: - decoded_nibble = 0b1000; - break; - case 0b10011: - decoded_nibble = 0b1001; - break; - case 0b10110: - decoded_nibble = 0b1010; - break; - case 0b10111: - decoded_nibble = 0b1011; - break; - case 0b11010: - decoded_nibble = 0b1100; - break; - case 0b11011: - decoded_nibble = 0b1101; - break; - case 0b11100: - decoded_nibble = 0b1110; - break; - case 0b11101: - decoded_nibble = 0b1111; - break; - default: - return; // Invalid GCR group, discard frame + case 0b11110: decoded_nibble = 0b0000; break; + case 0b01001: decoded_nibble = 0b0001; break; + case 0b10100: decoded_nibble = 0b0010; break; + case 0b10101: decoded_nibble = 0b0011; break; + case 0b01010: decoded_nibble = 0b0100; break; + case 0b01011: decoded_nibble = 0b0101; break; + case 0b01110: decoded_nibble = 0b0110; break; + case 0b01111: decoded_nibble = 0b0111; break; + case 0b10010: decoded_nibble = 0b1000; break; + case 0b10011: decoded_nibble = 0b1001; break; + case 0b10110: decoded_nibble = 0b1010; break; + case 0b10111: decoded_nibble = 0b1011; break; + case 0b11010: decoded_nibble = 0b1100; break; + case 0b11011: decoded_nibble = 0b1101; break; + case 0b11100: decoded_nibble = 0b1110; break; + case 0b11101: decoded_nibble = 0b1111; break; + default: return; // Invalid GCR group, discard frame. } - // Place the 4 decoded bits into the data_bytes array + // Place the 4 decoded bits into the data_bytes array. for (int k = 3; k >= 0; --k) { if (data_bit_idx < (DSHOT_TELEMETRY_FRAME_LENGTH_BITS + DSHOT_TELEMETRY_CRC_LENGTH_BITS)) @@ -631,7 +532,7 @@ void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *sym } } - // Now gcr_decoded_bytes contains the 10 telemetry bytes + 1 CRC byte. + // The gcr_decoded_bytes array now contains the 10 telemetry bytes + 1 CRC byte. // Perform CRC validation. uint8_t received_crc = gcr_decoded_bytes[DSHOT_TELEMETRY_FRAME_LENGTH_BYTES]; uint8_t calculated_crc = _calculateTelemetryCRC(gcr_decoded_bytes, DSHOT_TELEMETRY_FRAME_LENGTH_BYTES); @@ -639,7 +540,7 @@ void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *sym if (received_crc == calculated_crc) { dshot_telemetry_data_t telemetry_data; - // Extract from the first 10 bytes (excluding the CRC byte) + // Extract from the first 10 bytes (excluding the CRC byte). _extractTelemetryData(gcr_decoded_bytes, telemetry_data); _last_telemetry_data_atomic.store(telemetry_data); @@ -647,7 +548,7 @@ void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *sym } } -// This function is called by the RMT driver's ISR when a frame is received +// This function is called by the RMT driver's ISR when a frame is received. bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data) { DShotRMT *instance = static_cast(user_data); @@ -664,7 +565,7 @@ bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const if (erpm != DSHOT_NULL_PACKET) { - // Atomically store the new eRPM value and set the flag + // Atomically store the new eRPM value and set the flag. instance->_last_erpm_atomic.store(erpm); instance->_telemetry_ready_flag_atomic.store(true); } diff --git a/src/DShotRMT.h b/src/DShotRMT.h index 79c072e..217a004 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -21,7 +21,7 @@ // DShotRMT Library Version static constexpr uint8_t DSHOTRMT_MAJOR_VERSION = 0; static constexpr uint8_t DSHOTRMT_MINOR_VERSION = 9; -static constexpr uint8_t DSHOTRMT_PATCH_VERSION = 2; +static constexpr uint8_t DSHOTRMT_PATCH_VERSION = 5; // DShot Protocol Constants static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0; @@ -30,10 +30,10 @@ static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0; class DShotRMT { public: - // Constructor for DShotRMT. + // Constructs a new DShotRMT object using a GPIO pin. DShotRMT(gpio_num_t gpio, dshot_mode_t mode = DSHOT300, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); - // Constructor using pin number + // Constructs a new DShotRMT object using an Arduino-style integer pin number. DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); // Destructor @@ -103,10 +103,10 @@ private: // DShot Frame Timing and State Variables uint64_t _last_transmission_time_us = 0; // Timestamp of the last DShot frame transmission uint64_t _frame_timer_us = 0; // Minimum time required between DShot frames + float _percent_to_throttle_ratio = 0.0f; // Pre-calculated ratio for throttle percentage conversion uint16_t _last_throttle = 0; // Last transmitted throttle value dshot_packet_t _packet; // Current DShot packet being processed uint16_t _encoded_frame_value = 0; // Last encoded 16-bit DShot frame value - uint64_t _last_command_timestamp = 0; // Timestamp of the last command sent // Telemetry Related Variables std::atomic _last_erpm_atomic = 0; // Atomically stored last received eRPM value @@ -120,7 +120,6 @@ private: // Private Helper Functions for DShot Protocol Logic bool _isValidCommand(dshotCommands_e command) const; // Checks if a given DShot command is valid - dshot_result_t _executeCommand(dshotCommands_e command); // Executes a single DShot command dshot_packet_t _buildDShotPacket(const uint16_t &value) const; // Builds a DShot packet from a value (throttle or command) uint16_t _buildDShotFrameValue(const dshot_packet_t &packet) const; // Combines packet data into a 16-bit DShot frame value uint16_t _calculateCRC(const uint16_t &data) const; // Calculates the 4-bit CRC for a DShot frame @@ -133,6 +132,8 @@ private: bool IRAM_ATTR _isFrameIntervalElapsed() const; // Checks if enough time has passed since the last frame transmission void _recordFrameTransmissionTime(); // Records the current time as the last frame transmission time + dshot_result_t _sendRepeatedCommand(uint16_t value, uint16_t repeat_count, uint16_t delay_us); + // Static Callback Function for RMT RX Events void _cleanupRmtResources(); }; diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h index d1a9e06..76e436f 100644 --- a/src/dshot_definitions.h +++ b/src/dshot_definitions.h @@ -2,7 +2,7 @@ * @file dshot_definitions.h * @brief Defines DShot protocol constants, data structures, and command enums for DShotRMT library * @author Wastl Kraus - * @date 2025-10-04 + * @date 2025-11-29 * @license MIT */ @@ -11,47 +11,58 @@ #include #include -// DShot protocol definitions -static constexpr uint16_t DSHOT_FRAME_LENGTH = 16; // 11 throttle bits + 1 telemetry bit + 4 CRC bits +// ================================================================================= +// DShot Protocol Constants +// ================================================================================= + +// --- Frame Structure --- static constexpr uint16_t DSHOT_BITS_PER_FRAME = 16; -static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047; // Maximum throttle value (0-2047) -static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin +static constexpr uint16_t DSHOT_TELEMETRY_BIT_POSITION = 11; +static constexpr uint16_t DSHOT_CRC_BIT_SHIFT = 4; +static constexpr uint16_t DSHOT_CRC_MASK = 0x000F; + +// --- Throttle & Command Values --- +static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047; // Maximum throttle value (0-2047) +static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin static constexpr float DSHOT_PERCENT_MIN = 0.0f; static constexpr float DSHOT_PERCENT_MAX = 100.0f; -static constexpr uint16_t DSHOT_CMD_MIN = 0; // Minimum command value -static constexpr uint16_t DSHOT_CMD_MAX = 47; // Maximum command value -static constexpr uint16_t DSHOT_TELEMETRY_BIT_MASK = 0x0800; // Bit mask for telemetry request bit (11th bit) -static constexpr uint16_t DSHOT_CRC_MASK = 0x000F; // Bit mask for CRC bits +static constexpr uint16_t DSHOT_CMD_MIN = 0; // Minimum command value +static constexpr uint16_t DSHOT_CMD_MAX = 47; // Maximum command value +static constexpr uint16_t DSHOT_NULL_PACKET = 0; +static constexpr uint16_t DSHOT_FULL_PACKET = 0xFFFF; +static constexpr uint16_t DSHOT_TELEMETRY_INVALID = DSHOT_THROTTLE_MAX; +static constexpr uint16_t NO_DSHOT_TELEMETRY = 0; -// GCR frame definitions +// --- Command Behavior --- +static constexpr uint16_t DEFAULT_CMD_DELAY_US = 10; +static constexpr uint16_t DEFAULT_CMD_REPEAT_COUNT = 1; +static constexpr uint16_t SETTINGS_COMMAND_REPEATS = 10; +static constexpr uint16_t SETTINGS_COMMAND_DELAY_US = 5; + + +// ================================================================================= +// DShot Telemetry Constants +// ================================================================================= + +// --- GCR Frame Structure --- static constexpr uint16_t DSHOT_ERPM_FRAME_GCR_BITS = 21; // GCR bits in a DShot answer frame for eRPM static constexpr uint16_t DSHOT_TELEMETRY_FULL_GCR_BITS = 110; // GCR bits for a full 10-byte telemetry frame (80 data bits + 8 CRC bits = 88 bits, 88 * 5/4 = 110 GCR bits) -// Telemetry frame definitions +// --- Telemetry Payload Structure --- static constexpr uint16_t DSHOT_TELEMETRY_FRAME_LENGTH_BITS = 80; // 10 bytes * 8 bits/byte static constexpr uint16_t DSHOT_TELEMETRY_FRAME_LENGTH_BYTES = 10; -static constexpr uint16_t DSHOT_TELEMETRY_CRC_LENGTH_BITS = 8; // 8-bit CRC for telemetry +static constexpr uint16_t DSHOT_TELEMETRY_CRC_LENGTH_BITS = 8; // 8-bit CRC for telemetry -// Default motor magnet count for RPM calculation +// --- Motor Properties for RPM Calculation --- static constexpr uint16_t DEFAULT_MOTOR_MAGNET_COUNT = 14; +static constexpr uint16_t POLE_PAIRS_MIN = 1; +static constexpr uint16_t MAGNETS_PER_POLE_PAIR = 2; -// Defines the available DShot communication speeds. -enum dshot_mode_t : uint8_t -{ - DSHOT_OFF, - DSHOT150, - DSHOT300, - DSHOT600, - DSHOT1200 -}; -// Represents the 16-bit DShot data packet sent to the ESC. -typedef struct dshot_packet -{ - uint16_t throttle_value : 11; // 11-bit throttle value or command. - bool telemetric_request : 1; // 1-bit telemetry request flag. - uint16_t checksum : 4; // 4-bit CRC checksum. -} dshot_packet_t; +// ================================================================================= +// DShot Timing Constants (Protocol Level) +// ================================================================================= +static constexpr uint16_t DSHOT_PADDING_US = 20; // Pause between frames // Defines the bit length and high time for a '1' bit in microseconds for each DShot mode. typedef struct dshot_timing @@ -60,6 +71,32 @@ typedef struct dshot_timing double t1h_lenght_us; // High time duration for a '1' bit in microseconds. } dshot_timing_us_t; +// Timing parameters for each DShot mode +const dshot_timing_us_t DSHOT_TIMING_US[] = { + {0.00, 0.00}, // DSHOT_OFF + {6.67, 5.00}, // DSHOT150 + {3.33, 2.50}, // DSHOT300 + {1.67, 1.25}, // DSHOT600 + {0.83, 0.67} // DSHOT1200 +}; + + +// ================================================================================= +// ESP32 RMT Hardware Implementation Constants +// ================================================================================= + +// --- RMT Clock & Buffer Configuration --- +static constexpr auto DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT; +static constexpr auto DSHOT_RMT_RESOLUTION = 8000000; // 8 MHz resolution +static constexpr auto RMT_TICKS_PER_US = DSHOT_RMT_RESOLUTION / 1000000; // RMT Ticks per microsecond +static constexpr auto RMT_TX_BUFFER_SYMBOLS = 64; +static constexpr auto RMT_RX_BUFFER_SYMBOLS = DSHOT_TELEMETRY_FULL_GCR_BITS; +static constexpr auto RMT_QUEUE_DEPTH = 1; + +// --- RMT Receiver Configuration --- +static constexpr uint32_t DSHOT_PULSE_MIN_NS = 800; // 0.8us minimum pulse duration for receiver +static constexpr uint32_t DSHOT_PULSE_MAX_NS = 8000; // 8.0us maximum pulse duration for receiver + // Stores pre-calculated timing values in RMT ticks for efficient signal generation. typedef struct rmt_ticks { @@ -70,7 +107,43 @@ typedef struct rmt_ticks uint16_t t0l_ticks; // Low time duration for a '0' bit in RMT ticks. } rmt_ticks_t; -// Enum class for specific error and success codes + +// ================================================================================= +// Library Data Structures & Enums +// ================================================================================= + +// --- DShot Modes --- +enum dshot_mode_t : uint8_t +{ + DSHOT_OFF, + DSHOT150, + DSHOT300, + DSHOT600, + DSHOT1200 +}; + +// --- DShot Packet Structure --- +// Represents the 16-bit DShot data packet sent to the ESC. +typedef struct dshot_packet +{ + uint16_t throttle_value : 11; // 11-bit throttle value or command. + bool telemetric_request : 1; // 1-bit telemetry request flag. + uint16_t checksum : 4; // 4-bit CRC checksum. +} dshot_packet_t; + +// --- Telemetry Data Structure --- +// Structure for decoded DShot telemetry data (from ESC) +typedef struct dshot_telemetry_data +{ + uint16_t rpm; // Motor RPM + uint16_t voltage; // Voltage in mV + uint16_t current; // Current in mA + uint16_t consumption; // Consumption in mAh + int8_t temperature; // Temperature in Celsius + uint8_t errors; // Error flags / count +} dshot_telemetry_data_t; + +// --- Library Result Codes & Structure --- enum dshot_msg_code_t { DSHOT_NONE = 0, @@ -101,17 +174,6 @@ enum dshot_msg_code_t DSHOT_COMMAND_SUCCESS }; -// Structure for decoded DShot telemetry data (from ESC) -typedef struct dshot_telemetry_data -{ - uint16_t rpm; // Motor RPM - uint16_t voltage; // Voltage in mV - uint16_t current; // Current in mA - uint16_t consumption; // Consumption in mAh - int8_t temperature; // Temperature in Celsius - uint8_t errors; // Error flags / count -} dshot_telemetry_data_t; - // Contains the success status, an error code, and optional telemetry data. typedef struct dshot_result { @@ -123,6 +185,7 @@ typedef struct dshot_result bool telemetry_available; // Flag to indicate if telemetry_data is valid } dshot_result_t; +// --- DShot Commands --- // Standard DShot commands by "betaflight" enum dshotCommands_e { @@ -156,23 +219,6 @@ enum dshotCommands_e DSHOT_CMD_MAX_VALUE = 47 }; -// Custom status codes +// --- General Status & Helper Constants --- static constexpr int DSHOT_OK = 0; -static constexpr int DSHOT_ERROR = 1; - -// Configuration Constants -static constexpr auto DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT; -static constexpr auto DSHOT_RMT_RESOLUTION = 8000000; // 8 MHz resolution -static constexpr auto RMT_TICKS_PER_US = DSHOT_RMT_RESOLUTION / 1000000; // RMT Ticks per microsecond -static constexpr auto RMT_TX_BUFFER_SYMBOLS = 64; -static constexpr auto RMT_RX_BUFFER_SYMBOLS = DSHOT_TELEMETRY_FULL_GCR_BITS; -static constexpr auto RMT_QUEUE_DEPTH = 1; - -// Timing parameters for each DShot mode -const dshot_timing_us_t DSHOT_TIMING_US[] = { - {0.00, 0.00}, // DSHOT_OFF - {6.67, 5.00}, // DSHOT150 - {3.33, 2.50}, // DSHOT300 - {1.67, 1.25}, // DSHOT600 - {0.83, 0.67} // DSHOT1200 -}; \ No newline at end of file +static constexpr float CONVERSION_FACTOR_MILLI_TO_UNITS = 1000.0f; \ No newline at end of file diff --git a/src/dshot_init.cpp b/src/dshot_init.cpp index 237c621..089e157 100644 --- a/src/dshot_init.cpp +++ b/src/dshot_init.cpp @@ -19,7 +19,7 @@ dshot_result_t init_rmt_tx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_ch .trans_queue_depth = RMT_QUEUE_DEPTH, .flags = { .invert_out = is_bidirectional ? 1 : 0, - .init_level = is_bidirectional ? 0 : 1}}; + .init_level = 0}}; rmt_transmit_config_t rmt_tx_config = {}; // Initialize all members to zero rmt_tx_config.loop_count = 0; // No automatic loops - real-time calculation diff --git a/src/dshot_utils.h b/src/dshot_utils.h index c7a8ffc..557569c 100644 --- a/src/dshot_utils.h +++ b/src/dshot_utils.h @@ -167,8 +167,8 @@ inline void printDShotInfo(DShotRMT &dshot_rmt, Stream &output = Serial) output.println(" --- Full Telemetry Details ---"); output.printf(" Temp: %d C | Volt: %.2f V | Curr: %.2f A | Cons: %u mAh\n", telemetry_result.telemetry_data.temperature, - (float)telemetry_result.telemetry_data.voltage / 1000.0f, // Convert mV to V - (float)telemetry_result.telemetry_data.current / 1000.0f, // Convert mA to A + (float)telemetry_result.telemetry_data.voltage / CONVERSION_FACTOR_MILLI_TO_UNITS, // Convert mV to V + (float)telemetry_result.telemetry_data.current / CONVERSION_FACTOR_MILLI_TO_UNITS, // Convert mA to A telemetry_result.telemetry_data.consumption); output.printf(" Telemetry RPM: %u\n", telemetry_result.telemetry_data.rpm); }