diff --git a/include/config.h b/include/config.h index fc54a99..4117e2e 100644 --- a/include/config.h +++ b/include/config.h @@ -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 diff --git a/include/motor.h b/include/motor.h index ba1d434..d80bf04 100644 --- a/include/motor.h +++ b/include/motor.h @@ -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); diff --git a/src/motor.cpp b/src/motor.cpp index fa00f2e..7906768 100644 --- a/src/motor.cpp +++ b/src/motor.cpp @@ -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"); } } diff --git a/src/webserver.cpp b/src/webserver.cpp index c044c2c..9370251 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -199,8 +199,8 @@ const char index_html[] PROGMEM = R"rawliteral(
-
0%
- +
20%
+
@@ -220,7 +220,7 @@ const char index_html[] PROGMEM = R"rawliteral( 50%
- +
@@ -239,6 +239,12 @@ const char index_html[] PROGMEM = R"rawliteral( 0%
+ +
+ + +
+ When enabled, direction switches only after stall detection (time randomness disabled)
@@ -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"); }