Add stall-based pingpong return mode and calibration improvements

- Add MIN_PWM_PERCENT (20%) to ensure motor starts reliably
- Add CURRENT_CALIBRATION factor (0.33) for accurate current readings
- Add stall-based return option for pingpong mode (returns on stall instead of time)
- Update stall detection: 7A threshold, 1000ms confirm time
- Add DISABLE_STALL_DETECT config option
- Keep last speed setting when motor stops (UI improvement)
- Update webserver UI with 'Return on stall only' checkbox
This commit is contained in:
devdesk
2026-02-05 17:50:22 +02:00
parent ba27db4f55
commit 7a6617f9c0
4 changed files with 95 additions and 24 deletions

View File

@@ -24,6 +24,7 @@
#define PWM_RESOLUTION 8 // 8-bit resolution (0-255)
#define PWM_CHANNEL_R 0 // LEDC channel for RPWM
#define PWM_CHANNEL_L 1 // LEDC channel for LPWM
#define MIN_PWM_PERCENT 20 // Minimum PWM when motor is running (%)
// Current Sense Configuration
// BTS7960 current sense ratio: 8500:1 (kilo-amps)
@@ -33,8 +34,10 @@
#define SENSE_RESISTOR 1000.0f // 1kΩ sense resistor (ohms)
#define ADC_MAX 4095.0f // 12-bit ADC max value
#define ADC_VREF 3.3f // ADC reference voltage
#define STALL_CURRENT_THRESHOLD 4.0f // Current (amps) that indicates stall
#define STALL_DETECT_TIME_MS 2500 // Time to confirm stall (ms) - accounts for startup inrush
#define CURRENT_CALIBRATION 0.33f // Calibration factor (measured current / reported current)
#define STALL_CURRENT_THRESHOLD 7.0f // Current (amps) that indicates stall
#define STALL_DETECT_TIME_MS 1000 // Time to confirm stall (ms) - accounts for startup inrush
#define DISABLE_STALL_DETECT false // Set to true to disable stall detection
// Web Server
#define HTTP_PORT 80

View File

@@ -23,13 +23,14 @@ public:
void setStallCallback(void (*callback)(float current));
// Pingpong mode
void startPingpong(int speed, int timeMs, int speedRandomPercent, int timeRandomPercent);
void startPingpong(int speed, int timeMs, int speedRandomPercent, int timeRandomPercent, bool useStallReturn);
void stopPingpong();
bool isPingpongActive();
int getPingpongSpeed();
int getPingpongTime();
int getPingpongSpeedRandom();
int getPingpongTimeRandom();
bool getPingpongStallReturn();
private:
int _speed = 0;
@@ -50,6 +51,7 @@ 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);

View File

