pm-kit: jog-mode step counter + rate readout, and document the pendulum/jog feature
- Jog/test screen now shows a live step count and current/peak step rate; jog ceiling raised to ~1000 steps/s so you can probe past a motor's max and read the peak rate where it stalls -> set STEPPER_MAX_RATE just below that. - README: new "Pendulum (stepper motion)" section (wiring GP18-21, the config knobs, motion behaviour, jog/test mode) + the A-alone / A+B power-on chords; noted the GP19/20/21 conflict with the custom PM_K-1 board's ribbon. Pure ASCII; conformance 47/47; app.mpy precompiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
15755f4d0c
commit
36c7406d71
2 changed files with 45 additions and 3 deletions
|
|
@ -14,9 +14,12 @@ from the web editor over USB‑MIDI**, and plays through your **computer's speak
|
||||||
- **Appliance mode — default (just plug in / power up).** The *firmware* owns the filesystem, so it
|
- **Appliance mode — default (just plug in / power up).** The *firmware* owns the filesystem, so it
|
||||||
saves your practice log and writes set lists the editor pushes over USB‑MIDI. The drive is then
|
saves your practice log and writes set lists the editor pushes over USB‑MIDI. The drive is then
|
||||||
**read‑only to the computer** — which also **protects the firmware from accidental deletion**.
|
**read‑only to the computer** — which also **protects the firmware from accidental deletion**.
|
||||||
- **Editor mode — hold BUTTON A while plugging in.** The drive is **writable by the computer**, so you
|
- **Editor mode — hold BUTTON A *alone* while plugging in.** The drive is **writable by the computer**, so you
|
||||||
can drag `programs.json` / `code.py` / fonts on from any OS or browser (the universal fallback).
|
can drag `programs.json` / `code.py` / fonts on from any OS or browser (the universal fallback).
|
||||||
Reset afterwards to return to appliance mode.
|
Reset afterwards to return to appliance mode.
|
||||||
|
- **Stepper jog/test mode — hold BUTTON A + B *together* while plugging in.** A hidden screen where the
|
||||||
|
joystick spins the stepper CW/CCW for bring-up (see *Pendulum* below). This chord stays in appliance mode
|
||||||
|
(the drive is **not** flipped writable). Power-cycle with no buttons to return to normal.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
@ -91,6 +94,32 @@ The editor also syncs the device clock, so the practice log gets real wall‑clo
|
||||||
- **RGB LED** flashes the beat (amber accent / cyan normal / violet ghost); the **speaker** clicks to match.
|
- **RGB LED** flashes the beat (amber accent / cyan normal / violet ghost); the **speaker** clicks to match.
|
||||||
- The log is saved to `/history.json` (next to `programs.json`) in appliance mode and survives power‑cycles.
|
- The log is saved to `/history.json` (next to `programs.json`) in appliance mode and survives power‑cycles.
|
||||||
|
|
||||||
|
## Pendulum (stepper motion) — optional
|
||||||
|
|
||||||
|
The Kit can drive a **physical metronome pendulum**: a 4-input unipolar stepper (e.g. a ULN2003 board +
|
||||||
|
28BYJ-48) swung in time with the beat, plus a **matching pendulum drawn on the screen**.
|
||||||
|
|
||||||
|
- **Wiring:** controller IN1..IN4 → **GP18, GP19, GP20, GP21**; controller GND → a Pico GND (shared
|
||||||
|
ground). Power the motor from the controller's own supply, **not** the Pico. *(These four pins are free
|
||||||
|
on the EP‑0172 kit. On the custom PM_K‑1 board GP19/20/21 are already taken by SIG/CLIP LED + ground‑lift,
|
||||||
|
so the production pendulum will need a pin reassignment.)*
|
||||||
|
- **Motion:** the arm reaches an extreme **exactly on each beat**, then reverses; it reads the live beat
|
||||||
|
clock, so it follows tempo ramps. Coils **de‑energize when stopped**. The on‑screen pendulum (shown over
|
||||||
|
the practice log while playing) mirrors the arm exactly — and animates even with **no motor wired**.
|
||||||
|
- **Config (top of `code.py`):**
|
||||||
|
- `STEPPER_ENABLED` — off leaves the four pins free.
|
||||||
|
- `PEND_SWING_DEG` — total swing arc end‑to‑end, in degrees (default **120**). Single source of truth:
|
||||||
|
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.
|
||||||
|
|
||||||
## programs.json
|
## programs.json
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
|
||||||
|
|
@ -1148,9 +1148,17 @@ class App:
|
||||||
self.g_overlay.append(vectorio.Circle(pixel_shader=solid(C_PANEL), radius=7, x=PEND_PX, y=PEND_PY))
|
self.g_overlay.append(vectorio.Circle(pixel_shader=solid(C_PANEL), radius=7, x=PEND_PX, y=PEND_PY))
|
||||||
bob = vectorio.Circle(pixel_shader=solid(C_CYAN), radius=13, x=PEND_PX, y=PEND_PY - PEND_LEN)
|
bob = vectorio.Circle(pixel_shader=solid(C_CYAN), radius=13, x=PEND_PX, y=PEND_PY - PEND_LEN)
|
||||||
self.g_overlay.append(bob)
|
self.g_overlay.append(bob)
|
||||||
|
rg = displayio.Group(); self.g_overlay.append(rg) # live step-count + rate readout
|
||||||
|
def show_stats(total, rate, peak):
|
||||||
|
while len(rg): rg.pop()
|
||||||
|
t, w, h = make_text("steps: %d" % total, FONT_M, C_TXT, C_BG); t.x = 12; t.y = 158; rg.append(t)
|
||||||
|
t, w, h = make_text("rate: %d/s peak: %d/s" % (rate, peak), FONT_S, C_AMBER, C_BG)
|
||||||
|
t.x = 12; t.y = 186; rg.append(t)
|
||||||
|
show_stats(0, 0, 0)
|
||||||
self.display.refresh()
|
self.display.refresh()
|
||||||
time.sleep(0.1); center = self.jx.value
|
time.sleep(0.1); center = self.jx.value
|
||||||
last = time.monotonic(); lastdir = None
|
last = time.monotonic(); lastdir = None
|
||||||
|
total = 0; win = 0; peak = 0; tsample = time.monotonic()
|
||||||
while True:
|
while True:
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
dx = self.jx.value - center; mag = abs(dx)
|
dx = self.jx.value - center; mag = abs(dx)
|
||||||
|
|
@ -1158,9 +1166,9 @@ class App:
|
||||||
cw = dx > 0
|
cw = dx > 0
|
||||||
frac = (mag - JOY_DEADZONE) / (32768 - JOY_DEADZONE)
|
frac = (mag - JOY_DEADZONE) / (32768 - JOY_DEADZONE)
|
||||||
if frac > 1.0: frac = 1.0
|
if frac > 1.0: frac = 1.0
|
||||||
dt = 0.02 + (0.0015 - 0.02) * frac # full push = fast, just past deadzone = slow
|
dt = 0.02 + (0.001 - 0.02) * frac # full push ~1000 steps/s; just past deadzone = slow
|
||||||
if self.pend is not None and self.pend.ok and now - last >= dt:
|
if self.pend is not None and self.pend.ok and now - last >= dt:
|
||||||
self.pend.spin(cw); last = now
|
self.pend.spin(cw); last = now; total += 1; win += 1
|
||||||
if cw != lastdir: # direction changed -> update LED + needle
|
if cw != lastdir: # direction changed -> update LED + needle
|
||||||
lastdir = cw
|
lastdir = cw
|
||||||
if cw: self.led.set(0, 150, 0)
|
if cw: self.led.set(0, 150, 0)
|
||||||
|
|
@ -1177,6 +1185,11 @@ class App:
|
||||||
bob.x = PEND_PX; bob.y = PEND_PY - PEND_LEN
|
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)]
|
arm.points = [(PEND_PX - 4, PEND_PY), (PEND_PX + 4, PEND_PY), (PEND_PX, PEND_PY - PEND_LEN)]
|
||||||
self.display.refresh()
|
self.display.refresh()
|
||||||
|
if now - tsample >= 0.3: # ~3x/s: commanded-rate window -> readout (peak = motor ceiling)
|
||||||
|
rate = int(win / (now - tsample))
|
||||||
|
if rate > peak: peak = rate
|
||||||
|
show_stats(total, rate, peak); win = 0; tsample = now
|
||||||
|
self.display.refresh()
|
||||||
time.sleep(0.0005)
|
time.sleep(0.0005)
|
||||||
|
|
||||||
# ---------- audio + light ----------
|
# ---------- audio + light ----------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue