PM_X-1 0.0.4 + editor push diagnostics
Layout fixes (user reported BPM/time were still bumping the header at 0.0.3): - All Y coords below the header divider shifted down 6px: BPM 30->38, time 32->38, bar 44->50, train 52->58, setlist tab 66->72, title 82->88. - GRID_TOP 104 -> 110. Restored the Kit-style footer practice log: - LOG_TOP=218, LOG_ROWH=14, LOG_ROWS=6. - MAXLANES dropped from 6 visible to 4 visible (rowh capped at 26 so the grid doesn't run into the log). Tracks with more lanes still play silently. - _build_scene now appends g_log (with a divider above it). - draw_log() draws the current-track log into the footer; load() + _log_play() + the seam apply path all call it. The Practice-log menu entry is kept for the full scrollable history. Editor diagnostics for the firmware push (the user got chunk-1 ACK then the device's MIDI badge went gray, meaning chunks 2+ never reached it): - editor.html + editor-beta.html _pushFirmware() now logs every MIDI output + input it sees along with which ones _isDevicePort() matched, plus per- chunk send/ACK timing for the first 3 chunks and any failed chunk. - This narrows down whether the failure is (a) wrong-port routing (filter doesn't match the Pimoroni Explorer's name), (b) ACK never arriving back to the host, or (c) chunks sent fine but the device's RX buffer is dropping them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ea7bb9bfee
commit
3805c5ee00
3 changed files with 70 additions and 27 deletions
|
|
@ -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
|
// 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.
|
// 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) {
|
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]);
|
_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 CH = 64, total = Math.ceil(b64.length / CH); let done = 0;
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
for (let o = 0; o < b64.length; o += CH) {
|
for (let o = 0; o < b64.length; o += CH) {
|
||||||
const part = b64.slice(o, o + CH); const msg = [0xF0, 0x7D, 0x22];
|
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
|
for (let i = 0; i < part.length; i++) msg.push(part.charCodeAt(i)); // base64 is ASCII
|
||||||
msg.push(0xF7); _send(msg);
|
msg.push(0xF7);
|
||||||
// Generous per-chunk timeout: an occasional flash flush on the device can take several seconds.
|
const sendT = Date.now(); _send(msg);
|
||||||
const a = await _ack(10000);
|
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 (a !== true) return "chunk " + (done + 1) + "/" + total + (a === false ? " rejected" : " no-ack");
|
||||||
if (++done % 25 === 0) {
|
if (++done % 25 === 0) {
|
||||||
const el = ((Date.now() - t0) / 1000).toFixed(1);
|
const el = ((Date.now() - t0) / 1000).toFixed(1);
|
||||||
|
|
|
||||||
16
editor.html
16
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
|
// 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.
|
// 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) {
|
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]);
|
_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 CH = 64, total = Math.ceil(b64.length / CH); let done = 0;
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
for (let o = 0; o < b64.length; o += CH) {
|
for (let o = 0; o < b64.length; o += CH) {
|
||||||
const part = b64.slice(o, o + CH); const msg = [0xF0, 0x7D, 0x22];
|
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
|
for (let i = 0; i < part.length; i++) msg.push(part.charCodeAt(i)); // base64 is ASCII
|
||||||
msg.push(0xF7); _send(msg);
|
msg.push(0xF7);
|
||||||
// Generous per-chunk timeout: an occasional flash flush on the device can take several seconds.
|
const sendT = Date.now(); _send(msg);
|
||||||
const a = await _ack(10000);
|
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 (a !== true) return "chunk " + (done + 1) + "/" + total + (a === false ? " rejected" : " no-ack");
|
||||||
if (++done % 25 === 0) {
|
if (++done % 25 === 0) {
|
||||||
const el = ((Date.now() - t0) / 1000).toFixed(1);
|
const el = ((Date.now() - t0) / 1000).toFixed(1);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
import board, busio, digitalio, pwmio, displayio, vectorio, time, json, gc, os, supervisor
|
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
|
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)
|
DEVICE_ID = "X" # 'X' = Explorer, 'K' = 52Pi kit (per docs/livesync-protocol.md and the version reply)
|
||||||
try:
|
try:
|
||||||
import rtc # set from the editor's clock SysEx so the log has real timestamps
|
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
|
# 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.
|
# shape as the PM_K-1 Kit's portrait UI but in 240x320 instead of 320x480.
|
||||||
WIDTH, HEIGHT = 240, 320
|
WIDTH, HEIGHT = 240, 320
|
||||||
GRID_TOP = 104 # top of the pad grid (compact header + meters + title fit above)
|
GRID_TOP = 110 # 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)
|
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
|
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 -----
|
# ----- BUILT-IN playlists: same defaults as the Kit so the two firmwares feel identical -----
|
||||||
BUILTIN_SETLISTS = [
|
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_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_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_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)
|
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):
|
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
|
self._heavy_redraw_at = 0; self._heavy_log_pending = False; self._grid_li = None
|
||||||
while len(self.g_overlay): self.g_overlay.pop()
|
while len(self.g_overlay): self.g_overlay.pop()
|
||||||
self._reset_clock(); self.draw_bpm(); self.draw_status(); self.draw_train()
|
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):
|
def _prog_str(self):
|
||||||
parts = ['t' + str(self.bpm)]
|
parts = ['t' + str(self.bpm)]
|
||||||
if self.bars: parts.append('b' + str(self.bars))
|
if self.bars: parts.append('b' + str(self.bars))
|
||||||
|
|
@ -762,7 +765,7 @@ class App:
|
||||||
while len(self.g_overlay): self.g_overlay.pop()
|
while len(self.g_overlay): self.g_overlay.pop()
|
||||||
self._reset_clock()
|
self._reset_clock()
|
||||||
self.draw_bpm(); self.draw_status(); self.draw_train(); self.draw_meters()
|
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()
|
if running and not self.running: self.toggle()
|
||||||
elif (not running) and self.running: self.toggle()
|
elif (not running) and self.running: self.toggle()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1134,20 +1137,19 @@ class App:
|
||||||
def draw_bpm(self):
|
def draw_bpm(self):
|
||||||
if self.bpm == self._displayed_bpm: return
|
if self.bpm == self._displayed_bpm: return
|
||||||
self._displayed_bpm = self.bpm
|
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, 38, C_TXT, C_BG, FONT_M, right_edge=WIDTH-8)
|
||||||
self._place(self.g_bpm, str(self.bpm), 0, 30, C_TXT, C_BG, FONT_M, right_edge=WIDTH-8)
|
|
||||||
def draw_status(self):
|
def draw_status(self):
|
||||||
sl = self.setlists[self.sl]
|
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'])),
|
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)
|
6, 72, 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)
|
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=82 (FONT_M; ~16 px tall, fits above GRID_TOP=104)
|
# track title at y=88 (FONT_M; ~16 px tall, fits above GRID_TOP=110)
|
||||||
self._place(self.g_name, self.name[:22], 6, 82, C_TXT, C_BG, FONT_M)
|
self._place(self.g_name, self.name[:22], 6, 88, C_TXT, C_BG, FONT_M)
|
||||||
def draw_train(self):
|
def draw_train(self):
|
||||||
g = self.g_train
|
g = self.g_train
|
||||||
while len(g): g.pop()
|
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:
|
if self.ramp:
|
||||||
up = self.ramp['amt'] >= 0
|
up = self.ramp['amt'] >= 0
|
||||||
pts = [(0, 9), (12, 9), (12, 0)] if up else [(0, 0), (0, 9), (12, 9)]
|
pts = [(0, 9), (12, 9), (12, 0)] if up else [(0, 0), (0, 9), (12, 9)]
|
||||||
|
|
@ -1182,9 +1184,9 @@ class App:
|
||||||
else:
|
else:
|
||||||
ts = self._fmt_t(el); bs = "bar %s" % cur
|
ts = self._fmt_t(el); bs = "bar %s" % cur
|
||||||
if ts != self._lastTs:
|
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:
|
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) ----------
|
# ---------- pad grid (chunked rebuild; per-pad chunks so audio interleaves) ----------
|
||||||
def _padbase(self, L, s):
|
def _padbase(self, L, s):
|
||||||
|
|
@ -1197,8 +1199,8 @@ class App:
|
||||||
self.lane_pads = []; self.lane_lit = []
|
self.lane_pads = []; self.lane_lit = []
|
||||||
gc.collect()
|
gc.collect()
|
||||||
n = min(len(self.lanes), MAXLANES)
|
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
|
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
|
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}
|
self._grid = {'top': top, 'rowh': rowh, 'px0': px0, 'usable': usable, 'n': n}
|
||||||
m = self.lanes[0] if self.lanes else None
|
m = self.lanes[0] if self.lanes else None
|
||||||
if m is not 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,
|
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})
|
"dur": dur, "bars": self._m_steps // max(1, mlen), "name": self.play_name})
|
||||||
del self.log[200:]
|
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) ----------
|
# ---------- USB-MIDI in: SysEx assembler (clock + editor-pushed programs + live-sync) ----------
|
||||||
def _feed_midi(self, buf, n):
|
def _feed_midi(self, buf, n):
|
||||||
|
|
@ -1418,9 +1439,11 @@ class App:
|
||||||
self.draw_bpm(); self.draw_status(); self.draw_train(); self.draw_meters()
|
self.draw_bpm(); self.draw_status(); self.draw_train(); self.draw_meters()
|
||||||
if self._heavy_redraw_at and time.monotonic() >= self._heavy_redraw_at:
|
if self._heavy_redraw_at and time.monotonic() >= self._heavy_redraw_at:
|
||||||
self._heavy_redraw_at = 0
|
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:
|
if self._grid_li is not None:
|
||||||
self._grid_rebuild_step()
|
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()
|
tnow = time.monotonic()
|
||||||
if tnow >= self._uiNext:
|
if tnow >= self._uiNext:
|
||||||
self._uiNext = tnow + 0.25; self.draw_meters(); self.draw_bpm()
|
self._uiNext = tnow + 0.25; self.draw_meters(); self.draw_bpm()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue