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()