diff --git a/pico-cp/app.py b/pico-cp/app.py index ebe7274..750dc96 100644 --- a/pico-cp/app.py +++ b/pico-cp/app.py @@ -213,6 +213,8 @@ class Pendulum: def step_toward(self, target): # one half-step toward target if target > self.pos: self.phase += 1; self.pos += 1; self._write() elif target < self.pos: self.phase -= 1; self.pos -= 1; self._write() + def spin(self, cw): # one free half-step either way (jog/test mode) + self.phase += 1 if cw else -1; self._write() def release(self): # de-energize all coils (cool + quiet when idle) for d in self.io: d.value = False @@ -538,6 +540,7 @@ class App: self._pendNext = 0.0 # ~30fps cadence for the on-screen pendulum self.btnA = self._btn(P_BTNA); self.btnB = self._btn(P_BTNB) self._aPrev = True; self._bPrev = True + self._jog = (not self.btnA.value) and (not self.btnB.value) # both held at boot -> hidden jog/test mode self.jx = analogio.AnalogIn(P_JOYX); self.jy = analogio.AnalogIn(P_JOYY) self._joyNext = 0 self._touchDown = False; self._touchSeen = 0 @@ -1128,6 +1131,54 @@ class App: self._pend_arm.points = [(PEND_PX - 4, PEND_PY), (PEND_PX + 4, PEND_PY), (bx, by)] self.dirty = True + def _jog_loop(self): # hidden stepper jog/test (A+B held at boot) + # Joystick L/R spins the motor CCW/CW (speed by how far you push). The on-screen needle + RGB + # LED show direction. Runs forever; power-cycle with no buttons held to return to normal. + while len(self.g_overlay): self.g_overlay.pop() + 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), + ("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 + arm = vectorio.Polygon(pixel_shader=solid(C_DIM), + points=[(PEND_PX - 4, PEND_PY), (PEND_PX + 4, PEND_PY), (PEND_PX, PEND_PY - PEND_LEN)], x=0, y=0) + self.g_overlay.append(arm) + 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) + self.g_overlay.append(bob) + self.display.refresh() + time.sleep(0.1); center = self.jx.value + last = time.monotonic(); lastdir = None + while True: + now = time.monotonic() + 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.0015 - 0.02) * frac # full push = fast, just past deadzone = slow + if self.pend is not None and self.pend.ok and now - last >= dt: + self.pend.spin(cw); last = now + if cw != lastdir: # direction changed -> update LED + needle + 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 + 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() + time.sleep(0.0005) + # ---------- audio + light ---------- def click(self, level): self.spk.frequency = {2: 2300, 1: 1600, 3: 1050}.get(level, 1600) @@ -1893,6 +1944,8 @@ class App: if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7F if ok else 0x7E, 0xF7])) def run(self): + if self._jog: # A+B held at boot -> hidden stepper jog/test mode + self._jog_loop() if self.touch.addr is None: print("GT911 touch not found") boot = time.monotonic() diff --git a/pico-cp/boot.py b/pico-cp/boot.py index 4e3f74f..e8c4bd6 100644 --- a/pico-cp/boot.py +++ b/pico-cp/boot.py @@ -4,19 +4,22 @@ # /history.json and write /programs.json that the editor pushes over USB-MIDI. The drive is then # READ-ONLY to the computer — which also protects the firmware from accidental deletion. # -# HOLD BUTTON A (GP15) WHILE PLUGGING IN = editor mode: the drive is writable by the computer, so -# you can drag programs.json / code.py on from any OS or browser (the universal fallback). Reset +# HOLD BUTTON A (GP15) ALONE WHILE PLUGGING IN = editor mode: the drive is writable by the computer, +# so you can drag programs.json / code.py on from any OS or browser (the universal fallback). Reset # afterwards to return to appliance mode. # +# HOLD BUTTON A + B TOGETHER = the firmware's hidden stepper jog/test mode (app.py). That chord +# stays in appliance mode here (drive NOT flipped writable) so testing doesn't disturb the drive. +# # Also frees a USB endpoint (disables unused HID) and makes sure USB-MIDI is available. import board, digitalio, storage, usb_hid, usb_midi try: usb_hid.disable() except Exception: pass usb_midi.enable() -a = digitalio.DigitalInOut(board.GP15) -a.switch_to_input(pull=digitalio.Pull.UP) -appliance = a.value # value True (pull-up, not pressed) -> appliance mode -a.deinit() -if appliance: - try: storage.remount("/", readonly=False) # writable by code, read-only to the computer +a = digitalio.DigitalInOut(board.GP15); a.switch_to_input(pull=digitalio.Pull.UP) +b = digitalio.DigitalInOut(board.GP14); b.switch_to_input(pull=digitalio.Pull.UP) +editor = (not a.value) and b.value # editor mode = A pressed, B NOT pressed (A+B is the jog chord) +a.deinit(); b.deinit() +if not editor: + try: storage.remount("/", readonly=False) # appliance: writable by code, read-only to the computer except Exception: pass