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.
110 lines
3.8 KiB
JavaScript
110 lines
3.8 KiB
JavaScript
// OPFS-backed SD card store — the source of truth for /sdcard contents.
|
|
// It persists across page reloads, so "edit files, then reboot to apply" works
|
|
// by reloading the page: edits survive in OPFS and are seeded into the guest's
|
|
// MEMFS /sdcard on the next boot. Guest-side writes remain ephemeral.
|
|
|
|
const SD_DIR = 'sdcard';
|
|
const STATE_DIR = 'emulator';
|
|
|
|
async function sdRoot() {
|
|
const root = await navigator.storage.getDirectory();
|
|
return root.getDirectoryHandle(SD_DIR, { create: true });
|
|
}
|
|
|
|
async function resolveParent(path, create) {
|
|
const parts = path.split('/').filter(Boolean);
|
|
const filename = parts.pop();
|
|
let dir = await sdRoot();
|
|
for (const part of parts) {
|
|
dir = await dir.getDirectoryHandle(part, { create });
|
|
}
|
|
return [dir, filename];
|
|
}
|
|
|
|
export async function listSdFiles() {
|
|
const entries = [];
|
|
const walk = async (dir, prefix) => {
|
|
for await (const [name, handle] of dir.entries()) {
|
|
const path = prefix ? `${prefix}/${name}` : name;
|
|
if (handle.kind === 'directory') {
|
|
entries.push({ path: `${path}/`, directory: true });
|
|
await walk(handle, path);
|
|
} else {
|
|
entries.push({ path, size: (await handle.getFile()).size });
|
|
}
|
|
}
|
|
};
|
|
await walk(await sdRoot(), '');
|
|
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
return entries;
|
|
}
|
|
|
|
export async function readSdFile(path) {
|
|
const [dir, name] = await resolveParent(path, false);
|
|
const handle = await dir.getFileHandle(name);
|
|
return new Uint8Array(await (await handle.getFile()).arrayBuffer());
|
|
}
|
|
|
|
export async function writeSdFile(path, data) {
|
|
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
const [dir, name] = await resolveParent(path, true);
|
|
const handle = await dir.getFileHandle(name, { create: true });
|
|
const writable = await handle.createWritable();
|
|
await writable.write(bytes);
|
|
await writable.close();
|
|
}
|
|
|
|
export async function deleteSdFile(path) {
|
|
const [dir, name] = await resolveParent(path, false);
|
|
await dir.removeEntry(name, { recursive: true });
|
|
}
|
|
|
|
// Flatten to [{ path, bytes }] for seeding the guest's MEMFS /sdcard at boot.
|
|
export async function readAllSdFiles() {
|
|
const out = [];
|
|
for (const entry of await listSdFiles()) {
|
|
if (!entry.directory) {
|
|
out.push({ path: entry.path, bytes: await readSdFile(entry.path) });
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export async function seedDefaultsIfEmpty(defaults) {
|
|
const entries = await listSdFiles();
|
|
if (entries.some(entry => !entry.directory)) {
|
|
return;
|
|
}
|
|
for (const [path, contents] of defaults) {
|
|
await writeSdFile(path, contents);
|
|
}
|
|
}
|
|
|
|
export async function saveBootState(firmware, variant) {
|
|
const root = await navigator.storage.getDirectory();
|
|
const state = await root.getDirectoryHandle(STATE_DIR, { create: true });
|
|
const firmwareHandle = await state.getFileHandle('firmware.bin', { create: true });
|
|
const firmwareWriter = await firmwareHandle.createWritable();
|
|
await firmwareWriter.write(firmware);
|
|
await firmwareWriter.close();
|
|
const settingsHandle = await state.getFileHandle('settings.json', { create: true });
|
|
const settingsWriter = await settingsHandle.createWritable();
|
|
await settingsWriter.write(JSON.stringify({ variant, name: firmware.name || 'firmware.bin' }));
|
|
await settingsWriter.close();
|
|
}
|
|
|
|
export async function loadBootState() {
|
|
try {
|
|
const root = await navigator.storage.getDirectory();
|
|
const state = await root.getDirectoryHandle(STATE_DIR);
|
|
const settings = JSON.parse(await (await state.getFileHandle('settings.json')).getFile().then(file => file.text()));
|
|
const firmware = await (await state.getFileHandle('firmware.bin')).getFile();
|
|
return { firmware: new File([firmware], settings.name || 'firmware.bin'), variant: settings.variant || 'x3' };
|
|
} catch (error) {
|
|
if (error.name === 'NotFoundError') {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|