diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cfda7b..ee30cda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,139 +3,76 @@ name: ESP32 Build & Quality Check on: push: branches: [ '*' ] - release: - types: [ published ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true jobs: # ============================================================================ # Code Quality & Linting # ============================================================================ quality-check: - name: 'Code Quality' + name: 'Arduino Lint Check' runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Arduino CLI uses: arduino/setup-arduino-cli@v2 - - name: Cache Arduino data - uses: actions/cache@v4 - with: - path: | - ~/ - key: ${{ runner.os }}-arduino-${{ hashFiles('**/libraries/**') }} - restore-keys: | - ${{ runner.os }}-arduino- - - name: Install ESP32 core run: | - arduino-cli core update-index - arduino-cli core install esp32:esp32 + arduino-cli core update-index > /dev/null + arduino-cli core install esp32:esp32 > /dev/null - name: Arduino Lint - uses: arduino/arduino-lint-action@v1 + uses: arduino/arduino-lint-action@v2 with: path: ${{ github.workspace }} compliance: strict library-manager: update verbose: true - # ============================================================================ +# ============================================================================ # Compilation Test # ============================================================================ compile-test: - name: 'Compile Example Sketch' + name: 'Compile Example Sketches' runs-on: ubuntu-latest timeout-minutes: 15 + strategy: fail-fast: false matrix: - example: + examples: - "examples/dshot300/dshot300.ino" - "examples/command_manager/command_manager.ino" - build-flags: - - name: "Release" - flags: "Automated Build" + - "examples/web_control/web_control.ino" steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Arduino CLI uses: arduino/setup-arduino-cli@v2 - - name: Cache Arduino data - uses: actions/cache@v4 - with: - path: | - ~/ - key: ${{ runner.os }}-arduino-${{ hashFiles('**/libraries/**') }} - restore-keys: | - ${{ runner.os }}-arduino- - - - name: Install ESP32 core + - name: Install ESP32 Core and Dependencies run: | arduino-cli core update-index arduino-cli core install esp32:esp32 - - name: Install Repo as Library + 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: | - mkdir -p $HOME/Arduino/libraries - ln -s $PWD $HOME/Arduino/libraries/DShotRMT - - - name: Compile Example (${{ matrix.build-flags.name }}) - run: | - arduino-cli compile --fqbn esp32:esp32:esp32 ${{ matrix.example }} - - # ============================================================================ - # Static Code Analysis - # ============================================================================ - static-analysis: - name: 'Static Analysis' - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Setup Arduino CLI - uses: arduino/setup-arduino-cli@v2 - - - name: Cache Arduino data - uses: actions/cache@v4 - with: - path: | - ~/ - key: ${{ runner.os }}-arduino-${{ hashFiles('**/libraries/**') }} - restore-keys: | - ${{ runner.os }}-arduino- - - - name: Install ESP32 core - run: | - arduino-cli core update-index - arduino-cli core install esp32:esp32 - - - name: Install Cppcheck - run: sudo apt-get update && sudo apt-get install -y cppcheck - - - name: Run Cppcheck - run: | - cppcheck --enable=warning,performance \ - --std=c++17 \ - --language=c++ \ - --platform=unix32 \ - --inline-suppr \ - ./DShotRMT.cpp ./DShotRMT.h \ - ./DShotCommandManager.cpp ./DShotCommandManager.h + arduino-cli compile --fqbn esp32:esp32:esp32 --library ${{ github.workspace }} ${{ matrix.examples}} # ============================================================================ # Build Status Report @@ -144,7 +81,7 @@ jobs: name: 'Build Summary' runs-on: ubuntu-latest if: always() - needs: [quality-check, compile-test, static-analysis] + needs: [quality-check, compile-test] steps: - name: Create Build Summary @@ -168,19 +105,11 @@ jobs: echo "| 🔨 Compilation | ❌ Failed | Compilation errors detected |" >> $GITHUB_STEP_SUMMARY fi - # Static Analysis Status - if [[ "${{ needs.static-analysis.result }}" == "success" ]]; then - echo "| 🔍 Static Analysis | ✅ Passed | No critical issues found |" >> $GITHUB_STEP_SUMMARY - else - echo "| 🔍 Static Analysis | ❌ Failed | Issues detected by CPPCheck |" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY # Overall Status if [[ "${{ needs.quality-check.result }}" == "success" && - "${{ needs.compile-test.result }}" == "success" && - "${{ needs.static-analysis.result }}" == "success" ]]; then + "${{ needs.compile-test.result }}" == "success" ]]; then echo "## 🎉 All Checks Passed!" >> $GITHUB_STEP_SUMMARY echo "Your DShotRMT library is ready for deployment." >> $GITHUB_STEP_SUMMARY else diff --git a/.gitignore b/.gitignore index 304b85a..b2a6323 100644 --- a/.gitignore +++ b/.gitignore @@ -13,12 +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 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/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 98% rename from DShotRMT.cpp rename to src/DShotRMT.cpp index edcbd13..04331f3 100644 --- a/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -182,7 +182,7 @@ dshot_result_t DShotRMT::_initRXChannel() } // Callback for RMT RX -bool IRAM_ATTR DShotRMT::_rmt_rx_done_callback(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data) +bool DShotRMT::_rmt_rx_done_callback(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data) { DShotRMT *instance = static_cast(user_data); @@ -414,7 +414,7 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) } // Encode DShot packet into RMT symbol format (placed in IRAM for performance) -bool IRAM_ATTR DShotRMT::_encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols) +bool DShotRMT::_encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols) { _parsed_packet = _parseDShotPacket(packet); @@ -482,7 +482,7 @@ uint16_t DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) } // Check if enough time has passed for next transmission -bool IRAM_ATTR DShotRMT::_timer_signal() +bool DShotRMT::_timer_signal() { uint64_t current_time = esp_timer_get_time(); 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/src/web_content.h b/src/web_content.h new file mode 100644 index 0000000..1a4d8d9 --- /dev/null +++ b/src/web_content.h @@ -0,0 +1,356 @@ +/** + * @file web_content.h + * @brief DShotRMT_Control Website content with Arming Switch + * @author Wastl Kraus + * @date 2025-09-09 + * @license MIT + */ + +#pragma once + +// Web Site Content +const char index_html[] PROGMEM = R"rawliteral( + + + + + + + DShotRMT_Web + + + + +

DShotRMT Control Demo

+
+ +
+
+ ARMING SWITCH + +
+
+ DISARMED +
+
+ ⚠️ Motor control disabled when disarmed +
+
+ + +
+
0
+ +
+ +
+ RPM: -- +
+
+ + + + + +)rawliteral"; \ No newline at end of file