velxio/test/test_micropython_i2c_ssd130.../test.mjs

352 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* test_micropython_i2c_ssd1306_repro.mjs — Phase 1b reproduction test
*
* Phase 1 (test_micropython_i2c_minimal) confirmed that I2C(0,...), scan(),
* and a single-byte writeto(0x3C, b"\xA0") all work without rebooting.
* The bug must be triggered by something more specific that ssd1306.py does.
*
* This test walks the SSD1306_I2C init sequence step by step and prints a
* marker after every write, so we can see EXACTLY which call is the last
* one before the chip resets.
*
* What ssd1306.SSD1306_I2C(128, 64, i2c) does on construction:
* - For each cmd in a 27-entry init list:
* i2c.writeto(0x3C, bytes([0x80, cmd])) # 2-byte writes
* - self.fill(0) # framebuffer fill, no I2C
* - self.show()
* - 6× writeto(0x3C, bytes([0x80, addr_cmd])) # set col/page addrs
* - i2c.writevto(0x3C, [b"\x40", buffer]) # 1024-byte data dump
*
* Markers (last one printed = the suspect):
* mp_step0_ok — I2C ctor
* mp_step1_ok devs=… — scan
* mp_step2_ok — 2-byte writeto (cmd shape)
* mp_step3_iter N cmd=0xXX — each cmd of the 27-cmd init loop
* mp_step3_ok after_idx=27 — full init survived
* mp_step4_ok — show() prelude (6× addr writes)
* mp_step5_ok — writevto with small payload (8 bytes)
* mp_step6_ok — writevto with full framebuffer (1024 bytes)
* mp_done — all good, no reboot
*
* Run:
* node --experimental-websocket test/test_micropython_i2c_ssd1306_repro/test.mjs \
* --backend=http://localhost:3080 [--timeout=120]
*/
const BACKEND = process.env.BACKEND_URL
?? process.argv.find(a => a.startsWith('--backend='))?.slice(10)
?? 'http://localhost:8001';
const WS_BASE = BACKEND.replace(/^https?:/, m => m === 'https:' ? 'wss:' : 'ws:');
const SESSION = `test-mp-i2c-ssd1306-${Date.now()}`;
const TIMEOUT_S = parseInt(
process.argv.find(a => a.startsWith('--timeout='))?.slice(10) ?? '120'
);
const FIRMWARE_URL = 'https://micropython.org/resources/firmware/ESP32_GENERIC-20230426-v1.20.0.bin';
const FLASH_OFFSET = 0x1000;
const FLASH_SIZE = 4 * 1024 * 1024;
const INJECT_CODE = `
from machine import Pin, I2C
import time
ADDR = 0x3C
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
print("mp_step0_ok")
devs = i2c.scan()
print("mp_step1_ok devs=" + str(devs))
# step 2: same shape ssd1306.write_cmd uses (2-byte writeto with 0x80 prefix)
i2c.writeto(ADDR, bytes([0x80, 0xAE]))
print("mp_step2_ok")
# step 3: full 27-entry init sequence (verbatim from ssd1306.py for 128x64)
INIT = [
0xAE, # SET_DISP off
0x20, 0x00, # SET_MEM_ADDR horizontal
0x40, # SET_DISP_START_LINE | 0
0xA1, # SET_SEG_REMAP | 1
0xA8, 0x3F, # SET_MUX_RATIO 64-1
0xC8, # SET_COM_OUT_DIR | 8
0xD3, 0x00, # SET_DISP_OFFSET 0
0xDA, 0x12, # SET_COM_PIN_CFG 0x12
0xD5, 0x80, # SET_DISP_CLK_DIV
0xD9, 0xF1, # SET_PRECHARGE
0xDB, 0x30, # SET_VCOM_DESEL
0x81, 0xFF, # SET_CONTRAST
0xA4, # SET_ENTIRE_ON
0xA6, # SET_NORM_INV
0x8D, 0x14, # SET_CHARGE_PUMP
0xAF, # SET_DISP on
]
for idx, cmd in enumerate(INIT):
i2c.writeto(ADDR, bytes([0x80, cmd]))
print("mp_step3_iter " + str(idx) + " cmd=0x" + ("%02x" % cmd))
print("mp_step3_ok after_idx=" + str(len(INIT)))
# step 4: 6 address commands (.show() prelude)
for cmd in (0x21, 32, 32 + 127, 0x22, 0, 7):
i2c.writeto(ADDR, bytes([0x80, cmd]))
print("mp_step4_ok")
# step 5: writevto with small payload (8 bytes)
try:
i2c.writevto(ADDR, (b"\\x40", bytes([0]*8)))
print("mp_step5_ok")
except Exception as e:
print("mp_step5_err " + repr(e))
# step 6: writevto with full 1024-byte framebuffer payload
try:
i2c.writevto(ADDR, (b"\\x40", bytes([0]*1024)))
print("mp_step6_ok")
except Exception as e:
print("mp_step6_err " + repr(e))
print("mp_done")
`.trim();
const T0 = Date.now();
const ts = () => `[+${((Date.now() - T0) / 1000).toFixed(3)}s]`;
const C = {
INFO: '\x1b[36m', WARN: '\x1b[33m', ERROR: '\x1b[31m',
OK: '\x1b[32m', SERIAL: '\x1b[35m', RESET: '\x1b[0m',
};
const log = (lvl, ...a) => console.log(`${C[lvl] ?? ''}${ts()} [${lvl}]${C.RESET}`, ...a);
const info = (...a) => log('INFO', ...a);
const ok = (...a) => log('OK', ...a);
const warn = (...a) => log('WARN', ...a);
const err = (...a) => log('ERROR', ...a);
async function downloadFirmware() {
info(`Downloading MicroPython firmware from ${FIRMWARE_URL} ...`);
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 60_000);
try {
const res = await fetch(FIRMWARE_URL, { signal: ctrl.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
clearTimeout(t);
ok(`Downloaded ${bytes.length} bytes`);
return bytes;
} finally { clearTimeout(t); }
}
function buildFlashImage(firmware) {
const image = new Uint8Array(FLASH_SIZE).fill(0xFF);
image.set(firmware, FLASH_OFFSET);
return image;
}
const toBase64 = (bytes) => Buffer.from(bytes).toString('base64');
function runSimulation(firmware_b64) {
return new Promise((resolve) => {
const wsUrl = `${WS_BASE}/api/simulation/ws/${SESSION}`;
info(`Connecting WebSocket → ${wsUrl}`);
const ws = new WebSocket(wsUrl);
const result = {
replReady: false,
codeInjected: false,
markers: [],
lastMarker: null,
step3Iters: 0,
step3LastCmd: null,
step5Status: null,
step6Status: null,
done: false,
reboot: false,
systemEvents: [],
wsCloseCode: null,
serialBuf: '',
timedOut: false,
};
let replState = 'idle';
let serialBuf = '';
const globalTimer = setTimeout(() => {
info(`Global timeout (${TIMEOUT_S}s)`);
result.timedOut = true;
try { ws.close(); } catch {}
}, TIMEOUT_S * 1000);
const finish = () => {
clearTimeout(globalTimer);
resolve(result);
};
function sendCodeInRawRepl() {
if (result.codeInjected) return;
result.codeInjected = true;
info('Stage 3: raw REPL confirmed → sending code (64-byte chunks)');
const codeBytes = Array.from(new TextEncoder().encode(INJECT_CODE));
const CHUNK = 64, DELAY = 150;
let offset = 0;
const sendChunk = () => {
if (offset >= codeBytes.length) {
setTimeout(() => {
ws.send(JSON.stringify({ type: 'esp32_serial_input', data: { bytes: [0x04] } }));
info('Ctrl+D sent — code executing');
}, 300);
return;
}
const chunk = codeBytes.slice(offset, offset + CHUNK);
ws.send(JSON.stringify({ type: 'esp32_serial_input', data: { bytes: chunk } }));
offset += CHUNK;
setTimeout(sendChunk, DELAY);
};
sendChunk();
}
ws.addEventListener('open', () => {
ok('WebSocket connected');
ws.send(JSON.stringify({
type: 'start_esp32',
data: {
board: 'esp32',
firmware_b64,
sensors: [{ sensor_type: 'ssd1306', pin: 200 + 0x3C, addr: 0x3C }],
wifi_enabled: false,
},
}));
info('Sent start_esp32 with sensors=[{ssd1306@0x3C}]');
});
ws.addEventListener('message', ev => {
let msg;
try { msg = JSON.parse(ev.data); } catch { return; }
const { type, data } = msg;
if (type === 'system') {
result.systemEvents.push(data);
info(`system: ${JSON.stringify(data)}`);
if (data?.event === 'reboot' || data?.status === 'reboot') {
warn('!!! ESP32 REBOOTED — last marker before reboot: ' + (result.lastMarker || '(none)'));
result.reboot = true;
}
return;
}
if (type === 'serial_output') {
const text = data?.data ?? '';
serialBuf += text;
for (const ch of text) process.stdout.write(ch);
// 4-stage REPL state machine
if (replState === 'idle' && serialBuf.includes('Type "help()"')) {
replState = 'banner_seen';
info('Stage 1: banner seen → poking UART with \\r');
setTimeout(() => ws.send(JSON.stringify({
type: 'esp32_serial_input', data: { bytes: [0x0D] }
})), 800);
}
if (replState === 'banner_seen' && serialBuf.includes('>>>')) {
replState = 'prompt_seen';
result.replReady = true;
serialBuf = '';
ok('Stage 2: >>> seen → sending Ctrl+A');
setTimeout(() => ws.send(JSON.stringify({
type: 'esp32_serial_input', data: { bytes: [0x01] }
})), 200);
}
if (replState === 'prompt_seen' && serialBuf.includes('raw REPL')) {
replState = 'raw_repl_entered';
serialBuf = '';
setTimeout(sendCodeInRawRepl, 200);
}
// Parse markers line by line
let nl;
while ((nl = serialBuf.indexOf('\n')) !== -1) {
const line = serialBuf.slice(0, nl).replace(/\r$/, '');
serialBuf = serialBuf.slice(nl + 1);
if (!line.trim()) continue;
const m = line.match(/mp_(step\d|done)\w*(?:\s+(.+))?/);
if (m) {
const tag = 'mp_' + m[1] + (m[0].slice(3 + m[1].length).match(/^_[a-z]+/)?.[0] ?? '');
const tail = (m[2] || '').trim();
const full = tail ? `${tag} ${tail}` : tag;
result.markers.push(full);
result.lastMarker = full;
}
const iter = line.match(/mp_step3_iter (\d+) cmd=0x([0-9a-f]{2})/);
if (iter) {
result.step3Iters = parseInt(iter[1]) + 1;
result.step3LastCmd = '0x' + iter[2];
}
if (line.includes('mp_step5_ok')) result.step5Status = 'ok';
if (line.includes('mp_step5_err')) result.step5Status = line.slice(line.indexOf('mp_step5_err'));
if (line.includes('mp_step6_ok')) result.step6Status = 'ok';
if (line.includes('mp_step6_err')) result.step6Status = line.slice(line.indexOf('mp_step6_err'));
if (line.includes('mp_done')) {
result.done = true;
setTimeout(() => { try { ws.close(); } catch {} finish(); }, 400);
}
if (line.includes('Traceback')) warn(`TRACEBACK: ${line}`);
}
if (serialBuf.length > 8192) serialBuf = serialBuf.slice(-1024);
return;
}
if (type === 'error') {
err(`backend error: ${JSON.stringify(data)}`);
}
});
ws.addEventListener('close', ev => {
result.wsCloseCode = ev.code;
info(`WebSocket closed (code=${ev.code})`);
finish();
});
ws.addEventListener('error', ev => err('WebSocket error', ev.message ?? ''));
});
}
async function main() {
console.log('\n' + '='.repeat(70));
console.log(' Phase 1b — SSD1306_I2C init sequence step-by-step reproduction');
console.log('='.repeat(70) + '\n');
info(`Backend: ${BACKEND}`);
info(`Timeout: ${TIMEOUT_S}s`);
const fw = await downloadFirmware();
const image = buildFlashImage(fw);
const b64 = toBase64(image);
info(`Flash image: ${Math.round(b64.length / 1024)} KB base64`);
const r = await runSimulation(b64);
console.log('\n' + '─'.repeat(70));
console.log(' Results');
console.log('─'.repeat(70));
console.log(` REPL ready: ${r.replReady}`);
console.log(` Code injected: ${r.codeInjected}`);
console.log(` Last marker: ${r.lastMarker ?? '(none)'}`);
console.log(` Step3 init iters OK: ${r.step3Iters} / 27`);
console.log(` Step3 last cmd OK: ${r.step3LastCmd ?? '(none)'}`);
console.log(` Step5 (writevto 8B): ${r.step5Status ?? '(not reached)'}`);
console.log(` Step6 (writevto 1KB): ${r.step6Status ?? '(not reached)'}`);
console.log(` System reboot: ${r.reboot}`);
console.log(` WS close code: ${r.wsCloseCode ?? '(open)'}`);
console.log(` Timed out: ${r.timedOut}`);
console.log('─'.repeat(70) + '\n');
if (r.done && !r.reboot) {
ok('Full SSD1306 init sequence completed without reboot.');
process.exit(0);
}
if (r.reboot) {
err('REBOOT REPRODUCED. Suspect = last marker before reboot.');
console.log(' → ' + (r.lastMarker || '(no markers — reboot before any marker)'));
process.exit(1);
}
warn('Inconclusive: no reboot but no mp_done either. See serial output above.');
process.exit(3);
}
main().catch(e => { err('Fatal:', e.message); console.error(e.stack); process.exit(2); });