Add Web Server

- fix warnings and update action versions
- Web Server added in example
- prepare release 0.7.5
This commit is contained in:
Wastl Kraus 2025-09-10 15:37:24 +02:00 committed by GitHub
commit 066a06f767
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1199 additions and 344 deletions

View File

@ -3,139 +3,76 @@ name: ESP32 Build & Quality Check
on: on:
push: push:
branches: [ '*' ] branches: [ '*' ]
release:
types: [ published ]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
# ============================================================================ # ============================================================================
# Code Quality & Linting # Code Quality & Linting
# ============================================================================ # ============================================================================
quality-check: quality-check:
name: 'Code Quality' name: 'Arduino Lint Check'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Arduino CLI - name: Setup Arduino CLI
uses: arduino/setup-arduino-cli@v2 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
run: | run: |
arduino-cli core update-index arduino-cli core update-index > /dev/null
arduino-cli core install esp32:esp32 arduino-cli core install esp32:esp32 > /dev/null
- name: Arduino Lint - name: Arduino Lint
uses: arduino/arduino-lint-action@v1 uses: arduino/arduino-lint-action@v2
with: with:
path: ${{ github.workspace }} path: ${{ github.workspace }}
compliance: strict compliance: strict
library-manager: update library-manager: update
verbose: true verbose: true
# ============================================================================ # ============================================================================
# Compilation Test # Compilation Test
# ============================================================================ # ============================================================================
compile-test: compile-test:
name: 'Compile Example Sketch' name: 'Compile Example Sketches'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
example: examples:
- "examples/dshot300/dshot300.ino" - "examples/dshot300/dshot300.ino"
- "examples/command_manager/command_manager.ino" - "examples/command_manager/command_manager.ino"
build-flags: - "examples/web_control/web_control.ino"
- name: "Release"
flags: "Automated Build"
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Arduino CLI - name: Setup Arduino CLI
uses: arduino/setup-arduino-cli@v2 uses: arduino/setup-arduino-cli@v2
- name: Cache Arduino data - name: Install ESP32 Core and Dependencies
uses: actions/cache@v4
with:
path: |
~/
key: ${{ runner.os }}-arduino-${{ hashFiles('**/libraries/**') }}
restore-keys: |
${{ runner.os }}-arduino-
- name: Install ESP32 core
run: | run: |
arduino-cli core update-index arduino-cli core update-index
arduino-cli core install esp32:esp32 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: | run: |
mkdir -p $HOME/Arduino/libraries arduino-cli compile --fqbn esp32:esp32:esp32 --library ${{ github.workspace }} ${{ matrix.examples}}
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
# ============================================================================ # ============================================================================
# Build Status Report # Build Status Report
@ -144,7 +81,7 @@ jobs:
name: 'Build Summary' name: 'Build Summary'
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
needs: [quality-check, compile-test, static-analysis] needs: [quality-check, compile-test]
steps: steps:
- name: Create Build Summary - name: Create Build Summary
@ -168,19 +105,11 @@ jobs:
echo "| 🔨 Compilation | ❌ Failed | Compilation errors detected |" >> $GITHUB_STEP_SUMMARY echo "| 🔨 Compilation | ❌ Failed | Compilation errors detected |" >> $GITHUB_STEP_SUMMARY
fi 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 echo "" >> $GITHUB_STEP_SUMMARY
# Overall Status # Overall Status
if [[ "${{ needs.quality-check.result }}" == "success" && if [[ "${{ needs.quality-check.result }}" == "success" &&
"${{ needs.compile-test.result }}" == "success" && "${{ needs.compile-test.result }}" == "success" ]]; then
"${{ needs.static-analysis.result }}" == "success" ]]; then
echo "## 🎉 All Checks Passed!" >> $GITHUB_STEP_SUMMARY echo "## 🎉 All Checks Passed!" >> $GITHUB_STEP_SUMMARY
echo "Your DShotRMT library is ready for deployment." >> $GITHUB_STEP_SUMMARY echo "Your DShotRMT library is ready for deployment." >> $GITHUB_STEP_SUMMARY
else else

