A handheld game console built on the Saturn (ESP32-S3) board running the Jaculus JavaScript runtime. The deck itself holds no games — it boots, joins WiFi, and streams games on demand over UDP from a small zero-dependency Node.js server. The same server also keeps a shared leaderboard and an image gallery you can push to the deck.
Built for the Robotický tábor 2026 Robodeck platform.
Other versions: This is the networked build. For a fully offline build (no server, no WiFi, 18 games bundled on the device), see robodeck-ai-slop.
┌──────────────────────┐ WiFi / UDP :8788 ┌───────────────────────┐
│ Saturn / Robodeck │ ◄───────────────────────────► │ Node.js server.js │
│ deck-firmware.ts │ games · scores · time · pics │ games/ scores.json │
│ (Jaculus runtime) │ │ gallery · leaderboard│
└──────────────────────┘ └───────────────────────┘
The deck sends small JSON requests (game list, game code in chunks, leaderboard, time sync, score submit); the server answers. Every request carries a shared token that must match on both sides.
| File / folder | What it is |
|---|---|
deck-firmware.ts |
The firmware that runs on the Saturn. Becomes src/index.ts in a Jaculus project. |
server.js |
The Node.js server (game store + leaderboard + gallery + time). Zero dependencies. |
games/ |
The game library. One .js file per game, streamed to the deck. |
game-order.json |
Menu order, display names, tile colors, and disabled games. |
gallery-admin.html |
Web page for uploading images to the gallery (served at /gallery). |
- Node.js 22 LTS or newer (v16+ works for the server, but 22 LTS is recommended for the tools).
- Jaculus CLI tools:
Test it with
npm install -g jaculus-tools@latest
npx jac— it should print the help. - A Chromium-based browser (Chrome, Edge, Vivaldi) or recent Firefox for the web firmware installer (needs WebSerial).
- USB-to-UART driver for the Saturn — usually CP210x or CH340.
- On Linux, if CH340 isn't detected, uninstall the
brlttypackage, and add the udev rules.
- On Linux, if CH340 isn't detected, uninstall the
Don't want to host your own server? You can use my public VPS instead — skip Part 1 entirely and just set the
networkblock in Part 2c to:network: { enabled: true, host: "107.173.182.181", port: 8788, token: "CHANGE-ME-PLEASE2", deviceName: "ROBODECK", // ← pick your own unique name so you show up correctly on the leaderboard },Heads-up: it's a shared public server, so the leaderboard and gallery are common to everyone using it, and it may occasionally be down or reset.
The server has no npm dependencies — it uses only Node's built-in modules.
Keep this layout (the server expects these paths relative to server.js):
server.js
game-order.json
gallery-admin.html
games/
snake.js
tetris.js
...
Everything is set through env vars — there is no config file to edit.
| Variable | Default | Meaning |
|---|---|---|
PORT |
8788 |
Port for both the UDP (deck) and HTTP (web) servers. |
TOKEN |
CHANGE-ME-PLEASE |
Shared secret. Must match the deck's network.token. |
TZ_MIN |
120 |
Timezone offset in minutes for the deck clock (120 = UTC+2). |
GALLERY_BUNDLE_CHARS |
1200 |
UDP chunk size for gallery streaming. Raise (e.g. 6000) on reliable networks. |
⚠️ Always set your ownTOKEN. If left as the default the server prints a warning, and anyone on the network could talk to it.
# Linux / macOS
TOKEN="my-secret-token" PORT=8788 node server.js
# Windows (PowerShell)
$env:TOKEN="my-secret-token"; $env:PORT="8788"; node server.jsOn start you should see:
UDP listening on 8788 (deck protocol)
HTTP leaderboard + gallery on 8788
Games dir: .../games
Game config: .../game-order.json
The deck connects to the server by IP, so you need it for the firmware config.
# Linux / macOS
ip addr # or: hostname -I
# Windows
ipconfigUse the address on the same network the deck will join (e.g. 192.168.1.50).
http://YOUR-SERVER-IP:8788/— live leaderboard (auto-refreshes every 15s).http://YOUR-SERVER-IP:8788/gallery— upload/manage gallery images.http://YOUR-SERVER-IP:8788/health— returnsok(handy for uptime checks).
The server writes scores.json and gallery.json next to server.js — these
persist the leaderboard and gallery between restarts.
Three stages: flash the Jaculus runtime, configure WiFi, then build and upload the firmware.
This installs the JavaScript runtime onto the Saturn. You only need to do this once per board (or when updating Jaculus).
- Connect the Saturn to your computer with a USB-C cable.
- If the board keeps disconnecting/reconnecting, put it into boot mode:
hold the
BOOTbutton, pressEN, then releaseBOOT.
- If the board keeps disconnecting/reconnecting, put it into boot mode:
hold the
- Open the Jaculus web installer in Chrome/Edge/Vivaldi.
- Click Connect to device and pick the Saturn's serial port (typically
COM…,ttyACM…, or "USB JTAG/serial debug unit"). If unsure, unplug and replug — the port that disappears/reappears is the right one. - Click Flash firmware (ESP32-S3) and wait. Don't change any installer settings.
- When it finishes, unplug and replug the USB cable, then press the
ENbutton.
Verify:
npx jac list-ports # find your port
npx jac --port <port> versionWiFi is configured on the device with the jac CLI — not in the firmware
source. The deck's firmware only reads the current IP; you tell it which network
to join using these commands.
In every command below, replace
<port>with your serial port (fromnpx jac list-ports). You can drop--port <port>if only one device is connected.
Connect the deck to your WiFi network (Station mode):
# 1. Add your network's credentials
npx jac --port <port> wifi-add "YOUR_WIFI_SSID"
# 2. Switch the deck into Station mode (connect to that network)
npx jac --port <port> wifi-sta --no-ap-fallback
# 3. Check the result — this shows the current config and, once connected, the IP
npx jac --port <port> wifi-getOnce wifi-get reports an IP address, the deck can reach the server.
All available WiFi commands:
| Command | What it does |
|---|---|
wifi-get |
Display the current WiFi config and status. |
wifi-add |
Add a WiFi network (SSID + password). |
wifi-rm |
Remove a saved WiFi network. |
wifi-sta |
Station mode — connect to a WiFi network. |
wifi-ap |
Access-Point mode — the deck creates a hotspot. |
wifi-disable |
Turn WiFi off. |
If you're unsure of the exact arguments a command expects, run
npx jac wifi-add --help(works for any subcommand) to see its usage.
The server and the deck must be on the same network (or the deck's network
must be able to route UDP to the server on PORT).
The firmware is a Jaculus TypeScript project. deck-firmware.ts is the program's
entry point and goes in the project's src/index.ts.
1. Create a project (this also installs the Saturn libraries the firmware imports —
saturn, colors, button, adc, piezo, mpu6050, i2c, wifi, udp, fs):
npx jac project-create --from-device robodeck
cd robodeckThen copy deck-firmware.ts over src/index.ts.
2. Edit the network config at the top of the firmware (src/index.ts). Find
the CONFIG block and set the network section to match your server:
network: {
enabled: true,
host: "192.168.1.50", // ← your server's IP (from Part 1, step 4)
port: 8788, // ← must match the server's PORT
token: "my-secret-token", // ← must EXACTLY match the server's TOKEN
deviceName: "ROBODECK", // ← shows up on the leaderboard
},The rest of CONFIG maps the hardware (D-pad, joystick, slider, piezo, gyro) to
PMOD pins. Enable/disable and re-pin modules there to match how your Robodeck is
wired — the defaults match the standard build.
3. Build, flash, and watch the output:
npx jac build flash monitorThis compiles the project, uploads it, and opens the serial console so you can
see logs (wifi: connected, IP …, DL: …, etc.). Press Ctrl+C to leave the monitor.
On boot the deck shows a ROBODECK splash, waits for WiFi, fetches the game list, leaderboard, and time from the server, then drops into the menu.
Re-read on every game-list request, so you can edit it while the server runs.
order— the sequence games appear in. Any game files not listed still appear, after the ordered ones.disabled— game IDs (file names without.js) to hide.games— override the displaynameand tilecolor(RGB 0–255) per game.
- Drop a
<name>.jsfile intogames/. - Give it a header comment on the first line:
This sets the default menu label and tile color (overridable in
//! name=MYGAME color=255,120,0game-order.json). - Export a run function the deck will call —
export { fn as default }(or exportrun). It receives thegameApiobject (display,colors,piezo,setPx,drawText,drawRect,joyX,joyY,sliderPos,held,mpu,gameOverScreen, …). Look atgames/snake.jsfor the simplest complete example.
Menu names/labels are limited to the deck's built-in font: A–Z 0–9 and
space _ . : ! ? + / = ( ) - (lowercase is upper-cased automatically).
- Open
http://YOUR-SERVER-IP:8788/gallery. - Enter your server token when prompted, upload images, and save.
- On the deck, open the PHOTO / GALLERY menu item to view them.
Limits: up to 24 images, ~8 MB total raw upload.
Controls depend on your hardware config, but by default:
| Action | Input |
|---|---|
| Move in menu | D-pad up/down, or joystick |
| Select | D-pad right, or joystick click |
| Exit a game | Hold left + right together |
| Force reconnect | In menu, hold all four D-pad directions |
| Sync scores/time | In menu, hold up + down |
| Settings | Open the SETTINGS menu item (theme, WiFi/IP/server info) |
| Screensaver (clock) | Auto after ~5 min idle; any input wakes it |
Scores are submitted to the server automatically on game over (when online). The best score per game is shared across all decks using the same server + token.
| Symptom | Likely cause / fix |
|---|---|
| Deck stuck on WAIT WIFI | WiFi not configured or wrong password — redo 2b (wifi-add, wifi-sta, wifi-get). |
| SERVER RETRY / empty menu | Server not running, wrong host/port, or token mismatch. Confirm the deck's token equals the server's TOKEN. |
| Connects but no games | games/ is empty, or everything is in disabled. Check game-order.json. |
| E META / E CHNK / DL FAIL | UDP packets dropped. Try a smaller GALLERY_BUNDLE_CHARS, move closer to the AP, or use a less congested network. |
| Deck keeps disconnecting while flashing | Put it in boot mode: hold BOOT, press EN, release BOOT. |
| Wrong clock time | Set TZ_MIN on the server to your offset in minutes (e.g. 60 = UTC+1, 120 = UTC+2). |
| Server warns about insecure token | You didn't set TOKEN. Set it, and update the firmware to match. |
jac/driver issues |
See the Jaculus troubleshooting guide. |
- Jaculus runtime & tools: https://jaculus.org
- Robodeck build & lessons: https://2026.robotickytabor.cz
- Firmware install (web): https://installer.jaculus.org/
{ "order": ["snake", "flap", "dino", "..."], // menu order, by file name (no .js) "disabled": ["beat"], // hidden from the menu "games": { // per-game display overrides "snake": { "name": "SNAKE", "color": [0, 255, 0] } } }