De Arcadekast die Niets Draait
Auteur: Juha Lilja
Beschrijving
Dit is een arcadekast van 1 inch breed die tientallen spellen draait. In werkelijkheid is het een M5Stack Atom S3R die fungeert als terminal. De spellen draaien op een eigen pc; alleen het scherm en de invoer worden overgedragen. Dit werkt vergelijkbaar met VNC of TeamViewer. Het scherm heeft een resolutie van 128x128, de invoer bestaat uit een tilt-sensor en een knop (het scherm zelf), en de communicatie verloopt via wifi. Hiermee worden alle vereisten van een terminal vervuld (de auteur heeft in het verleden met VT220's gewerkt).
Om de terminal te demonstreren was een overkoepelend thema nodig, wat leidde tot het idee van een arcadekast die oude DOS-games draait. Hiervoor is dosbox aan de serverzijde ingezet, waarbij games uit de ExoDos-bibliotheek zijn gekoppeld. Het apparaat detecteert automatisch de optimale manier om het beeld te streamen. De invoer wordt 30 keer per seconde teruggestuurd naar de server.
Hoewel het technisch gezien slechts een terminal is, fungeert het in dit geval als het hart van een volledig werkende poppenhuis-arcadekast voor DOS-games. Het zou theoretisch ook een C64-emulator kunnen aansturen.
Technische Details
Ontstaan
Het project begon met een ongebruikte devboard op het bureau. Eerdere tests liepen snel tegen de limieten van het bord aan, wat leidde tot de vraag of DOOM erop zou kunnen draaien. Omdat veel mensen dit al gedaan hadden, verschoof de focus naar de eigenschappen van het bord. Aangezien het bord beschikt over alles wat een terminal nodig heeft, werd besloten DOOM niet op het apparaat zelf, maar op de pc te draaien.
Software en Streaming
Dosbox is aan de serverzijde geïmplementeerd met games uit de eXoDOS-bibliotheek. DOOM draait met een native doomgeneric binary. Ook is videostreaming getest met een clip van Skyrim (geen emulatie, maar een opname via dezelfde pijplijn). Er zijn momenteel 28 titels in het menu en een demoreel van 41.
Het apparaat detecteert automatisch de optimale manier om het beeld te streamen zodat de ESP32 dit zo snel mogelijk kan verwerken (jpeg-decodering, raw blit en deflate worden eerst op het bord gemeten; vervolgens wordt elke rechthoek gewaardeerd in milliseconden). Rechthoeken worden via UDP verzonden. De invoer (ruwe tilt-waarden en knop-bits) wordt 30 keer per seconde naar de server gestuurd.
De Stack
- Device: AtomS3R, UIFlow2 MicroPython v1.27, één
main.py, geen custom firmware build. - Server: Python 3 met numpy en Pillow, plus OpenCV voor de videotitels.
- DOOM:
doomgeneric, C, gecompileerd metzig ccop Windows. - DOS: js-dos emulators 8.4.2, DOSBox gecompileerd naar WebAssembly, draaiend headless in Node.
- Games: eXoDOS, 7.629 zip-archieven op schijf.
De code en uitgebreide technische notities staan op GitHub (apn201/Micro_Arcade). De server draait voor de demo op een pc. Hoewel een Raspberry Pi het plan was, staat de eXoDOS-bibliotheek op de pc en was het configureren van een headless DOSBox op de Pi een onnodige strijd.
Doel van het Project
VNC en thin clients bestaan al lang, maar het verschil nu is de prijs van de ontvangende kant. Het bord kost ongeveer 15 dollar en krijgt via de pijplijn zoveel geheugen en rekenkracht als de machine aan de andere kant heeft.
Een scherm dat zelf niets bezit, is ideaal voor plekken waar een pc of tablet te groot is, of waar apparatuur kapot kan gaan of gestolen kan worden (bijv. galerijen, etalages, informatiepunten). Als het apparaat kapot gaat of wordt gestolen, is het verlies beperkt tot 15 dollar en zijn er geen data verloren, aangezien alles op de server staat.
Dit concept is ook toepasbaar op industriële systemen. Oude besturings-pc's kunnen worden gevirtualiseerd, terwijl een operator via een goedkoop bord van 15 dollar slechts een specifiek deel van het framebuffer over UDP kan inzien, zonder OS, bewegende delen of een groot aanvalsoppervlak.
Componenten
- 1 × M5Stack AtomS3R: ESP32-S3R8, 0.85" 128x128 LCD, BMI270 IMU, WiFi.
- 1 × USB-C kabel: Voor de stroomvoorziening.
- 1 × 3D-geprinte behuizing: Gebaseerd op Retromaker's arcade cabinet van Printables, aangepast voor de Atom. Geprint op een Prusa M4S (houtlook PLA voor de zijkanten, zwart voor de rest).
- 1 × PC of Raspberry Pi: Draait de Python-service en de games.
- 1 × Python 3, numpy, Pillow: De streaming-service (OpenCV optioneel voor video).
Ontwikkelingslogboeken
De behuizing
Om het apparaat eruit te laten zien als een arcadekast is een behuizing geprint die klein genoeg is om tussen twee vingers vast te houden. Omdat het een "domme terminal" is, kan het alles tonen; zo zit ook Skyrim in de demoreel (als video, gecentreerd en gecropt naar het vierkante scherm).
Integratie met eXoDOS
Aanvankelijk werden games gekopieerd uit eXoDOS, maar dit was inefficiënt voor grote CD-games. Nu accepteert js-dos een lijst van zips. Een spel start nu vanuit twee zips: een kleine gegenereerde zip met dosbox.conf en mapstructuren, en de originele eXoDOS zip.
Geheugen is de beperkende factor; een spel neemt ongeveer vijf keer de grootte van de zip in beslag tijdens het draaien. Spellen op 1 GB discs passen daarom niet.
De demoreel
De kast heeft een "attract mode". De demoreel speelt games achter elkaar af met een autopilot (bijv. in Wolfenstein wordt er constant vooruit gelopen en geschoten). Wanneer de gebruiker de kast oppakt, neemt de eigen invoer het over. Na zes seconden inactiviteit keert de autopilot terug. Dubbelklikken springt naar het volgende spel.
De reel bevat ook games die niet speelbaar zijn met de beperkte invoer, zoals full-motion video games (Mad Dog McCree, Crime Patrol) en Dragon's Lair (dat verrassend genoeg wel speelbaar bleek via tilt en tap).
Opstartsequenties (Boot Keys)
Veel DOS-titels openen met geluidskaart-prompts of "press any key"-schermen. Omdat er geen toetsenbord is, krijgt elk spel een gescript opstartproces.
Voorbeeld Indianapolis 500 (verkort):
"boot_keys": [
{ "wait": 3500, "key": "1" },
{ "wait": 3500, "key": "enter" },
{ "wait": 2500, "key": "enter" },
...
{ "wait": 2500, "key": "esc" },
{ "wait": 3000, "key": "enter" }
]
Om dit te automatiseren is audition.py geschreven. Deze tool script de toetsen en analyseert screenshots om te bepalen of het spel reageert op controls (PLAY), een demo afspeelt (DEMO), stilstaat (STATIC) of nog in een tekstprompt zit (TEXT).
Curatie van games
Vanwege de beperkte besturing (tilt-sensor en één knop) zijn vooral race- en vliegspellen geschikt.
| Categorie | Titel | Waarom |
|---|---|---|
| Drive | Stunts (1990) | Polygonaal 3D, sturen door te kantelen |
| Drive | Test Drive III (1990) | Full 3D open roads |
| Drive | Stunt Driver (1990) | Loops, jumps, chase camera |
| Drive | Indianapolis 500 (1989) | Simulatie (voorloper iRacing) |
| Drive | SkyRoads (1993) | Sturen en springen, helder beeld op 128x128 |
| Fly | F-15 Strike Eagle II (1992) | Tilt fungeert als joystick |
| Shoot | Wolfenstein 3D, etc. | Diverse shooters |
Van één spel naar een bibliotheek
De client is simpel: verbinden en tekenen. Het menu is geen firmware-functie, maar een frame-bron die een lijst tekent. Bij het kiezen van een spel wisselt de server de bron, past de schaal aan (bijv. van 128x128 naar 320x200) en laadt het control-profiel.
Aan de DOS-zijde wordt js-dos in Node gebruikt (zonder browser). Elk spel is een apart Node-proces dat frames naar de Python-service pusht.
Besturing: Eén knop en een tilt-sensor
De bewegingssensor fungeert als joystick. De server middelt de waarden (EMA, 0.4) en past een deadzone van 60 mg toe om flikkeringen te voorkomen.
De knop (het hele scherm) ondersteunt drie gebaren:
- Klik: Fire, jump, throttle (hoofdactie).
- Dubbelklik (400 ms): Terug naar het menu.
- Lange druk (900 ms): Specifieke acties.
DOOM op de server
DOOM gebruikt doomgeneric met een platformlaag die frames via loopback TCP naar de Python-service stuurt.
Bij het compileren met zig cc ontstonden problemen door UBSan traps (undefined behaviour bij sprite init) en miscompilaties bij -O1 of hoger. De build-script pinnt daarom de settings:
case "$CC" in
*zig*) OPT="${MD_OPT:--O0}" ;;
*) OPT="${MD_OPT:--O2}" ;;
esac
CFLAGS="$CFLAGS -fno-strict-aliasing"
case "$CC" in
*zig*|*clang*) CFLAGS="$CFLAGS -fno-sanitize=undefined" ;;
esac
Netwerkproblemen en oplossingen
Vier bugs die leken op WiFi-problemen, maar dat niet waren:
- Bevroren pixels: De encoder diffte frames tegen de laatste verzonden frame, niet tegen wat het apparaat daadwerkelijk toonde. Oplossing: De server difft nu tegen de laatste door het apparaat bevestigde
frame_id. - UDP fragmentatie: De AtomS3R UDP-socket houdt slechts één datagram vast. Fragmenten worden nu 3 ms uit elkaar gepaced.
- WiFi modem sleep: De radio ging in slaap tussen DTIM-beacons, wat leidde tot vertragingen van 100-300 ms. Oplossing:
wlan.config(pm=network.WLAN.PM_NONE). - Socket incompatibiliteit: UIFlow2's socket miste
recvinto. De firmware probeert nu verschillende methoden (recvinto,readinto,recv,recvfrom) tot er één werkt.
Het tekenproces is efficiënt: drawJpg gebruikt een memoryview slice, waardoor JPEG-data direct van de receive-buffer naar de decoder gaat zonder kopie.
Het draadprotocol (MDOOM/2)
Het protocol gebruikt UDP op poort 20002, little-endian.
- Common Header:
magic 0x4D ('M') | type u8 | session u16 - HELLO: Versie, caps, breedte, hoogte, token.
- INPUT: Sequence, buttons, ax, ay, az, lastframeid.
- FRAME: Frame_id, fragment-index, fragment-count, flags, length.
Er zijn vier coderingen: SOLID (één kleur), RAW565, DEFLATE565 en JPEG.
Prestaties meten
In plaats van de kleinste payload te kiezen (zoals VNC), kiest de encoder de methode die het apparaat het snelst kan verwerken.
Metingen op de AtomS3R:
drawJpg(128x128): 10.94 ms (0.67 µs/px)drawRawBuf(128x32): 3.68 ms (0.90 µs/px)deflate(1066 → 8192 bytes): 15.25 ms (3.72 µs/px)
JPEG wint bijna altijd. "Dirty rectangles" worden berekend via numpy op 32x32 tegels. Als meer dan 60% van de tegels verandert, wordt het hele frame verzonden.
Installatie-instructies
- Hardware verzamelen: Een M5Stack AtomS3R en een USB-C kabel. Flash de stock UIFlow2 MicroPython v1.27.
- Prestaties meten: Kopieer
client/caps_test.pynaar het apparaat en voer dit uit. De resultaten (JPEG, raw blit, deflate kosten) worden door de server gebruikt voor de codering. - Server installeren: Voer
pip install -r requirements.txtuit op een PC of Pi. Voegpip install opencv-python-headlesstoe voor video-ondersteuning.
Groetjes,