From 7f102ac9bb5c58bb03e0c2595db4e9ab824087db Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Tue, 9 Sep 2025 17:19:57 +0200 Subject: [PATCH] Add Web Server - fix warnings and update action versions - Web Server added in example - prepare release 0.7.5 --- .github/workflows/ci.yml | 37 +- .gitignore | 6 +- DShotRMT.h | 230 +--------- README.md | 150 +++++- examples/dshot300/dshot300.ino | 317 ++----------- examples/web_control/web_control.ino | 433 ++++++++++++++++++ library.properties | 11 +- .../DShotCommandManager.cpp | 0 .../DShotCommandManager.h | 0 DShotRMT.cpp => src/DShotRMT.cpp | 0 src/DShotRMT.h | 237 ++++++++++ dshot_commands.h => src/dshot_commands.h | 0 {web => src}/web_content.h | 2 + 13 files changed, 886 insertions(+), 537 deletions(-) create mode 100644 examples/web_control/web_control.ino rename DShotCommandManager.cpp => src/DShotCommandManager.cpp (100%) rename DShotCommandManager.h => src/DShotCommandManager.h (100%) rename DShotRMT.cpp => src/DShotRMT.cpp (100%) create mode 100644 src/DShotRMT.h rename dshot_commands.h => src/dshot_commands.h (100%) rename {web => src}/web_content.h (99%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5754795..ee30cda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: library-manager: update verbose: true - # ============================================================================ +# ============================================================================ # Compilation Test # ============================================================================ compile-test: @@ -50,28 +50,29 @@ jobs: examples: - "examples/dshot300/dshot300.ino" - "examples/command_manager/command_manager.ino" + - "examples/web_control/web_control.ino" steps: - name: Checkout Repository uses: actions/checkout@v5 - - name: Compile Example Sketches - uses: arduino/compile-sketches@v1 - with: - fqbn: esp32:esp32:esp32 - platforms: | - - name: esp32:esp32 - source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - libraries: | - # Install the library from the local path. - - name: ArduinoJson - - name: Async TCP - - name: ESP Async WebServer - - name: WiFi - - source-path: ./ - sketch-paths: ${{ matrix.examples}} - cli-compile-flags: | - - --warnings="none" + - name: Setup Arduino CLI + uses: arduino/setup-arduino-cli@v2 + + - name: Install ESP32 Core and Dependencies + run: | + arduino-cli core update-index + arduino-cli core install esp32:esp32 + + arduino-cli lib install "ArduinoJson" + + # Workround for ESPAsyncWebServer + git clone https://github.com/ESP32Async/ESPAsyncWebServer ~/Arduino/libraries/ESPAsyncWebServer + git clone https://github.com/ESP32Async/AsyncTCP ~/Arduino/libraries/AsyncTCP + + - name: Compile Sketch + run: | + arduino-cli compile --fqbn esp32:esp32:esp32 --library ${{ github.workspace }} ${{ matrix.examples}} # ============================================================================ # Build Status Report diff --git a/.gitignore b/.gitignore index b92f635..b2a6323 100644 --- a/.gitignore +++ b/.gitignore @@ -13,13 +13,11 @@ # Built Visual Studio Code Extensions *.vsix -# Caching ESP32 Builds +# Builds +*.code-workspace buildCache build examples/dshot300/debug.cfg examples/dshot300/esp32.svd examples/dshot300/debug_custom.json examples/dshot300/debug.svd -/build -/.github/chatmodes -web/control.html diff --git a/DShotRMT.h b/DShotRMT.h index c3aabe0..367f706 100644 --- a/DShotRMT.h +++ b/DShotRMT.h @@ -6,231 +6,5 @@ * @license MIT */ -#pragma once - -#include -#include -#include -#include -#include - -// DShot Protocol Constants -static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0; -static constexpr auto DSHOT_THROTTLE_MIN = 48; -static constexpr auto DSHOT_THROTTLE_MAX = 2047; -static constexpr auto DSHOT_BITS_PER_FRAME = 16; -static constexpr auto DSHOT_PAUSE_US = 30; // Additional frame pause time -static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000; -static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111; -static constexpr auto DSHOT_CRC_MASK = 0b0000000000001111; -static constexpr auto DSHOT_RX_TIMEOUT_MS = 2; // Never reached, just a timeeout -static constexpr auto GCR_BITS_PER_FRAME = 21; // Number of GCR bits in a DShot answer frame (1 start + 16 data + 4 CRC) -static constexpr auto DEFAULT_MOTOR_MAGNET_COUNT = 14; -static constexpr auto MAGNETS_PER_POLE_PAIR = 2; -static constexpr auto MIN_POLE_PAIRS = 1; -static constexpr auto NO_DSHOT_ERPM = 0; -static constexpr auto NO_DSHOT_RPM = 0; - -// RMT Configuration Constants -constexpr auto DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT; -constexpr auto DSHOT_RMT_RESOLUTION = 10 * 1000 * 1000; // 10 MHz resolution -constexpr auto RMT_BUFFER_SIZE = DSHOT_BITS_PER_FRAME; -constexpr auto RMT_BUFFER_SYMBOLS = 64; -constexpr auto RMT_QUEUE_DEPTH = 1; - -// Smallest pulse for DShot1200 is 2us. Largest for DShot150 is 40us. -// The range is set from 3us (3000ns) to 60us (60000ns) to be safe across all modes. -constexpr uint32_t DSHOT_PULSE_MIN = 3000; -constexpr uint32_t DSHOT_PULSE_MAX = 60000; - -// DShot Modes -typedef enum -{ - DSHOT_OFF, - DSHOT150, - DSHOT300, - DSHOT600, - DSHOT1200 -} dshot_mode_t; - -// DShot Packet -typedef struct -{ - uint16_t throttle_value : 11; - bool telemetric_request : 1; - uint16_t checksum : 4; -} dshot_packet_t; - -// DShot Timing Configuration -typedef struct -{ - uint32_t frame_length_us; - uint16_t ticks_per_bit; - uint16_t ticks_one_high; - uint16_t ticks_one_low; - uint16_t ticks_zero_high; - uint16_t ticks_zero_low; -} dshot_timing_t; - -// Error handling -typedef struct -{ - bool success; - const char *msg; -} dshot_result_t; - -// DShot telemetry result -typedef struct -{ - bool success; - uint16_t erpm; - uint16_t motor_rpm; - const char *msg; -} dshot_telemetry_result_t; - -// Naming convention -typedef dshotCommands_e dshot_commands_t; - -// --- HELPERS --- -void printDShotResult(dshot_result_t &result, Stream &output = Serial); -void printDShotTelemetry(dshot_telemetry_result_t &result, Stream &output = Serial); - -// -class DShotRMT -{ -public: - // Constructor with GPIO enum - explicit DShotRMT(gpio_num_t gpio = GPIO_NUM_16, dshot_mode_t mode = DSHOT300, bool is_bidirectional = false); - - // Constructor with pin number - DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional); - - // Destructor for "better" code - ~DShotRMT(); - - // Initialize the RMT module and DShot config - dshot_result_t begin(); - - // Send throttle value (48-2047) - dshot_result_t sendThrottle(uint16_t throttle); - - // Send DShot command (0-47) - dshot_result_t sendCommand(uint16_t command); - - // --- GETTERS --- - gpio_num_t getGPIO() const { return _gpio; } - uint16_t getDShotPacket() const { return _parsed_packet; } - bool is_bidirectional() const { return _is_bidirectional; } - dshot_mode_t getMode() const { return _mode; } - dshot_telemetry_result_t getTelemetry(uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); - - // --- INFO --- - void printDShotInfo(Stream &output = Serial) const; - void printCpuInfo(Stream &output = Serial) const; - - // --- DEPRECATED METHODS --- - [[deprecated("Use sendThrottle() instead")]] - bool setThrottle(uint16_t throttle) - { - auto result = sendThrottle(throttle); - return result.success; - } - - [[deprecated("Use sendCommand() instead")]] - bool sendDShotCommand(uint16_t command) - { - auto result = sendCommand(command); - return result.success; - } - - [[deprecated("Use getTelemetry() instead")]] - uint32_t getMotorRPM(uint8_t magnet_count) - { - auto result = getTelemetry(magnet_count); - return result.success; - } - -private: - // --- CONFIG --- - gpio_num_t _gpio; - dshot_mode_t _mode; - bool _is_bidirectional; - uint32_t _frame_timer_us; - const dshot_timing_t &_timing_config; - uint16_t _last_throttle; - - // --- TIMING & PACKET VARIABLES --- - uint64_t _last_transmission_time; - uint16_t _parsed_packet; - dshot_packet_t _packet; - uint8_t _bitPositions[DSHOT_BITS_PER_FRAME]; - uint16_t _level0; - uint16_t _level1; - - // --- RMT HARDWARE HANDLES --- - rmt_channel_handle_t _rmt_tx_channel; - rmt_channel_handle_t _rmt_rx_channel; - rmt_encoder_handle_t _dshot_encoder; - - // --- RMT CONFIG STRUCTURES --- - rmt_tx_channel_config_t _tx_channel_config; - rmt_rx_channel_config_t _rx_channel_config; - rmt_transmit_config_t _transmit_config; - rmt_receive_config_t _receive_config; - - // --- INITS --- - dshot_result_t _initTXChannel(); - dshot_result_t _initRXChannel(); - dshot_result_t _initDShotEncoder(); - - // --- PACKET MANAGEMENT --- - dshot_packet_t _buildDShotPacket(const uint16_t value); - uint16_t _parseDShotPacket(const dshot_packet_t &packet); - uint16_t _calculateCRC(const uint16_t data); - void _preCalculateBitPositions(); - - // --- FRAME PROCESSING --- - dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); - bool IRAM_ATTR _encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols); - uint16_t _decodeDShotFrame(const rmt_symbol_word_t *symbols); - - // --- TIMING CONTROL --- - bool IRAM_ATTR _timer_signal(); - bool _timer_reset(); - - // -- CALLBACKS --- - rmt_rx_event_callbacks_t _rx_event_callbacks; - volatile rmt_symbol_word_t _rx_symbols_direct[GCR_BITS_PER_FRAME]; - volatile uint16_t _last_erpm_atomic; - volatile bool _telemetry_ready_flag; - static bool IRAM_ATTR _rmt_rx_done_callback(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data); - - // --- DSHOT DEFAULTS --- - static constexpr auto const DSHOT_TELEMETRY_INVALID = (0xffff); - - // --- CONSTANTS & ERROR MESSAGES --- - static constexpr bool DSHOT_OK = 0; - static constexpr bool DSHOT_ERROR = 1; - - static constexpr char const *NONE = ""; - static constexpr char const *UNKNOWN_ERROR = "Unknown Error!"; - static constexpr char const *INIT_SUCCESS = "SignalGeneratorRMT initialized successfully"; - static constexpr char const *INIT_FAILED = "SignalGeneratorRMT init failed!"; - static constexpr char const *TX_INIT_SUCCESS = "TX RMT channel initialized successfully"; - static constexpr char const *TX_INIT_FAILED = "TX RMT channel init failed!"; - static constexpr char const *RX_INIT_SUCCESS = "RX RMT channel initialized successfully"; - static constexpr char const *RX_INIT_FAILED = "RX RMT channel init failed!"; - static constexpr char const *RX_BUFFER_FAILED = "RX RMT buffer init failed!"; - static constexpr char const *ENCODER_INIT_SUCCESS = "RMT encoder initialized successfully"; - static constexpr char const *ENCODER_INIT_FAILED = "RMT encoder init failed!"; - static constexpr char const *TRANSMISSION_SUCCESS = "Transmission successfully"; - 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 *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!"; - static constexpr char const *TELEMETRY_FAILED = "No valid Telemetric Frame received!"; - static constexpr char const *INVALID_MAGNET_COUNT = "Invalid motor magnet count!"; - static constexpr char const *TIMING_CORRECTION = "Timing correction!"; -}; + #include "src/DShotRMT.h" + \ No newline at end of file diff --git a/README.md b/README.md index 8c3a75c..618a589 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ A modern, robust C++ library for generating DShot signals on the ESP32 using the new ESP-IDF 5 RMT encoder API (`rmt_tx.h` / `rmt_rx.h`). Supports all standard DShot modes (150, 300, 600, 1200) and features continuous frame transmission with configurable timing. -**Now with BiDirectional DShot support and advanced command management!** + +**Now with BiDirectional DShot support, advanced command management, and modern web control interface!** > The legacy version (using the old `rmt.h` API) is still available in the `oldAPI` branch. @@ -14,8 +15,11 @@ Supports all standard DShot modes (150, 300, 600, 1200) and features continuous - **All DShot Modes:** DSHOT150, DSHOT300 (default), DSHOT600, DSHOT1200 - **BiDirectional DShot:** Full support for RPM telemetry feedback +- **Web Control Interface:** Modern responsive web UI with WiFi access point - **Advanced Command Manager:** High-level API for ESC configuration and control -- **Command Sequences:** Predefined initialization and calibration sequences +- **Safety Features:** Arming/disarming system with motor lockout protection +- **Dual Control Options:** Web interface and serial console control +- **Real-time Telemetry:** Live RPM monitoring and data display - **Hardware-Timed Signals:** Independent, precise signal generation using ESP32 RMT peripheral - **Configurable Timing:** Ensures ESCs can reliably detect frame boundaries - **Error Handling:** Comprehensive result reporting with success/failure status @@ -46,6 +50,25 @@ lib_deps = git clone https://github.com/derdoktor667/DShotRMT.git ``` +### Dependencies + +The library requires these additional libraries for full functionality: + +**Core DShotRMT (always required):** +- ESP32 Arduino Core + +**Web Interface Example (dshot300.ino):** +```ini +lib_deps = + https://github.com/derdoktor667/DShotRMT + bblanchon/ArduinoJson + https://github.com/ESP32Async/ESPAsyncWebServer + https://github.com/ESP32Async/AsyncTCP ~/Arduino/libraries/AsyncTCP +``` + +**Command Manager Example:** +- No additional dependencies required + --- ## ⚡ Quick Start @@ -79,6 +102,64 @@ void loop() { } ``` +### Web Control Interface + +```cpp +#include +#include +#include + +DShotRMT motor(17, DSHOT300, false); +AsyncWebServer server(80); +AsyncWebSocket ws("/ws"); + +void setup() { + // Initialize motor + motor.begin(); + + // Create WiFi Access Point + WiFi.softAP("DShotRMT Control", "12345678"); + + // Setup web interface + ws.onEvent(onWsEvent); + server.addHandler(&ws); + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send_P(200, "text/html", index_html); + }); + server.begin(); + + // Access at http://10.10.10.1 +} + +void loop() { + // Handle WebSocket communication and motor control + ws.cleanupClients(); +} +``` + +### Advanced Command Management + +```cpp +#include +#include + +DShotRMT motor(17, DSHOT300, false); +DShotCommandManager cmdManager(motor); + +void setup() { + motor.begin(); + cmdManager.begin(); +} + +void loop() { + // High-level ESC control + cmdManager.stopMotor(); + cmdManager.activateBeacon(1); + cmdManager.setSpinDirection(false); + cmdManager.executeInitSequence(); +} +``` + ### Bidirectional DShot (RPM Telemetry) ```cpp @@ -108,15 +189,59 @@ void loop() { --- +## 🌐 Web Control Interface + +The DShotRMT library now includes a modern web interface for wireless motor control: + +### Features +- **Responsive Design:** Works on mobile phones, tablets, and desktop computers +- **WiFi Access Point:** Creates hotspot "DShotRMT Control" (Password: 12345678) +- **Safety System:** Arming/disarming switch prevents accidental motor activation +- **Real-time Control:** Instant throttle response via WebSocket communication +- **Live Telemetry:** Real-time RPM display (bidirectional mode only) +- **Auto-reconnect:** Automatically reconnects on connection loss + +### Web Interface Access +1. Connect to WiFi network: **"DShotRMT Control"** +2. Password: **12345678** +3. Open browser and navigate to: **http://10.10.10.1** + +### Safety Features +- Motor control is **disabled by default** (disarmed state) +- Toggle the **ARMING SWITCH** to enable motor control +- Throttle slider is **locked** when disarmed +- **Emergency stop** resets all values to safe state + +### Technical Implementation +- **AsyncWebServer** for HTTP requests +- **WebSocket** communication for real-time data +- **JSON** message format for data exchange +- **WiFi SoftAP** mode for standalone operation +- **Automatic client cleanup** prevents memory leaks + +### ⚠️ Known Issus +Make sure you are using these libraries for [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) and [AsyncTCP](https://github.com/ESP32Async/AsyncTCP) to use "web_control.ino" example sketch. + +--- + ## 📚 Examples The library includes comprehensive examples: -### 1. Basic DShot Control (`dshot300.ino`) -- Simple throttle control -- Command execution -- Serial interface for testing -- Telemetry reading (if bidirectional enabled) +### 1. Basic DShot Control with Web Interface (`dshot300.ino`) +- **Web Control Interface:** Modern responsive web UI accessible at `http://10.10.10.1` +- **WiFi Access Point:** Creates hotspot "DShotRMT Control" for wireless control +- **Safety Features:** Arming/disarming system with motor safety lockout +- **Real-time Data:** Live RPM telemetry display (bidirectional mode) +- **Dual Control:** Both web interface and serial console control +- **WebSocket Communication:** Real-time bidirectional data exchange + +**Web Interface Features:** +- Responsive design optimized for mobile and desktop +- Visual arming switch with safety lockout +- Smooth throttle slider with real-time feedback +- Live RPM monitoring display +- Automatic reconnection on connection loss ### 2. Advanced Command Management (`command_manager.ino`) Interactive ESC control with full menu system: @@ -143,12 +268,11 @@ Advanced Commands: ### Supported DShot Modes -| Mode | Bitrate | Bit Time | Frame Time | Use Case | -|----------|-------------|----------|------------|----------| -| DSHOT150 | 150 kbit/s | 6.67 µs | ~107 µs | Long wires, EMI-prone | -| DSHOT300 | 300 kbit/s | 3.33 µs | ~53 µs | Standard (recommended) | -| DSHOT600 | 600 kbit/s | 1.67 µs | ~27 µs | High performance | -| DSHOT1200| 1200 kbit/s | 0.83 µs | ~13 µs | Racing applications | +| DSHOT | Bitrate | TH1 | TH0 | Bit Time (µs) | Frame Time (µs) | +|-------|-------------|-------|--------|---------------|-----------------| +| 150 | 150 kbit/s | 5.00 | 2.50 | 6.67 | ~106.72 | +| 300 | 300 kbit/s | 2.50 | 1.25 | 3.33 | ~53.28 | +| 600 | 600 kbit/s | 1.25 | 0.625 | 1.67 | ~26.72 | ### GPIO Configuration ```cpp diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index 6037f5f..4c7d8ff 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -2,31 +2,19 @@ * @file dshot300.ino * @brief Demo sketch for DShotRMT library * @author Wastl Kraus - * @date 2025-09-09 + * @date 2025-06-11 * @license MIT */ #include #include -#include "web/web_content.h" -#include -#include -#include -#include - -// Wifi Configuration -static constexpr auto *ssid = "DShotRMT Control"; -static constexpr auto *password = "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 = Serial0; static constexpr auto USB_SERIAL_BAUD = 115200; // Motor configuration - Pin number or GPIO_PIN +// static constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_17; static constexpr auto MOTOR01_PIN = 17; // Supported: DSHOT150, DSHOT300, DSHOT600, (DSHOT1200) @@ -41,95 +29,59 @@ static constexpr auto MOTOR01_MAGNET_COUNT = 14; // Creates the motor instance DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL); -// Web Server Configuration -AsyncWebServer server(80); -AsyncWebSocket ws("/ws"); - -// Global variables -static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; -static bool isArmed = false; -static bool continuous_throttle = true; - -// Helpers (forward declaration) -void printMenu(); -void handleSerialInput(const String &input); -void handleWebSocketMessage(void *arg, uint8_t *data, size_t len); -void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); -void setArmingStatus(bool armed); - // void setup() { + // Starts the USB Serial Port USB_SERIAL.begin(USB_SERIAL_BAUD); + // Initialize DShot Signal motor01.begin(); + + // Print CPU Info motor01.printCpuInfo(); - // Set IP Address - WiFi.softAPConfig(local_IP, gateway, subnet); - - // Start Wifi Access Point - USB_SERIAL.println("\nStarting Access Point..."); - WiFi.softAP(ssid, password); - - IPAddress IP = WiFi.softAPIP(); - - USB_SERIAL.print("Access Point IP address: "); - USB_SERIAL.println(IP); - - // Init WebSockets and Webserver - USB_SERIAL.println("\nStarting Webserver..."); - ws.onEvent(onWsEvent); - server.addHandler(&ws); - - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send_P(200, "text/html", index_html); }); - - server.begin(); - USB_SERIAL.println("HTTP server started."); - - // Initialize with disarmed state - setArmingStatus(false); - + // printMenu(); } +// void loop() { - static uint64_t last_stats_update = 0; - static uint64_t last_serial_update = 0; + // Safety first + static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; + static bool continuous_throttle = true; + + // Time Measurement + static uint64_t last_stats_print = 0; // Handle serial input if (USB_SERIAL.available() > 0) { String input = USB_SERIAL.readStringUntil('\n'); input.trim(); + if (input.length() > 0) { - handleSerialInput(input); + handleSerialInput(input, throttle, continuous_throttle); } } - // Send throttle value only if armed and continuous mode is enabled - if (isArmed && continuous_throttle && throttle > 0) + // Send throttle value in continuous mode + if (continuous_throttle) { motor01.sendThrottle(throttle); } - else if (!isArmed && continuous_throttle) - { - // Ensure motor is stopped when disarmed - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - } // Print motor stats every 3 seconds in continuous mode - if ((esp_timer_get_time() - last_serial_update >= 3000000)) + if (continuous_throttle && (esp_timer_get_time() - last_stats_print >= 3000000)) { motor01.printDShotInfo(); USB_SERIAL.println(" "); - // Get Motor RPM if bidirectional and armed - if (IS_BIDIRECTIONAL && isArmed) + // Get Motor RPM if bidirectional + if (IS_BIDIRECTIONAL) { dshot_telemetry_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); printDShotTelemetry(telem_result); @@ -138,55 +90,7 @@ void loop() USB_SERIAL.println("Type 'help' to show Menu"); // Time Stamp - last_serial_update = esp_timer_get_time(); - } - - // Update Webserver data every second - if (esp_timer_get_time() - last_stats_update >= 1000000) - { - last_stats_update = esp_timer_get_time(); - - JsonDocument doc; - doc["throttle"] = isArmed ? throttle : 0; - doc["armed"] = isArmed; - - if (IS_BIDIRECTIONAL && isArmed) - { - dshot_telemetry_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - doc["rpm"] = telem_result.motor_rpm; - } - else - { - doc["rpm"] = "N/A"; - } - - String json_output; - serializeJson(doc, json_output); - - // Update clients with the new data - ws.textAll(json_output); - } - - ws.cleanupClients(); -} - -// -void setArmingStatus(bool armed) -{ - isArmed = armed; - - if (!armed) - { - // Safety: Stop motor and reset throttle when disarming - throttle = 0; - continuous_throttle = false; - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - USB_SERIAL.println(" "); - USB_SERIAL.println("=== MOTOR DISARMED - SAFETY STOP EXECUTED ==="); - } - else - { - continuous_throttle = true; + last_stats_print = esp_timer_get_time(); } } @@ -194,74 +98,50 @@ void setArmingStatus(bool armed) void printMenu() { USB_SERIAL.println(" "); - USB_SERIAL.println("***********************************************"); - USB_SERIAL.println(" --- DShotRMT Demo & Web UI --- "); - 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.println("***********************************************"); - USB_SERIAL.println(" cmd - Send DShot command (0 - 47)"); - USB_SERIAL.println(" info - Show motor info"); + USB_SERIAL.println("*******************************************"); + USB_SERIAL.println(" DShotRMT Demo "); + USB_SERIAL.println("*******************************************"); + USB_SERIAL.println(" - Set throttle (48 – 2047)"); + 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(" rpm - Get telemetry data"); } - USB_SERIAL.println("***********************************************"); - USB_SERIAL.println(" h / help - Show this Menu"); - USB_SERIAL.println("***********************************************"); - USB_SERIAL.printf(" Current Status: %s\n", isArmed ? "ARMED" : "DISARMED"); - USB_SERIAL.println("***********************************************"); + USB_SERIAL.println("*******************************************"); + USB_SERIAL.println(" h / help - Show this Menu"); + USB_SERIAL.println("*******************************************"); } -// Handle serial inputs and updates global variables -void handleSerialInput(const String &input) +// +void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous_throttle) { - if (input == "arm") - { - setArmingStatus(true); - } - else if (input == "disarm") - { - setArmingStatus(false); - } - else if (input == "0") + if (input == "0") { + // Stop motor throttle = 0; - continuous_throttle = false; + continuous_throttle = true; dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); printDShotResult(result); } else if (input == "info") { motor01.printDShotInfo(); - USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); } else if (input == "rpm" && IS_BIDIRECTIONAL) { - if (isArmed) - { - dshot_telemetry_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printDShotTelemetry(result); - } - else - { - USB_SERIAL.println("Cannot read RPM - Motor is DISARMED"); - } + dshot_telemetry_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + printDShotTelemetry(result); } else if (input.startsWith("cmd ")) { - if (!isArmed) - { - USB_SERIAL.println("Cannot send command - Motor is DISARMED. Use 'arm' command first."); - return; - } - continuous_throttle = false; + + // 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); @@ -276,127 +156,24 @@ void handleSerialInput(const String &input) { printMenu(); } - else if (input == "status") - { - USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); - USB_SERIAL.printf("Current Throttle: %u\n", throttle); - USB_SERIAL.printf("Continuous Mode: %s\n", continuous_throttle ? "ACTIVE" : "INACTIVE"); - } else { + // Parse input throttle value int throttle_value = input.toInt(); if (throttle_value >= DSHOT_THROTTLE_MIN && throttle_value <= DSHOT_THROTTLE_MAX) { - if (!isArmed) - { - USB_SERIAL.println("Cannot set throttle - Motor is DISARMED. Use 'arm' command first."); - return; - } - throttle = throttle_value; continuous_throttle = true; dshot_result_t result = motor01.sendThrottle(throttle); - - if (result.success) - { - USB_SERIAL.printf("Throttle set to %u (continuous mode active)\n", throttle); - } - } - else if (throttle_value == 0) - { - throttle = 0; - continuous_throttle = false; - dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); printDShotResult(result); } else { USB_SERIAL.println(" "); - USB_SERIAL.printf("Invalid input: '%s'\n", input.c_str()); + USB_SERIAL.printf("Invalid input: '%s'\n", input); USB_SERIAL.printf("Valid throttle range: %d - %d\n", DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX); - USB_SERIAL.println("Use 'arm' to enable motor control"); } } } - -// Websocket request processing -void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) -{ - JsonDocument doc; - DeserializationError error = deserializeJson(doc, data, len); - - if (error) - { - USB_SERIAL.print(F("deserializeJson() failed: ")); - USB_SERIAL.println(error.c_str()); - return; - } - - // Handle arming status - if (doc.containsKey("armed")) - { - bool armed = doc["armed"]; - setArmingStatus(armed); - } - - // Handle throttle value (only if armed) - if (doc.containsKey("throttle") && isArmed) - { - uint16_t web_throttle = doc["throttle"]; - - // Check for valid throttle value - if (web_throttle == 0) - { - throttle = 0; - continuous_throttle = false; - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - } - else if (web_throttle >= DSHOT_THROTTLE_MIN && web_throttle <= DSHOT_THROTTLE_MAX) - { - throttle = web_throttle; - continuous_throttle = true; - } - } - else if (doc.containsKey("throttle") && !isArmed) - { - throttle = 0; - continuous_throttle = false; - // Ignore throttle commands when disarmed - USB_SERIAL.println("Web throttle command ignored - Motor is DISARMED"); - } -} - -// Websocket request handler -void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) -{ - switch (type) - { - case WS_EVT_CONNECT: - USB_SERIAL.printf("Web Client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); - - // Send current arming status to new client - { - JsonDocument doc; - doc["armed"] = isArmed; - doc["throttle"] = isArmed ? throttle : 0; - String json_output; - serializeJson(doc, json_output); - client->text(json_output); - } - break; - - case WS_EVT_DISCONNECT: - USB_SERIAL.printf("Web Client #%u disconnected\n", client->id()); - break; - - case WS_EVT_DATA: - handleWebSocketMessage(arg, data, len); - break; - - case WS_EVT_PONG: - case WS_EVT_ERROR: - break; - } -} diff --git a/examples/web_control/web_control.ino b/examples/web_control/web_control.ino new file mode 100644 index 0000000..b7ce329 --- /dev/null +++ b/examples/web_control/web_control.ino @@ -0,0 +1,433 @@ +/** + * @file dshot300.ino + * @brief Demo sketch for DShotRMT library + * @author Wastl Kraus + * @date 2025-09-09 + * @license MIT + */ + +#include +#include + +#include + +#include +#include +#include + +// Wifi Configuration +static constexpr auto *ssid = "DShotRMT Control"; +static constexpr auto *password = "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 auto MOTOR01_PIN = 17; + +// Supported: DSHOT150, DSHOT300, DSHOT600, (DSHOT1200) +static constexpr dshot_mode_t DSHOT_MODE = DSHOT300; + +// BiDirectional DShot Support (default: false) +static constexpr auto IS_BIDIRECTIONAL = false; + +// 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); + +// Web Server Configuration +AsyncWebServer server(80); +AsyncWebSocket ws("/ws"); + +// Global variables +static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; +static bool isArmed = false; +static bool continuous_throttle = true; + +// Helpers (forward declaration) +void printMenu(); +void handleSerialInput(const String &input); +void handleWebSocketMessage(void *arg, uint8_t *data, size_t len); +void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); +void setArmingStatus(bool armed); + +// +void setup() +{ + USB_SERIAL.begin(USB_SERIAL_BAUD); + + motor01.begin(); + motor01.printCpuInfo(); + + // Set IP Address + WiFi.softAPConfig(local_IP, gateway, subnet); + + // Start Wifi Access Point + USB_SERIAL.println("\nStarting Access Point..."); + WiFi.softAP(ssid, password); + + IPAddress IP = WiFi.softAPIP(); + + USB_SERIAL.print("Access Point IP address: "); + USB_SERIAL.println(IP); + + // Init WebSockets and Webserver + USB_SERIAL.println("\nStarting Webserver..."); + ws.onEvent(onWsEvent); + server.addHandler(&ws); + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send_P(200, "text/html", index_html); }); + + server.begin(); + USB_SERIAL.println("HTTP server started."); + + // Initialize with disarmed state + setArmingStatus(false); + + printMenu(); +} + +void loop() +{ + static uint64_t last_serial_update = 0; + static uint16_t last_sent_throttle = DSHOT_CMD_MOTOR_STOP; + static bool last_sent_armed = false; + static String last_sent_rpm = "N/A"; + + // Handle serial input + if (USB_SERIAL.available() > 0) + { + String input = USB_SERIAL.readStringUntil('\n'); + input.trim(); + if (input.length() > 0) + { + handleSerialInput(input); + } + } + + // Send throttle value only if armed and continuous mode is enabled + if (isArmed && continuous_throttle && throttle > 0) + { + motor01.sendThrottle(throttle); + } + else if (!isArmed && continuous_throttle) + { + // Ensure motor is stopped when disarmed + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + } + + // Print motor stats every 3 seconds in continuous mode + if ((esp_timer_get_time() - last_serial_update >= 3000000)) + { + motor01.printDShotInfo(); + + USB_SERIAL.println(" "); + + // Get Motor RPM if bidirectional and armed + if (IS_BIDIRECTIONAL && isArmed) + { + dshot_telemetry_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + printDShotTelemetry(telem_result); + } + + USB_SERIAL.println("Type 'help' to show Menu"); + + // Time Stamp + last_serial_update = esp_timer_get_time(); + } + + // Update JSON on data change + String current_rpm = "N/A"; + + if (IS_BIDIRECTIONAL && isArmed) + { + dshot_telemetry_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + current_rpm = String(telem_result.motor_rpm); + } + + if (throttle != last_sent_throttle || isArmed != last_sent_armed || current_rpm != last_sent_rpm) + { + // Generate JSON for Webserver + JsonDocument doc; + doc["throttle"] = isArmed ? throttle : 0; + doc["armed"] = isArmed; + doc["rpm"] = current_rpm; + + String json_output; + json_output.reserve(256); + serializeJson(doc, json_output); + + if (ws.count() > 0) + { + ws.textAll(json_output); + } + + // Update last run + last_sent_throttle = throttle; + last_sent_armed = isArmed; + last_sent_rpm = current_rpm; + } + + ws.cleanupClients(); +} + +// +void setArmingStatus(bool armed) +{ + isArmed = armed; + + if (!armed) + { + // Safety: Stop motor and reset throttle when disarming + throttle = 0; + continuous_throttle = false; + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + USB_SERIAL.println(" "); + USB_SERIAL.println("=== MOTOR DISARMED - SAFETY STOP EXECUTED ==="); + } + else + { + continuous_throttle = true; + } +} + +// +void printMenu() +{ + USB_SERIAL.println(" "); + USB_SERIAL.println("***********************************************"); + USB_SERIAL.println(" --- DShotRMT Demo & Web UI --- "); + 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.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("***********************************************"); + USB_SERIAL.printf(" Current Status: %s\n", isArmed ? "ARMED" : "DISARMED"); + USB_SERIAL.println("***********************************************"); +} + +// Handle serial inputs and updates global variables +void handleSerialInput(const String &input) +{ + if (input == "arm") + { + setArmingStatus(true); + } + else if (input == "disarm") + { + setArmingStatus(false); + } + else if (input == "0") + { + throttle = 0; + continuous_throttle = false; + dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + printDShotResult(result); + } + else if (input == "info") + { + motor01.printDShotInfo(); + USB_SERIAL.println(" "); + USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); + } + else if (input == "rpm" && IS_BIDIRECTIONAL) + { + if (isArmed) + { + dshot_telemetry_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + printDShotTelemetry(result); + } + else + { + USB_SERIAL.println(" "); + USB_SERIAL.println("Cannot read RPM - Motor is DISARMED"); + } + } + else if (input.startsWith("cmd ")) + { + if (!isArmed) + { + USB_SERIAL.println(" "); + USB_SERIAL.println("Cannot send command - Motor is DISARMED. Use 'arm' command first."); + return; + } + + continuous_throttle = false; + 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.println(" "); + USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); + } + } + else if (input == "h" || input == "help") + { + printMenu(); + } + else if (input == "status") + { + USB_SERIAL.println(" "); + USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); + USB_SERIAL.printf("Current Throttle: %u\n", throttle); + USB_SERIAL.printf("Continuous Mode: %s\n", continuous_throttle ? "ACTIVE" : "INACTIVE"); + } + else + { + int throttle_value = input.toInt(); + + if (throttle_value >= DSHOT_THROTTLE_MIN && throttle_value <= DSHOT_THROTTLE_MAX) + { + if (!isArmed) + { + USB_SERIAL.println(" "); + USB_SERIAL.println("Cannot set throttle - Motor is DISARMED. Use 'arm' command first."); + return; + } + + throttle = throttle_value; + continuous_throttle = true; + + dshot_result_t result = motor01.sendThrottle(throttle); + + if (result.success) + { + USB_SERIAL.println(" "); + USB_SERIAL.printf("Throttle set to %u (continuous mode active)\n", throttle); + } + } + else if (throttle_value == 0) + { + throttle = 0; + continuous_throttle = false; + dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + printDShotResult(result); + } + else + { + USB_SERIAL.println(" "); + USB_SERIAL.printf("Invalid input: '%s'\n", input.c_str()); + USB_SERIAL.printf("Valid throttle range: %d - %d\n", DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX); + USB_SERIAL.println("Use 'arm' to enable motor control"); + } + } +} + +// Websocket request processing +void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) +{ + JsonDocument doc; + DeserializationError error = deserializeJson(doc, data, len); + + if (error) + { + USB_SERIAL.print(F("deserializeJson() failed: ")); + USB_SERIAL.println(error.c_str()); + return; + } + + // + bool armedFromWeb = false; + + // Handle arming status + if (doc.containsKey("armed")) + { + bool armed = doc["armed"]; + setArmingStatus(armed); + armedFromWeb = true; + } + + // Handle throttle value (only if armed) + if (doc.containsKey("throttle") && isArmed) + { + uint16_t web_throttle = doc["throttle"]; + + // Check for valid throttle value + if (web_throttle == 0) + { + throttle = 0; + continuous_throttle = false; + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + } + else if (web_throttle >= DSHOT_THROTTLE_MIN && web_throttle <= DSHOT_THROTTLE_MAX) + { + throttle = web_throttle; + continuous_throttle = true; + } + } + else if (doc.containsKey("throttle") && !isArmed) + { + throttle = 0; + continuous_throttle = false; + // Ignore throttle commands when disarmed + USB_SERIAL.println(" "); + USB_SERIAL.println("Web throttle command ignored - Motor is DISARMED"); + } + + // Webserver arms with DSHOT_THROTTLE_MIN + if (armedFromWeb && isArmed) + { + throttle = DSHOT_THROTTLE_MIN; + continuous_throttle = true; + motor01.sendThrottle(throttle); + USB_SERIAL.println(" "); + USB_SERIAL.println("Motor armed via Web - throttle set to 48"); + } +} + +// Websocket request handler +void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) +{ + switch (type) + { + case WS_EVT_CONNECT: + USB_SERIAL.printf("Web Client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); + + // Send current arming status to new client + { + JsonDocument doc; + doc["armed"] = isArmed; + doc["throttle"] = isArmed ? throttle : 0; + String json_output; + serializeJson(doc, json_output); + client->text(json_output); + } + break; + + case WS_EVT_DISCONNECT: + USB_SERIAL.printf("Web Client #%u disconnected\n", client->id()); + break; + + case WS_EVT_DATA: + handleWebSocketMessage(arg, data, len); + break; + + case WS_EVT_PONG: + case WS_EVT_ERROR: + break; + } +} diff --git a/library.properties b/library.properties index 19b7cda..1676765 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,12 @@ name=DShotRMT -version=0.7.2 -author=derdoktor667 -maintainer=derdoktor667 +version=0.7.5 +author=Wastl Kraus +maintainer=Wastl Kraus +license=MIT sentence=DShotRMT Library supporting all DShot Types and speeds. Tested with BlHeli_S. -paragraph=This library can control a BlHeli_S by using encoded DShot commands. +paragraph=This library can control a BlHeli_S by using encoded DShot commands. Features bidirectional DShot support for RPM telemetry. category=Signal Input/Output url=https://github.com/derdoktor667/DShotRMT architectures=esp32 +provides_includes=DShotRMT.h, DShotCommandManager.h, dshot_commands.h, web_content.h +depends=ArduinoJson \ No newline at end of file diff --git a/DShotCommandManager.cpp b/src/DShotCommandManager.cpp similarity index 100% rename from DShotCommandManager.cpp rename to src/DShotCommandManager.cpp diff --git a/DShotCommandManager.h b/src/DShotCommandManager.h similarity index 100% rename from DShotCommandManager.h rename to src/DShotCommandManager.h diff --git a/DShotRMT.cpp b/src/DShotRMT.cpp similarity index 100% rename from DShotRMT.cpp rename to src/DShotRMT.cpp diff --git a/src/DShotRMT.h b/src/DShotRMT.h new file mode 100644 index 0000000..1fa2d1c --- /dev/null +++ b/src/DShotRMT.h @@ -0,0 +1,237 @@ +/** + * @file DShotRMT.h + * @brief DShot signal generation using ESP32 RMT with bidirectional support + * @author Wastl Kraus + * @date 2025-06-11 + * @license MIT + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +// DShot Protocol Constants +static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0; +static constexpr auto DSHOT_THROTTLE_MIN = 48; +static constexpr auto DSHOT_THROTTLE_MAX = 2047; +static constexpr auto DSHOT_BITS_PER_FRAME = 16; +static constexpr auto DSHOT_PAUSE_US = 30; // Additional frame pause time +static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000; +static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111; +static constexpr auto DSHOT_CRC_MASK = 0b0000000000001111; +static constexpr auto DSHOT_RX_TIMEOUT_MS = 2; // Never reached, just a timeeout +static constexpr auto GCR_BITS_PER_FRAME = 21; // Number of GCR bits in a DShot answer frame (1 start + 16 data + 4 CRC) +static constexpr auto DEFAULT_MOTOR_MAGNET_COUNT = 14; +static constexpr auto MAGNETS_PER_POLE_PAIR = 2; +static constexpr auto MIN_POLE_PAIRS = 1; +static constexpr auto NO_DSHOT_ERPM = 0; +static constexpr auto NO_DSHOT_RPM = 0; + +// RMT Configuration Constants +constexpr auto DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT; +constexpr auto DSHOT_RMT_RESOLUTION = 10 * 1000 * 1000; // 10 MHz resolution +constexpr auto RMT_BUFFER_SIZE = DSHOT_BITS_PER_FRAME; +constexpr auto RMT_BUFFER_SYMBOLS = 64; +constexpr auto RMT_QUEUE_DEPTH = 1; + +// Smallest pulse for DShot1200 is 2us. Largest for DShot150 is 40us. +// The range is set from 3us (3000ns) to 60us (60000ns) to be safe across all modes. +constexpr uint32_t DSHOT_PULSE_MIN = 3000; +constexpr uint32_t DSHOT_PULSE_MAX = 60000; + +// DShot Modes +typedef enum +{ + DSHOT_OFF, + DSHOT150, + DSHOT300, + DSHOT600, + DSHOT1200 +} dshot_mode_t; + +// DShot Packet +typedef struct +{ + uint16_t throttle_value : 11; + bool telemetric_request : 1; + uint16_t checksum : 4; +} dshot_packet_t; + +// DShot Timing Configuration +typedef struct +{ + uint32_t frame_length_us; + uint16_t ticks_per_bit; + uint16_t ticks_one_high; + uint16_t ticks_one_low; + uint16_t ticks_zero_high; + uint16_t ticks_zero_low; +} dshot_timing_t; + +// Error handling +typedef struct +{ + bool success; + const char *msg; +} dshot_result_t; + +// DShot telemetry result +typedef struct +{ + bool success; + uint16_t erpm; + uint16_t motor_rpm; + const char *msg; +} dshot_telemetry_result_t; + +// Naming convention +typedef dshotCommands_e dshot_commands_t; + +// --- HELPERS --- +void printDShotResult(dshot_result_t &result, Stream &output = Serial); +void printDShotTelemetry(dshot_telemetry_result_t &result, Stream &output = Serial); + +// +class DShotRMT +{ +public: + // Constructor with GPIO enum + explicit DShotRMT(gpio_num_t gpio = GPIO_NUM_16, dshot_mode_t mode = DSHOT300, bool is_bidirectional = false); + + // Constructor with pin number + DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional); + + // Destructor for "better" code + ~DShotRMT(); + + // Initialize the RMT module and DShot config + dshot_result_t begin(); + + // Send throttle value (48-2047) + dshot_result_t sendThrottle(uint16_t throttle); + + // Send DShot command (0-47) + dshot_result_t sendCommand(uint16_t command); + + // --- GETTERS --- + gpio_num_t getGPIO() const { return _gpio; } + uint16_t getDShotPacket() const { return _parsed_packet; } + bool is_bidirectional() const { return _is_bidirectional; } + dshot_mode_t getMode() const { return _mode; } + dshot_telemetry_result_t getTelemetry(uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); + + // --- INFO --- + void printDShotInfo(Stream &output = Serial) const; + void printCpuInfo(Stream &output = Serial) const; + + // --- DEPRECATED METHODS --- + [[deprecated("Use sendThrottle() instead")]] + bool setThrottle(uint16_t throttle) + { + auto result = sendThrottle(throttle); + return result.success; + } + + [[deprecated("Use sendCommand() instead")]] + bool sendDShotCommand(uint16_t command) + { + auto result = sendCommand(command); + return result.success; + } + + [[deprecated("Use getTelemetry() instead")]] + uint32_t getMotorRPM(uint8_t magnet_count) + { + auto result = getTelemetry(magnet_count); + return result.success; + } + +private: + // --- CONFIG --- + gpio_num_t _gpio; + dshot_mode_t _mode; + bool _is_bidirectional; + uint32_t _frame_timer_us; + const dshot_timing_t &_timing_config; + uint16_t _last_throttle; + + // --- TIMING & PACKET VARIABLES --- + uint64_t _last_transmission_time; + uint16_t _parsed_packet; + dshot_packet_t _packet; + uint8_t _bitPositions[DSHOT_BITS_PER_FRAME]; + uint16_t _level0; + uint16_t _level1; + + // --- RMT HARDWARE HANDLES --- + rmt_channel_handle_t _rmt_tx_channel; + rmt_channel_handle_t _rmt_rx_channel; + rmt_encoder_handle_t _dshot_encoder; + + // --- RMT CONFIG STRUCTURES --- + rmt_tx_channel_config_t _tx_channel_config; + rmt_rx_channel_config_t _rx_channel_config; + rmt_transmit_config_t _transmit_config; + rmt_receive_config_t _receive_config; + + // --- INITS --- + dshot_result_t _initTXChannel(); + dshot_result_t _initRXChannel(); + dshot_result_t _initDShotEncoder(); + + // --- PACKET MANAGEMENT --- + dshot_packet_t _buildDShotPacket(const uint16_t value); + uint16_t _parseDShotPacket(const dshot_packet_t &packet); + uint16_t _calculateCRC(const uint16_t data); + void _preCalculateBitPositions(); + + // --- FRAME PROCESSING --- + dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); + bool IRAM_ATTR _encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols); + uint16_t _decodeDShotFrame(const rmt_symbol_word_t *symbols); + + // --- TIMING CONTROL --- + bool IRAM_ATTR _timer_signal(); + bool _timer_reset(); + + // -- CALLBACKS --- + rmt_rx_event_callbacks_t _rx_event_callbacks; + volatile rmt_symbol_word_t _rx_symbols_direct[GCR_BITS_PER_FRAME]; + volatile uint16_t _last_erpm_atomic; + volatile bool _telemetry_ready_flag; + static bool IRAM_ATTR _rmt_rx_done_callback(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data); + + // --- DSHOT DEFAULTS --- + static constexpr auto const DSHOT_TELEMETRY_INVALID = (0xffff); + + // --- CONSTANTS & ERROR MESSAGES --- + static constexpr bool DSHOT_OK = 0; + static constexpr bool DSHOT_ERROR = 1; + + static constexpr char const *NONE = ""; + static constexpr char const *UNKNOWN_ERROR = "Unknown Error!"; + static constexpr char const *INIT_SUCCESS = "SignalGeneratorRMT initialized successfully"; + static constexpr char const *INIT_FAILED = "SignalGeneratorRMT init failed!"; + static constexpr char const *TX_INIT_SUCCESS = "TX RMT channel initialized successfully"; + static constexpr char const *TX_INIT_FAILED = "TX RMT channel init failed!"; + static constexpr char const *RX_INIT_SUCCESS = "RX RMT channel initialized successfully"; + static constexpr char const *RX_INIT_FAILED = "RX RMT channel init failed!"; + static constexpr char const *RX_BUFFER_FAILED = "RX RMT buffer init failed!"; + static constexpr char const *ENCODER_INIT_SUCCESS = "RMT encoder initialized successfully"; + static constexpr char const *ENCODER_INIT_FAILED = "RMT encoder init failed!"; + static constexpr char const *TRANSMISSION_SUCCESS = "Transmission successfully"; + 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 *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!"; + static constexpr char const *TELEMETRY_FAILED = "No valid Telemetric Frame received!"; + static constexpr char const *INVALID_MAGNET_COUNT = "Invalid motor magnet count!"; + static constexpr char const *TIMING_CORRECTION = "Timing correction!"; +}; diff --git a/dshot_commands.h b/src/dshot_commands.h similarity index 100% rename from dshot_commands.h rename to src/dshot_commands.h diff --git a/web/web_content.h b/src/web_content.h similarity index 99% rename from web/web_content.h rename to src/web_content.h index d831a1e..1a4d8d9 100644 --- a/web/web_content.h +++ b/src/web_content.h @@ -6,6 +6,8 @@ * @license MIT */ +#pragma once + // Web Site Content const char index_html[] PROGMEM = R"rawliteral(