feat(gateway): power the motors down after 1 h idle, with a re-zero wizard - #82
Merged
Merged
Conversation
…hat costs The steppers were energized 24/7: holding current, heat in the drivers and coil whine on a machine that is idle most of the day. `$MD` existed only behind a manual button and nothing ever fired it. Turning them off is the easy half. With no limit switches, no homing and no encoder feedback, the energized steppers are the only thing holding the gantry on the work origin the operator set by hand — and FluidNC goes on reporting the pre-shutdown coordinates whatever the gantry actually did. `posReady` already means "the position is trustworthy", but nothing cleared it while the daemon stayed connected, which is exactly what a power-down creates. Left alone, the daemon would keep persisting a position recorded after the gantry was free and hand it to `G10 L20` on the next connect. With soft limits disabled per-axis, the next plot drives into the frame. So auto-off without the trust handling is worse than leaving the motors on, and the two halves ship together. - Idle auto-off on a 30 s tick, default 1 h, `0` = never (which doubles as the keep-the-motors-on override). Gated on `isPlotting()` — the existing predicate, reused rather than reimplemented, because it already encodes the pen-change hold: deliberately Idle with an empty queue and very much mid-job. - A power-down (timer or the manual button — same free gantry, same lost origin) clears `posReady`, rewrites the state file with `trusted: false` synchronously, and broadcasts. A file with no flag predates this and is still restored, so no machine loses its remembered home to the upgrade. - `restoreSavedPosition` refuses an untrusted file instead of reinstating it, which is what gives the feature a daemon-restart story. - The gateway refuses Plot and Go to home with the reason, next to the command handlers, so a second tab cannot believe the origin is fine. Jog stays available — the operator needs it to reach the corner. Stop still stops; while untrusted it runs the abort without the rapid to an origin nobody believes. - `src/grbl/motorPower.ts` holds both rules as pure functions, unit-tested, including every way the timer must not fire. - A re-zero wizard opens on the transition and from the Home/calibration panel, says the gantry will not fight back, and ends on the same `setWorkZero()` call Calibrate makes. A banner keeps it visible until it is cleared. Daemon-driven rather than FluidNC's own idle disable: `$MD` is documented and `$ME` is not, and the status report carries no motor-enable field — a dropout the daemon cannot see is a lost origin it cannot flag. Nothing new is sent to re-energize; the motors come back on the first move, and confirming that is a hardware task. Refs #81
This was referenced Sep 14, 2026
Merged
BernardJen
added a commit
that referenced
this pull request
Sep 14, 2026
Idle motor power-down with a re-zero wizard (#81, #82). The steppers no longer sit at holding current around the clock: after a configurable idle period, default 1 hour, the daemon de-energizes them. The half that makes it safe rather than dangerous ships with it. This machine has no limit switches and FluidNC has no encoder feedback, so freeing the gantry also loses the work origin while the controller goes on reporting the old coordinates. The daemon now marks the position untrusted the moment the motors drop, stops persisting it, writes `trusted: false` into the state file so a restart cannot reinstate it, and refuses Plot and Go to home for every connected client. A wizard walks the operator back to the paper's corner. Minor, not patch: new behaviour on a machine that previously never powered its motors down, a new shared setting, and a new field in the gateway snapshot. Nothing breaks on upgrade — a state file written by an older daemon carries no trust flag and is still restored, so no machine loses its remembered home. Hardware verification is still outstanding (see the unchecked ⚙ HARDWARE tasks in openspec/changes/idle-motor-power-down/tasks.md); the change is deliberately not archived until the operator confirms it on the UUNA TEK. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #81.
What this is
The steppers were energized around the clock. They are now de-energized (
$MD) after aconfigurable idle period — default 1 hour,
0to never — and because freeing the gantry on amachine with no limit switches also loses the work origin, the daemon now says so, refuses the
motion that depends on it, and walks the operator back through setting it.
Both halves are here on purpose. Auto-off without the trust handling is worse than leaving the
motors on: the daemon would keep persisting a position recorded after the gantry was free and hand
it to
G10 L20as the work origin on the next connect, with soft limits disabled per-axis andnothing downstream to stop the plot.
How it works
src/grbl/motorPower.ts— the two rules as pure functions:shouldDropMotors(the timer vsthe busy predicate) and
reduceMotorPower(the trust state machine), pluscanRestorePosition.Inside the coverage floor, and where all the branches are. 27 tests.
isPlotting(), not a second predicate. It already encodesinflight,queued,isPaused,penChange !== null,RunandHold— and the pen-change hold is thecase that matters: deliberately
Idlewith an empty queue, exempt from the UI's stall watchdog,and very much mid-job. Dropping the motors there would land the rest of the drawing offset on a
half-finished sheet that cannot be restarted.
Run/Jogstatus, not every command. Abrowser autosaving its session every couple of seconds must not hold the steppers on forever.
trusted. A power-down writestrusted: falsesynchronously(fsync + rename), and
restoreSavedPositionrefuses such a file instead of reinstating it. Thatis what gives this a restart story without controller-side support. A file written by an older
daemon has no flag and is still restored — no machine loses its remembered home to the upgrade.
origin is fine. Plot and Go to home are refused with the reason. Jog stays allowed — the operator
needs it to reach the corner; the motion is fine, the coordinates are fiction. Stop still stops:
while untrusted it runs the abort without
stopAndReturnHome's rapid to an origin nobody believes.src/ui/RezeroWizard.tsx, modelled on the registration wizard: what happened → push or jog tothe paper's top-left corner (with the motors' actual state spelled out) → Set home, which is the
same
setWorkZero()call Calibrate makes. Opens on the transition and from the Home/calibrationpanel; a banner holds the state until it is cleared.
The open question in the issue, answered
Daemon-driven
$MD, not FluidNC's controller-side idle disable. FluidNC documents$Motor/Disable/$MDand no$ME, and its status report carries no motor-enable field — so acontroller-side dropout is one the daemon cannot detect, which is precisely the half that makes
this safe rather than dangerous. Nothing new is sent to re-energize either: the motors come back
with the first move, the wizard says so, and confirming it is a hardware task
(FluidNC commands and settings).
Not done, deliberately
0 = neverinSettings covers the same need with one mechanism instead of two.
narrowed it by setting
lastWposto0,0,0afterG10 L20 P1 X0 Y0 Z0(true by construction),which is the honest half of the fix. Closing it entirely means synthesising a WCO from a 5 Hz
status, which is exactly the kind of position-restore cleverness that has passed review here and
failed on the Pi. Not worth it for a window that already existed.
openspec/changes/idle-motor-power-down/tasks.mdare left unchecked and the change is notarchived. Four of them: the drivers audibly dropping out, the gantry genuinely free, the motors
re-energizing on the first move, and a pen-change hold outlasting the idle period without losing
register.
Verified
mise run cigreen. Coverage up on every metric (branches 72.91% → 73.62%). Verified in thebrowser: the banner, the disabled Plot / Go to home with the reason in their titles, both wizard
steps, and the Settings field.
Spec
openspec/changes/idle-motor-power-down/— adds themotor-powercapability, modifieswork-coordinates,gateway-protocolandapp-settings.🤖 Generated with Claude Code