@@ -52,7 +52,7 @@ void MotorController::setDirection(int dir) {
}
void MotorController::stop() {
_speed = 0;
// Don't reset _speed - keep last speed setting
_direction = 0;
_stalled = false;
_stallStartTime = 0;
@@ -117,8 +117,14 @@ void MotorController::setStallCallback(void (*callback)(float current)) {
}
void MotorController::applyMotorState() {
// Apply minimum PWM when motor is running
int effectiveSpeed = _speed;
if (_speed > 0 && _speed < MIN_PWM_PERCENT) {
effectiveSpeed = MIN_PWM_PERCENT;
}
// Convert percentage to 8-bit PWM value
int pwmValue = map(_speed, 0, 100, 0, 255);
int pwmValue = map(effectiveSpeed, 0, 100, 0, 255);
if (_direction == 0 || _speed == 0) {
// Stop - both PWM outputs off
@@ -151,6 +157,9 @@ float MotorController::readCurrentSense(int pin) {
// Therefore: I_load = V * CURRENT_SENSE_RATIO / R_sense
float current = (voltage * CURRENT_SENSE_RATIO) / SENSE_RESISTOR;
// Apply calibration factor (adjust for real-world measurement differences)
current *= CURRENT_CALIBRATION;
return current;
#else
return 0.0f;
@@ -158,26 +167,31 @@ float MotorController::readCurrentSense(int pin) {
}
// Pingpong mode implementation
void MotorController::startPingpong(int speed, int timeMs, int speedRandomPercent, int timeRandomPercent) {
void MotorController::startPingpong(int speed, int timeMs, int speedRandomPercent, int timeRandomPercent, bool useStallReturn) {
_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);
_pingpongCurrentTime = applyRandomness(_pingpongBaseTime, _pingpongTimeRandomPercent);
// Time randomness disabled when using stall return
_pingpongCurrentTime = _pingpongUseStallReturn ? _pingpongBaseTime : 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%%)\n",
Serial.printf("Pingpong started: speed=%d%% (base=%d, rand=%d%%), time=%dms (base=%d, rand=%d%%), stallReturn=%s\n",
_pingpongCurrentSpeed, _pingpongBaseSpeed, _pingpongSpeedRandomPercent,
_pingpongCurrentTime, _pingpongBaseTime, _pingpongTimeRandomPercent);
_pingpongCurrentTime, _pingpongBaseTime, _pingpongTimeRandomPercent,
_pingpongUseStallReturn ? "true" : "false");
}
void MotorController::stopPingpong() {
@@ -206,17 +220,41 @@ int MotorController::getPingpongTimeRandom() {
return _pingpongTimeRandomPercent;
}
bool MotorController::getPingpongStallReturn() {
return _pingpongUseStallReturn;
}
void MotorController::updatePingpong() {
if (!_pingpongActive) return;
bool shouldSwitch = false;
unsigned long now = millis();
if ((now - _pingpongLastSwitch) >= (unsigned long)_pingpongCurrentTime) {
// Time to switch direction
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) {
// Switch direction
_pingpongDirection = -_pingpongDirection;
// Reset stall state for new direction
_stalled = false;
_stallStartTime = 0;
// Apply randomness for next cycle
_pingpongCurrentSpeed = applyRandomness(_pingpongBaseSpeed, _pingpongSpeedRandomPercent);
_pingpongCurrentTime = applyRandomness(_pingpongBaseTime, _pingpongTimeRandomPercent);
// Time randomness disabled when using stall return
_pingpongCurrentTime = _pingpongUseStallReturn ? _pingpongBaseTime : applyRandomness(_pingpongBaseTime, _pingpongTimeRandomPercent);
_pingpongLastSwitch = now;
// Apply new state
@@ -224,8 +262,9 @@ void MotorController::updatePingpong() {
_direction = _pingpongDirection;
applyMotorState();
Serial.printf("Pingpong switch: dir=%d, speed=%d%%, next_time=%dms\n",
_pingpongDirection, _pingpongCurrentSpeed, _pingpongCurrentTime);
Serial.printf("Pingpong switch: dir=%d, speed=%d%%, next_time=%dms, stallReturn=%s\n",
_pingpongDirection, _pingpongCurrentSpeed, _pingpongCurrentTime,
_pingpongUseStallReturn ? "true" : "false");
}
}

View File

@@ -199,8 +199,8 @@ const char index_html[] PROGMEM = R"rawliteral(
<div class="slider-container">
<label>Speed</label>
<div class="speed-value"><span id="speedValue">0</span>%</div>
<input type="range" class="slider" id="speedSlider" min="0" max="100" value="0">
<div class="speed-value"><span id="speedValue">20</span>%</div>
<input type="range" class="slider" id="speedSlider" min="20" max="100" value="20">
</div>
<div class="buttons">
@@ -220,7 +220,7 @@ const char index_html[] PROGMEM = R"rawliteral(
<label>Speed</label>
<span class="value"><span id="ppSpeedVal">50</span>%</span>
</div>
<input type="range" class="setting-slider" id="ppSpeed" min="10" max="100" value="50">
<input type="range" class="setting-slider" id="ppSpeed" min="20" max="100" value="50">
<div class="setting-row" style="margin-top:15px;">
<label>Time before return</label>
@@ -239,6 +239,12 @@ 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">
@@ -263,6 +269,16 @@ 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;
@@ -289,8 +305,7 @@ const char index_html[] PROGMEM = R"rawliteral(
function stopMotor() {
currentDir = 0;
slider.value = 0;
speedVal.textContent = '0';
// Don't reset slider - keep last speed
fetch('/stop')
.then(r => r.text())
.then(() => updateStatus());
@@ -301,7 +316,8 @@ const char index_html[] PROGMEM = R"rawliteral(
speed: ppSpeed.value,
time: ppTime.value,
speedRand: ppSpeedRand.value,
timeRand: ppTimeRand.value
timeRand: ppStallReturn.checked ? 0 : ppTimeRand.value,
stallReturn: ppStallReturn.checked ? 1 : 0
});
fetch('/pingpong/start?' + params)
.then(r => r.text())
@@ -324,8 +340,12 @@ const char index_html[] PROGMEM = R"rawliteral(
fetch('/status')
.then(r => r.json())
.then(data => {
slider.value = data.speed;
speedVal.textContent = data.speed;
// Only update speed display if motor is running (direction != 0)
// When stopped, keep the last speed setting
if (data.direction != 0) {
slider.value = data.speed;
speedVal.textContent = data.speed;
}
currentDir = data.direction;
updateStatus();
@@ -356,6 +376,8 @@ 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');
@@ -411,7 +433,8 @@ void handleStatus() {
",\"ppSpeed\":" + String(motor.getPingpongSpeed()) +
",\"ppTime\":" + String(motor.getPingpongTime()) +
",\"ppSpeedRand\":" + String(motor.getPingpongSpeedRandom()) +
",\"ppTimeRand\":" + String(motor.getPingpongTimeRandom()) + "}";
",\"ppTimeRand\":" + String(motor.getPingpongTimeRandom()) +
",\"ppStallReturn\":" + (motor.getPingpongStallReturn() ? "true" : "false") + "}";
server.send(200, "application/json", json);
}
@@ -420,6 +443,7 @@ void handlePingpongStart() {
int time = 2000;
int speedRand = 0;
int timeRand = 0;
bool stallReturn = false;
if (server.hasArg("speed")) {
speed = server.arg("speed").toInt();
@@ -433,8 +457,11 @@ 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);
motor.startPingpong(speed, time, speedRand, timeRand, stallReturn);
server.send(200, "text/plain", "OK");
}