The client didn't change. It still just connects and draws. First it was DOOM. Then it was a menu of 20-odd DOS titles, and picking one spins up a real DOSBox on the server, scaled down and streamed over, with input going back the other way. DOSBox itself could never fit on an ESP32-S3. A 6DOF flight sim and a DOS platformer are the same amount of "running on the ESP32," which is zero.
The menu isn't a firmware feature either. It's just another frame source that draws a list at 128x128. When you pick a game the source swaps underneath, the picture changes shape from 128x128 to 320x200, and the server rebuilds its scaler and swaps in that game's control profile. The device gets rectangles the whole time.
The DOS side is js-dos in Node, no browser. Each game is a separate Node process that grabs frames from the emulator and pushes them to the Python service. If the socket is backed up it drops the frame, because only the newest one matters:
ci.events().onFrame((rgb, rgba) => { sendFrame(rgb || rgba, width, height, rgb ? 3 : 4);
});
function sendFrame(buf, width, height, channels) { if (!socketWritable) { framesDropped++; return; } // 16-byte header, then the raw RGB socketWritable = sock.write(header) && sock.write(Buffer.from(buf.buffer, buf.byteOffset, buf.length));
}
One trap there: ci.sendKeyEvent() wants GLFW key codes, not DOSBox's own key numbering. Left arrow is 263, Enter 257, Esc 256, left Ctrl 341. My first table used DOSBox's ordinals and nothing responded.
Bundles get built straight from eXoDOS. from-exodos.py opens the game's zip, reads that game's own dosbox.conf, pulls the launch command and the cd into a subfolder out of [autoexec], and writes a .jsdos bundle with cycles=max. The zip writer is hand-rolled in under 90 lines of Node so the project doesn't need a zip dependency. That turned into a bug: I didn't write directory entries, and the emulator creates a file's parent folder but not its grandparent. So Wolfenstein 3D, which eXoDOS keeps two levels down in WOLF3D/WOLF3D/, died on load with WOLF3D/HELP: No such file or directory. The fix is to emit every parent directory before the first file:
for (const f of files) { const parts = f.rel.split("/"); for (let i = 1; i < parts.length; i++) { const dir = parts.slice(0, i).join("/") + "/"; if (!seen.has(dir)) { seen.add(dir); dirs.push({ rel: dir, data: Buffer.alloc(0), dir: true }); } }
}
Juha Lilja
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.