From fc9bbd213f1bbfa29e2839be4884c173f7c32d35 Mon Sep 17 00:00:00 2001 From: devdesk Date: Sat, 21 Feb 2026 23:32:28 +0200 Subject: [PATCH] Add GPIO12 startup/activity heartbeat LED behavior --- include/heartbeat.h | 11 +++++++ src/heartbeat.cpp | 71 +++++++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 5 ++++ src/webserver.cpp | 36 +++++++++++++---------- 4 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 include/heartbeat.h create mode 100644 src/heartbeat.cpp diff --git a/include/heartbeat.h b/include/heartbeat.h new file mode 100644 index 0000000..0a5b30c --- /dev/null +++ b/include/heartbeat.h @@ -0,0 +1,11 @@ +#ifndef HEARTBEAT_H +#define HEARTBEAT_H + +#include + +void setupHeartbeat(); +void startupHeartbeatBlink(uint8_t count = 5); +void triggerHeartbeatBlink(); +void updateHeartbeat(); + +#endif diff --git a/src/heartbeat.cpp b/src/heartbeat.cpp new file mode 100644 index 0000000..645a1c8 --- /dev/null +++ b/src/heartbeat.cpp @@ -0,0 +1,71 @@ +#include +#include "heartbeat.h" + +namespace { +constexpr uint8_t HEARTBEAT_PIN = 12; +constexpr uint16_t STARTUP_ON_MS = 120; +constexpr uint16_t STARTUP_OFF_MS = 120; +constexpr uint16_t ACTIVITY_ON_MS = 45; +constexpr uint16_t ACTIVITY_OFF_MS = 70; +constexpr uint16_t TRIGGER_THROTTLE_MS = 35; +constexpr uint8_t MAX_PENDING_BLINKS = 6; + +uint8_t pendingBlinks = 0; +uint32_t phaseStartMs = 0; +uint32_t lastTriggerMs = 0; +bool ledOn = false; +} + +void setupHeartbeat() { + pinMode(HEARTBEAT_PIN, OUTPUT); + digitalWrite(HEARTBEAT_PIN, LOW); + ledOn = false; + phaseStartMs = millis(); +} + +void startupHeartbeatBlink(uint8_t count) { + for (uint8_t i = 0; i < count; ++i) { + digitalWrite(HEARTBEAT_PIN, HIGH); + delay(STARTUP_ON_MS); + digitalWrite(HEARTBEAT_PIN, LOW); + delay(STARTUP_OFF_MS); + } +} + +void triggerHeartbeatBlink() { + const uint32_t now = millis(); + if (now - lastTriggerMs < TRIGGER_THROTTLE_MS) { + return; + } + + lastTriggerMs = now; + if (pendingBlinks < MAX_PENDING_BLINKS) { + pendingBlinks++; + } +} + +void updateHeartbeat() { + const uint32_t now = millis(); + + if (pendingBlinks == 0) { + if (ledOn) { + ledOn = false; + digitalWrite(HEARTBEAT_PIN, LOW); + } + return; + } + + if (!ledOn && (now - phaseStartMs >= ACTIVITY_OFF_MS)) { + ledOn = true; + phaseStartMs = now; + digitalWrite(HEARTBEAT_PIN, HIGH); + return; + } + + if (ledOn && (now - phaseStartMs >= ACTIVITY_ON_MS)) { + ledOn = false; + phaseStartMs = now; + digitalWrite(HEARTBEAT_PIN, LOW); + pendingBlinks--; + } +} diff --git a/src/main.cpp b/src/main.cpp index 62a9446..06c4ef9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include #include #include "config.h" +#include "heartbeat.h" #include "motor.h" #include "webserver.h" @@ -36,6 +37,9 @@ void setupWiFi() { void setup() { Serial.begin(115200); delay(1000); + + setupHeartbeat(); + startupHeartbeatBlink(5); Serial.println("\n============================="); Serial.println(" Dual BTS7960 Motor Controller"); @@ -61,5 +65,6 @@ void loop() { handleWebServer(); motor1.update(); // Update current sensing and logging for motor 1 motor2.update(); // Update current sensing and logging for motor 2 + updateHeartbeat(); delay(1); } diff --git a/src/webserver.cpp b/src/webserver.cpp index 8547496..12822ea 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -1,10 +1,16 @@ #include #include +#include "heartbeat.h" #include "motor.h" #include "config.h" WebServer server(HTTP_PORT); +static void sendWithHeartbeat(int code, const char* contentType, const String& content) { + triggerHeartbeatBlink(); + server.send(code, contentType, content); +} + const char index_html[] PROGMEM = R"rawliteral( @@ -585,74 +591,74 @@ void handleRoot() { void handleMotor1Drive() { if (!server.hasArg("value")) { - server.send(400, "text/plain", "Missing value"); + sendWithHeartbeat(400, "text/plain", "Missing value"); return; } applyDriveValue(motor1, server.arg("value").toInt()); - server.send(200, "text/plain", "OK"); + sendWithHeartbeat(200, "text/plain", "OK"); } void handleMotor2Drive() { if (!server.hasArg("value")) { - server.send(400, "text/plain", "Missing value"); + sendWithHeartbeat(400, "text/plain", "Missing value"); return; } applyDriveValue(motor2, server.arg("value").toInt()); - server.send(200, "text/plain", "OK"); + sendWithHeartbeat(200, "text/plain", "OK"); } void handleMotor1Speed() { if (server.hasArg("value")) { motor1.setSpeed(server.arg("value").toInt()); - server.send(200, "text/plain", "OK"); + sendWithHeartbeat(200, "text/plain", "OK"); } else { - server.send(400, "text/plain", "Missing value"); + sendWithHeartbeat(400, "text/plain", "Missing value"); } } void handleMotor1Direction() { if (server.hasArg("value")) { motor1.setDirection(server.arg("value").toInt()); - server.send(200, "text/plain", "OK"); + sendWithHeartbeat(200, "text/plain", "OK"); } else { - server.send(400, "text/plain", "Missing value"); + sendWithHeartbeat(400, "text/plain", "Missing value"); } } void handleMotor1Stop() { motor1.stop(); - server.send(200, "text/plain", "OK"); + sendWithHeartbeat(200, "text/plain", "OK"); } void handleMotor2Speed() { if (server.hasArg("value")) { motor2.setSpeed(server.arg("value").toInt()); - server.send(200, "text/plain", "OK"); + sendWithHeartbeat(200, "text/plain", "OK"); } else { - server.send(400, "text/plain", "Missing value"); + sendWithHeartbeat(400, "text/plain", "Missing value"); } } void handleMotor2Direction() { if (server.hasArg("value")) { motor2.setDirection(server.arg("value").toInt()); - server.send(200, "text/plain", "OK"); + sendWithHeartbeat(200, "text/plain", "OK"); } else { - server.send(400, "text/plain", "Missing value"); + sendWithHeartbeat(400, "text/plain", "Missing value"); } } void handleMotor2Stop() { motor2.stop(); - server.send(200, "text/plain", "OK"); + sendWithHeartbeat(200, "text/plain", "OK"); } void handleStop() { motor1.stop(); motor2.stop(); - server.send(200, "text/plain", "OK"); + sendWithHeartbeat(200, "text/plain", "OK"); } void handleSpeed() {