Switch the motors off when the designs are done or stopped

After the last design (or a Stop), the player sends $SLP (GRBL and FluidNC
sleep), so the motors stop holding and the table is quiet. The page shows
"Motors off"; the next design wakes the controller (reset + unlock) and homes
first, since the arm can be moved by hand while the motors are off. The jog
panel wakes it too. Setting: motion.motors_off_when_done (default true).

Checked on the real Uno: Idle -> $SLP -> Sleep -> reset -> Alarm -> $X -> Idle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ckochandClaude Opus 5.5 committed 2026-11-01 14:19:12 -05:00
1 parent 8967271e90
commit 7afe562124
10 files changed
+104 -10

No files matched your search

+12
View File
@@ -353,6 +353,18 @@ def cmd_play(cfg, args):
def _play(c, cfg, args, items):
try:
_play_list(c, cfg, args, items)
finally:
if cfg['motion'].get('motors_off_when_done', True):
try:
c.motors_off() # done, stopped or failed: let the motors rest
print(' motors off')
except Exception:
pass # e.g. the USB link has gone
def _play_list(c, cfg, args, items):
if not args.no_home:
print(' homing rho ...', flush=True)
c.home_rho(cfg)
+2
View File
@@ -26,6 +26,8 @@ DEFAULTS = {
},
'motion': {
'ball_speed_mm_s': 25.0, # drawing speed of the ball across the sand
'motors_off_when_done': True, # switch the motors off when the designs are done or stopped
# (quiet, cool); the next design homes first
'max_theta_deg_s': 60.0, # arm spin limit (10 rpm)
'max_rho_mm_s': 25.0, # carriage limit
'theta_accel_deg_s2': 120.0,
+26
View File
@@ -346,7 +346,33 @@ class Controller:
self._presses = 0
return self.query()
def motors_off(self):
"""Switch the motors off with the sleep command ($SLP; FluidNC has it too). They stop
holding, so the arm turns by hand and the position is lost: before moving again the
controller needs waking (a reset, see wake_up) and the table homing."""
self.guard = None
if self.query().state != 'Sleep':
self.command('$SLP')
self.query()
def wake_up(self):
"""Out of sleep: only a reset wakes it, and it then waits in ALARM until unlocked."""
if self.query().state != 'Sleep':
return
self.realtime(RT_RESET)
t_end, seen = time.monotonic() + 3, None
while time.monotonic() < t_end:
for line in self._lines():
if 'grbl' in line.lower() or 'fluidnc' in line.lower():
seen = time.monotonic()
if seen and time.monotonic() - seen > 0.2:
break
time.sleep(0.02)
self.pending.clear()
self.query()
def unlock(self):
self.wake_up()
if self.query().state == 'Alarm':
self.command('$X')
+6 -1
View File
@@ -212,7 +212,8 @@ class Panel:
def status(self):
st = self.c.status
return {'state': 'Disconnected' if not self.connected else self.busy or (st.state + (':' + st.sub if st.sub else '')),
state = 'Motors off' if st.state == 'Sleep' else st.state + (':' + st.sub if st.sub else '')
return {'state': 'Disconnected' if not self.connected else self.busy or state,
'connected': self.connected,
'theta': st.wpos[0], 'rho': st.wpos[1], 'switch': 'Y' in st.pins, 'pins': st.pins,
'homed': self.homed, 'steps': self.steps, 'error': self.error}
@@ -224,6 +225,9 @@ class Panel:
feed = 1200 if axis == 'X' else 600 # 20 deg/s, 10 mm/s
with self.lock:
st = self.c.query()
if st.state == 'Sleep': # motors switched off after a design: wake them
self.c.unlock()
st = self.c.query()
if st.state == 'Alarm':
return False, 'alarm: press Unlock first'
inward = axis == 'Y' and d < 0
@@ -263,6 +267,7 @@ class Panel:
def unlock(self):
with self.lock:
self.c.unlock() # also wakes the motors after a design
self.c.command('$X')
return True, 'unlocked'
+7
View File
@@ -275,6 +275,13 @@ class Player:
finally:
c.guard = None
c.abort = False
if self.cfg['motion'].get('motors_off_when_done', True) and self.p.connected:
try:
c.motors_off() # done or stopped: let the motors rest
self.p.homed = False # the arm can drift now, so home before the next one
log('motors off')
except Exception as e:
log(f'could not switch the motors off: {e!r}')
self.now = None
self.state = 'idle'