From 13ff00e99bdd95817fe86243912d53a07f991553 Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Thu, 11 Sep 2025 13:58:22 +0200 Subject: [PATCH 1/4] Add Web Client example --- .github/workflows/ci.yml | 1 + README.md | 4 +- examples/command_manager/command_manager.ino | 138 +++-- examples/dshot300/dshot300.ino | 8 +- examples/web_client/web_client.ino | 570 +++++++++++++++++++ examples/web_control/web_control.ino | 158 ++--- src/DShotRMT.cpp | 26 +- src/DShotRMT.h | 18 +- 8 files changed, 755 insertions(+), 168 deletions(-) create mode 100644 examples/web_client/web_client.ino diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee30cda..6ce98f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,7 @@ jobs: - "examples/dshot300/dshot300.ino" - "examples/command_manager/command_manager.ino" - "examples/web_control/web_control.ino" + - "examples/web_client/web_client.ino" steps: - name: Checkout Repository diff --git a/README.md b/README.md index 618a589..fd1382b 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,9 @@ The library requires these additional libraries for full functionality: ```ini lib_deps = https://github.com/derdoktor667/DShotRMT - bblanchon/ArduinoJson + https://github.com/bblanchon/ArduinoJson https://github.com/ESP32Async/ESPAsyncWebServer - https://github.com/ESP32Async/AsyncTCP ~/Arduino/libraries/AsyncTCP + https://github.com/ESP32Async/AsyncTCP ``` **Command Manager Example:** diff --git a/examples/command_manager/command_manager.ino b/examples/command_manager/command_manager.ino index 9b4e974..13b2228 100644 --- a/examples/command_manager/command_manager.ino +++ b/examples/command_manager/command_manager.ino @@ -29,9 +29,9 @@ DShotCommandManager commandManager(motor01); static volatile uint16_t throttle_now = 0; // Helper function to print telemetry results -void printTelemetryResult(const dshot_telemetry_result_t &result) +void printTelemetryResult(const dshot_result_t &result) { - if (result.success) + if (result.success && (result.erpm > 0 || result.motor_rpm > 0)) { USB_SERIAL.printf("Telemetry: eRPM=%u, Motor RPM=%u\n", result.erpm, result.motor_rpm); } @@ -90,7 +90,7 @@ void loop() // Get Motor RPM if (IS_BIDIRECTIONAL) { - dshot_telemetry_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); printTelemetryResult(telem_result); } @@ -112,60 +112,81 @@ void handleUserInput(const String &input) USB_SERIAL.print("Stopping motor... "); cmd_result = commandManager.stopMotor(); printResult(cmd_result); + return; } - else if (input == "2") + + if (input == "2") { USB_SERIAL.print("Activating beacon 1... "); cmd_result = commandManager.activateBeacon(1); printResult(cmd_result); + return; } - else if (input == "3") + + if (input == "3") { USB_SERIAL.print("Setting normal spin direction... "); cmd_result = commandManager.setSpinDirection(false); printResult(cmd_result); + return; } - else if (input == "4") + + if (input == "4") { USB_SERIAL.print("Setting reversed spin direction... "); cmd_result = commandManager.setSpinDirection(true); printResult(cmd_result); + return; } - else if (input == "5") + + if (input == "5") { USB_SERIAL.print("Getting ESC Info... "); cmd_result = commandManager.requestESCInfo(); printResult(cmd_result); + return; } - else if (input == "6") + + if (input == "6") { USB_SERIAL.print("Turning LED 0 ON... "); cmd_result = commandManager.setLED(0, true); printResult(cmd_result); + return; } - else if (input == "7") + + if (input == "7") { USB_SERIAL.print("Turning LED 0 OFF... "); cmd_result = commandManager.setLED(0, false); printResult(cmd_result); + return; } - else if (input == "h" || input == "help") + + if (input == "h" || input == "help") { printMenu(); + return; } - else if (input == "info") + + if (input == "info") { motor01.printDShotInfo(); + return; } - else if (input == "rpm" && IS_BIDIRECTIONAL) + + if (input == "rpm" && IS_BIDIRECTIONAL) { - dshot_telemetry_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); printTelemetryResult(result); + return; } - else if (input.startsWith("cmd ")) + + if (input.startsWith("cmd ")) { // Direct command execution: "cmd 5" sends command 5 int cmd_num = input.substring(4).toInt(); + if (DShotCommandManager::isValidCommand(static_cast(cmd_num))) { USB_SERIAL.printf("Sending command %d (%s)... ", cmd_num, @@ -177,11 +198,14 @@ void handleUserInput(const String &input) { USB_SERIAL.printf("Invalid command number: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); } + return; } - else if (input.startsWith("throttle ")) + + if (input.startsWith("throttle ")) { // Throttle control: "throttle 1000" sets throttle to 1000 int throttle_value = input.substring(9).toInt(); + if (throttle_value >= DSHOT_THROTTLE_MIN && throttle_value <= DSHOT_THROTTLE_MAX) { throttle_now = throttle_value; @@ -195,8 +219,10 @@ void handleUserInput(const String &input) { USB_SERIAL.println("Continuous throttle mode enabled. Send '0' or 'throttle 0' to stop."); } + return; } - else if (throttle_value == 0) + + if (throttle_value == 0) { throttle_now = 0; USB_SERIAL.println("Continuous throttle stopped."); @@ -204,61 +230,63 @@ void handleUserInput(const String &input) // Send stop command dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); printResult(result); + return; } - else - { - USB_SERIAL.printf("Invalid throttle value: %d (valid range: %d-%d, use 0 to stop)\n", - throttle_value, DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX); - } + + USB_SERIAL.printf("Invalid throttle value: %d (valid range: %d-%d, use 0 to stop)\n", + throttle_value, DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX); + return; } - else if (input == "0") + + if (input == "0") { // Quick stop throttle_now = 0; USB_SERIAL.print("Emergency stop... "); dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); printResult(result); + return; } - else if (input.startsWith("repeat ")) + + if (input.startsWith("repeat ")) { // Repeat command: "repeat cmd 5 count 10" - sends command 5 ten times String params = input.substring(7); - if (params.startsWith("cmd ")) - { - int space_pos = params.indexOf(' ', 4); - if (space_pos > 0 && params.substring(space_pos + 1).startsWith("count ")) - { - int cmd_num = params.substring(4, space_pos).toInt(); - int repeat_count = params.substring(space_pos + 7).toInt(); - if (DShotCommandManager::isValidCommand(static_cast(cmd_num)) && - repeat_count > 0 && repeat_count <= 100) - { - USB_SERIAL.printf("Sending command %d (%s) %d times... ", cmd_num, - DShotCommandManager::getCommandName(static_cast(cmd_num)), - repeat_count); - cmd_result = commandManager.sendCommand(static_cast(cmd_num), repeat_count); - printResult(cmd_result); - } - else - { - USB_SERIAL.println("Invalid command or repeat count (1-100)"); - } - } - else - { - USB_SERIAL.println("Usage: repeat cmd count "); - } - } - else + if (!params.startsWith("cmd ")) { USB_SERIAL.println("Usage: repeat cmd count "); + return; } + + int space_pos = params.indexOf(' ', 4); + + if (space_pos <= 0 || !params.substring(space_pos + 1).startsWith("count ")) + { + USB_SERIAL.println("Usage: repeat cmd count "); + return; + } + + int cmd_num = params.substring(4, space_pos).toInt(); + int repeat_count = params.substring(space_pos + 7).toInt(); + + if (!DShotCommandManager::isValidCommand(static_cast(cmd_num)) || + repeat_count <= 0 || repeat_count > 100) + { + USB_SERIAL.println("Invalid command or repeat count (1-100)"); + return; + } + + USB_SERIAL.printf("Sending command %d (%s) %d times... ", cmd_num, + DShotCommandManager::getCommandName(static_cast(cmd_num)), + repeat_count); + cmd_result = commandManager.sendCommand(static_cast(cmd_num), repeat_count); + printResult(cmd_result); + return; } - else - { - USB_SERIAL.printf("Unknown command: '%s'. Type 'h' or 'help' for help.\n", input.c_str()); - } + + // Unknown command + USB_SERIAL.printf("Unknown command: '%s'. Type 'h' or 'help' for help.\n", input.c_str()); } // @@ -313,7 +341,7 @@ void printMenu() USB_SERIAL.println(" status - Show system status"); if (IS_BIDIRECTIONAL) { - USB_SERIAL.println(" rpm - Get telemetry data"); + USB_SERIAL.println(" rpm - Get telemetry data"); } USB_SERIAL.println(" h / help - Show this Menu"); USB_SERIAL.println("**********************************************"); diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index 4c7d8ff..70f4c77 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -83,8 +83,8 @@ void loop() // Get Motor RPM if bidirectional if (IS_BIDIRECTIONAL) { - dshot_telemetry_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printDShotTelemetry(telem_result); + dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + printDShotResult(telem_result); } USB_SERIAL.println("Type 'help' to show Menu"); @@ -132,8 +132,8 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous } else if (input == "rpm" && IS_BIDIRECTIONAL) { - dshot_telemetry_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printDShotTelemetry(result); + dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + printDShotResult(result); } else if (input.startsWith("cmd ")) { diff --git a/examples/web_client/web_client.ino b/examples/web_client/web_client.ino new file mode 100644 index 0000000..1bc226e --- /dev/null +++ b/examples/web_client/web_client.ino @@ -0,0 +1,570 @@ +/** + * @file web_client.ino + * @brief DShotRMT Web Control as WiFi Client + * @author Wastl Kraus + * @date 2025-09-11 + * @license MIT + */ + +#include +#include + +#include + +#include +#include +#include + +// WiFi Client Configuration - UPDATE THESE VALUES! +static constexpr auto *WIFI_SSID = "YOUR_SSID"; // Enter your WiFi SSID +static constexpr auto *WIFI_PASSWORD = "YOUR_PASSWORD"; // Enter your WiFi password + +// Connection timeout in milliseconds +static constexpr auto WIFI_CONNECT_TIMEOUT = 20000; + +// USB serial port settings +static constexpr auto &USB_SERIAL = Serial; +static constexpr auto USB_SERIAL_BAUD = 115200; + +// Motor configuration - Pin number or GPIO_PIN +static constexpr 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 last_sent_armed = false; +static bool continuous_throttle = true; + +// WiFi status tracking +static bool wifi_connected = false; + +// 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); +bool connectToWiFi(); +void printWiFiStatus(); + +// +void setup() +{ + USB_SERIAL.begin(USB_SERIAL_BAUD); + USB_SERIAL.println(); + USB_SERIAL.println("DShotRMT Web Client Demo Starting..."); + + motor01.begin(); + motor01.printCpuInfo(); + + // Connect to WiFi network + USB_SERIAL.println("\nConnecting to WiFi network..."); + wifi_connected = connectToWiFi(); + + if (wifi_connected) + { + // 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."); + + printWiFiStatus(); + } + else + { + USB_SERIAL.println("\n*** WARNING: WiFi connection failed! ***"); + USB_SERIAL.println("*** Web interface not available ***"); + USB_SERIAL.println("*** Only serial control available ***"); + } + + // 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 String last_sent_rpm = "N/A"; + static uint64_t last_wifi_check = 0; + + // Check WiFi connection every 30 seconds + if (esp_timer_get_time() - last_wifi_check >= 30000000) + { + if (WiFi.status() != WL_CONNECTED && wifi_connected) + { + USB_SERIAL.println("\n*** WiFi connection lost! ***"); + wifi_connected = false; + } + else if (WiFi.status() == WL_CONNECTED && !wifi_connected) + { + USB_SERIAL.println("\n*** WiFi connection restored! ***"); + wifi_connected = true; + printWiFiStatus(); + } + + last_wifi_check = esp_timer_get_time(); + } + + // 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_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + printDShotResult(telem_result); + } + + USB_SERIAL.println("Type 'help' to show Menu"); + + // Time Stamp + last_serial_update = esp_timer_get_time(); + } + + // Update JSON on data change (only if WiFi connected) + if (wifi_connected) + { + String current_rpm = "N/A"; + + if (IS_BIDIRECTIONAL && isArmed) + { + dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + if (telem_result.success && telem_result.motor_rpm > 0) + { + 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(); + } +} + +// Connect to WiFi network +bool connectToWiFi() +{ + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + + USB_SERIAL.printf("Connecting to '%s'", WIFI_SSID); + + uint32_t start_time = millis(); + + while (WiFi.status() != WL_CONNECTED && (millis() - start_time) < WIFI_CONNECT_TIMEOUT) + { + delay(500); + USB_SERIAL.print("."); + } + + if (WiFi.status() == WL_CONNECTED) + { + USB_SERIAL.println(" Connected!"); + return true; + } + else + { + USB_SERIAL.println(" Failed!"); + USB_SERIAL.printf("WiFi Status: %d\n", WiFi.status()); + return false; + } +} + +// Print current WiFi status +void printWiFiStatus() +{ + if (WiFi.status() == WL_CONNECTED) + { + USB_SERIAL.println(); + USB_SERIAL.println("***********************************************"); + USB_SERIAL.println(" --- WIFI INFO --- "); + USB_SERIAL.println("***********************************************"); + USB_SERIAL.printf("SSID: %s\n", WiFi.SSID().c_str()); + USB_SERIAL.printf("IP Address: %s\n", WiFi.localIP().toString().c_str()); + USB_SERIAL.printf("Gateway: %s\n", WiFi.gatewayIP().toString().c_str()); + USB_SERIAL.printf("Subnet: %s\n", WiFi.subnetMask().toString().c_str()); + USB_SERIAL.printf("DNS: %s\n", WiFi.dnsIP().toString().c_str()); + USB_SERIAL.printf("Signal Strength: %d dBm\n", WiFi.RSSI()); + USB_SERIAL.printf("MAC Address: %s\n", WiFi.macAddress().c_str()); + USB_SERIAL.println("***********************************************"); + USB_SERIAL.printf("Web Interface: http://%s\n", WiFi.localIP().toString().c_str()); + USB_SERIAL.println("***********************************************"); + } + else + { + USB_SERIAL.println("WiFi not connected!"); + } +} + +// +void setArmingStatus(bool armed) +{ + isArmed = armed; + + if (armed) + { + continuous_throttle = true; + // dirty - force JSON update for WebSocket clients + last_sent_armed = !armed; // Trigger JSON send in next loop + return; + } + + // 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 ==="); + + // Force JSON update for WebSocket clients + last_sent_armed = !armed; // Trigger JSON send in next loop +} + +// +void printMenu() +{ + USB_SERIAL.println(" "); + USB_SERIAL.println("***********************************************"); + USB_SERIAL.println(" --- DShotRMT Web Client Demo --- "); + USB_SERIAL.println("***********************************************"); + + if (wifi_connected) + { + USB_SERIAL.printf(" Web Interface: http://%s \n", WiFi.localIP().toString().c_str()); + } + else + { + USB_SERIAL.println(" Web Interface: NOT AVAILABLE "); + } + + USB_SERIAL.println("***********************************************"); + USB_SERIAL.println(" arm - Arm motor"); + USB_SERIAL.println(" disarm - Disarm motor (safety)"); + USB_SERIAL.println(" - Set throttle (48 – 2047)"); + USB_SERIAL.println(" 0 - Stop motor"); + USB_SERIAL.println("***********************************************"); + USB_SERIAL.println(" cmd - Send DShot command (0 - 47)"); + USB_SERIAL.println(" info - Show motor info"); + USB_SERIAL.println(" wifi - Show WiFi status"); + USB_SERIAL.println(" reconnect - Reconnect to WiFi"); + 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.printf(" WiFi Status: %s\n", wifi_connected ? "CONNECTED" : "DISCONNECTED"); + USB_SERIAL.println("***********************************************"); +} + +// Handle serial inputs and updates global variables +void handleSerialInput(const String &input) +{ + if (input == "arm") + { + setArmingStatus(true); + return; + } + + if (input == "0" || input == "disarm") + { + setArmingStatus(false); + return; + } + + if (input == "info") + { + motor01.printDShotInfo(); + USB_SERIAL.println(" "); + USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); + return; + } + + if (input == "wifi") + { + printWiFiStatus(); + return; + } + + if (input == "reconnect") + { + USB_SERIAL.println("Reconnecting to WiFi..."); + WiFi.disconnect(); + delay(1000); + wifi_connected = connectToWiFi(); + if (wifi_connected) + { + printWiFiStatus(); + } + return; + } + + if (input == "rpm" && IS_BIDIRECTIONAL) + { + if (isArmed) + { + dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + printDShotResult(result); + } + else + { + USB_SERIAL.println(" "); + USB_SERIAL.println("Cannot read RPM - Motor is DISARMED"); + } + return; + } + + 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); + } + return; + } + + if (input == "h" || input == "help") + { + printMenu(); + return; + } + + 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"); + USB_SERIAL.printf("WiFi Status: %s\n", wifi_connected ? "CONNECTED" : "DISCONNECTED"); + if (wifi_connected) + { + USB_SERIAL.printf("IP Address: %s\n", WiFi.localIP().toString().c_str()); + } + return; + } + + // Handle throttle input + 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); + } + return; + } + + if (throttle_value == 0) + { + throttle = 0; + continuous_throttle = false; + dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + printDShotResult(result); + return; + } + + // Invalid input + 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")) + { + if (!isArmed) + { + throttle = 0; + continuous_throttle = false; + // Ignore throttle commands when disarmed + USB_SERIAL.println(" "); + USB_SERIAL.println("Web throttle command ignored - Motor is DISARMED"); + return; + } + + uint16_t web_throttle = doc["throttle"]; + + 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; + } + } + + // Webserver arms with DSHOT_THROTTLE_MIN + if (armedFromWeb && isArmed) + { + continuous_throttle = true; + motor01.sendThrottle(throttle); + USB_SERIAL.println(" "); + USB_SERIAL.printf("Motor armed via Web - throttle set to %i\n", throttle); + } +} + +// Websocket request handler +void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) +{ + switch (type) + { + case WS_EVT_CONNECT: + USB_SERIAL.printf("Web Client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); + + // Send current arming status to new client + { + JsonDocument doc; + doc["armed"] = isArmed; + doc["throttle"] = isArmed ? throttle : 0; + String json_output; + serializeJson(doc, json_output); + client->text(json_output); + } + break; + + case WS_EVT_DISCONNECT: + USB_SERIAL.printf("Web Client #%u disconnected\n", client->id()); + break; + + case WS_EVT_DATA: + handleWebSocketMessage(arg, data, len); + break; + + case WS_EVT_PONG: + case WS_EVT_ERROR: + break; + } +} diff --git a/examples/web_control/web_control.ino b/examples/web_control/web_control.ino index b7ce329..3f1e400 100644 --- a/examples/web_control/web_control.ino +++ b/examples/web_control/web_control.ino @@ -1,5 +1,5 @@ /** - * @file dshot300.ino + * @file web_control.ino * @brief Demo sketch for DShotRMT library * @author Wastl Kraus * @date 2025-09-09 @@ -95,6 +95,7 @@ void setup() printMenu(); } +// void loop() { static uint64_t last_serial_update = 0; @@ -134,8 +135,8 @@ void loop() // 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); + dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + printDShotResult(telem_result); } USB_SERIAL.println("Type 'help' to show Menu"); @@ -149,8 +150,11 @@ void loop() if (IS_BIDIRECTIONAL && isArmed) { - dshot_telemetry_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - current_rpm = String(telem_result.motor_rpm); + dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + if (telem_result.success && telem_result.motor_rpm > 0) + { + current_rpm = String(telem_result.motor_rpm); + } } if (throttle != last_sent_throttle || isArmed != last_sent_armed || current_rpm != last_sent_rpm) @@ -184,19 +188,18 @@ 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 + if (armed) { continuous_throttle = true; + return; } + + // 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 ==="); } // @@ -232,38 +235,35 @@ void handleSerialInput(const String &input) if (input == "arm") { setArmingStatus(true); + return; } - else if (input == "disarm") + if (input == "0" || input == "disarm") { setArmingStatus(false); + return; } - 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") + if (input == "info") { motor01.printDShotInfo(); USB_SERIAL.println(" "); USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); + return; } - else if (input == "rpm" && IS_BIDIRECTIONAL) + if (input == "rpm" && IS_BIDIRECTIONAL) { if (isArmed) { - dshot_telemetry_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printDShotTelemetry(result); + dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + printDShotResult(result); } else { USB_SERIAL.println(" "); USB_SERIAL.println("Cannot read RPM - Motor is DISARMED"); } + return; } - else if (input.startsWith("cmd ")) + if (input.startsWith("cmd ")) { if (!isArmed) { @@ -274,6 +274,7 @@ void handleSerialInput(const String &input) 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); @@ -284,57 +285,61 @@ void handleSerialInput(const String &input) USB_SERIAL.println(" "); USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); } + return; } - else if (input == "h" || input == "help") + if (input == "h" || input == "help") { printMenu(); + return; } - else if (input == "status") + 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"); + return; } - else + + // Handle throttle input + int throttle_value = input.toInt(); + + if (throttle_value >= DSHOT_THROTTLE_MIN && throttle_value <= DSHOT_THROTTLE_MAX) { - 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 + if (!isArmed) { 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"); + 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); + } + return; } + + if (throttle_value == 0) + { + throttle = 0; + continuous_throttle = false; + dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + printDShotResult(result); + return; + } + + // Invalid input + 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 @@ -350,7 +355,6 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) return; } - // bool armedFromWeb = false; // Handle arming status @@ -362,11 +366,20 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) } // Handle throttle value (only if armed) - if (doc.containsKey("throttle") && isArmed) + if (doc.containsKey("throttle")) { + if (!isArmed) + { + throttle = 0; + continuous_throttle = false; + // Ignore throttle commands when disarmed + USB_SERIAL.println(" "); + USB_SERIAL.println("Web throttle command ignored - Motor is DISARMED"); + return; + } + uint16_t web_throttle = doc["throttle"]; - // Check for valid throttle value if (web_throttle == 0) { throttle = 0; @@ -379,23 +392,14 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) 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"); + USB_SERIAL.printf("Motor armed via Web - throttle set to %i\n", throttle); } } diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 04331f3..d64cdec 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -254,11 +254,11 @@ dshot_result_t DShotRMT::sendCommand(uint16_t command) return _sendDShotFrame(_packet); } -// Get telemetry data with timing and error handling -dshot_telemetry_result_t DShotRMT::getTelemetry(uint16_t magnet_count) +// Get telemetry data +dshot_result_t DShotRMT::getTelemetry(uint16_t magnet_count) { - // Result container - dshot_telemetry_result_t result = {false, NO_DSHOT_ERPM, NO_DSHOT_RPM, TELEMETRY_FAILED}; + // Result container with unified structure + dshot_result_t result = {false, TELEMETRY_FAILED, NO_DSHOT_ERPM, NO_DSHOT_RPM}; // Check if bidirectional mode is enabled if (!_is_bidirectional) @@ -550,21 +550,13 @@ void DShotRMT::printCpuInfo(Stream &output) const // --- HELPERS --- void printDShotResult(dshot_result_t &result, Stream &output) { - output.printf("Status: %s - %s\n", result.success ? "SUCCESS" : "FAILED", result.msg); - output.println(" "); -} + output.printf("Status: %s - %s", result.success ? "SUCCESS" : "FAILED", result.msg); -// -void printDShotTelemetry(dshot_telemetry_result_t &result, Stream &output) -{ - if (result.success) + // Print telemetry data if available + if (result.success && (result.erpm > 0 || result.motor_rpm > 0)) { - output.printf("Telemetry: eRPM=%u, Motor RPM=%u\n", result.erpm, result.motor_rpm); - } - else - { - output.printf("Telemetry: FAILED - %s\n", result.msg); + output.printf(" | eRPM: %u, Motor RPM: %u", result.erpm, result.motor_rpm); } - output.println(" "); + output.println(); } diff --git a/src/DShotRMT.h b/src/DShotRMT.h index 1fa2d1c..3a4d466 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -73,28 +73,20 @@ typedef struct uint16_t ticks_zero_low; } dshot_timing_t; -// Error handling +// Unified DShot result structure typedef struct { bool success; const char *msg; + uint16_t erpm; + uint16_t motor_rpm; } 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 @@ -123,7 +115,7 @@ public: 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); + dshot_result_t getTelemetry(uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); // --- INFO --- void printDShotInfo(Stream &output = Serial) const; @@ -148,7 +140,7 @@ public: uint32_t getMotorRPM(uint8_t magnet_count) { auto result = getTelemetry(magnet_count); - return result.success; + return result.motor_rpm; } private: From 8aa37ff74eba27dc62e17160c9cc473eeacc4ea8 Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Thu, 11 Sep 2025 14:26:37 +0200 Subject: [PATCH 2/4] Fix Web Switch sync --- examples/web_client/web_client.html | 347 ++++++++++++++++++++++++++++ examples/web_client/web_client.ino | 2 +- src/web_content.h | 4 + 3 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 examples/web_client/web_client.html diff --git a/examples/web_client/web_client.html b/examples/web_client/web_client.html new file mode 100644 index 0000000..71aa2eb --- /dev/null +++ b/examples/web_client/web_client.html @@ -0,0 +1,347 @@ + + + + + + + DShotRMT_Web + + + + +

