Skip to content

feat(gateway): power the motors down after 1 h idle, with a re-zero wizard - #82

Merged
BernardJen merged 1 commit into
mainfrom
claude/motor-powerdown-rezero-wizard-d542ab
Sep 14, 2026
Merged

BernardJen merged 1 commit into
mainfrom
claude/motor-powerdown-rezero-wizard-d542ab

Conversation

@BernardJen

Copy link
Copy Markdown
Contributor

Closes #81.

What this is

The steppers were energized around the clock. They are now de-energized ($MD) after a
configurable idle period — default 1 hour, 0 to never — and because freeing the gantry on a
machine 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 L20 as the work origin on the next connect, with soft limits disabled per-axis and
nothing downstream to stop the plot.

How it works

  • src/grbl/motorPower.ts — the two rules as pure functions: shouldDropMotors (the timer vs
    the busy predicate) and reduceMotorPower (the trust state machine), plus canRestorePosition.
    Inside the coverage floor, and where all the branches are. 27 tests.
  • The timer reuses isPlotting(), not a second predicate. It already encodes inflight,
    queued, isPaused, penChange !== null, Run and Hold — and the pen-change hold is the
    case that matters: deliberately Idle with 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.
  • The activity clock counts motion commands and Run/Jog status, not every command. A
    browser autosaving its session every couple of seconds must not hold the steppers on forever.
  • The saved state file carries trusted. A power-down writes trusted: false synchronously
    (fsync + rename), and restoreSavedPosition refuses such a file instead of reinstating it. That
    is 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.
  • The gate is in the gateway, next to the command handlers, so a second tab cannot believe the
    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 to
    the 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/calibration
    panel; 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 / $MD and no $ME, and its status report carries no motor-enable field — so a
controller-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

  • No session-scoped "keep motors on" toggle. The issue lists it as a consider; 0 = never in
    Settings covers the same need with one mechanism instead of two.
  • A ~200 ms window after Set home in which a daemon crash could persist a stale position: I
    narrowed it by setting lastWpos to 0,0,0 after G10 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.
  • Hardware verification. Per AGENTS.md, the ⚙ HARDWARE tasks in
    openspec/changes/idle-motor-power-down/tasks.md are left unchecked and the change is not
    archived. 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 ci green. Coverage up on every metric (branches 72.91% → 73.62%). Verified in the
browser: 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 the motor-power capability, modifies
work-coordinates, gateway-protocol and app-settings.

🤖 Generated with Claude Code

…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
@BernardJen
BernardJen merged commit bb8d0b9 into main Sep 14, 2026
2 checks passed
@BernardJen
BernardJen deleted the claude/motor-powerdown-rezero-wizard-d542ab branch September 14, 2026 15:32
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Power the motors down after 1 h idle, with a re-zero wizard to bring them back

1 participant