Remove all stall detection logic, add 100ms current logging

- Removed all STALL_* configuration parameters from config.h
- Simplified motor.h: removed stall-related methods and member variables
- Simplified motor.cpp: deleted checkStall(), resetStallDetection()
- Added frequent current logging (100ms) for data collection
- Removed stall callback system from main.cpp
- Simplified pingpong mode: time-based only, removed stall-return option
- Updated webserver: removed stall warning UI, removed stallReturn checkbox
- Updated status JSON: removed stalled and ppStallReturn fields

This version is for testing with new beefy PSU to collect current data
before designing new stall detection algorithm.
This commit is contained in:
devdesk
2026-02-05 20:59:38 +02:00
parent 241d1ae457
commit fcfee5fa66
5 changed files with 32 additions and 300 deletions

View File

@@ -37,22 +37,8 @@
#define ADC_VREF 3.3f // ADC reference voltage
#define CURRENT_CALIBRATION 1.0f // Calibration factor (measured current / reported current)
// Stall Detection Configuration (delta-based)
// Detects sudden current spikes above the rolling average
#define STALL_DELTA_THRESHOLD 1.2f // Current spike above average that indicates stall (amps)
#define STALL_EMA_ALPHA 0.1f // EMA smoothing factor (0.1 = slow, 0.5 = fast response)
#define STALL_EMA_BASELINE 2.0f // Expected running current baseline for EMA seeding (amps)
#define STALL_STABILIZE_MS 300 // Ignore stalls for this long after motor starts/changes
#define STALL_CONFIRM_MS 50 // Current must exceed threshold for this long to confirm stall
#define DISABLE_STALL_DETECT false // Set to true to disable stall detection
// Under-current stall detection: motor commanded but drawing no current
#define STALL_UNDERCURRENT_THRESHOLD 0.5f // If current below this for too long = stall (amps)
#define STALL_UNDERCURRENT_MS 1000 // Under-current must persist this long to confirm stall
// Repeated spike stall detection: catches oscillating stall (driver protection cycling)
#define STALL_CANDIDATE_COUNT 5 // Number of stall candidates in window to confirm stall
#define STALL_CANDIDATE_WINDOW_MS 2000 // Time window for counting stall candidates
// Current logging interval for data collection
#define CURRENT_LOG_INTERVAL_MS 100 // Log current readings every 100ms
// Web Server
#define HTTP_PORT 80

View File

@@ -10,53 +10,33 @@ public:
void setSpeed(int speed); // 0-100 percentage
void setDirection(int dir); // -1=reverse, 0=stop, 1=forward
void stop();
void update(); // Call in loop() for stall detection
void update(); // Call in loop() for current monitoring
int getSpeed();
int getDirection();
float getCurrentRight(); // Current in amps (forward direction)
float getCurrentLeft(); // Current in amps (reverse direction)
float getCurrentActive(); // Current from active direction
bool isStalled(); // True if stall detected
// Callback for stall events
void setStallCallback(void (*callback)(float current));
// Pingpong mode
void startPingpong(int speed, int timeMs, int speedRandomPercent, int timeRandomPercent, bool useStallReturn);
// Pingpong mode (time-based only)
void startPingpong(int speed, int timeMs, int speedRandomPercent, int timeRandomPercent);
void stopPingpong();
bool isPingpongActive();
int getPingpongSpeed();
int getPingpongTime();
int getPingpongSpeedRandom();
int getPingpongTimeRandom();
bool getPingpongStallReturn();
private:
int _speed = 0;
int _direction = 0;
float _currentRight = 0;
float _currentLeft = 0;
bool _stalled = false;
unsigned long _stallStartTime = 0;
void (*_stallCallback)(float current) = nullptr;
// ADC zero-current offset (calibrated at startup)
int _adcOffsetRight = 0;
int _adcOffsetLeft = 0;
// Delta-based stall detection (rolling average tracking)
float _currentEMA = 0; // Exponential moving average of current
unsigned long _motorStartTime = 0; // When motor started (for stabilization period)
bool _emaInitialized = false; // EMA needs seeding on first reading
// Under-current stall detection
unsigned long _undercurrentStartTime = 0; // When under-current condition started
// Repeated spike detection (oscillating stall pattern)
int _stallCandidateCount = 0; // Number of stall candidates in current window
unsigned long _stallCandidateWindowStart = 0; // When current counting window started
// Pingpong state
bool _pingpongActive = false;
int _pingpongBaseSpeed = 50;
@@ -67,13 +47,10 @@ private:
int _pingpongCurrentTime = 2000;
unsigned long _pingpongLastSwitch = 0;
int _pingpongDirection = 1;
bool _pingpongUseStallReturn = false; // Return only after stall detection
void applyMotorState();
float readCurrentSense(int pin);
void calibrateCurrentOffset();
void checkStall();
void resetStallDetection();
void updatePingpong();
int applyRandomness(int baseValue, int randomPercent);
};

