You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The steppers are energized 24/7, holding current on a machine that is idle most of the
time. Turn them off automatically after a configurable idle period (default 1 h). Because
powering them down frees the gantry — and this machine has no way to find home again — the
next use must go through a re-zero wizard: move the head to the paper's top-left corner,
set the position, motors back on.
Motivation
Nothing ever de-energizes the motors. motorsOff()
(src/grbl/GrblController.ts:640, FluidNC $MD) exists only
behind a manual button (src/ui/App.tsx:1928), and there is no idle
timer, no keep-alive setting, and no controller-side idle disable configured anywhere in the
daemon. An unattended plotter therefore sits at full holding current indefinitely — wasted
power, needless heat in the drivers and motors, coil whine in the room, and wear for no work
done. The machine is idle far more than it plots.
Why this needs a wizard
Turning the motors off is the easy half. The consequence is the hard half: this machine has no limit switches and no homing ($22=0, no $H — AGENTS.md), so the work origin exists
only because the operator put it there by hand, and the energized steppers are the only thing
holding the gantry in place. Once they drop out the gantry can be nudged or sag on the long
axis, and FluidNC has no encoder feedback — it keeps reporting the pre-shutdown coordinates
whatever the gantry actually did. The reported position becomes a claim, not a measurement.
The daemon currently has no way to express that:
posReady (gateway/server.ts:98) is the flag whose entire job is
"the position is trustworthy". It is cleared on disconnect
(gateway/server.ts:387) and set again by a restore
(gateway/server.ts:244) or by Calibrate
(gateway/server.ts:534) — but nothing clears it while the daemon
stays connected, which is exactly the case a motor power-down creates. Note the motorsOff handler (gateway/server.ts:540) sits two cases below setWorkZero, which does posReady = true; persistState(); the new path is its mirror
image and belongs in the same switch.
persistState (gateway/server.ts:102) keeps writing the position
to disk, and on the next connect restoreSavedPosition
(gateway/server.ts:233) feeds it back via G10 L20 and reinstates
it as the work origin.
So adding auto-off without the trust handling would be actively worse than leaving the motors
on: the daemon would confidently restore an origin recorded before the gantry was free to move,
and with soft limits disabled per-axis (gateway/server.ts:452) nothing
stops a plot from driving into the frame. The two halves ship together.
The same applies to the existing Motors off button, which frees the gantry deliberately
(its doc comment: "so the gantry can be moved by hand") and today marks nothing stale. It
should feed the same wizard.
Never fire mid-job. Beyond the obvious Run/queued cases, a pen-change hold is
deliberately Idle with an empty queue and no motion
(isPlotting, gateway/server.ts:308, and src/ui/App.tsx:146)
and is explicitly exempt from the 20 s stall watchdog
(src/ui/App.tsx:87). A job waiting for a hand must not be counted as
idle: dropping the motors there would shift the gantry mid-plot and the rest of the drawing
would land offset on a half-finished sheet. There is no resume and no second sheet. isPlotting — today used only by the self-update guard
(gateway/server.ts:614) — already encodes every one of these cases
and is the right basis for the idle timer.
Consider an explicit Keep motors on override for a session where the operator is stepping
away mid-setup and wants the origin preserved.
2. Position becomes untrusted when the motors drop
On auto-off (and on manual Motors off): clear posReady, stop persisting, and make sure
a position recorded on the wrong side of a shutdown is never restored as the origin — the
saved state needs a flag, or the file must be invalidated at shutdown.
Surface it in the snapshot and as a gateway event (src/gateway/protocol.ts) so every
connected client agrees. One machine, one truth.
3. Gate motion while untrusted
Refuse Plot, Go to home, and the registration wizard's jog-and-Set steps, enforced in
the gateway rather than the UI — the browser is a thin client. Jogging stays allowed (the
operator may need it to reach the corner) but its coordinates mean nothing until re-zeroed.
Refusals name the reason ("motors powered down after 1 h idle — home is lost"), never fail
silently.
4. Re-zero wizard
Opens when the operator next tries to use the machine, and from the Home/calibration panel:
Explain that the motors were powered down and home is gone.
State that the motors are off — the operator is about to push the gantry and needs to know
it will not fight back.
Move the head to the paper's top-left corner (by hand, or jog once re-enabled).
Confirm: motors energized, position trusted, persistence resumed.
The Home/calibration panel already tells the operator exactly this sequence in prose
(src/ui/App.tsx:1907) — the wizard makes it mandatory instead of
advisory. Model the interaction on the registration wizard
(src/ui/RegistrationWizard.tsx), which already does
jog-to-a-point-and-press-Set well.
Acceptance criteria
Motors power down automatically after a configurable idle period; default 1 h.
Auto-off never fires while a plot is running, paused, or held at a pen change.
Auto-off and manual Motors off both mark the position untrusted.
While untrusted the gateway refuses Plot / Go to home with a reason, for every connected
client — not just the one that saw the event.
A position recorded after a motor shutdown is never silently restored as the work origin.
The re-zero wizard walks the corner-and-Set sequence and restores trusted state on
completion.
Unit tests: idle timer vs. the busy predicate (including the pen-change hold), and the
trust state machine (idle → untrusted → re-zeroed → trusted).
Hardware-verified: motors audibly drop out after the timeout, the gantry is free, the
wizard blocks plotting until home is reset, and a plot after re-zeroing lands correctly
(leave unchecked until the operator confirms on the machine — AGENTS.md).
Open question
FluidNC can disable idle steppers itself (a controller-side idle timeout) rather than the
daemon sending $MD on a timer. Controller-side is simpler and survives a daemon restart, but
the daemon then has to detect the dropout to invalidate the origin, and it is not clear
FluidNC reports motor-enable state in its status. Daemon-driven $MD means the daemon always
knows exactly when it happened. Leaning daemon-driven for that reason — worth confirming
against the machine's config.
Relationship
Touches work-coordinates and machine-status (openspec/specs/). Timeout setting relates to #14
(shared settings) and #57 (Settings page). Reuses the wizard pattern from the registration
wizard (v1.2.0). Part of epic #10.
Summary
The steppers are energized 24/7, holding current on a machine that is idle most of the
time. Turn them off automatically after a configurable idle period (default 1 h). Because
powering them down frees the gantry — and this machine has no way to find home again — the
next use must go through a re-zero wizard: move the head to the paper's top-left corner,
set the position, motors back on.
Motivation
Nothing ever de-energizes the motors.
motorsOff()(src/grbl/GrblController.ts:640, FluidNC
$MD) exists onlybehind a manual button (src/ui/App.tsx:1928), and there is no idle
timer, no keep-alive setting, and no controller-side idle disable configured anywhere in the
daemon. An unattended plotter therefore sits at full holding current indefinitely — wasted
power, needless heat in the drivers and motors, coil whine in the room, and wear for no work
done. The machine is idle far more than it plots.
Why this needs a wizard
Turning the motors off is the easy half. The consequence is the hard half: this machine has
no limit switches and no homing (
$22=0, no$H— AGENTS.md), so the work origin existsonly because the operator put it there by hand, and the energized steppers are the only thing
holding the gantry in place. Once they drop out the gantry can be nudged or sag on the long
axis, and FluidNC has no encoder feedback — it keeps reporting the pre-shutdown coordinates
whatever the gantry actually did. The reported position becomes a claim, not a measurement.
The daemon currently has no way to express that:
posReady(gateway/server.ts:98) is the flag whose entire job is"the position is trustworthy". It is cleared on disconnect
(gateway/server.ts:387) and set again by a restore
(gateway/server.ts:244) or by Calibrate
(gateway/server.ts:534) — but nothing clears it while the daemon
stays connected, which is exactly the case a motor power-down creates. Note the
motorsOffhandler (gateway/server.ts:540) sits two cases belowsetWorkZero, which doesposReady = true; persistState(); the new path is its mirrorimage and belongs in the same switch.
persistState(gateway/server.ts:102) keeps writing the positionto disk, and on the next connect
restoreSavedPosition(gateway/server.ts:233) feeds it back via
G10 L20and reinstatesit as the work origin.
So adding auto-off without the trust handling would be actively worse than leaving the motors
on: the daemon would confidently restore an origin recorded before the gantry was free to move,
and with soft limits disabled per-axis (gateway/server.ts:452) nothing
stops a plot from driving into the frame. The two halves ship together.
The same applies to the existing Motors off button, which frees the gantry deliberately
(its doc comment: "so the gantry can be moved by hand") and today marks nothing stale. It
should feed the same wizard.
Proposal
1. Idle auto-off
issue
$MD. Default 1 h, configurable (belongs with shared settings, Persist app settings on the gateway (shared across clients) #14 / Settings page Move machine, pen and drawing settings off the main page into a Settings page #57).Run/queued cases, a pen-change hold isdeliberately Idle with an empty queue and no motion
(
isPlotting, gateway/server.ts:308, andsrc/ui/App.tsx:146)
and is explicitly exempt from the 20 s stall watchdog
(src/ui/App.tsx:87). A job waiting for a hand must not be counted as
idle: dropping the motors there would shift the gantry mid-plot and the rest of the drawing
would land offset on a half-finished sheet. There is no resume and no second sheet.
isPlotting— today used only by the self-update guard(gateway/server.ts:614) — already encodes every one of these cases
and is the right basis for the idle timer.
away mid-setup and wants the origin preserved.
2. Position becomes untrusted when the motors drop
posReady, stop persisting, and make surea position recorded on the wrong side of a shutdown is never restored as the origin — the
saved state needs a flag, or the file must be invalidated at shutdown.
src/gateway/protocol.ts) so everyconnected client agrees. One machine, one truth.
3. Gate motion while untrusted
the gateway rather than the UI — the browser is a thin client. Jogging stays allowed (the
operator may need it to reach the corner) but its coordinates mean nothing until re-zeroed.
silently.
4. Re-zero wizard
Opens when the operator next tries to use the machine, and from the Home/calibration panel:
it will not fight back.
setWorkZero()(src/grbl/GrblController.ts:614),the same call the Calibrate button already makes.
The Home/calibration panel already tells the operator exactly this sequence in prose
(src/ui/App.tsx:1907) — the wizard makes it mandatory instead of
advisory. Model the interaction on the registration wizard
(src/ui/RegistrationWizard.tsx), which already does
jog-to-a-point-and-press-Set well.
Acceptance criteria
client — not just the one that saw the event.
completion.
trust state machine (idle → untrusted → re-zeroed → trusted).
wizard blocks plotting until home is reset, and a plot after re-zeroing lands correctly
(leave unchecked until the operator confirms on the machine — AGENTS.md).
Open question
FluidNC can disable idle steppers itself (a controller-side idle timeout) rather than the
daemon sending
$MDon a timer. Controller-side is simpler and survives a daemon restart, butthe daemon then has to detect the dropout to invalidate the origin, and it is not clear
FluidNC reports motor-enable state in its status. Daemon-driven
$MDmeans the daemon alwaysknows exactly when it happened. Leaning daemon-driven for that reason — worth confirming
against the machine's config.
Relationship
Touches work-coordinates and machine-status (
openspec/specs/). Timeout setting relates to #14(shared settings) and #57 (Settings page). Reuses the wizard pattern from the registration
wizard (v1.2.0). Part of epic #10.