diff --git a/editor-beta.html b/editor-beta.html index 7a74624..b9b0c3e 100644 --- a/editor-beta.html +++ b/editor-beta.html @@ -1274,16 +1274,26 @@ function _b64(u8) { let s = ""; for (let i = 0; i < u8.length; i++) s += String. // waiting for each ACK. The device base64-decodes each chunk to /app.new, verifies the .mpy header, then // A/B-installs + reboots. CH is small (and a multiple of 4) so a chunk fits the Pico's USB-MIDI RX buffer. async function _pushFirmware(b64) { + // Diagnostic: list every MIDI output the editor sees + which pass _isDevicePort + console.log("[fw] outputs:", _midiOutputs().map(o => ({ name: o.name || "?", match: _isDevicePort(o) }))); + console.log("[fw] inputs:", _midiInputs() .map(i => ({ name: i.name || "?", match: _isDevicePort(i) }))); + console.log("[fw] sending BEGIN (0x21)"); _send([0xF0, 0x7D, 0x21, 0xF7]); - if (await _ack(5000) !== true) return "handshake"; + const beginA = await _ack(5000); + console.log("[fw] BEGIN ack:", beginA); + if (beginA !== true) return "handshake"; const CH = 64, total = Math.ceil(b64.length / CH); let done = 0; const t0 = Date.now(); for (let o = 0; o < b64.length; o += CH) { const part = b64.slice(o, o + CH); const msg = [0xF0, 0x7D, 0x22]; for (let i = 0; i < part.length; i++) msg.push(part.charCodeAt(i)); // base64 is ASCII - msg.push(0xF7); _send(msg); - // Generous per-chunk timeout: an occasional flash flush on the device can take several seconds. + msg.push(0xF7); + const sendT = Date.now(); _send(msg); const a = await _ack(10000); + const ackT = Date.now(); + if (done < 3 || a !== true) { // log the first few chunks + any failure + console.log("[fw] chunk " + (done + 1) + "/" + total + " sent (" + msg.length + " bytes) ack=" + a + " after " + (ackT - sendT) + "ms"); + } if (a !== true) return "chunk " + (done + 1) + "/" + total + (a === false ? " rejected" : " no-ack"); if (++done % 25 === 0) { const el = ((Date.now() - t0) / 1000).toFixed(1); diff --git a/editor.html b/editor.html index de2bfef..1915633 100644 --- a/editor.html +++ b/editor.html @@ -1267,16 +1267,26 @@ function _b64(u8) { let s = ""; for (let i = 0; i < u8.length; i++) s += String. // waiting for each ACK. The device base64-decodes each chunk to /app.new, verifies the .mpy header, then // A/B-installs + reboots. CH is small (and a multiple of 4) so a chunk fits the Pico's USB-MIDI RX buffer. async function _pushFirmware(b64) { + // Diagnostic: list every MIDI output the editor sees + which pass _isDevicePort + console.log("[fw] outputs:", _midiOutputs().map(o => ({ name: o.name || "?", match: _isDevicePort(o) }))); + console.log("[fw] inputs:", _midiInputs() .map(i => ({ name: i.name || "?", match: _isDevicePort(i) }))); + console.log("[fw] sending BEGIN (0x21)"); _send([0xF0, 0x7D, 0x21, 0xF7]); - if (await _ack(5000) !== true) return "handshake"; + const beginA = await _ack(5000); + console.log("[fw] BEGIN ack:", beginA); + if (beginA !== true) return "handshake"; const CH = 64, total = Math.ceil(b64.length / CH); let done = 0; const t0 = Date.now(); for (let o = 0; o < b64.length; o += CH) { const part = b64.slice(o, o + CH); const msg = [0xF0, 0x7D, 0x22]; for (let i = 0; i < part.length; i++) msg.push(part.charCodeAt(i)); // base64 is ASCII - msg.push(0xF7); _send(msg); - // Generous per-chunk timeout: an occasional flash flush on the device can take several seconds. + msg.push(0xF7); + const sendT = Date.now(); _send(msg); const a = await _ack(10000); + const ackT = Date.now(); + if (done < 3 || a !== true) { // log the first few chunks + any failure + console.log("[fw] chunk " + (done + 1) + "/" + total + " sent (" + msg.length + " bytes) ack=" + a + " after " + (ackT - sendT) + "ms"); + } if (a !== true) return "chunk " + (done + 1) + "/" + total + (a === false ? " rejected" : " no-ack"); if (++done % 25 === 0) { const el = ((Date.now() - t0) / 1000).toFixed(1); diff --git a/pico-explorer/app.py b/pico-explorer/app.py index aaffd0b..e79b982 100644 --- a/pico-explorer/app.py +++ b/pico-explorer/app.py @@ -13,7 +13,7 @@ import board, busio, digitalio, pwmio, displayio, vectorio, time, json, gc, os, supervisor supervisor.runtime.autoreload = False # we write our own files (log + pushed programs); never self-restart -APP_VERSION = "0.0.3" # firmware version (the A/B updater pushes/compares this) +APP_VERSION = "0.0.4" # firmware version (the A/B updater pushes/compares this) DEVICE_ID = "X" # 'X' = Explorer, 'K' = 52Pi kit (per docs/livesync-protocol.md and the version reply) try: import rtc # set from the editor's clock SysEx so the log has real timestamps @@ -55,10 +55,11 @@ P_SDA, P_SCL = board.GP20, board.GP21 # QwSTEMMA (unuse # call display.rotation = DISPLAY_ROTATION (above) to turn it portrait so the layout has the same # shape as the PM_K-1 Kit's portrait UI but in 240x320 instead of 320x480. WIDTH, HEIGHT = 240, 320 -GRID_TOP = 104 # top of the pad grid (compact header + meters + title fit above) -MAXLANES = 6 # lanes shown on the pad grid (parser still accepts more; they just play silent visually) +GRID_TOP = 110 # top of the pad grid (compact header + meters + title fit above) +MAXLANES = 4 # lanes visible on the pad grid (parser still accepts more; they just play silent) +LOG_TOP, LOG_ROWH, LOG_ROWS = 218, 14, 6 # footer practice log: rows below the grid like the Kit MIN_LOG_SEC = 5 # don't log plays shorter than this -LOG_MENU_ROWS = 8 # log entries shown in the Practice-log menu screen +LOG_MENU_ROWS = 8 # log entries shown in the Practice-log menu screen (longer history view) # ----- BUILT-IN playlists: same defaults as the Kit so the two firmwares feel identical ----- BUILTIN_SETLISTS = [ @@ -429,7 +430,9 @@ class App: self.g_idx = displayio.Group(); root.append(self.g_idx) # set-list tab (y ~118) self.g_cont = displayio.Group(); root.append(self.g_cont) # CONT (auto-advance) toggle (y ~118) self.g_name = displayio.Group(); root.append(self.g_name) # track title (y ~134) - self.g_grid = displayio.Group(); root.append(self.g_grid) # lanes x step pads (y >= GRID_TOP=138) + self.g_grid = displayio.Group(); root.append(self.g_grid) # lanes x step pads (y >= GRID_TOP) + root.append(rect(0, LOG_TOP - 4, WIDTH, 1, C_PANEL)) # divider above the footer practice log + self.g_log = displayio.Group(); root.append(self.g_log) # practice history (Kit-style footer) self.g_overlay = displayio.Group(); root.append(self.g_overlay) # modals (drawn on top) def _place(self, group, s, x, y, fg, bg, font, right_edge=None): @@ -469,7 +472,7 @@ class App: self._heavy_redraw_at = 0; self._heavy_log_pending = False; self._grid_li = None while len(self.g_overlay): self.g_overlay.pop() self._reset_clock(); self.draw_bpm(); self.draw_status(); self.draw_train() - self.build_grid() + self.build_grid(); self.draw_log() def _prog_str(self): parts = ['t' + str(self.bpm)] if self.bars: parts.append('b' + str(self.bars)) @@ -762,7 +765,7 @@ class App: while len(self.g_overlay): self.g_overlay.pop() self._reset_clock() self.draw_bpm(); self.draw_status(); self.draw_train(); self.draw_meters() - self.build_grid() + self.build_grid(); self.draw_log() if running and not self.running: self.toggle() elif (not running) and self.running: self.toggle() except Exception as e: @@ -1134,20 +1137,19 @@ class App: def draw_bpm(self): if self.bpm == self._displayed_bpm: return self._displayed_bpm = self.bpm - # Smaller than the Kit's BPM: FONT_M instead of FONT_L. Kit's FONT_L was ~30px tall at 480 - too big proportionally at 320. - self._place(self.g_bpm, str(self.bpm), 0, 30, C_TXT, C_BG, FONT_M, right_edge=WIDTH-8) + self._place(self.g_bpm, str(self.bpm), 0, 38, C_TXT, C_BG, FONT_M, right_edge=WIDTH-8) def draw_status(self): sl = self.setlists[self.sl] - # setlist tab line at y=66; muted = built-in, cyan = your own + # setlist tab line at y=72; muted = built-in, cyan = your own self._place(self.g_idx, "%s %d/%d" % (sl['title'][:13], self.idx + 1, len(sl['items'])), - 6, 66, C_MUTE if sl['builtin'] else C_CYAN, C_BG, FONT_S) - self._place(self.g_cont, "CONT", 0, 66, C_GREEN if self.continue_on else C_DIM, C_BG, FONT_S, right_edge=WIDTH-6) - # track title at y=82 (FONT_M; ~16 px tall, fits above GRID_TOP=104) - self._place(self.g_name, self.name[:22], 6, 82, C_TXT, C_BG, FONT_M) + 6, 72, C_MUTE if sl['builtin'] else C_CYAN, C_BG, FONT_S) + self._place(self.g_cont, "CONT", 0, 72, C_GREEN if self.continue_on else C_DIM, C_BG, FONT_S, right_edge=WIDTH-6) + # track title at y=88 (FONT_M; ~16 px tall, fits above GRID_TOP=110) + self._place(self.g_name, self.name[:22], 6, 88, C_TXT, C_BG, FONT_M) def draw_train(self): g = self.g_train while len(g): g.pop() - x = 6; y = 52 # ramp / gap-trainer indicators below the meters row, above the setlist tab + x = 6; y = 58 # ramp / gap-trainer indicators below the meters row, above the setlist tab if self.ramp: up = self.ramp['amt'] >= 0 pts = [(0, 9), (12, 9), (12, 0)] if up else [(0, 0), (0, 9), (12, 9)] @@ -1182,9 +1184,9 @@ class App: else: ts = self._fmt_t(el); bs = "bar %s" % cur if ts != self._lastTs: - self._place(self.g_time, ts, 6, 32, C_TXT, C_BG, FONT_S); self._lastTs = ts + self._place(self.g_time, ts, 6, 38, C_TXT, C_BG, FONT_S); self._lastTs = ts if bs != self._lastBs: - self._place(self.g_bar, bs, 6, 44, C_MUTE, C_BG, FONT_S); self._lastBs = bs + self._place(self.g_bar, bs, 6, 50, C_MUTE, C_BG, FONT_S); self._lastBs = bs # ---------- pad grid (chunked rebuild; per-pad chunks so audio interleaves) ---------- def _padbase(self, L, s): @@ -1197,8 +1199,8 @@ class App: self.lane_pads = []; self.lane_lit = [] gc.collect() n = min(len(self.lanes), MAXLANES) - top = GRID_TOP; rowh = min(30, ((HEIGHT - 6) - top) // max(1, n)) # more vertical room in portrait -> taller rows - px0 = 48; usable = WIDTH - 8 - px0 - 8; gridh = n * rowh # narrower screen -> tighter lane-label column + top = GRID_TOP; rowh = min(26, ((LOG_TOP - 6) - top) // max(1, n)) # leave room for the footer log below + px0 = 48; usable = WIDTH - 8 - px0 - 8; gridh = n * rowh # narrower screen -> tighter lane-label column self._grid = {'top': top, 'rowh': rowh, 'px0': px0, 'usable': usable, 'n': n} m = self.lanes[0] if self.lanes else None if m is not None: @@ -1286,7 +1288,26 @@ class App: self.log.insert(0, {"t": "%02d:%02d" % (t.tm_hour, t.tm_min), "bpm": self.play_bpm, "dur": dur, "bars": self._m_steps // max(1, mlen), "name": self.play_name}) del self.log[200:] - self._save_log() + self._save_log(); self.draw_log() + def draw_log(self): # footer practice log (this track only), Kit-style + g = self.g_log + while len(g): g.pop() + gc.collect() + hdr, w, h = make_text("PRACTICE LOG", FONT_S, C_MUTE, C_BG); hdr.x = 6; hdr.y = LOG_TOP; g.append(hdr) + rows = [(i, e) for i, e in enumerate(self.log) if e.get("name") == self.name] + if not rows: + tg, w, h = make_text("no plays over 5s yet", FONT_S, C_DIM, C_BG) + tg.x = 6; tg.y = LOG_TOP + LOG_ROWH + 2; g.append(tg) + self.dirty = True; return + y = LOG_TOP + LOG_ROWH + 2 + for k in range(min(LOG_ROWS, len(rows))): + _oi, e = rows[k] + dur = "%d:%02d" % (e["dur"] // 60, e["dur"] % 60) + bars = e.get("bars", 0); bstr = (" %dbar" % bars) if bars else "" + line = "%s %3dbpm %s%s" % (e.get("t", "--:--"), e["bpm"], dur, bstr) + tg, w, h = make_text(line, FONT_S, C_TXT, C_BG); tg.x = 6; tg.y = y; g.append(tg) + y += LOG_ROWH + self.dirty = True # ---------- USB-MIDI in: SysEx assembler (clock + editor-pushed programs + live-sync) ---------- def _feed_midi(self, buf, n): @@ -1418,9 +1439,11 @@ class App: self.draw_bpm(); self.draw_status(); self.draw_train(); self.draw_meters() if self._heavy_redraw_at and time.monotonic() >= self._heavy_redraw_at: self._heavy_redraw_at = 0 - self._grid_rebuild_start() + self._grid_rebuild_start(); self._heavy_log_pending = True if self._grid_li is not None: self._grid_rebuild_step() + elif self._heavy_log_pending: # grid done -> redraw footer log + self._heavy_log_pending = False; self.draw_log() tnow = time.monotonic() if tnow >= self._uiNext: self._uiNext = tnow + 0.25; self.draw_meters(); self.draw_bpm()