From 8783b1a24b4fd10016e5ed97269b387a61c9953b Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Tue, 9 Sep 2025 17:19:57 +0200 Subject: [PATCH] ...add Web Server ...fix warnings and update action versions ...Web Server added --- .github/workflows/ci.yml | 56 +----- .gitignore | 1 + examples/dshot300/dshot300.ino | 317 ++++++++++++++++++++++++----- web/web_content.h | 354 +++++++++++++++++++++++++++++++++ 4 files changed, 633 insertions(+), 95 deletions(-) create mode 100644 web/web_content.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdecb41..5754795 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: # Code Quality & Linting # ============================================================================ quality-check: - name: 'Code Quality' + name: 'Arduino Lint Check' runs-on: ubuntu-latest timeout-minutes: 10 @@ -50,9 +50,6 @@ jobs: examples: - "examples/dshot300/dshot300.ino" - "examples/command_manager/command_manager.ino" - build-flags: - - name: "Release" - flags: "Automated Build" steps: - name: Checkout Repository @@ -67,44 +64,15 @@ jobs: source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json libraries: | # Install the library from the local path. - - source-path: ./ + - name: ArduinoJson + - name: Async TCP + - name: ESP Async WebServer + - name: WiFi + - source-path: ./ sketch-paths: ${{ matrix.examples}} cli-compile-flags: | - --warnings="none" - # ============================================================================ - # Static Code Analysis - # ============================================================================ - static-analysis: - name: 'Static Analysis' - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - - - name: Setup Arduino CLI - uses: arduino/setup-arduino-cli@v2 - - - name: Install ESP32 core - run: | - arduino-cli core update-index > /dev/null - arduino-cli core install esp32:esp32 > /dev/null - - - 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 # ============================================================================ @@ -112,7 +80,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 @@ -136,19 +104,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..b92f635 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ examples/dshot300/debug_custom.json examples/dshot300/debug.svd /build /.github/chatmodes +web/control.html diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index 4c7d8ff..6037f5f 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -2,19 +2,31 @@ * @file dshot300.ino * @brief Demo sketch for DShotRMT library * @author Wastl Kraus - * @date 2025-06-11 + * @date 2025-09-09 * @license MIT */ #include #include +#include "web/web_content.h" +#include +#include +#include +#include + +// Wifi Configuration +static constexpr auto *ssid = "DShotRMT Control"; +static constexpr auto *password = "12345678"; + +IPAddress local_IP(10, 10, 10, 1); +IPAddress gateway(0, 0, 0, 0); +IPAddress subnet(255, 255, 255, 0); // USB serial port settings -static constexpr auto &USB_SERIAL = Serial0; +static constexpr auto &USB_SERIAL = Serial; static constexpr auto USB_SERIAL_BAUD = 115200; // Motor configuration - Pin number or GPIO_PIN -// static constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_17; static constexpr auto MOTOR01_PIN = 17; // Supported: DSHOT150, DSHOT300, DSHOT600, (DSHOT1200) @@ -29,59 +41,95 @@ static constexpr auto MOTOR01_MAGNET_COUNT = 14; // Creates the motor instance DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL); +// Web Server Configuration +AsyncWebServer server(80); +AsyncWebSocket ws("/ws"); + +// Global variables +static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; +static bool isArmed = false; +static bool continuous_throttle = true; + +// Helpers (forward declaration) +void printMenu(); +void handleSerialInput(const String &input); +void handleWebSocketMessage(void *arg, uint8_t *data, size_t len); +void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); +void setArmingStatus(bool armed); + // void setup() { - // Starts the USB Serial Port USB_SERIAL.begin(USB_SERIAL_BAUD); - // Initialize DShot Signal motor01.begin(); - - // Print CPU Info motor01.printCpuInfo(); - // + // Set IP Address + WiFi.softAPConfig(local_IP, gateway, subnet); + + // Start Wifi Access Point + USB_SERIAL.println("\nStarting Access Point..."); + WiFi.softAP(ssid, password); + + IPAddress IP = WiFi.softAPIP(); + + USB_SERIAL.print("Access Point IP address: "); + USB_SERIAL.println(IP); + + // Init WebSockets and Webserver + USB_SERIAL.println("\nStarting Webserver..."); + ws.onEvent(onWsEvent); + server.addHandler(&ws); + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send_P(200, "text/html", index_html); }); + + server.begin(); + USB_SERIAL.println("HTTP server started."); + + // Initialize with disarmed state + setArmingStatus(false); + printMenu(); } -// void loop() { - // Safety first - static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; - static bool continuous_throttle = true; - - // Time Measurement - static uint64_t last_stats_print = 0; + static uint64_t last_stats_update = 0; + static uint64_t last_serial_update = 0; // Handle serial input if (USB_SERIAL.available() > 0) { String input = USB_SERIAL.readStringUntil('\n'); input.trim(); - if (input.length() > 0) { - handleSerialInput(input, throttle, continuous_throttle); + handleSerialInput(input); } } - // Send throttle value in continuous mode - if (continuous_throttle) + // 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 (continuous_throttle && (esp_timer_get_time() - last_stats_print >= 3000000)) + if ((esp_timer_get_time() - last_serial_update >= 3000000)) { motor01.printDShotInfo(); USB_SERIAL.println(" "); - // Get Motor RPM if bidirectional - if (IS_BIDIRECTIONAL) + // 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); @@ -90,7 +138,55 @@ void loop() USB_SERIAL.println("Type 'help' to show Menu"); // Time Stamp - last_stats_print = esp_timer_get_time(); + last_serial_update = esp_timer_get_time(); + } + + // Update Webserver data every second + if (esp_timer_get_time() - last_stats_update >= 1000000) + { + last_stats_update = esp_timer_get_time(); + + JsonDocument doc; + doc["throttle"] = isArmed ? throttle : 0; + doc["armed"] = isArmed; + + if (IS_BIDIRECTIONAL && isArmed) + { + dshot_telemetry_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + doc["rpm"] = telem_result.motor_rpm; + } + else + { + doc["rpm"] = "N/A"; + } + + String json_output; + serializeJson(doc, json_output); + + // Update clients with the new data + ws.textAll(json_output); + } + + ws.cleanupClients(); +} + +// +void setArmingStatus(bool armed) +{ + isArmed = armed; + + if (!armed) + { + // Safety: Stop motor and reset throttle when disarming + throttle = 0; + continuous_throttle = false; + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + USB_SERIAL.println(" "); + USB_SERIAL.println("=== MOTOR DISARMED - SAFETY STOP EXECUTED ==="); + } + else + { + continuous_throttle = true; } } @@ -98,50 +194,74 @@ void loop() void printMenu() { USB_SERIAL.println(" "); - USB_SERIAL.println("*******************************************"); - USB_SERIAL.println(" DShotRMT Demo "); - USB_SERIAL.println("*******************************************"); - USB_SERIAL.println(" - Set throttle (48 – 2047)"); - USB_SERIAL.println(" 0 - Stop motor"); - USB_SERIAL.println("*******************************************"); - USB_SERIAL.println(" cmd - Send DShot command (0 - 47)"); - USB_SERIAL.println(" info - Show motor info"); + 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(" rpm - Get telemetry data"); } - USB_SERIAL.println("*******************************************"); - USB_SERIAL.println(" h / help - Show this Menu"); - USB_SERIAL.println("*******************************************"); + 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("***********************************************"); } -// -void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous_throttle) +// Handle serial inputs and updates global variables +void handleSerialInput(const String &input) { - if (input == "0") + if (input == "arm") + { + setArmingStatus(true); + } + else if (input == "disarm") + { + setArmingStatus(false); + } + else if (input == "0") { - // Stop motor throttle = 0; - continuous_throttle = true; + continuous_throttle = false; dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); printDShotResult(result); } else if (input == "info") { motor01.printDShotInfo(); + USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); } else if (input == "rpm" && IS_BIDIRECTIONAL) { - dshot_telemetry_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printDShotTelemetry(result); + if (isArmed) + { + dshot_telemetry_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + printDShotTelemetry(result); + } + else + { + USB_SERIAL.println("Cannot read RPM - Motor is DISARMED"); + } } else if (input.startsWith("cmd ")) { + if (!isArmed) + { + USB_SERIAL.println("Cannot send command - Motor is DISARMED. Use 'arm' command first."); + return; + } + continuous_throttle = false; - - // Send DShot command int cmd_num = input.substring(4).toInt(); - if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); @@ -156,24 +276,127 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous { printMenu(); } + else if (input == "status") + { + USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); + USB_SERIAL.printf("Current Throttle: %u\n", throttle); + USB_SERIAL.printf("Continuous Mode: %s\n", continuous_throttle ? "ACTIVE" : "INACTIVE"); + } else { - // Parse input throttle value int throttle_value = input.toInt(); if (throttle_value >= DSHOT_THROTTLE_MIN && throttle_value <= DSHOT_THROTTLE_MAX) { + if (!isArmed) + { + USB_SERIAL.println("Cannot set throttle - Motor is DISARMED. Use 'arm' command first."); + return; + } + throttle = throttle_value; continuous_throttle = true; dshot_result_t result = motor01.sendThrottle(throttle); + + if (result.success) + { + USB_SERIAL.printf("Throttle set to %u (continuous mode active)\n", throttle); + } + } + else if (throttle_value == 0) + { + throttle = 0; + continuous_throttle = false; + dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); printDShotResult(result); } else { USB_SERIAL.println(" "); - USB_SERIAL.printf("Invalid input: '%s'\n", input); + 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; + } + + // Handle arming status + if (doc.containsKey("armed")) + { + bool armed = doc["armed"]; + setArmingStatus(armed); + } + + // Handle throttle value (only if armed) + if (doc.containsKey("throttle") && isArmed) + { + uint16_t web_throttle = doc["throttle"]; + + // Check for valid throttle value + if (web_throttle == 0) + { + throttle = 0; + continuous_throttle = false; + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + } + else if (web_throttle >= DSHOT_THROTTLE_MIN && web_throttle <= DSHOT_THROTTLE_MAX) + { + throttle = web_throttle; + continuous_throttle = true; + } + } + else if (doc.containsKey("throttle") && !isArmed) + { + throttle = 0; + continuous_throttle = false; + // Ignore throttle commands when disarmed + USB_SERIAL.println("Web throttle command ignored - Motor is DISARMED"); + } +} + +// Websocket request handler +void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) +{ + switch (type) + { + case WS_EVT_CONNECT: + USB_SERIAL.printf("Web Client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); + + // Send current arming status to new client + { + JsonDocument doc; + doc["armed"] = isArmed; + doc["throttle"] = isArmed ? throttle : 0; + String json_output; + serializeJson(doc, json_output); + client->text(json_output); + } + break; + + case WS_EVT_DISCONNECT: + USB_SERIAL.printf("Web Client #%u disconnected\n", client->id()); + break; + + case WS_EVT_DATA: + handleWebSocketMessage(arg, data, len); + break; + + case WS_EVT_PONG: + case WS_EVT_ERROR: + break; + } +} diff --git a/web/web_content.h b/web/web_content.h new file mode 100644 index 0000000..d831a1e --- /dev/null +++ b/web/web_content.h @@ -0,0 +1,354 @@ +/** + * @file web_content.h + * @brief DShotRMT_Control Website content with Arming Switch + * @author Wastl Kraus + * @date 2025-09-09 + * @license MIT + */ + +// 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