Add pingpong mode with speed, time, and randomness settings
- Add pingpong oscillation mode that alternates motor direction - Settings: base speed (10-100%), time before return (0.5-10s) - Randomness controls for both speed (0-50%) and time (0-50%) - Web UI with sliders and start/stop buttons - API endpoints: /pingpong/start and /pingpong/stop - Real-time status polling shows pingpong active state
This commit is contained in:
@@ -21,6 +21,15 @@ public:
|
||||
|
||||
// Callback for stall events
|
||||
void setStallCallback(void (*callback)(float current));
|
||||
|
||||
// Pingpong mode
|
||||
void startPingpong(int speed, int timeMs, int speedRandomPercent, int timeRandomPercent);
|
||||
void stopPingpong();
|
||||
bool isPingpongActive();
|
||||
int getPingpongSpeed();
|
||||
int getPingpongTime();
|
||||
int getPingpongSpeedRandom();
|
||||
int getPingpongTimeRandom();
|
||||
|
||||
private:
|
||||
int _speed = 0;
|
||||
@@ -31,9 +40,22 @@ private:
|
||||
unsigned long _stallStartTime = 0;
|
||||
void (*_stallCallback)(float current) = nullptr;
|
||||
|
||||
// Pingpong state
|
||||
bool _pingpongActive = false;
|
||||
int _pingpongBaseSpeed = 50;
|
||||
int _pingpongBaseTime = 2000;
|
||||
int _pingpongSpeedRandomPercent = 0;
|
||||
int _pingpongTimeRandomPercent = 0;
|
||||
int _pingpongCurrentSpeed = 50;
|
||||
int _pingpongCurrentTime = 2000;
|
||||
unsigned long _pingpongLastSwitch = 0;
|
||||
int _pingpongDirection = 1;
|
||||
|
||||
void applyMotorState();
|
||||
float readCurrentSense(int pin);
|
||||
void checkStall();
|
||||
void updatePingpong();
|
||||
int applyRandomness(int baseValue, int randomPercent);
|
||||
};
|
||||
|
||||
extern MotorController motor;
|
||||
|
||||
@@ -78,6 +78,9 @@ void MotorController::update() {
|
||||
// Check for stall condition
|
||||
checkStall();
|
||||
#endif
|
||||
|
||||
// Update pingpong mode
|
||||
updatePingpong();
|
||||
}
|
||||
|
||||
int MotorController::getSpeed() {
|
||||
@@ -154,6 +157,89 @@ float MotorController::readCurrentSense(int pin) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Pingpong mode implementation
|
||||
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);
|
||||
|
||||
_pingpongDirection = 1;
|
||||
_pingpongCurrentSpeed = applyRandomness(_pingpongBaseSpeed, _pingpongSpeedRandomPercent);
|
||||
_pingpongCurrentTime = applyRandomness(_pingpongBaseTime, _pingpongTimeRandomPercent);
|
||||
_pingpongLastSwitch = millis();
|
||||
_pingpongActive = true;
|
||||
|
||||
// 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",
|
||||
_pingpongCurrentSpeed, _pingpongBaseSpeed, _pingpongSpeedRandomPercent,
|
||||
_pingpongCurrentTime, _pingpongBaseTime, _pingpongTimeRandomPercent);
|
||||
}
|
||||
|
||||
void MotorController::stopPingpong() {
|
||||
_pingpongActive = false;
|
||||
stop();
|
||||
Serial.println("Pingpong stopped");
|
||||
}
|
||||
|
||||
bool MotorController::isPingpongActive() {
|
||||
return _pingpongActive;
|
||||
}
|
||||
|
||||
int MotorController::getPingpongSpeed() {
|
||||
return _pingpongBaseSpeed;
|
||||
}
|
||||
|
||||
int MotorController::getPingpongTime() {
|
||||
return _pingpongBaseTime;
|
||||
}
|
||||
|
||||
int MotorController::getPingpongSpeedRandom() {
|
||||
return _pingpongSpeedRandomPercent;
|
||||
}
|
||||
|
||||
int MotorController::getPingpongTimeRandom() {
|
||||
return _pingpongTimeRandomPercent;
|
||||
}
|
||||
|
||||
void MotorController::updatePingpong() {
|
||||
if (!_pingpongActive) return;
|
||||
|
||||
unsigned long now = millis();
|
||||
if ((now - _pingpongLastSwitch) >= (unsigned long)_pingpongCurrentTime) {
|
||||
// Time to switch direction
|
||||
_pingpongDirection = -_pingpongDirection;
|
||||
|
||||
// Apply randomness for next cycle
|
||||
_pingpongCurrentSpeed = applyRandomness(_pingpongBaseSpeed, _pingpongSpeedRandomPercent);
|
||||
_pingpongCurrentTime = applyRandomness(_pingpongBaseTime, _pingpongTimeRandomPercent);
|
||||
_pingpongLastSwitch = now;
|
||||
|
||||
// Apply new state
|
||||
_speed = _pingpongCurrentSpeed;
|
||||
_direction = _pingpongDirection;
|
||||
applyMotorState();
|
||||
|
||||
Serial.printf("Pingpong switch: dir=%d, speed=%d%%, next_time=%dms\n",
|
||||
_pingpongDirection, _pingpongCurrentSpeed, _pingpongCurrentTime);
|
||||
}
|
||||
}
|
||||
|
||||
int MotorController::applyRandomness(int baseValue, int randomPercent) {
|
||||
if (randomPercent == 0) return baseValue;
|
||||
|
||||
int maxVariation = (baseValue * randomPercent) / 100;
|
||||
int variation = random(-maxVariation, maxVariation + 1);
|
||||
int result = baseValue + variation;
|
||||
|
||||
// Ensure result stays positive and reasonable
|
||||
return max(1, result);
|
||||
}
|
||||
|
||||
void MotorController::checkStall() {
|
||||
#if CURRENT_SENSING_ENABLED
|
||||
// Only check stall when motor should be running
|
||||
|
||||
@@ -14,20 +14,21 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
<title>Motor Control</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
margin: 0;
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
h1 { color: #00d9ff; margin-bottom: 30px; }
|
||||
h2 { color: #00d9ff; margin: 20px 0 15px 0; font-size: 20px; }
|
||||
.status {
|
||||
background: #16213e;
|
||||
padding: 15px;
|
||||
@@ -66,10 +67,10 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
display: none;
|
||||
}
|
||||
.stall-warning.active { display: block; }
|
||||
.slider-container {
|
||||
background: #16213e;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
.slider-container {
|
||||
background: #16213e;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.slider {
|
||||
@@ -88,9 +89,9 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -108,11 +109,69 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
.btn-reverse { background: #ff5252; color: white; }
|
||||
.btn-stop { background: #ff9100; color: white; flex-basis: 100%; }
|
||||
.btn:hover { filter: brightness(1.1); }
|
||||
.speed-value {
|
||||
font-size: 48px;
|
||||
color: #00d9ff;
|
||||
.speed-value {
|
||||
font-size: 48px;
|
||||
color: #00d9ff;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.section-divider {
|
||||
border-top: 2px solid #0f3460;
|
||||
margin: 30px 0;
|
||||
}
|
||||
.pingpong-section {
|
||||
background: #16213e;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.pingpong-status {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.pingpong-status.active {
|
||||
background: #00c853;
|
||||
color: white;
|
||||
}
|
||||
.pingpong-status.inactive {
|
||||
background: #444;
|
||||
color: #aaa;
|
||||
}
|
||||
.setting-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.setting-row label {
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
}
|
||||
.setting-row .value {
|
||||
color: #00d9ff;
|
||||
font-weight: bold;
|
||||
min-width: 60px;
|
||||
}
|
||||
.setting-slider {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
-webkit-appearance: none;
|
||||
background: #0f3460;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.setting-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background: #00d9ff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-pingpong-start { background: #9c27b0; color: white; }
|
||||
.btn-pingpong-stop { background: #607d8b; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -149,6 +208,43 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
<button class="btn btn-reverse" onclick="setDir(-1)">REVERSE</button>
|
||||
<button class="btn btn-stop" onclick="stopMotor()">STOP</button>
|
||||
</div>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<h2>🏓 Pingpong Mode</h2>
|
||||
|
||||
<div class="pingpong-section">
|
||||
<div class="pingpong-status inactive" id="pingpongStatus">INACTIVE</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<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">
|
||||
|
||||
<div class="setting-row" style="margin-top:15px;">
|
||||
<label>Time before return</label>
|
||||
<span class="value"><span id="ppTimeVal">2.0</span>s</span>
|
||||
</div>
|
||||
<input type="range" class="setting-slider" id="ppTime" min="500" max="10000" value="2000" step="100">
|
||||
|
||||
<div class="setting-row" style="margin-top:15px;">
|
||||
<label>Speed randomness</label>
|
||||
<span class="value"><span id="ppSpeedRandVal">0</span>%</span>
|
||||
</div>
|
||||
<input type="range" class="setting-slider" id="ppSpeedRand" min="0" max="50" value="0">
|
||||
|
||||
<div class="setting-row" style="margin-top:15px;">
|
||||
<label>Time randomness</label>
|
||||
<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>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="btn btn-pingpong-start" onclick="startPingpong()">START PINGPONG</button>
|
||||
<button class="btn btn-pingpong-stop" onclick="stopPingpong()">STOP PINGPONG</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -157,6 +253,17 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
const dirStatus = document.getElementById('dirStatus');
|
||||
let currentDir = 0;
|
||||
|
||||
// Pingpong controls
|
||||
const ppSpeed = document.getElementById('ppSpeed');
|
||||
const ppTime = document.getElementById('ppTime');
|
||||
const ppSpeedRand = document.getElementById('ppSpeedRand');
|
||||
const ppTimeRand = document.getElementById('ppTimeRand');
|
||||
const ppSpeedVal = document.getElementById('ppSpeedVal');
|
||||
const ppTimeVal = document.getElementById('ppTimeVal');
|
||||
const ppSpeedRandVal = document.getElementById('ppSpeedRandVal');
|
||||
const ppTimeRandVal = document.getElementById('ppTimeRandVal');
|
||||
const ppStatus = document.getElementById('pingpongStatus');
|
||||
|
||||
slider.oninput = function() {
|
||||
speedVal.textContent = this.value;
|
||||
};
|
||||
@@ -167,6 +274,12 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
.then(console.log);
|
||||
};
|
||||
|
||||
// Pingpong slider handlers
|
||||
ppSpeed.oninput = function() { ppSpeedVal.textContent = this.value; };
|
||||
ppTime.oninput = function() { ppTimeVal.textContent = (this.value / 1000).toFixed(1); };
|
||||
ppSpeedRand.oninput = function() { ppSpeedRandVal.textContent = this.value; };
|
||||
ppTimeRand.oninput = function() { ppTimeRandVal.textContent = this.value; };
|
||||
|
||||
function setDir(dir) {
|
||||
currentDir = dir;
|
||||
fetch('/direction?value=' + dir)
|
||||
@@ -183,6 +296,24 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
.then(() => updateStatus());
|
||||
}
|
||||
|
||||
function startPingpong() {
|
||||
const params = new URLSearchParams({
|
||||
speed: ppSpeed.value,
|
||||
time: ppTime.value,
|
||||
speedRand: ppSpeedRand.value,
|
||||
timeRand: ppTimeRand.value
|
||||
});
|
||||
fetch('/pingpong/start?' + params)
|
||||
.then(r => r.text())
|
||||
.then(console.log);
|
||||
}
|
||||
|
||||
function stopPingpong() {
|
||||
fetch('/pingpong/stop')
|
||||
.then(r => r.text())
|
||||
.then(console.log);
|
||||
}
|
||||
|
||||
function updateStatus() {
|
||||
if (currentDir > 0) dirStatus.textContent = 'FORWARD';
|
||||
else if (currentDir < 0) dirStatus.textContent = 'REVERSE';
|
||||
@@ -210,6 +341,26 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
} else {
|
||||
stallWarning.classList.remove('active');
|
||||
}
|
||||
|
||||
// Update pingpong status
|
||||
if (data.pingpong) {
|
||||
ppStatus.textContent = 'ACTIVE';
|
||||
ppStatus.classList.remove('inactive');
|
||||
ppStatus.classList.add('active');
|
||||
// Update sliders to match current settings
|
||||
ppSpeed.value = data.ppSpeed;
|
||||
ppSpeedVal.textContent = data.ppSpeed;
|
||||
ppTime.value = data.ppTime;
|
||||
ppTimeVal.textContent = (data.ppTime / 1000).toFixed(1);
|
||||
ppSpeedRand.value = data.ppSpeedRand;
|
||||
ppSpeedRandVal.textContent = data.ppSpeedRand;
|
||||
ppTimeRand.value = data.ppTimeRand;
|
||||
ppTimeRandVal.textContent = data.ppTimeRand;
|
||||
} else {
|
||||
ppStatus.textContent = 'INACTIVE';
|
||||
ppStatus.classList.remove('active');
|
||||
ppStatus.classList.add('inactive');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -255,16 +406,51 @@ void handleStatus() {
|
||||
",\"direction\":" + String(motor.getDirection()) +
|
||||
",\"currentR\":" + String(motor.getCurrentRight(), 2) +
|
||||
",\"currentL\":" + String(motor.getCurrentLeft(), 2) +
|
||||
",\"stalled\":" + (motor.isStalled() ? "true" : "false") + "}";
|
||||
",\"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()) + "}";
|
||||
server.send(200, "application/json", json);
|
||||
}
|
||||
|
||||
void handlePingpongStart() {
|
||||
int speed = 50;
|
||||
int time = 2000;
|
||||
int speedRand = 0;
|
||||
int timeRand = 0;
|
||||
|
||||
if (server.hasArg("speed")) {
|
||||
speed = server.arg("speed").toInt();
|
||||
}
|
||||
if (server.hasArg("time")) {
|
||||
time = server.arg("time").toInt();
|
||||
}
|
||||
if (server.hasArg("speedRand")) {
|
||||
speedRand = server.arg("speedRand").toInt();
|
||||
}
|
||||
if (server.hasArg("timeRand")) {
|
||||
timeRand = server.arg("timeRand").toInt();
|
||||
}
|
||||
|
||||
motor.startPingpong(speed, time, speedRand, timeRand);
|
||||
server.send(200, "text/plain", "OK");
|
||||
}
|
||||
|
||||
void handlePingpongStop() {
|
||||
motor.stopPingpong();
|
||||
server.send(200, "text/plain", "OK");
|
||||
}
|
||||
|
||||
void setupWebServer() {
|
||||
server.on("/", handleRoot);
|
||||
server.on("/speed", handleSpeed);
|
||||
server.on("/direction", handleDirection);
|
||||
server.on("/stop", handleStop);
|
||||
server.on("/status", handleStatus);
|
||||
server.on("/pingpong/start", handlePingpongStart);
|
||||
server.on("/pingpong/stop", handlePingpongStop);
|
||||
|
||||
server.begin();
|
||||
Serial.println("Web server started on port 80");
|
||||
|
||||
Reference in New Issue
Block a user