DShotRMT Control Demo

+
+ +
+
+ ARMING SWITCH + +
+
+ DISARMED +
+
+ ⚠️ Motor control disabled when disarmed +
+
+ + +
+
0
+ +
+ +
+ RPM: -- +
+
+ + + + + diff --git a/examples/web_client/web_client.ino b/examples/web_client/web_client.ino index 1bc226e..79c9004 100644 --- a/examples/web_client/web_client.ino +++ b/examples/web_client/web_client.ino @@ -16,7 +16,7 @@ #include // WiFi Client Configuration - UPDATE THESE VALUES! -static constexpr auto *WIFI_SSID = "YOUR_SSID"; // Enter your WiFi SSID +static constexpr auto *WIFI_SSID = "YOUR_SSID"; // Enter your WiFi SSID static constexpr auto *WIFI_PASSWORD = "YOUR_PASSWORD"; // Enter your WiFi password // Connection timeout in milliseconds diff --git a/src/web_content.h b/src/web_content.h index 1a4d8d9..d66c2fc 100644 --- a/src/web_content.h +++ b/src/web_content.h @@ -311,6 +311,10 @@ const char index_html[] PROGMEM = R"rawliteral( // Update UI based on arming status function updateArmingUI() { + + // Synch checkbox, as well + armingSwitch.checked = isArmed; + if (isArmed) { armingStatus.innerText = 'ARMED'; armingStatus.className = 'status-armed'; From 18df99bdd079aa92afe0d67715eb0a858b50cff0 Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Thu, 11 Sep 2025 18:02:25 +0200 Subject: [PATCH 3/4] Fix repository security --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ce98f5..23500d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,6 @@ name: ESP32 Build & Quality Check +permissions: + contents: read on: push: From 84233ac20b0024aabaed35c8ce343c934b5c63a0 Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Thu, 11 Sep 2025 18:26:33 +0200 Subject: [PATCH 4/4] Features and Security Update --- examples/web_client/web_client.html | 347 ---------------------------- library.properties | 2 +- src/web_content.h | 9 +- 3 files changed, 6 insertions(+), 352 deletions(-) delete mode 100644 examples/web_client/web_client.html diff --git a/examples/web_client/web_client.html b/examples/web_client/web_client.html deleted file mode 100644 index 71aa2eb..0000000 --- a/examples/web_client/web_client.html +++ /dev/null @@ -1,347 +0,0 @@ - - - - - - - DShotRMT_Web - - - - -

DShotRMT Control Demo

-
- -
-
- ARMING SWITCH - -
-
- DISARMED -
-
- ⚠️ Motor control disabled when disarmed -
-
- - -
-
0
- -
- -
- RPM: -- -
-
- - - - - diff --git a/library.properties b/library.properties index 1676765..346113b 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=DShotRMT -version=0.7.5 +version=0.7.6 author=Wastl Kraus maintainer=Wastl Kraus license=MIT diff --git a/src/web_content.h b/src/web_content.h index d66c2fc..65bbe01 100644 --- a/src/web_content.h +++ b/src/web_content.h @@ -16,7 +16,7 @@ const char index_html[] PROGMEM = R"rawliteral( - DShotRMT_Web + DShotRMT Web Client