b22934fe0d
Browser SD files are persisted in OPFS (web/sdstore.js) and edited in the Files tab. At boot the browser builds a deterministic 64 MiB FAT32 image (web/fat32.js) with nested directories and VFAT long names, then mounts it as a raw block device, bypassing QEMU's broken populated-vvfat WASM coroutine path. Guest writes mutate only the ephemeral image. Exposes a hidden #frame-generation indicator that increments on every e-ink flush, so automation can wait on real display updates instead of fixed sleeps. Adds tests/ with a Makefile runner: 'make test' for JS + FAT32/mtools checks, 'make browser-test' for a headless-Firefox boot of the official CrossPoint 1.4.1 firmware that navigates to Test/example.txt. Bumps third_party/qemu to the SPI-SD/CMD13 fixes and updates README/ROADMAP/ISSUES for the new architecture.
68 lines
3.4 KiB
JavaScript
68 lines
3.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { BUTTONS, DEFAULT_SD_FILES, EXAMPLE_SD_TEXT, formatBytes, setButton, mergeFirmware } from '../web/app.js';
|
|
import { buildFat32Image, FAT32_IMAGE_SIZE } from '../web/fat32.js';
|
|
|
|
// Merge - app image is placed at 0x10000 in a 0xFF-erased 16 MB flash with bootloader and partitions.
|
|
const bootloader = new Uint8Array([1, 2, 3]);
|
|
const partitions = new Uint8Array([4, 5, 6]);
|
|
const app = new Uint8Array(1024);
|
|
app[0] = 0xe9;
|
|
const flash = mergeFirmware(app, bootloader, partitions);
|
|
assert.equal(flash.length, 16 * 1024 * 1024);
|
|
assert.deepEqual(flash.slice(0, 3), bootloader);
|
|
assert.deepEqual(flash.slice(0x8000, 0x8003), partitions);
|
|
assert.equal(flash[0x10000], 0xe9);
|
|
assert.equal(flash[0x20000], 0xff);
|
|
assert.throws(() => mergeFirmware(new Uint8Array([0x00]), bootloader, partitions), /0xE9/);
|
|
|
|
assert.equal(DEFAULT_SD_FILES.get('Test/example.txt'), EXAMPLE_SD_TEXT);
|
|
assert.equal(EXAMPLE_SD_TEXT.split('Lorem ipsum').length - 1, 10);
|
|
assert.equal(formatBytes(512), '512 B');
|
|
assert.equal(formatBytes(64 * 1024 * 1024), '64.0 MiB');
|
|
|
|
const fatPayload = new TextEncoder().encode('A'.repeat(700));
|
|
const sdImage = buildFat32Image([
|
|
{ path: 'Test/example.txt', bytes: fatPayload },
|
|
{ path: 'Books/A long filename.txt', bytes: new TextEncoder().encode('book') },
|
|
]);
|
|
const sdView = new DataView(sdImage.buffer);
|
|
assert.equal(sdImage.length, FAT32_IMAGE_SIZE);
|
|
assert.equal(sdView.getUint16(11, true), 512);
|
|
assert.equal(sdImage[13], 1);
|
|
assert.equal(sdView.getUint32(32, true), FAT32_IMAGE_SIZE / 512);
|
|
assert.equal(sdView.getUint32(44, true), 2);
|
|
assert.deepEqual([...sdImage.slice(510, 512)], [0x55, 0xaa]);
|
|
const fatSectors = sdView.getUint32(36, true);
|
|
const fatOffset = 32 * 512;
|
|
const dataOffset = (32 + 2 * fatSectors) * 512;
|
|
const root = sdImage.subarray(dataOffset, dataOffset + 512);
|
|
const testEntryOffset = Array.from({ length: 16 }, (_, i) => i * 32)
|
|
.find(offset => root[offset + 11] !== 0x0f && new TextDecoder().decode(root.subarray(offset, offset + 8)).trim() === 'TEST');
|
|
assert.notEqual(testEntryOffset, undefined);
|
|
const testCluster = new DataView(root.buffer, root.byteOffset + testEntryOffset, 32).getUint16(26, true);
|
|
const testDirectory = sdImage.subarray(dataOffset + (testCluster - 2) * 512, dataOffset + (testCluster - 1) * 512);
|
|
const fileOffset = Array.from({ length: 16 }, (_, i) => i * 32)
|
|
.find(offset => new TextDecoder().decode(testDirectory.subarray(offset, offset + 8)).trim() === 'EXAMPLE');
|
|
const fileEntry = new DataView(testDirectory.buffer, testDirectory.byteOffset + fileOffset, 32);
|
|
const fileCluster = fileEntry.getUint16(26, true);
|
|
assert.equal(fileEntry.getUint32(28, true), fatPayload.length);
|
|
assert.equal(sdView.getUint32(fatOffset + fileCluster * 4, true), fileCluster + 1);
|
|
assert.equal(sdView.getUint32(fatOffset + (fileCluster + 1) * 4, true), 0x0fffffff);
|
|
assert.deepEqual(sdImage.slice(dataOffset + (fileCluster - 2) * 512, dataOffset + (fileCluster - 2) * 512 + fatPayload.length), fatPayload);
|
|
|
|
const calls = [];
|
|
const module = {
|
|
_xteink_wasm_set_adc: (...args) => calls.push(['adc', ...args]),
|
|
_xteink_wasm_set_gpio: (...args) => calls.push(['gpio', ...args]),
|
|
};
|
|
setButton(module, 'confirm', true);
|
|
setButton(module, 'confirm', false);
|
|
setButton(module, 'power', true);
|
|
setButton(module, 'power', false);
|
|
assert.deepEqual(calls, [
|
|
['adc', BUTTONS.confirm.channel, BUTTONS.confirm.value],
|
|
['adc', BUTTONS.confirm.channel, 4095],
|
|
['gpio', 3, 0],
|
|
['gpio', 3, 1],
|
|
]);
|