From 25dc34c6e3d61e6664d0d853969caa38e1bb168f Mon Sep 17 00:00:00 2001 From: devdesk Date: Fri, 20 Feb 2026 14:30:46 +0200 Subject: [PATCH] Replace stall/homing flow with signed slider drive control This streamlines manual dual-motor operation by using spring-centered signed sliders and synchronized drive modes while keeping live current monitoring in the UI and API. --- include/config.h | 6 - include/homing.h | 10 - include/motor.h | 15 - readme.md | 41 +-- src/main.cpp | 115 ------- src/motor.cpp | 65 +--- src/webserver.cpp | 844 +++++++++++++++++++++++++++++----------------- 7 files changed, 542 insertions(+), 554 deletions(-) delete mode 100644 include/homing.h diff --git a/include/config.h b/include/config.h index 3231931..c49b7b7 100644 --- a/include/config.h +++ b/include/config.h @@ -69,12 +69,6 @@ const MotorPins MOTOR2_PINS = { // Current logging interval for data collection #define CURRENT_LOG_INTERVAL_MS 100 // Log current readings every 100ms -// Simple Stall Detection -// Based on clean data: Running ~2A, Stall ~17A (8.5x difference) -#define STALL_THRESHOLD 8.0f // Amps - midpoint between 2A run and 17A stall -#define STALL_CONFIRM_SAMPLES 3 // Debounce: 3 samples = 300ms at 100ms interval -#define STALL_STABILIZE_MS 500 // Ignore current spikes for 500ms after direction change - // Web Server #define HTTP_PORT 80 diff --git a/include/homing.h b/include/homing.h deleted file mode 100644 index 420ba79..0000000 --- a/include/homing.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef HOMING_H -#define HOMING_H - -// Coordinated movement functions (defined in main.cpp) -int toggleCoordinatedMovement(int speed); -void stopCoordinatedMovement(); -int getCoordinatedState(); -bool isCoordinatedMovementDone(); - -#endif diff --git a/include/motor.h b/include/motor.h index c371021..e0a7b23 100644 --- a/include/motor.h +++ b/include/motor.h @@ -4,9 +4,6 @@ #include #include "config.h" -// Stall callback function type -typedef void (*StallCallback)(); - class MotorController { public: // Constructor with pin configuration and motor name @@ -25,11 +22,6 @@ public: float getCurrentActive(); // Current from active direction const char* getName(); // Get motor name for logging - // Stall detection - bool isStalled(); - void setStallCallback(StallCallback callback); - void resetStallDetection(); - private: MotorPins _pins; const char* _name; @@ -43,19 +35,12 @@ private: int _adcOffsetRight = 0; int _adcOffsetLeft = 0; - // Stall detection state - bool _stalled = false; - int _stallConfirmCount = 0; - StallCallback _stallCallback = nullptr; - unsigned long _lastDirectionChangeTime = 0; - // Static flag to track if enable pins are already configured static bool _enablePinsConfigured; void applyMotorState(); float readCurrentSense(int pin); void calibrateCurrentOffset(); - void checkStall(); }; // Two motor controller instances diff --git a/readme.md b/readme.md index e360346..76726b7 100644 --- a/readme.md +++ b/readme.md @@ -1,15 +1,13 @@ # Dual BTS7960 Motor Controller -ESP32-based differential drive robot controller with web interface, using two BTS7960 dual H-bridge drivers with current sensing and stall protection. +ESP32-based differential drive robot controller with web interface, using two BTS7960 dual H-bridge drivers with live current sensing. ## Features - Web-based dual motor control panel with real-time current monitoring -- Independent Forward/Reverse/Stop control for each motor with speed sliders (20-100%) +- Two independent spring-to-center sliders (one per motor), each supporting forward and reverse - Current sensing on both H-bridge sides for each motor -- Sample-based stall detection with automatic motor shutoff (per motor) - ADC offset calibration at startup for accurate current readings -- Stall warning displayed on web interface per motor - Emergency stop for both motors ## Hardware @@ -98,9 +96,6 @@ Key settings in [`include/config.h`](include/config.h): | Setting | Default | Description | |---------|---------|-------------| -| `STALL_THRESHOLD` | 8.0A | Current threshold for stall detection | -| `STALL_CONFIRM_SAMPLES` | 3 | Number of consecutive samples to confirm stall (~300ms) | -| `STALL_STABILIZE_MS` | 500ms | Ignore current spikes after direction change | | `PWM_FREQ` | 20kHz | PWM frequency (reduces motor noise) | | `MIN_PWM_PERCENT` | 20% | Minimum PWM when motor is running | | `CURRENT_LOG_INTERVAL_MS` | 100ms | Current sampling/logging interval | @@ -128,11 +123,10 @@ Access the control panel at `http://10.81.2.185` (or the IP shown on serial moni ### Features -- **Dual Motor Panels**: Side-by-side controls for Motor 1 (Left) and Motor 2 (Right) +- **Dual Motor Panels**: Side-by-side controls for Motor 1 and Motor 2 - **Current Display**: Real-time forward (FWD) and reverse (REV) current readings per motor -- **Direction Status**: Shows FORWARD, REVERSE, or STOPPED per motor -- **Speed Sliders**: Adjustable from 20% to 100% per motor -- **Stall Warning**: Red banner per motor when stall is detected +- **Spring-to-Center Sliders**: Signed sliders (`-100..100`) per motor +- **Bidirectional Drive**: Positive values = forward, negative values = reverse, `0` = stop - **Emergency Stop**: Global button to stop both motors immediately ### API Endpoints @@ -141,9 +135,11 @@ Access the control panel at `http://10.81.2.185` (or the IP shown on serial moni |----------|--------|------------|-------------| | `/` | GET | - | Control panel HTML page | | `/status` | GET | - | JSON with both motors' status | +| `/motor1/drive` | GET | `value` (-100..100) | Set motor 1 speed + direction in one command | | `/motor1/speed` | GET | `value` (0-100) | Set motor 1 speed percentage | | `/motor1/direction` | GET | `value` (-1, 0, 1) | Set motor 1 direction | | `/motor1/stop` | GET | - | Stop motor 1 | +| `/motor2/drive` | GET | `value` (-100..100) | Set motor 2 speed + direction in one command | | `/motor2/speed` | GET | `value` (0-100) | Set motor 2 speed percentage | | `/motor2/direction` | GET | `value` (-1, 0, 1) | Set motor 2 direction | | `/motor2/stop` | GET | - | Stop motor 2 | @@ -159,30 +155,19 @@ Access the control panel at `http://10.81.2.185` (or the IP shown on serial moni "speed": 50, "direction": 1, "currentR": 2.35, - "currentL": 0.00, - "stalled": false + "currentL": 0.00 }, "motor2": { "speed": 50, "direction": -1, "currentR": 0.00, - "currentL": 1.87, - "stalled": false + "currentL": 1.87 } } ``` -## Stall Protection +## Control Behavior -The stall detection uses a sample-based approach for reliability (independent per motor): - -1. **Threshold**: Current above 8.0A indicates potential stall (based on observed ~2A running vs ~17A stalled) -2. **Debounce**: 3 consecutive samples above threshold confirms stall (~300ms at 100ms intervals) -3. **Stabilization**: Ignores current spikes for 500ms after direction changes - -When stall is confirmed on a motor: -1. That motor stops immediately -2. Serial log: `MotorX STALL DETECTED! Current=X.XXA (threshold=8.0A)` -3. Web interface shows red "STALL!" warning on that motor's panel - -To resume operation, send a new direction command via the web interface. +- Each motor is controlled by its own signed slider in the web UI. +- Releasing a slider returns it to `0` and immediately stops that motor. +- Current sensing remains active for both directions on both motors and is shown live in the UI. diff --git a/src/main.cpp b/src/main.cpp index f7dd571..62a9446 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,118 +4,6 @@ #include "motor.h" #include "webserver.h" -// Coordinated movement state -enum CoordinatedState { - COORD_IDLE = 0, - COORD_MOVING_REV = 1, // Homing - moving to reverse end - COORD_MOVING_FWD = 2 // Moving to forward end -}; - -// Global coordinated movement state -static CoordinatedState coordState = COORD_IDLE; -static bool motor1Done = false; -static bool motor2Done = false; -static int coordSpeed = 50; // Default coordinated movement speed - -// Get current coordinated movement state -int getCoordinatedState() { - return (int)coordState; -} - -// Check if coordinated movement is complete -bool isCoordinatedMovementDone() { - return motor1Done && motor2Done; -} - -// Called when motor 1 stall is detected -void onMotor1Stall() { - Serial.println("Motor1 stall callback triggered - stopping motor!"); - motor1.stop(); - - // Mark motor1 as done if in coordinated movement - if (coordState != COORD_IDLE) { - motor1Done = true; - Serial.println("Motor1 reached end position (stall)"); - - if (isCoordinatedMovementDone()) { - Serial.println("Coordinated movement complete - both motors at end position"); - } - } -} - -// Called when motor 2 stall is detected -void onMotor2Stall() { - Serial.println("Motor2 stall callback triggered - stopping motor!"); - motor2.stop(); - - // Mark motor2 as done if in coordinated movement - if (coordState != COORD_IDLE) { - motor2Done = true; - Serial.println("Motor2 reached end position (stall)"); - - if (isCoordinatedMovementDone()) { - Serial.println("Coordinated movement complete - both motors at end position"); - } - } -} - -// Start coordinated movement in specified direction -// dir: -1 = reverse (homing), 1 = forward -void startCoordinatedMovement(int dir, int speed) { - coordSpeed = speed; - motor1Done = false; - motor2Done = false; - - if (dir < 0) { - coordState = COORD_MOVING_REV; - Serial.printf("Starting coordinated REVERSE movement at %d%% speed\n", speed); - } else { - coordState = COORD_MOVING_FWD; - Serial.printf("Starting coordinated FORWARD movement at %d%% speed\n", speed); - } - - // Start both motors - motor1.setSpeed(speed); - motor1.setDirection(dir); - - motor2.setSpeed(speed); - motor2.setDirection(dir); -} - -// Toggle coordinated movement (for button press) -// Returns: new state (0=idle, 1=moving_rev, 2=moving_fwd) -int toggleCoordinatedMovement(int speed) { - if (coordState == COORD_IDLE) { - // First press: start moving reverse (homing) - startCoordinatedMovement(-1, speed); - return COORD_MOVING_REV; - } else if (coordState == COORD_MOVING_REV) { - // If currently moving rev or completed rev, switch to forward - // Stop any still-moving motors first - if (!motor1Done) motor1.stop(); - if (!motor2Done) motor2.stop(); - startCoordinatedMovement(1, speed); - return COORD_MOVING_FWD; - } else { - // Currently moving fwd or completed fwd, switch to reverse - // Stop any still-moving motors first - if (!motor1Done) motor1.stop(); - if (!motor2Done) motor2.stop(); - startCoordinatedMovement(-1, speed); - return COORD_MOVING_REV; - } -} - -// Stop coordinated movement -void stopCoordinatedMovement() { - coordState = COORD_IDLE; - motor1Done = false; - motor2Done = false; - motor1.stop(); - motor2.stop(); - Serial.println("Coordinated movement stopped"); -} - void setupWiFi() { Serial.println("Connecting to WiFi..."); @@ -156,10 +44,7 @@ void setup() { // Initialize motor controllers motor1.begin(); - motor1.setStallCallback(onMotor1Stall); - motor2.begin(); - motor2.setStallCallback(onMotor2Stall); // Connect to WiFi setupWiFi(); diff --git a/src/motor.cpp b/src/motor.cpp index 75c8965..41c48c7 100644 --- a/src/motor.cpp +++ b/src/motor.cpp @@ -65,12 +65,7 @@ void MotorController::setSpeed(int speed) { } void MotorController::setDirection(int dir) { - int newDir = constrain(dir, -1, 1); - if (newDir != _direction) { - _lastDirectionChangeTime = millis(); - resetStallDetection(); - } - _direction = newDir; + _direction = constrain(dir, -1, 1); applyMotorState(); } @@ -79,7 +74,6 @@ void MotorController::stop() { _direction = 0; ledcWrite(_pins.pwm_channel_r, 0); ledcWrite(_pins.pwm_channel_l, 0); - resetStallDetection(); Serial.printf("%s: STOPPED (manual)\n", _name); } @@ -99,8 +93,6 @@ void MotorController::update() { _name, _currentRight, _currentLeft, _direction, _speed); } - // Check for stall condition - checkStall(); #endif } @@ -133,61 +125,6 @@ const char* MotorController::getName() { return _name; } -// Stall detection -bool MotorController::isStalled() { - return _stalled; -} - -void MotorController::setStallCallback(StallCallback callback) { - _stallCallback = callback; -} - -void MotorController::resetStallDetection() { - _stalled = false; - _stallConfirmCount = 0; -} - -void MotorController::checkStall() { - // Only check when motor is running - if (_direction == 0 || _speed == 0) { - resetStallDetection(); - return; - } - - // Skip stall check during stabilization period after direction change - // (prevents false positives from inrush current spikes) - if ((millis() - _lastDirectionChangeTime) < STALL_STABILIZE_MS) { - return; - } - - float activeCurrent = getCurrentActive(); - - // Simple threshold-based stall detection with debounce - if (activeCurrent > STALL_THRESHOLD) { - _stallConfirmCount++; - - if (_stallConfirmCount >= STALL_CONFIRM_SAMPLES && !_stalled) { - _stalled = true; - Serial.printf("%s STALL DETECTED! Current=%.2fA (threshold=%.1fA)\n", - _name, activeCurrent, STALL_THRESHOLD); - - // Call stall callback if registered - if (_stallCallback != nullptr) { - _stallCallback(); - } - } - } else { - // Reset counter if current drops below threshold - _stallConfirmCount = 0; - - // Clear stall flag if current returns to normal - if (_stalled) { - _stalled = false; - Serial.printf("%s: Stall condition cleared\n", _name); - } - } -} - void MotorController::applyMotorState() { // Apply minimum PWM when motor is running int effectiveSpeed = _speed; diff --git a/src/webserver.cpp b/src/webserver.cpp index c4c674d..8547496 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -2,11 +2,9 @@ #include #include "motor.h" #include "config.h" -#include "homing.h" WebServer server(HTTP_PORT); -// HTML page for dual motor control const char index_html[] PROGMEM = R"rawliteral( @@ -16,342 +14,598 @@ const char index_html[] PROGMEM = R"rawliteral(
-

Dual Motor Control

- -
- -
-

Motor 1 (Left)

-
⚠️ STALL!
- -
-
-
FWD
-
0.00A
+

Dual Motor Sliders

+ +
+
+

Left

+
State: STOPPED
+
+
CURRENT METER
+
CURRENT0.00A
+
+
0
+
+
+
-
-
REV
-
0.00A
+
FWDSTOPREV
+
+
+ +
+

Right

+
State: STOPPED
+
+
CURRENT METER
+
CURRENT0.00A
+
+
0
+
+
+
+
FWDSTOPREV
- -
- STOPPED -
- -
-
20%
- -
- -
- - - -
-
- - -
-

Motor 2 (Right)

-
⚠️ STALL!
- -
-
-
FWD
-
0.00A
-
-
-
REV
-
0.00A
-
-
- -
- STOPPED -
- -
-
20%
- -
- -
- - - -
-
+
- -
-

Global Controls

-
- - + +
+
+ + + +
+
SYNCHRONIZED DRIVE DISABLED
+
+
+ +
+
+ Smooth % + + 40 +
-
)rawliteral"; +static void applyDriveValue(MotorController& motorCtrl, int value) { + int drive = constrain(value, -100, 100); + if (drive == 0) { + motorCtrl.stop(); + return; + } + + motorCtrl.setSpeed(abs(drive)); + motorCtrl.setDirection(drive > 0 ? 1 : -1); +} + void handleRoot() { server.send(200, "text/html", index_html); } -// Motor 1 handlers +void handleMotor1Drive() { + if (!server.hasArg("value")) { + server.send(400, "text/plain", "Missing value"); + return; + } + + applyDriveValue(motor1, server.arg("value").toInt()); + server.send(200, "text/plain", "OK"); +} + +void handleMotor2Drive() { + if (!server.hasArg("value")) { + server.send(400, "text/plain", "Missing value"); + return; + } + + applyDriveValue(motor2, server.arg("value").toInt()); + server.send(200, "text/plain", "OK"); +} + void handleMotor1Speed() { if (server.hasArg("value")) { - int speed = server.arg("value").toInt(); - motor1.setSpeed(speed); + motor1.setSpeed(server.arg("value").toInt()); server.send(200, "text/plain", "OK"); } else { server.send(400, "text/plain", "Missing value"); @@ -360,8 +614,7 @@ void handleMotor1Speed() { void handleMotor1Direction() { if (server.hasArg("value")) { - int dir = server.arg("value").toInt(); - motor1.setDirection(dir); + motor1.setDirection(server.arg("value").toInt()); server.send(200, "text/plain", "OK"); } else { server.send(400, "text/plain", "Missing value"); @@ -373,11 +626,9 @@ void handleMotor1Stop() { server.send(200, "text/plain", "OK"); } -// Motor 2 handlers void handleMotor2Speed() { if (server.hasArg("value")) { - int speed = server.arg("value").toInt(); - motor2.setSpeed(speed); + motor2.setSpeed(server.arg("value").toInt()); server.send(200, "text/plain", "OK"); } else { server.send(400, "text/plain", "Missing value"); @@ -386,8 +637,7 @@ void handleMotor2Speed() { void handleMotor2Direction() { if (server.hasArg("value")) { - int dir = server.arg("value").toInt(); - motor2.setDirection(dir); + motor2.setDirection(server.arg("value").toInt()); server.send(200, "text/plain", "OK"); } else { server.send(400, "text/plain", "Missing value"); @@ -399,33 +649,12 @@ void handleMotor2Stop() { server.send(200, "text/plain", "OK"); } -// Global stop (emergency stop both motors) void handleStop() { - stopCoordinatedMovement(); // Also stops coordinated movement state + motor1.stop(); + motor2.stop(); server.send(200, "text/plain", "OK"); } -// Homing / coordinated movement handler -void handleHoming() { - int speed = 50; // Default speed - if (server.hasArg("speed")) { - speed = server.arg("speed").toInt(); - if (speed < 20) speed = 20; - if (speed > 100) speed = 100; - } - - int newState = toggleCoordinatedMovement(speed); - - // Return JSON with state info - String json = "{"; - json += "\"state\":" + String(newState) + ","; - json += "\"motor1Done\":" + String(isCoordinatedMovementDone() || (motor1.getDirection() == 0 && newState != 0) ? "true" : "false") + ","; - json += "\"motor2Done\":" + String(isCoordinatedMovementDone() || (motor2.getDirection() == 0 && newState != 0) ? "true" : "false"); - json += "}"; - server.send(200, "application/json", json); -} - -// Legacy handlers (map to motor1 for backwards compatibility) void handleSpeed() { handleMotor1Speed(); } @@ -434,63 +663,46 @@ void handleDirection() { handleMotor1Direction(); } -// Status handler - returns both motors' data and coordinated state void handleStatus() { String json = "{"; - - // Motor 1 status + json += "\"motor1\":{"; json += "\"speed\":" + String(motor1.getSpeed()) + ","; json += "\"direction\":" + String(motor1.getDirection()) + ","; json += "\"currentR\":" + String(motor1.getCurrentRight(), 2) + ","; - json += "\"currentL\":" + String(motor1.getCurrentLeft(), 2) + ","; - json += "\"stalled\":" + String(motor1.isStalled() ? "true" : "false"); + json += "\"currentL\":" + String(motor1.getCurrentLeft(), 2); json += "},"; - - // Motor 2 status + json += "\"motor2\":{"; json += "\"speed\":" + String(motor2.getSpeed()) + ","; json += "\"direction\":" + String(motor2.getDirection()) + ","; json += "\"currentR\":" + String(motor2.getCurrentRight(), 2) + ","; - json += "\"currentL\":" + String(motor2.getCurrentLeft(), 2) + ","; - json += "\"stalled\":" + String(motor2.isStalled() ? "true" : "false"); - json += "},"; - - // Coordinated movement status - int coordState = getCoordinatedState(); - json += "\"coordState\":" + String(coordState) + ","; - // Motor is "done" if stalled/stopped during coordinated movement - bool m1Done = (coordState != 0) && (motor1.getDirection() == 0); - bool m2Done = (coordState != 0) && (motor2.getDirection() == 0); - json += "\"motor1Done\":" + String(m1Done ? "true" : "false") + ","; - json += "\"motor2Done\":" + String(m2Done ? "true" : "false"); - + json += "\"currentL\":" + String(motor2.getCurrentLeft(), 2); + json += "}"; + json += "}"; server.send(200, "application/json", json); } void setupWebServer() { server.on("/", handleRoot); - - // Motor 1 endpoints + + server.on("/motor1/drive", handleMotor1Drive); server.on("/motor1/speed", handleMotor1Speed); server.on("/motor1/direction", handleMotor1Direction); server.on("/motor1/stop", handleMotor1Stop); - - // Motor 2 endpoints + + server.on("/motor2/drive", handleMotor2Drive); server.on("/motor2/speed", handleMotor2Speed); server.on("/motor2/direction", handleMotor2Direction); server.on("/motor2/stop", handleMotor2Stop); - - // Legacy endpoints (backwards compatibility - map to motor1) + server.on("/speed", handleSpeed); server.on("/direction", handleDirection); - - // Global endpoints - server.on("/stop", handleStop); // Emergency stop both motors + + server.on("/stop", handleStop); server.on("/status", handleStatus); - server.on("/homing", handleHoming); // Coordinated movement / homing - + server.begin(); Serial.println("Web server started on port 80"); }