Implement proper stepper idling. Should close #3

This commit is contained in:
osiu97
2026-07-08 19:31:51 +02:00
parent 90458e2e17
commit c6f73a3b8d
5 changed files with 70 additions and 6 deletions
+2
View File
@@ -92,11 +92,13 @@ Name: WijiBoard
| `HOMEALL` | Home both motors simultaneously | | `HOMEALL` | Home both motors simultaneously |
| `HOME1` | Home motor 1 | | `HOME1` | Home motor 1 |
| `HOME2` | Home motor 2 | | `HOME2` | Home motor 2 |
| `DISABLE` | Disable steppers (un-hold) and require rehoming. Auto-triggers after 30 mins (`30 * 60 * 1000` ms) of inactivity |
| `POS` | Request current positions (triggers NOTIFY) | | `POS` | Request current positions (triggers NOTIFY) |
### Status notifications (firmware → browser) ### Status notifications (firmware → browser)
`P:<s1>,<s2>` — current step positions for motor 1 and 2, sent every 200 ms while moving. `P:<s1>,<s2>` — current step positions for motor 1 and 2, sent every 200 ms while moving.
`SYS:IDLE_TIMEOUT` — Sent when motors are automatically disabled due to 30 mins of inactivity.
--- ---
+36
View File
@@ -86,6 +86,14 @@
<!-- BLE actions --> <!-- BLE actions -->
<div class="topbar-actions"> <div class="topbar-actions">
<!-- Disable steppers button (hidden by default) -->
<button class="btn btn-sm btn-ghost" id="btn-disable-steppers" style="display:none; margin-right:8px; padding:0 8px; color: var(--danger-color);" title="Disable Motors (Requires Rehoming)" aria-label="Disable Steppers">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
<line x1="12" y1="2" x2="12" y2="12"></line>
</svg>
</button>
<!-- BLE status pill --> <!-- BLE status pill -->
<div class="ble-status" id="ble-status" aria-live="polite"> <div class="ble-status" id="ble-status" aria-live="polite">
<div class="ble-dot" id="ble-dot"></div> <div class="ble-dot" id="ble-dot"></div>
@@ -202,6 +210,34 @@
window.dispatchEvent(new Event('pwa-installable')); window.dispatchEvent(new Event('pwa-installable'));
}); });
// ── Disable Steppers & Idle Timeout Logic ──────────────────────
const btnDisable = document.getElementById('btn-disable-steppers');
btnDisable.addEventListener('click', async () => {
if (BLE.isConnected()) {
await BLE.write('DISABLE').catch(e => UI.log(e.message, 'error'));
localStorage.setItem('wiji_homed', 'false');
UI.log('Steppers manually disabled. Rehoming required.', 'warn');
const warningEl = document.getElementById('home-warning');
if (warningEl) warningEl.style.display = 'block';
}
});
document.addEventListener('ble:status', (e) => {
if (e.detail === 'SYS:IDLE_TIMEOUT') {
localStorage.setItem('wiji_homed', 'false');
UI.log('Steppers disabled due to 30 mins inactivity. Rehoming required.', 'warn');
const warningEl = document.getElementById('home-warning');
if (warningEl) warningEl.style.display = 'block';
}
});
BLE.on('connected', () => {
btnDisable.style.display = 'inline-flex';
});
BLE.on('disconnected', () => {
btnDisable.style.display = 'none';
});
// ── Check Web Bluetooth availability ────────────────────────── // ── Check Web Bluetooth availability ──────────────────────────
if (!navigator.bluetooth) { if (!navigator.bluetooth) {
setTimeout(() => { setTimeout(() => {
+30 -5
View File
@@ -15,6 +15,7 @@
* HOME1 → Home motor 1 (Right) * HOME1 → Home motor 1 (Right)
* HOME2 → Home motor 2 (Left) * HOME2 → Home motor 2 (Left)
* HOMEALL → Home both motors simultaneously * HOMEALL → Home both motors simultaneously
* DISABLE → Disable steppers (un-hold) and require rehoming
* POS → Request current positions (triggers NOTIFY) * POS → Request current positions (triggers NOTIFY)
* *
* BLE Status Characteristic (NOTIFY): * BLE Status Characteristic (NOTIFY):
@@ -72,6 +73,11 @@ bool bleConnected = false;
unsigned long lastNotify = 0; unsigned long lastNotify = 0;
const unsigned long NOTIFY_INTERVAL_MS = 200; const unsigned long NOTIFY_INTERVAL_MS = 200;
// ─── Idle timeout tracker ─────────────────────────────────────────
unsigned long lastMotorActive = 0;
const unsigned long IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
bool motorsDisabled = true; // Assume disabled at startup until moved
// ─── Forward declarations ───────────────────────────────────────── // ─── Forward declarations ─────────────────────────────────────────
void parseCommand(const String &cmd); void parseCommand(const String &cmd);
void sendPosition(); void sendPosition();
@@ -193,6 +199,14 @@ void parseCommand(const String &cmd) {
sendPosition(); sendPosition();
return; return;
} }
// DISABLE
if (cmd == "DISABLE") {
stepper1.disableOutputs();
stepper2.disableOutputs();
motorsDisabled = true;
Serial.println("[SYS] Steppers manually disabled.");
return;
}
// POS explicit position request // POS explicit position request
if (cmd == "POS") { if (cmd == "POS") {
sendPosition(); sendPosition();
@@ -280,6 +294,22 @@ void loop() {
stepper1.run(); stepper1.run();
stepper2.run(); stepper2.run();
bool moving = stepper1.isRunning() || stepper2.isRunning() || homingState != HOME_IDLE;
if (moving) {
lastMotorActive = millis();
motorsDisabled = false;
} else if (!motorsDisabled && (millis() - lastMotorActive > IDLE_TIMEOUT_MS)) {
stepper1.disableOutputs();
stepper2.disableOutputs();
motorsDisabled = true;
Serial.println("[SYS] Idle timeout reached. Steppers disabled.");
if (bleConnected && pStatusChar) {
pStatusChar->setValue("SYS:IDLE_TIMEOUT");
pStatusChar->notify();
}
}
if (homingState == HOME_ALL_PHASE1) { if (homingState == HOME_ALL_PHASE1) {
if (stepper1.distanceToGo() == 0 && stepper2.distanceToGo() == 0) { if (stepper1.distanceToGo() == 0 && stepper2.distanceToGo() == 0) {
stepper1.setCurrentPosition(1024); stepper1.setCurrentPosition(1024);
@@ -291,8 +321,6 @@ void loop() {
} }
} else if (homingState == HOME_ALL_PHASE2) { } else if (homingState == HOME_ALL_PHASE2) {
if (stepper1.distanceToGo() == 0 && stepper2.distanceToGo() == 0) { if (stepper1.distanceToGo() == 0 && stepper2.distanceToGo() == 0) {
stepper1.disableOutputs();
stepper2.disableOutputs();
stepper1.setMaxSpeed(currentMaxSpeed); stepper1.setMaxSpeed(currentMaxSpeed);
stepper2.setMaxSpeed(currentMaxSpeed); stepper2.setMaxSpeed(currentMaxSpeed);
stepper1.setAcceleration(currentAccel); stepper1.setAcceleration(currentAccel);
@@ -309,7 +337,6 @@ void loop() {
} }
} else if (homingState == HOME_1_PHASE2) { } else if (homingState == HOME_1_PHASE2) {
if (stepper1.distanceToGo() == 0) { if (stepper1.distanceToGo() == 0) {
stepper1.disableOutputs();
stepper1.setMaxSpeed(currentMaxSpeed); stepper1.setMaxSpeed(currentMaxSpeed);
stepper1.setAcceleration(currentAccel); stepper1.setAcceleration(currentAccel);
homingState = HOME_IDLE; homingState = HOME_IDLE;
@@ -322,7 +349,6 @@ void loop() {
} }
} else if (homingState == HOME_2_PHASE2) { } else if (homingState == HOME_2_PHASE2) {
if (stepper2.distanceToGo() == 0) { if (stepper2.distanceToGo() == 0) {
stepper2.disableOutputs();
stepper2.setMaxSpeed(currentMaxSpeed); stepper2.setMaxSpeed(currentMaxSpeed);
stepper2.setAcceleration(currentAccel); stepper2.setAcceleration(currentAccel);
homingState = HOME_IDLE; homingState = HOME_IDLE;
@@ -332,7 +358,6 @@ void loop() {
// Periodic position notify while motors are moving // Periodic position notify while motors are moving
if (bleConnected) { if (bleConnected) {
unsigned long now = millis(); unsigned long now = millis();
bool moving = stepper1.isRunning() || stepper2.isRunning() || homingState != HOME_IDLE;
if (moving && (now - lastNotify >= NOTIFY_INTERVAL_MS)) { if (moving && (now - lastNotify >= NOTIFY_INTERVAL_MS)) {
lastNotify = now; lastNotify = now;
sendPosition(); sendPosition();
+1
View File
@@ -18,6 +18,7 @@
* SPD:<n> → set max speed (steps/sec) * SPD:<n> → set max speed (steps/sec)
* ACC:<n> → set acceleration (steps/sec²) * ACC:<n> → set acceleration (steps/sec²)
* HOME → zero both steppers * HOME → zero both steppers
* DISABLE → disable steppers (un-hold)
* POS → request position report * POS → request position report
* *
* Status characteristic (NOTIFY): * Status characteristic (NOTIFY):
+1 -1
View File
@@ -15,7 +15,7 @@ const HomeSection = {
<div class="section-page fade-in"> <div class="section-page fade-in">
<div class="section-header"> <div class="section-header">
<h2>WijiBoard</h2> <h2>WijiBoard</h2>
<p>WiFi Spirit Board — SCARA arm controller. Select a section from the sidebar to get started.</p> <p>Bluetooth Spirit Board — SCARA arm controller. Select a section from the sidebar to get started.</p>
</div> </div>
<div class="grid-2" style="margin-bottom:16px"> <div class="grid-2" style="margin-bottom:16px">