diff --git a/pico-cp/README.md b/pico-cp/README.md index 1aa673f..30980eb 100644 --- a/pico-cp/README.md +++ b/pico-cp/README.md @@ -112,13 +112,16 @@ The Kit can drive a **physical metronome pendulum**: a 4-input unipolar stepper drives the screen graphic exactly and the motor. - `STEPPER_STEPS_PER_REV` — your motor's half‑steps per full turn (28BYJ‑48 half‑step ≈ 4096); maps degrees → steps. - - `STEPPER_MAX_RATE` — fastest half‑steps/sec the motor can follow; the swing **auto‑shrinks** (rather than - desync) when a beat is too short to sweep the full arc. At wide swings/fast tempi the screen still shows - the full angle while the arm does what it physically can. -- **Jog / test mode** (hold **A + B** at boot): the joystick spins the motor **L = CCW, R = CW** (speed by - how far you push), with an on‑screen direction needle + RGB LED and a **live step counter + rate readout**. - Push to full deflection and note the **peak rate** where the motor begins to stall, then set - `STEPPER_MAX_RATE` just below it. Power‑cycle (no buttons) to exit. + - `STEPPER_MAX_RATE` — top half‑steps/sec the motor sustains smoothly. **Jog mode spins at this rate**, and + the pendulum **auto‑shrinks** its arc (rather than desync) when a beat is too short to sweep the full angle. + - `STEPPER_ACCEL` — ramp (half‑steps/sec²) used to reach top speed without stalling; lower it if the motor + stalls/buzzes when starting. + - `STEPPER_JOG_START` — jog kickoff rate from rest (keep at or below the motor's pull‑in rate). +- **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 on‑screen + needle + RGB LED and a **live step counter + rate readout**. *Tuning:* raise `STEPPER_MAX_RATE` until the + motor starts skipping, then back off; if it stalls *starting*, lower `STEPPER_ACCEL` / `STEPPER_JOG_START`. + Power‑cycle (no buttons) to exit. ## programs.json diff --git a/pico-cp/app.py b/pico-cp/app.py index df1d58d..4410356 100644 --- a/pico-cp/app.py +++ b/pico-cp/app.py @@ -71,7 +71,9 @@ STEPPER_ENABLED = True # set False if no motor is wired (the pins just sta PEND_SWING_DEG = 120 # total swing arc, end-to-end, in degrees - drives BOTH the screen graphic and the arm STEPPER_STEPS_PER_REV = 4096 # your motor's half-steps per full 360 turn (28BYJ-48 half-step ~4096); maps deg -> steps STEPPER_ARC = round(STEPPER_STEPS_PER_REV * PEND_SWING_DEG / 360.0) # half-steps for one end-to-end swing -STEPPER_MAX_RATE = 900 # max half-steps/sec the motor can follow; auto-shrinks the arc at fast tempi +STEPPER_MAX_RATE = 600 # top half-steps/sec the motor sustains smoothly (jog spins here; tune via jog mode) +STEPPER_ACCEL = 1800 # half-steps/sec^2 ramp so it reaches top speed without stalling (lower if it stalls) +STEPPER_JOG_START = 150 # jog kickoff half-steps/sec from rest (keep <= the motor's pull-in rate) # ----- pins (fixed by the EP-0172 board) ----- P_SCK, P_MOSI, P_CS, P_DC, P_RST = board.GP2, board.GP3, board.GP5, board.GP6, board.GP7 @@ -1138,7 +1140,7 @@ class App: self.g_overlay.append(rect(0, 40, WIDTH, HEIGHT - 40, C_BG)) # cover the normal UI for s, fnt, col, yy in (("STEPPER JOG TEST", FONT_M, C_CYAN, 64), ("Joystick L = CCW R = CW", FONT_S, C_TXT, 96), - ("push further = faster", FONT_S, C_DIM, 114), + ("spins at max speed (ramped)", FONT_S, C_DIM, 114), ("power-cycle (no buttons) to exit", FONT_S, C_DIM, 132)): tg, w, h = make_text(s, fnt, col, C_BG); tg.x = 12; tg.y = yy; self.g_overlay.append(tg) self.g_overlay.append(rect(PEND_PX - 26, PEND_PY + 4, 52, 4, C_PANEL)) # stand @@ -1157,37 +1159,37 @@ class App: show_stats(0, 0, 0) self.display.refresh() time.sleep(0.1); center = self.jx.value - total = 0; win = 0; peak = 0; cw = True; dt = 1.0; lastdir = None + def set_needle(d): # d = +1 CW (green), -1 CCW (blue), 0 centre (off) + if d > 0: self.led.set(0, 150, 0); a = PEND_THETA + elif d < 0: self.led.set(0, 0, 255); a = -PEND_THETA + else: self.led.set(0, 0, 0); a = 0.0 + bx = PEND_PX + int(PEND_LEN * math.sin(a)); by = PEND_PY - int(PEND_LEN * math.cos(a)) + bob.x = bx; bob.y = by + arm.points = [(PEND_PX - 4, PEND_PY), (PEND_PX + 4, PEND_PY), (bx, by)] + self.display.refresh() + # 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; + # reversing decelerates through zero, then accelerates the other way. + spin = 0; cur = 0.0; total = 0; win = 0; peak = 0 now = time.monotonic(); last = now; tsample = now; tjoy = now while True: # no per-iteration sleep: tight step timing in this mode now = time.monotonic() - if now - tjoy >= 0.004: # poll the joystick ~250x/s, OFF the step hot-path - tjoy = now - dx = self.jx.value - center; mag = abs(dx) - if mag > JOY_DEADZONE: - cw = dx > 0 - frac = (mag - JOY_DEADZONE) / (32768 - JOY_DEADZONE) - if frac > 1.0: frac = 1.0 - dt = 0.02 + (0.0006 - 0.02) * frac # commanded up to ~1600 steps/s (find the motor's real wall) - if cw != lastdir: # direction changed -> LED + needle (rare, so refresh here) - lastdir = cw - if cw: self.led.set(0, 150, 0) - else: self.led.set(0, 0, 255) - ang = PEND_THETA if cw else -PEND_THETA - bx = PEND_PX + int(PEND_LEN * math.sin(ang)); by = PEND_PY - int(PEND_LEN * math.cos(ang)) - bob.x = bx; bob.y = by - arm.points = [(PEND_PX - 4, PEND_PY), (PEND_PX + 4, PEND_PY), (bx, by)] - self.display.refresh() - elif lastdir is not None: # back to center -> stop, release, recentre needle - lastdir = None; dt = 1.0 + if now - tjoy >= 0.004: # control update (joystick + accel), off the step hot-path + cdt = now - tjoy; tjoy = now + dx = self.jx.value - center + want = (1 if dx > 0 else -1) if abs(dx) > JOY_DEADZONE else 0 + if spin == 0 and want != 0: # start from rest at the safe pull-in rate + spin = want; cur = float(STEPPER_JOG_START); set_needle(spin) + target = float(STEPPER_MAX_RATE) if (spin != 0 and want == spin) else 0.0 + if cur < target: cur = min(target, cur + STEPPER_ACCEL * cdt) # accelerate + 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 self.pend is not None and self.pend.ok: self.pend.release() - self.led.set(0, 0, 0) - bob.x = PEND_PX; bob.y = PEND_PY - PEND_LEN - arm.points = [(PEND_PX - 4, PEND_PY), (PEND_PX + 4, PEND_PY), (PEND_PX, PEND_PY - PEND_LEN)] - self.display.refresh() - if lastdir is not None and self.pend is not None and self.pend.ok and now - last >= dt: - self.pend.spin(lastdir); last = now; total += 1; win += 1 - if now - tsample >= 1.0: # readout once/s (one tiny refresh, not 3+ hitches/s) + if want == 0: spin = 0; set_needle(0) + 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: + self.pend.spin(spin > 0); last = now; total += 1; win += 1 + if now - tsample >= 1.0: # step-rate readout, once/s (one tiny refresh) rate = int(win / (now - tsample)) if rate > peak: peak = rate show_stats(total, rate, peak); win = 0; tsample = now