5
.gitignore vendored
View File

@ -13,12 +13,11 @@
# Built Visual Studio Code Extensions # Built Visual Studio Code Extensions
*.vsix *.vsix
# Caching ESP32 Builds # Builds
*.code-workspace
buildCache buildCache
build build
examples/dshot300/debug.cfg examples/dshot300/debug.cfg
examples/dshot300/esp32.svd examples/dshot300/esp32.svd
examples/dshot300/debug_custom.json examples/dshot300/debug_custom.json
examples/dshot300/debug.svd examples/dshot300/debug.svd
/build
/.github/chatmodes

View File

@ -6,231 +6,5 @@
* @license MIT * @license MIT
*/ */
#pragma once #include "src/DShotRMT.h"
#include <Arduino.h>
#include <dshot_commands.h>
#include <driver/gpio.h>
#include <driver/rmt_tx.h>
#include <driver/rmt_rx.h>
// 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!";
};

150
README.md
View File

@ -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`). 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. 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. > 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 - **All DShot Modes:** DSHOT150, DSHOT300 (default), DSHOT600, DSHOT1200
- **BiDirectional DShot:** Full support for RPM telemetry feedback - **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 - **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 - **Hardware-Timed Signals:** Independent, precise signal generation using ESP32 RMT peripheral
- **Configurable Timing:** Ensures ESCs can reliably detect frame boundaries - **Configurable Timing:** Ensures ESCs can reliably detect frame boundaries
- **Error Handling:** Comprehensive result reporting with success/failure status - **Error Handling:** Comprehensive result reporting with success/failure status
@ -46,6 +50,25 @@ lib_deps =
git clone https://github.com/derdoktor667/DShotRMT.git 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 ## ⚡ Quick Start
@ -79,6 +102,64 @@ void loop() {
} }
``` ```
### Web Control Interface
```cpp
#include <DShotRMT.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
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 <DShotRMT.h>
#include <DShotCommandManager.h>
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) ### Bidirectional DShot (RPM Telemetry)
```cpp ```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 ## 📚 Examples
The library includes comprehensive examples: The library includes comprehensive examples:
### 1. Basic DShot Control (`dshot300.ino`) ### 1. Basic DShot Control with Web Interface (`dshot300.ino`)
- Simple throttle control - **Web Control Interface:** Modern responsive web UI accessible at `http://10.10.10.1`
- Command execution - **WiFi Access Point:** Creates hotspot "DShotRMT Control" for wireless control
- Serial interface for testing - **Safety Features:** Arming/disarming system with motor safety lockout
- Telemetry reading (if bidirectional enabled) - **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`) ### 2. Advanced Command Management (`command_manager.ino`)
Interactive ESC control with full menu system: Interactive ESC control with full menu system:
@ -143,12 +268,11 @@ Advanced Commands:
### Supported DShot Modes ### Supported DShot Modes
| Mode | Bitrate | Bit Time | Frame Time | Use Case | | DSHOT | Bitrate | TH1 | TH0 | Bit Time (µs) | Frame Time (µs) |
|----------|-------------|----------|------------|----------| |-------|-------------|-------|--------|---------------|-----------------|
| DSHOT150 | 150 kbit/s | 6.67 µs | ~107 µs | Long wires, EMI-prone | | 150 | 150 kbit/s | 5.00 | 2.50 | 6.67 | ~106.72 |
| DSHOT300 | 300 kbit/s | 3.33 µs | ~53 µs | Standard (recommended) | | 300 | 300 kbit/s | 2.50 | 1.25 | 3.33 | ~53.28 |
| DSHOT600 | 600 kbit/s | 1.67 µs | ~27 µs | High performance | | 600 | 600 kbit/s | 1.25 | 0.625 | 1.67 | ~26.72 |
| DSHOT1200| 1200 kbit/s | 0.83 µs | ~13 µs | Racing applications |
### GPIO Configuration ### GPIO Configuration
```cpp ```cpp

View File

@ -0,0 +1,433 @@
/**
* @file dshot300.ino
* @brief Demo sketch for DShotRMT library
* @author Wastl Kraus
* @date 2025-09-09
* @license MIT
*/
#include <Arduino.h>
#include <WiFi.h>
#include <DShotRMT.h>
#include <ArduinoJson.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
// 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(" <value> - Set throttle (48 2047)");
USB_SERIAL.println(" 0 - Stop motor");
USB_SERIAL.println("***********************************************");
USB_SERIAL.println(" cmd <number> - Send DShot command (0 - 47)");
USB_SERIAL.println(" info - Show motor info");
if (IS_BIDIRECTIONAL)
{
USB_SERIAL.println(" rpm - Get telemetry data");
}
USB_SERIAL.println("***********************************************");
USB_SERIAL.println(" h / help - Show this Menu");
USB_SERIAL.println("***********************************************");
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;
}
}

View File

@ -1,9 +1,12 @@
name=DShotRMT name=DShotRMT
version=0.7.2 version=0.7.5
author=derdoktor667 author=Wastl Kraus <wir-sind-die-matrix.de>
maintainer=derdoktor667 maintainer=Wastl Kraus <wir-sind-die-matrix.de>
license=MIT
sentence=DShotRMT Library supporting all DShot Types and speeds. Tested with BlHeli_S. 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 category=Signal Input/Output
url=https://github.com/derdoktor667/DShotRMT url=https://github.com/derdoktor667/DShotRMT
architectures=esp32 architectures=esp32
provides_includes=DShotRMT.h, DShotCommandManager.h, dshot_commands.h, web_content.h
depends=ArduinoJson

View File

@ -182,7 +182,7 @@ dshot_result_t DShotRMT::_initRXChannel()
} }
// Callback for RMT RX // 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<DShotRMT *>(user_data); DShotRMT *instance = static_cast<DShotRMT *>(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) // 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); _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 // 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(); uint64_t current_time = esp_timer_get_time();

237
src/DShotRMT.h Normal file
View File

@ -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 <Arduino.h>
#include <dshot_commands.h>
#include <web_content.h>
#include <driver/gpio.h>
#include <driver/rmt_tx.h>
#include <driver/rmt_rx.h>
// 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!";
};

356
src/web_content.h Normal file
View File

@ -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(
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>DShotRMT_Web</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background-color: #2c3e50;
color: #ecf0f1;
margin: 0;
height: 100dvh;
justify-content: center;
}
h1 {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 20px;
}
.control-container {
background-color: #34495e;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
text-align: center;
width: 85%;
max-width: 500px;
}
/* Arming Switch Styles */
.arming-section {
margin-bottom: 25px;
padding: 15px;
background-color: #2c3e50;
border-radius: 8px;
border: 2px solid #e74c3c;
}
.arming-switch {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
margin-bottom: 10px;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider-switch {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #e74c3c;
-webkit-transition: .4s;
transition: .4s;
border-radius: 34px;
}
.slider-switch:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
input:checked+.slider-switch {
background-color: #27ae60;
}
input:checked+.slider-switch:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
.arming-label {
font-size: 1.2em;
font-weight: bold;
}
.arming-status {
font-size: 0.9em;
margin-top: 5px;
}
.status-disarmed {
color: #e74c3c;
}
.status-armed {
color: #27ae60;
}
/* Throttle Section */
.throttle-section {
opacity: 0.3;
transition: opacity 0.3s ease;
}
.throttle-section.armed {
opacity: 1;
}
#throttleValue {
font-size: 2.5em;
font-weight: bold;
color: #3498db;
margin-bottom: 20px;
}
#throttleSlider {
appearance: none;
width: 100%;
height: 25px;
background: #2c3e50;
outline: none;
border-radius: 12px;
}
#throttleSlider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 40px;
height: 40px;
background: #3498db;
cursor: pointer;
border-radius: 50%;
}
#throttleSlider::-moz-range-thumb {
width: 40px;
height: 40px;
background: #3498db;
cursor: pointer;
border-radius: 50%;
border: none;
}
.stats {
margin-top: 20px;
font-size: 1.2em;
}
.stats span {
font-weight: bold;
color: #e67e22;
}
.warning-text {
color: #e74c3c;
font-size: 0.9em;
margin-top: 10px;
font-style: italic;
}
</style>
</head>
<body>
<h1>DShotRMT Control Demo</h1>
<div class="control-container">
<!-- Arming Section -->
<div class="arming-section">
<div class="arming-switch">
<span class="arming-label">ARMING SWITCH</span>
<label class="switch">
<input type="checkbox" id="armingSwitch">
<span class="slider-switch"></span>
</label>
</div>
<div class="arming-status">
<span id="armingStatus" class="status-disarmed">DISARMED</span>
</div>
<div class="warning-text">
Motor control disabled when disarmed
</div>
</div>
<!-- Throttle Section -->
<div class="throttle-section" id="throttleSection">
<div id="throttleValue">0</div>
<input type="range" min="48" max="2047" value="0" id="throttleSlider" disabled>
</div>
<div class="stats">
RPM: <span id="rpmValue">--</span>
</div>
</div>
<script>
const gateway = `ws://${window.location.hostname}/ws`;
let websocket;
let isArmed = false;
// Init WebSocket
window.addEventListener('load', () => {
initWebSocket();
});
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
function onOpen(event) {
console.log('Connection opened');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
// Getting data from sketch
function onMessage(event) {
try {
const data = JSON.parse(event.data);
if (data.rpm !== undefined) {
document.getElementById('rpmValue').innerText = data.rpm;
}
// Sync web and serial throttle inputs
if (data.throttle !== undefined) {
if (isArmed) {
document.getElementById('throttleSlider').value = data.throttle;
document.getElementById('throttleValue').innerText = data.throttle;
}
}
// Sync arming status if received from ESP32
if (data.armed !== undefined) {
isArmed = data.armed;
updateArmingUI();
}
} catch (e) {
console.error("Error parsing JSON: ", e);
}
}
// Elements
const slider = document.getElementById('throttleSlider');
const sliderValue = document.getElementById('throttleValue');
const armingSwitch = document.getElementById('armingSwitch');
const armingStatus = document.getElementById('armingStatus');
const throttleSection = document.getElementById('throttleSection');
// Arming switch event
armingSwitch.addEventListener('change', () => {
isArmed = armingSwitch.checked;
updateArmingUI();
// Send arming status to ESP32
const message = JSON.stringify({
"armed": isArmed,
"throttle": isArmed ? parseInt(slider.value) : 0
});
console.log("Sending arming status: ", message);
websocket.send(message);
// If disarmed, set throttle to 0
if (!isArmed) {
slider.value = 0;
sliderValue.innerText = 0;
}
});
// Update UI based on arming status
function updateArmingUI() {
if (isArmed) {
armingStatus.innerText = 'ARMED';
armingStatus.className = 'status-armed';
throttleSection.classList.add('armed');
slider.disabled = false;
} else {
armingStatus.innerText = 'DISARMED';
armingStatus.className = 'status-disarmed';
throttleSection.classList.remove('armed');
slider.disabled = true;
slider.value = 0;
sliderValue.innerText = 0;
}
}
// Throttle slider event
slider.addEventListener('input', () => {
if (!isArmed) {
slider.disabled = true;
slider.value = 0;
sliderValue.innerText = 0;
return;
}
const throttle = slider.value;
sliderValue.innerText = throttle;
const message = JSON.stringify({
"throttle": parseInt(throttle),
"armed": isArmed
});
console.log("Sending throttle: ", message);
websocket.send(message);
});
// Initialize UI
updateArmingUI();
</script>
</body>
</html>
)rawliteral";