pm-kit: fix jumpy jog (stop drawing mid-spin) - smooth steady spin

The ~1s hitch was the once-per-second readout: show_stats() allocates text
bitmaps (GC pause) and display.refresh() blocks the SPI blit, both stalling the
step loop exactly every second. Now the rate is measured silently while spinning
and the readout (steps + peak) is redrawn only when you release; a gc.collect()
on release + before spinning keeps the heap clean. Steady spin does zero display
work -> smooth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-06-05 22:13:07 -05:00
parent a7e8061a9b
commit 9651e8bc6a
2 changed files with 14 additions and 11 deletions

View file

@ -122,9 +122,10 @@ The Kit can drive a **physical metronome pendulum**: a 4-input unipolar stepper
powercycle. powercycle.
- **Jog / test mode** (hold **A + B** at boot): the joystick sets **direction only****L = CCW, R = CW** — and - **Jog / test mode** (hold **A + B** at boot): the joystick sets **direction only****L = CCW, R = CW** — and
the motor **accelerates to `STEPPER_MAX_RATE`** (reversing decelerates through zero first), with an onscreen the motor **accelerates to `STEPPER_MAX_RATE`** (reversing decelerates through zero first), with an onscreen
needle + RGB LED and a **live step counter + rate readout**. *Tuning:* raise `STEPPER_MAX_RATE` until the needle + RGB LED. The **step count + peakrate readout updates when you release** (drawing midspin would
motor starts skipping, then back off; if it stalls *starting*, lower `STEPPER_ACCEL` / `STEPPER_JOG_START`. stall the step loop and make it jumpy, so the spin itself stays glitchfree). *Tuning:* hold to spin, release
Powercycle (no buttons) to exit. to read the peak; raise `STEPPER_MAX_RATE` until the motor skips, then back off; if it stalls *starting*,
lower `STEPPER_ACCEL` / `STEPPER_JOG_START`. Powercycle (no buttons) to exit.
## programs.json ## programs.json

View file

@ -1179,8 +1179,9 @@ class App:
# Joystick = DIRECTION only (no fine speed). Spin at STEPPER_MAX_RATE, reached via an # Joystick = DIRECTION only (no fine speed). Spin at STEPPER_MAX_RATE, reached via an
# acceleration ramp (STEPPER_ACCEL) so the motor doesn't stall trying to start at top speed; # acceleration ramp (STEPPER_ACCEL) so the motor doesn't stall trying to start at top speed;
# reversing decelerates through zero, then accelerates the other way. # reversing decelerates through zero, then accelerates the other way.
spin = 0; cur = 0.0; total = 0; win = 0; peak = 0 spin = 0; cur = 0.0; total = 0; win = 0; peak = 0; lastrate = 0
now = time.monotonic(); last = now; tsample = now; tjoy = now now = time.monotonic(); last = now; tsample = now; tjoy = now
gc.collect() # clean heap before spinning (avoid a GC pause mid-spin)
while True: # no per-iteration sleep: tight step timing in this mode while True: # no per-iteration sleep: tight step timing in this mode
now = time.monotonic() now = time.monotonic()
if now - tjoy >= 0.004: # control update (joystick + accel), off the step hot-path if now - tjoy >= 0.004: # control update (joystick + accel), off the step hot-path
@ -1194,15 +1195,16 @@ class App:
elif cur > target: cur = max(0.0, cur - STEPPER_ACCEL * cdt) # decelerate elif cur > target: cur = max(0.0, cur - STEPPER_ACCEL * cdt) # decelerate
if cur <= 0.0 and spin != 0 and want != spin: # finished slowing -> stop, or flip direction if cur <= 0.0 and spin != 0 and want != spin: # finished slowing -> stop, or flip direction
if self.pend is not None and self.pend.ok: self.pend.release() if self.pend is not None and self.pend.ok: self.pend.release()
if want == 0: spin = 0; set_needle(0) if want == 0: # stopped -> now safe to draw the readout + tidy the heap
else: spin = want; cur = float(STEPPER_JOG_START); set_needle(spin) spin = 0; show_stats(total, lastrate, peak); set_needle(0); gc.collect()
else:
spin = want; cur = float(STEPPER_JOG_START); set_needle(spin)
if spin != 0 and cur > 0.0 and self.pend is not None and self.pend.ok and now - last >= 1.0 / cur: if spin != 0 and cur > 0.0 and self.pend is not None and self.pend.ok and now - last >= 1.0 / cur:
self.pend.spin(spin > 0); last = now; total += 1; win += 1 self.pend.spin(spin > 0); last = now; total += 1; win += 1
if now - tsample >= 1.0: # step-rate readout, once/s (one tiny refresh) if now - tsample >= 1.0: # measure rate SILENTLY (drawing here is what hitched it)
rate = int(win / (now - tsample)) lastrate = int(win / (now - tsample))
if rate > peak: peak = rate if lastrate > peak: peak = lastrate
show_stats(total, rate, peak); win = 0; tsample = now win = 0; tsample = now
self.display.refresh()
# ---------- audio + light ---------- # ---------- audio + light ----------
def click(self, level): def click(self, level):