View File

@@ -33,32 +33,18 @@ void setupWiFi() {
}
}
// Stall protection callback - stops motor immediately when stall detected
// (unless pingpong with stallReturn is active - it handles stall by switching direction)
void onMotorStall(float current) {
if (motor.isPingpongActive() && motor.getPingpongStallReturn()) {
// Pingpong will handle stall by switching direction
Serial.printf("STALL DETECTED: Pingpong will switch direction (current: %.2fA)\n", current);
return;
}
Serial.printf("STALL PROTECTION: Stopping motor (current: %.2fA)\n", current);
motor.stop();
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n=============================");
Serial.println(" BTS7960 Motor Controller");
Serial.println(" (Stall Detection Removed)");
Serial.println("=============================\n");
// Initialize motor controller
motor.begin();
// Register stall protection callback
motor.setStallCallback(onMotorStall);
// Connect to WiFi
setupWiFi();
@@ -72,6 +58,6 @@ void setup() {
void loop() {
handleWebServer();
motor.update(); // Update current sensing and stall detection
motor.update(); // Update current sensing and logging
delay(1);
}

View File

@@ -45,34 +45,21 @@ void MotorController::begin() {
void MotorController::setSpeed(int speed) {
_speed = constrain(speed, 0, 100);
resetStallDetection();
applyMotorState();
}
void MotorController::setDirection(int dir) {
_direction = constrain(dir, -1, 1);
resetStallDetection();
applyMotorState();
}
void MotorController::stop() {
// Don't reset _speed - keep last speed setting
_direction = 0;
resetStallDetection();
ledcWrite(PWM_CHANNEL_R, 0);
ledcWrite(PWM_CHANNEL_L, 0);
}
void MotorController::resetStallDetection() {
_stalled = false;
_stallStartTime = 0;
_undercurrentStartTime = 0;
_stallCandidateCount = 0;
_stallCandidateWindowStart = 0;
_emaInitialized = false; // Re-seed EMA on motor state change
_motorStartTime = millis();
}
void MotorController::update() {
#if CURRENT_SENSING_ENABLED
static unsigned long lastPrintTime = 0;
@@ -81,17 +68,13 @@ void MotorController::update() {
_currentRight = readCurrentSense(R_IS_PIN);
_currentLeft = readCurrentSense(L_IS_PIN);
// Log current readings every 500ms when motor is running
if ((_direction != 0 || _speed != 0) && (millis() - lastPrintTime > 500)) {
lastPrintTime = millis();
float activeCurrent = getCurrentActive();
float delta = activeCurrent - _currentEMA;
Serial.printf("Current: R=%.2fA L=%.2fA Active=%.2fA (avg=%.2fA, delta=%.2fA, thresh=%.1fA)\n",
_currentRight, _currentLeft, activeCurrent, _currentEMA, delta, STALL_DELTA_THRESHOLD);
// Log current readings frequently for data collection
unsigned long now = millis();
if (now - lastPrintTime >= CURRENT_LOG_INTERVAL_MS) {
lastPrintTime = now;
Serial.printf("CURRENT: R=%.2fA L=%.2fA dir=%d spd=%d\n",
_currentRight, _currentLeft, _direction, _speed);
}
// Check for stall condition
checkStall();
#endif
// Update pingpong mode
@@ -123,14 +106,6 @@ float MotorController::getCurrentActive() {
return 0.0f;
}
bool MotorController::isStalled() {
return _stalled;
}
void MotorController::setStallCallback(void (*callback)(float current)) {
_stallCallback = callback;
}
void MotorController::applyMotorState() {
// Apply minimum PWM when motor is running
int effectiveSpeed = _speed;
@@ -206,32 +181,27 @@ void MotorController::calibrateCurrentOffset() {
#endif
}
// Pingpong mode implementation
void MotorController::startPingpong(int speed, int timeMs, int speedRandomPercent, int timeRandomPercent, bool useStallReturn) {
// Pingpong mode implementation (time-based only)
void MotorController::startPingpong(int speed, int timeMs, int speedRandomPercent, int timeRandomPercent) {
_pingpongBaseSpeed = constrain(speed, 0, 100);
_pingpongBaseTime = constrain(timeMs, 100, 30000);
_pingpongSpeedRandomPercent = constrain(speedRandomPercent, 0, 100);
_pingpongTimeRandomPercent = constrain(timeRandomPercent, 0, 100);
_pingpongUseStallReturn = useStallReturn;
_pingpongDirection = 1;
_pingpongCurrentSpeed = applyRandomness(_pingpongBaseSpeed, _pingpongSpeedRandomPercent);
// Time randomness disabled when using stall return
_pingpongCurrentTime = _pingpongUseStallReturn ? _pingpongBaseTime : applyRandomness(_pingpongBaseTime, _pingpongTimeRandomPercent);
_pingpongCurrentTime = applyRandomness(_pingpongBaseTime, _pingpongTimeRandomPercent);
_pingpongLastSwitch = millis();
_pingpongActive = true;
_stalled = false; // Reset stall state when starting pingpong
_stallStartTime = 0;
// Apply initial state
_speed = _pingpongCurrentSpeed;
_direction = _pingpongDirection;
applyMotorState();
Serial.printf("Pingpong started: speed=%d%% (base=%d, rand=%d%%), time=%dms (base=%d, rand=%d%%), stallReturn=%s\n",
Serial.printf("Pingpong started: speed=%d%% (base=%d, rand=%d%%), time=%dms (base=%d, rand=%d%%)\n",
_pingpongCurrentSpeed, _pingpongBaseSpeed, _pingpongSpeedRandomPercent,
_pingpongCurrentTime, _pingpongBaseTime, _pingpongTimeRandomPercent,
_pingpongUseStallReturn ? "true" : "false");
_pingpongCurrentTime, _pingpongBaseTime, _pingpongTimeRandomPercent);
}
void MotorController::stopPingpong() {
@@ -260,41 +230,19 @@ int MotorController::getPingpongTimeRandom() {
return _pingpongTimeRandomPercent;
}
bool MotorController::getPingpongStallReturn() {
return _pingpongUseStallReturn;
}
void MotorController::updatePingpong() {
if (!_pingpongActive) return;
bool shouldSwitch = false;
unsigned long now = millis();
if (_pingpongUseStallReturn) {
// Switch direction only when stall is detected
if (_stalled) {
shouldSwitch = true;
Serial.println("Pingpong: stall detected, switching direction");
}
} else {
// Time-based switching
if ((now - _pingpongLastSwitch) >= (unsigned long)_pingpongCurrentTime) {
shouldSwitch = true;
}
}
if (shouldSwitch) {
// Time-based switching
if ((now - _pingpongLastSwitch) >= (unsigned long)_pingpongCurrentTime) {
// Switch direction
_pingpongDirection = -_pingpongDirection;
// Full stall detection reset for new direction
// This triggers STALL_STABILIZE_MS grace period to ignore motor inrush current
resetStallDetection();
// Apply randomness for next cycle
_pingpongCurrentSpeed = applyRandomness(_pingpongBaseSpeed, _pingpongSpeedRandomPercent);
// Time randomness disabled when using stall return
_pingpongCurrentTime = _pingpongUseStallReturn ? _pingpongBaseTime : applyRandomness(_pingpongBaseTime, _pingpongTimeRandomPercent);
_pingpongCurrentTime = applyRandomness(_pingpongBaseTime, _pingpongTimeRandomPercent);
_pingpongLastSwitch = now;
// Apply new state
@@ -302,9 +250,8 @@ void MotorController::updatePingpong() {
_direction = _pingpongDirection;
applyMotorState();
Serial.printf("Pingpong switch: dir=%d, speed=%d%%, next_time=%dms, stallReturn=%s\n",
_pingpongDirection, _pingpongCurrentSpeed, _pingpongCurrentTime,
_pingpongUseStallReturn ? "true" : "false");
Serial.printf("Pingpong switch: dir=%d, speed=%d%%, next_time=%dms\n",
_pingpongDirection, _pingpongCurrentSpeed, _pingpongCurrentTime);
}
}
@@ -318,120 +265,3 @@ int MotorController::applyRandomness(int baseValue, int randomPercent) {
// Ensure result stays positive and reasonable
return max(1, result);
}
void MotorController::checkStall() {
#if CURRENT_SENSING_ENABLED
if (DISABLE_STALL_DETECT) return;
// Only check stall when motor should be running
if (_direction == 0 || _speed == 0) {
_stalled = false;
_stallStartTime = 0;
_undercurrentStartTime = 0;
_emaInitialized = false;
return;
}
unsigned long now = millis();
float activeCurrent = getCurrentActive();
// Initialize EMA at expected baseline (prevents inrush from looking like stall)
if (!_emaInitialized) {
_currentEMA = STALL_EMA_BASELINE;
_emaInitialized = true;
}
// During stabilization: DON'T update EMA - keep baseline to avoid drift toward 0
// This lets stall detection work correctly even if starting from stalled position
if ((now - _motorStartTime) < STALL_STABILIZE_MS) {
return;
}
// Calculate delta from average
float delta = activeCurrent - _currentEMA;
// Check if current spike exceeds delta threshold
bool spikeDetected = (delta > STALL_DELTA_THRESHOLD);
// Update EMA only when no spike is being investigated
// This prevents EMA from "chasing" up to the spike during detection window
if (!spikeDetected && !_stalled) {
_currentEMA = (STALL_EMA_ALPHA * activeCurrent) + ((1.0f - STALL_EMA_ALPHA) * _currentEMA);
}
if (spikeDetected) {
if (_stallStartTime == 0) {
// Start timing potential stall
_stallStartTime = now;
// Count this as a stall candidate for repeated-spike detection
if (_stallCandidateWindowStart == 0) {
_stallCandidateWindowStart = now;
_stallCandidateCount = 1;
} else if ((now - _stallCandidateWindowStart) > STALL_CANDIDATE_WINDOW_MS) {
// Window expired, start new window
_stallCandidateWindowStart = now;
_stallCandidateCount = 1;
} else {
_stallCandidateCount++;
}
Serial.printf("Stall candidate #%d: current=%.2fA, avg=%.2fA, delta=%.2fA\n",
_stallCandidateCount, activeCurrent, _currentEMA, delta);
// Check if enough candidates to confirm stall (oscillating pattern)
if (_stallCandidateCount >= STALL_CANDIDATE_COUNT && !_stalled) {
_stalled = true;
Serial.printf("STALL DETECTED (repeated spikes)! %d candidates in %lums\n",
_stallCandidateCount, now - _stallCandidateWindowStart);
if (_stallCallback != nullptr) {
_stallCallback(activeCurrent);
}
}
} else if ((now - _stallStartTime) > STALL_CONFIRM_MS) {
// Sustained spike - stall confirmed
if (!_stalled) {
_stalled = true;
Serial.printf("STALL DETECTED! Current: %.2fA (avg: %.2fA, delta: %.2fA)\n",
activeCurrent, _currentEMA, delta);
// Call callback if registered
if (_stallCallback != nullptr) {
_stallCallback(activeCurrent);
}
}
}
} else {
// Current normal, reset spike stall timing (but keep EMA updating)
if (_stallStartTime != 0) {
Serial.printf("Stall candidate cleared: current=%.2fA, avg=%.2fA\n",
activeCurrent, _currentEMA);
}
_stallStartTime = 0;
// Don't clear _stalled here - under-current/repeated-spike detection may have set it
}
// Under-current stall detection: motor commanded but not drawing current
// This catches stalls where the motor can't even start (e.g., already at end stop)
if (!_stalled && activeCurrent < STALL_UNDERCURRENT_THRESHOLD) {
if (_undercurrentStartTime == 0) {
_undercurrentStartTime = now;
} else if ((now - _undercurrentStartTime) > STALL_UNDERCURRENT_MS) {
_stalled = true;
Serial.printf("STALL DETECTED (under-current)! Current: %.2fA (threshold: %.2fA)\n",
activeCurrent, STALL_UNDERCURRENT_THRESHOLD);
if (_stallCallback != nullptr) {
_stallCallback(activeCurrent);
}
}
} else if (activeCurrent >= STALL_UNDERCURRENT_THRESHOLD) {
// Current is normal, reset under-current timer and clear stall
_undercurrentStartTime = 0;
if (!spikeDetected) {
_stalled = false;
}
}
#endif
}

View File

@@ -58,15 +58,6 @@ const char index_html[] PROGMEM = R"rawliteral(
font-size: 12px;
color: #888;
}
.stall-warning {
background: #ff5252;
color: white;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
display: none;
}
.stall-warning.active { display: block; }
.slider-container {
background: #16213e;
padding: 20px;
@@ -178,18 +169,14 @@ const char index_html[] PROGMEM = R"rawliteral(
<div class="container">
<h1>Motor Control</h1>
<div class="stall-warning" id="stallWarning">
STALL DETECTED - Motor Stopped
</div>
<div class="current-display">
<div>
<div class="current-label">CURRENT (Active)</div>
<div class="current-value"><span id="currentActive">0.00</span>A</div>
<div class="current-label">CURRENT R</div>
<div class="current-value"><span id="currentR">0.00</span>A</div>
</div>
<div>
<div class="current-label">THRESHOLD</div>
<div class="current-value" style="color:#ff9100;">4.0A</div>
<div class="current-label">CURRENT L</div>
<div class="current-value"><span id="currentL">0.00</span>A</div>
</div>
</div>
@@ -239,12 +226,6 @@ const char index_html[] PROGMEM = R"rawliteral(
<span class="value"><span id="ppTimeRandVal">0</span>%</span>
</div>
<input type="range" class="setting-slider" id="ppTimeRand" min="0" max="50" value="0">
<div class="setting-row" style="margin-top:15px;">
<label for="ppStallReturn">Return on stall only</label>
<input type="checkbox" id="ppStallReturn" style="width:25px;height:25px;">
</div>
<small style="color:#888;display:block;margin-top:5px;">When enabled, direction switches only after stall detection (time randomness disabled)</small>
</div>
<div class="buttons">
@@ -269,16 +250,6 @@ const char index_html[] PROGMEM = R"rawliteral(
const ppSpeedRandVal = document.getElementById('ppSpeedRandVal');
const ppTimeRandVal = document.getElementById('ppTimeRandVal');
const ppStatus = document.getElementById('pingpongStatus');
const ppStallReturn = document.getElementById('ppStallReturn');
// Disable time randomness when stall return is checked
ppStallReturn.onchange = function() {
ppTimeRand.disabled = this.checked;
if (this.checked) {
ppTimeRand.value = 0;
ppTimeRandVal.textContent = '0';
}
};
slider.oninput = function() {
speedVal.textContent = this.value;
@@ -316,8 +287,7 @@ const char index_html[] PROGMEM = R"rawliteral(
speed: ppSpeed.value,
time: ppTime.value,
speedRand: ppSpeedRand.value,
timeRand: ppStallReturn.checked ? 0 : ppTimeRand.value,
stallReturn: ppStallReturn.checked ? 1 : 0
timeRand: ppTimeRand.value
});
fetch('/pingpong/start?' + params)
.then(r => r.text())
@@ -350,17 +320,8 @@ const char index_html[] PROGMEM = R"rawliteral(
updateStatus();
// Update current display
const active = data.direction > 0 ? data.currentR :
data.direction < 0 ? data.currentL : 0;
document.getElementById('currentActive').textContent = active.toFixed(2);
// Show/hide stall warning
const stallWarning = document.getElementById('stallWarning');
if (data.stalled) {
stallWarning.classList.add('active');
} else {
stallWarning.classList.remove('active');
}
document.getElementById('currentR').textContent = data.currentR.toFixed(2);
document.getElementById('currentL').textContent = data.currentL.toFixed(2);
// Update pingpong status
if (data.pingpong) {
@@ -376,8 +337,6 @@ const char index_html[] PROGMEM = R"rawliteral(
ppSpeedRandVal.textContent = data.ppSpeedRand;
ppTimeRand.value = data.ppTimeRand;
ppTimeRandVal.textContent = data.ppTimeRand;
ppStallReturn.checked = data.ppStallReturn;
ppTimeRand.disabled = data.ppStallReturn;
} else {
ppStatus.textContent = 'INACTIVE';
ppStatus.classList.remove('active');
@@ -428,13 +387,11 @@ void handleStatus() {
",\"direction\":" + String(motor.getDirection()) +
",\"currentR\":" + String(motor.getCurrentRight(), 2) +
",\"currentL\":" + String(motor.getCurrentLeft(), 2) +
",\"stalled\":" + (motor.isStalled() ? "true" : "false") +
",\"pingpong\":" + (motor.isPingpongActive() ? "true" : "false") +
",\"ppSpeed\":" + String(motor.getPingpongSpeed()) +
",\"ppTime\":" + String(motor.getPingpongTime()) +
",\"ppSpeedRand\":" + String(motor.getPingpongSpeedRandom()) +
",\"ppTimeRand\":" + String(motor.getPingpongTimeRandom()) +
",\"ppStallReturn\":" + (motor.getPingpongStallReturn() ? "true" : "false") + "}";
",\"ppTimeRand\":" + String(motor.getPingpongTimeRandom()) + "}";
server.send(200, "application/json", json);
}
@@ -443,7 +400,6 @@ void handlePingpongStart() {
int time = 2000;
int speedRand = 0;
int timeRand = 0;
bool stallReturn = false;
if (server.hasArg("speed")) {
speed = server.arg("speed").toInt();
@@ -457,11 +413,8 @@ void handlePingpongStart() {
if (server.hasArg("timeRand")) {
timeRand = server.arg("timeRand").toInt();
}
if (server.hasArg("stallReturn")) {
stallReturn = server.arg("stallReturn").toInt() != 0;
}
motor.startPingpong(speed, time, speedRand, timeRand, stallReturn);
motor.startPingpong(speed, time, speedRand, timeRand);
server.send(200, "text/plain", "OK");
}