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.
This commit is contained in:
devdesk
2026-02-20 14:30:46 +02:00
parent 4c30610af0
commit 25dc34c6e3
7 changed files with 542 additions and 554 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -4,9 +4,6 @@
#include <Arduino.h>
#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

View File

@@ -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.

View File

@@ -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();

View File

@@ -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;

File diff suppressed because it is too large Load Diff