158 lines
7.2 KiB
TypeScript
158 lines
7.2 KiB
TypeScript
/**
|
|
* Regression test for GitHub issue #122
|
|
* https://github.com/davidmonterocrespo24/velxio/issues/122
|
|
*
|
|
* The user (chenxyzj) reported three sub-problems running MicroPython on ESP32:
|
|
*
|
|
* 1. AttributeError: 'Pin' object has no attribute 'toggle'
|
|
* → Pin.toggle() was added to the ESP32 port in MicroPython v1.21 (Oct
|
|
* 2023). The firmware Velxio bundles is v1.20.0 (April 2023), so the
|
|
* default Blink demo crashes immediately.
|
|
*
|
|
* 2. ESP32-C3: "Detected size(2048k) smaller than the size in the binary
|
|
* image header(4096k). Probe failed."
|
|
* → padToFlashSize() was forcing a 4 MB minimum only for plain 'esp32',
|
|
* leaving every other variant (S3, C3, …) padded to 2 MB. Every
|
|
* official MicroPython ESP32 build is compiled with
|
|
* CONFIG_ESPTOOLPY_FLASHSIZE_4MB so the SPI flash driver bails out at
|
|
* boot when the QEMU flash is smaller.
|
|
*
|
|
* 3. WLAN crash on STA_IF connect (LoadStorePIFAddrError) — *not* covered
|
|
* here. That requires real QEMU and lives in
|
|
* test/backend/e2e/test_micropython_esp32.mjs as a manual diagnostic.
|
|
*
|
|
* This file contains pure Vitest unit tests — no QEMU, no network, no DOM.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { padToFlashSize } from '../simulation/Esp32MicroPythonLoader';
|
|
import type { BoardKind } from '../types/board';
|
|
|
|
// ── Synthetic firmware payloads ───────────────────────────────────────────────
|
|
//
|
|
// Real MicroPython firmware blobs are 1.6+ MB. For these tests we just need a
|
|
// payload smaller than 4 MB whose first byte is the ESP32 image magic (0xE9)
|
|
// so we can spot-check the placement.
|
|
function makeFirmware(bytes: number): Uint8Array {
|
|
const fw = new Uint8Array(bytes).fill(0x00);
|
|
fw[0] = 0xe9; // ESP image magic
|
|
return fw;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Issue #122 fix #1 — every ESP32 variant must pad to AT LEAST 4 MB
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('issue #122 — ESP32-C3 flash size mismatch', () => {
|
|
const SIZE_4MB = 4 * 1024 * 1024;
|
|
const SAMPLE_FW = makeFirmware(1_700_000); // ≈ a real MicroPython payload
|
|
|
|
// The variants the loader's toFirmwareVariant() routes to each base build
|
|
// (esp32, esp32-s3, esp32-c3). All three groups need ≥ 4 MB.
|
|
const ALL_VARIANTS: BoardKind[] = [
|
|
'esp32',
|
|
'esp32-devkit-c-v4',
|
|
'esp32-cam',
|
|
'wemos-lolin32-lite',
|
|
'esp32-s3',
|
|
'xiao-esp32-s3',
|
|
'arduino-nano-esp32',
|
|
'esp32-c3',
|
|
'xiao-esp32-c3',
|
|
'aitewinrobot-esp32c3-supermini',
|
|
];
|
|
|
|
for (const kind of ALL_VARIANTS) {
|
|
it(`pads ${kind} to ≥ 4 MB`, () => {
|
|
const padded = padToFlashSize(SAMPLE_FW, kind);
|
|
expect(padded.length).toBeGreaterThanOrEqual(SIZE_4MB);
|
|
// Padding goes on the high end — every byte beyond the firmware
|
|
// (and beyond any LX6 0x1000 offset) must be 0xFF.
|
|
const lastByte = padded[padded.length - 1];
|
|
expect(lastByte).toBe(0xff);
|
|
});
|
|
}
|
|
|
|
it('regression: ESP32-C3 must NOT pad to only 2 MB (the original bug)', () => {
|
|
const padded = padToFlashSize(SAMPLE_FW, 'esp32-c3');
|
|
// 2 MB is the value that produced the user's
|
|
// "Detected size(2048k) smaller than the size in the binary image header(4096k)"
|
|
// panic at boot. The fix forces 4 MB.
|
|
expect(padded.length).not.toBe(2 * 1024 * 1024);
|
|
expect(padded.length).toBeGreaterThanOrEqual(SIZE_4MB);
|
|
});
|
|
|
|
it('LX6 (esp32) places firmware at flash offset 0x1000 (2nd-stage bootloader)', () => {
|
|
const padded = padToFlashSize(SAMPLE_FW, 'esp32');
|
|
expect(padded[0x1000]).toBe(0xe9);
|
|
// Bytes before 0x1000 are pre-firmware and stay 0xFF (the ROM bootloader
|
|
// reads its own code from the chip, not from flash).
|
|
expect(padded[0]).toBe(0xff);
|
|
expect(padded[0xfff]).toBe(0xff);
|
|
});
|
|
|
|
it('S3 / C3 place the combined image at offset 0', () => {
|
|
for (const kind of ['esp32-s3', 'esp32-c3'] as BoardKind[]) {
|
|
const padded = padToFlashSize(SAMPLE_FW, kind);
|
|
expect(padded[0]).toBe(0xe9);
|
|
}
|
|
});
|
|
|
|
it('emits exactly one of QEMU\'s valid flash sizes (2/4/8/16 MB)', () => {
|
|
const VALID = [2, 4, 8, 16].map((mb) => mb * 1024 * 1024);
|
|
for (const kind of ALL_VARIANTS) {
|
|
const padded = padToFlashSize(SAMPLE_FW, kind);
|
|
expect(VALID).toContain(padded.length);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Issue #122 fix #2 — default ESP32 MicroPython demo must NOT use Pin.toggle()
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
//
|
|
// Pin.toggle() raises AttributeError on the ESP32 port in v1.20.0 (the
|
|
// firmware Velxio ships). We can't import the constant directly from
|
|
// useEditorStore.ts (it isn't exported), but we can grep the source.
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
|
|
describe('issue #122 — default ESP32 MicroPython demo uses portable APIs', () => {
|
|
const SRC = readFileSync(
|
|
resolve(__dirname, '../store/useEditorStore.ts'),
|
|
'utf-8',
|
|
);
|
|
|
|
function extractBlock(name: string): string {
|
|
// Match `const NAME = \`...\`;` (template literal across multiple lines).
|
|
const re = new RegExp(`const\\s+${name}\\s*=\\s*\`([\\s\\S]*?)\`;`);
|
|
const m = SRC.match(re);
|
|
if (!m) throw new Error(`Could not find ${name} in useEditorStore.ts`);
|
|
return m[1];
|
|
}
|
|
|
|
it('DEFAULT_ESP32_MICROPYTHON_CONTENT does not call Pin.toggle()', () => {
|
|
const code = extractBlock('DEFAULT_ESP32_MICROPYTHON_CONTENT');
|
|
// Pin.toggle() is the exact API that raised AttributeError for the user.
|
|
// The fix uses pin.value(bool) which is portable across every MicroPython
|
|
// port and version, so the script must never reference .toggle().
|
|
expect(code).not.toMatch(/\.toggle\(\)/);
|
|
});
|
|
|
|
it('DEFAULT_ESP32_MICROPYTHON_CONTENT uses .value() for portability', () => {
|
|
const code = extractBlock('DEFAULT_ESP32_MICROPYTHON_CONTENT');
|
|
expect(code).toMatch(/\.value\(/);
|
|
});
|
|
|
|
it('Pico (RP2) demo may keep .toggle() — that port has it since v1.18', () => {
|
|
// The RP2 port's `Pin.toggle()` predates v1.20.0, so the Pico demo can
|
|
// safely keep using it. This test exists so that someone "cleaning up"
|
|
// the Pico demo to match the ESP32 fix knows it isn't required.
|
|
const code = extractBlock('DEFAULT_MICROPYTHON_CONTENT');
|
|
// Either form is fine; we just assert it doesn't fail to import or call
|
|
// an unknown method.
|
|
expect(code).toMatch(/Pin\(25, Pin\.OUT\)/);
|
|
});
|
|
});
|