Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenSpand OS

An interactive program launcher for the Sinclair ZX81 fitted with an OpenSpand expansion.

Browse the SD card, walk into subfolders, and launch .p programs with a joystick or the keyboard — with a live date/time from the OpenSpand real‑time clock and a per‑game joystick configuration that's applied automatically when you launch.

A companion osos‑esp32 ESP32‑S2 bridge sits on the OpenSpand serial header and gives the launcher WiFi: upload programs from a browser, browse the web (B), download games from an online library (L), and auto‑update OSOS itself from this repo's GitHub releases (U). A built‑in file manager (F) moves, renames, and creates folders on the SD card.

OPENSPAND OS  2026-05-30  14:23:55
DIR:/GAMES
--------------------------------
 ..             CONFIG
>CIV.P          C=SET KEYS
 CHESS.P        S=SERIAL IN
 INVADERS.P     U=UPDATE OS
--------------------------------
7UP 6DN 5,8PG 0RUN C=KEY Q=X

Features

  • Scrollable list of files and folders — more than one screen, up to 250 entries per directory.
  • Page up / down for fast travel through long directories.
  • Subfolder navigation: enter a folder, select the .. entry to go up.
  • Live date & time from the OpenSpand RTC.
  • Atari‑style joystick (game port) and keyboard control, polled together.
  • Per‑game joystick key mapping, saved on the SD card and applied (via the OpenSpand CONFIG command) right before the game loads.
  • Serial program transfer — send a .p straight from a PC (or the osos‑esp32 WiFi bridge) over the serial port; each block is streamed straight to the SD card as it arrives, so any size up to the full ~16 KB transfers. Saved under its real filename (the bridge sends it; a bare zxsvr server falls back to INBOX.P), then launched like any other file.
  • Only .p files launch — selecting a non‑.p (a MIDI file, a text file…) shows NOT A .P FILE instead of loading garbage and resetting the machine.
  • Auto‑update over WiFi — with the osos‑esp32 bridge, press U to pull the latest menu.p from this repo's GitHub releases and reboot into it.
  • Catalog reading, list rendering, and the entire navigation loop run in Z80 machine code — browsing is near‑instant despite BASIC being far too slow on a 3.25 MHz ZX81.

Controls

Action Key / Joystick
Move up / down 7 / 6 (or stick up / down)
Page up / down 5 / 8 (or stick left / right)
Run program / enter folder 0 (or fire)
Up one level select the .. entry
Configure the selected game's joystick keys J
Receive a .p over serial S
Update OSOS over WiFi (osos‑esp32 bridge) U
Browse the web (osos‑esp32 bridge) B
Search + download games from the online library (osos‑esp32 bridge) L
File manager — move / rename / new folder F
Quit launcher Q

Works with an Atari‑compatible joystick on the OpenSpand game port, or with the keyboard. (When the joystick is in CONFIG‑J keyboard‑injection mode it can interfere with real keyboard reads; the launcher reads the joystick raw, so the controller works regardless.)

Per‑game joystick configuration

Different games expect different keys. Highlight a .p game and press J — the right panel prompts you to press a key for each direction in turn (Up, Down, Left, Right, Fire). Whatever key you press is recorded for that direction, and when all five are set the mapping is saved as a hidden GAME.C file next to the game.

When you later launch that game, the launcher issues CONFIG "J=…" so the OpenSpand injects your chosen keys for the joystick — no need to reconfigure the interface by hand. Games without a .c file launch with whatever joystick config is already set. The .c files are hidden from the listing.

Serial program transfer (dev workflow)

Pull a .p straight from your PC over the OpenSpand's serial port — handy for testing a program you just built without shuffling the SD card. OSOS is a zxsvr client (ZXpand‑Vitamins/serial‑server), so the PC server just waits and the ZX81 starts the transfer.

  1. On the PC, start the server pointed at your file (leave it running):
    • macOS/Linux: ./zxserver.sh yourprog.p (a tiny Python re‑implementation of the protocol; edit the device path at the top — ls /dev/cu.* on macOS)
    • Windows: a stock zxsvr.exe no longer works as‑is (see Length de‑glitch below) — it would need a one‑line patch to reply with the 3×‑redundant length. zxserver.sh already does.
  2. On the ZX81, press S. The screen blanks for ~2 s while it pulls the file in FAST mode.
  3. Each block is streamed straight to the SD card as it arrives (saved as INBOX.P, or the real filename if the WiFi bridge supplies one), then the launcher re‑lists the folder. Select the file and launch it like any other program. Press S again any time — the server keeps running for the next transfer.

Because blocks are written to SD as they come in (only a 256‑byte scratch in RAM, never the whole file), any size .p transfers — up to the full ~16 KB a stock ZX81 can run.

Protocol (zxsvr, ZX81‑initiated pull, 38400 8N1): the ZX81 sends 'I' and the server replies with the file length; then for each 256‑byte block the ZX81 sends 'T', blockNum, blockLen (blockLen 0 = 256) and the server returns that many data bytes plus a 2‑byte checksum; finally the ZX81 sends 'X'. Because the data arrives back‑to‑back with no per‑byte handshake, the receive runs in FAST mode so the full‑speed CPU keeps the OpenSpand's 32‑byte serial FIFO drained.

Length de‑glitch (V2012): the OpenSpand serial receive side corrupts exactly one byte of a verb's reply (a hardware/timing quirk — always a single byte, never the last; the back‑to‑back 'T' data stream is unaffected). A 2‑byte length can't survive that — 16250 (0x3F7A) comes back as 16374 (0x3FF6) and overruns RAMTOP. So the 'I' reply is sent 3× redundantlo,lo,lo,hi,hi,hi — and the ZX81 takes the majority of each triple; one glitched copy can never out‑vote the two good ones, wherever it lands.

Because the glitch is on the ZX81 side, it hits any sender equally, so a stock 2‑byte zxsvr.exe reply is unreliable here and OSOS no longer accepts it (it waits for 6 length bytes). The bundled zxserver.sh sends the 6; a real zxsvr.exe just needs its 'I' reply changed to lo,lo,lo,hi,hi,hi.

WiFi bridge & auto‑update (osos‑esp32)

For day‑to‑day use, the osos‑esp32 ESP32‑S2 bridge stays permanently on the OpenSpand serial header and runs the zxsvr server itself, so no PC script is needed:

  • Send a program — upload a .p in the bridge's web UI, then press S on the ZX81; it pulls it over WiFi and saves it under its original filename (e.g. CHESS.P).
  • Auto‑update OSOS — press U. The ZX81 asks the bridge (zxsvr 'U' verb, carrying its own version) whether a newer menu.p is published here on GitHub; if so it pulls it, saves it as MENU.P, and LOADs it — booting the new version. The bridge mirrors the latest release automatically.

The bench zxserver.sh / zxsvr.exe path above still works for a direct PC‑to‑ZX81 link without the bridge. See the osos‑esp32 repo for firmware, wiring, and the full serial‑protocol spec.

Any serial operation (S/U/B/L) that gets no reply — e.g. the bridge isn't connected — aborts instead of hanging: press SPACE to bail immediately, or it times out on its own and shows NO BRIDGE, returning to the menu.

Releasing a new version

Bump VER="V…" in build_menu.py, rebuild, commit the new menu.p, then push a matching tag (git tag -a v1991 && git push origin v1991). The release workflow attaches menu.p + a generated version.json to the GitHub Release; the bridge mirrors it and the U key installs it.

Web browser (osos‑esp32 bridge)

A Lynx‑style text web browser: the osos‑esp32 bridge fetches a URL, renders the HTML to ZX81 text (uppercase, 32 columns, numbered (N) link markers), and the ZX81 displays it. Press B in the launcher to enter it.

In the browser Key / Joystick
Scroll a page stick up / down, or SPACE (down)
Follow a link type its number, then ENTER
Back (previous page) stick left
Clear the typed number fire
Type a URL U, then type it and ENTER
Exit to the launcher Q

The URL editor reads the keyboard matrix (not INKEY$, which the joystick‑injection mode blocks). Most characters are direct; the URL symbols are shifted: / = SHIFT+V, : = SHIFT+Z, - = SHIFT+J, ? = SHIFT+C, = = SHIFT+L, and DELETE = SHIFT+0. You can also set the entry URL from the bridge's web UI. JavaScript‑rendered content can't be shown (only what's in the served HTML), and non‑ASCII letters (æ ø å …) render as spaces.

Online program library (osos‑esp32 bridge)

Search the ZX81 TOSEC archive and download games straight to the SD card — entirely from the ZX81, no PC. Press L in the launcher, type part of a title, and ENTER. The osos‑esp32 bridge searches its catalog and returns a numbered list; type a number and ENTER and the bridge fetches that game's zip from archive.org, inflates the .p, and the ZX81 saves it under an 8‑char name and re‑lists the directory so it's ready to launch.

In the library Key / Joystick
Type a search title keyboard (matrix), then ENTER
Scroll the results stick up / down
Download a result type its number, then ENTER
New search U
Exit to the launcher Q

It reuses the web browser's on‑screen editor and list display, so the same keyboard rules apply (see above). The catalog downloads to the bridge once on first use.

File manager

Press F on a selected entry to open the file menu — move, rename, or make a folder, all on the SD card from the ZX81. A program's hidden per‑game joystick config (.c) is carried along automatically.

In the file menu Key
Move the file M
Rename the file R
New folder (in the current directory) N
Back to the launcher Q
  • Movepick up / drop: press M, then navigate into the target folder as usual (0 enters folders, .. goes up) — the panel shows what you're carrying — and press M again to drop it there. Q cancels.
  • Rename — type a new base name and ENTER; the original extension is kept (e.g. CHESS.PCHESS2.P).
  • New folder — type a name and ENTER.

These use the OpenSpand firmware's own file operations (f_rename / f_mkdir), so a move is a single atomic rename — no copy, any file size. Names are letters and digits.

Install

Copy menu.p to the root of your SD card. Rename it to MENU.P to have OpenSpand auto‑boot it on power‑up.

By default OpenSpand still requires SHIFT to be held down at power‑up for the auto‑boot to trigger (config byte bit 7). To make it boot straight into OSOS with no key held down, enter CONFIG "C=7F" at the BASIC prompt — SHIFT+G types the CONFIG keyword (OpenSpand's overlay ROM replaces the LLIST token with it). This only applies live; to persist it across power cycles, open OpenSpand's physical config menu (the config button on the board) and press S to save.

Build

menu.p is generated by build_menu.py, which hand‑assembles the embedded Z80 machine code, resolves all addresses, and emits the complete ZX81 BASIC:

python3 build_menu.py          # writes menu.bas
python3 build_menu.py --build  # also converts menu.bas -> menu.p
                               # (macOS, via the VSCode zx81-bastop extension)

Or convert menu.basmenu.p yourself with the maziac.zx81-bastop VSCode extension (right‑click the file, or run the build task in .vscode/tasks.json).

The BASIC is written with labels rather than hard line numbers — build_menu.py runs a small two‑pass pass that assigns line numbers and resolves GOTO/GOSUB targets (the same trick the Z80 assembler in the file uses for jumps), so code can be reordered freely.

Tunables live at the top of build_menu.py: SW (max name length / list width), MAXE (max entries per directory), V (visible rows), RPTN (hold‑to‑scroll repeat speed), HKDIV (idle clock/refresh cadence), PCOL (config‑panel column).

How it works (notes for the curious)

The interesting parts were all dictated by ZX81/OpenSpand quirks:

  • Catalog read via the low‑level PRT3 ports. The high‑level ZXPAND "OPE CAT" recurses forever (directory_stat) and hard‑resets the machine, so the launcher drives the directory ports directly from a small Z80 routine in line 1's REM, one entry at a time.
  • List drawn straight to the display file (DFILE) in machine code. PRINT AT costs ~60 ms per row (≈1 s for a full redraw); writing characters directly to screen memory is near‑instant.
  • The navigation loop lives in machine code. A NAV routine polls the raw joystick and the keyboard matrix, moves the selection, scrolls, redraws, and handles auto‑repeat — all in Z80. BASIC's loop is a thin shell that only reacts to actions (launch / configure / quit), so interpreted‑BASIC overhead is kept off the hot path entirely.
  • Per‑game config uses plain SAVE/LOAD + CONFIG. Config files are written/read as raw memory blocks (SAVE ">NAME.C;addr,len" / LOAD "NAME.C;addr"), and applied with the CONFIG command — which on OpenSpand is the LLIST token, not a ZXPAND/LPRINT verb. No machine code needed for any of it.
  • Everything — Z80 and BASIC — is produced by build_menu.py.

Vibe‑coded

This project was vibe‑coded: built end‑to‑end through an iterative, conversational collaboration with an AI assistant (Anthropic's Claude), driven by real hardware testing and a lot of back‑and‑forth debugging. The OpenSpand command interface and directory format were reverse‑engineered from the firmware sources and confirmed with on‑hardware probes, and the ZX81 gotchas (the PRINT AT row limit, the directory_stat crash, Z80 register‑opcode traps, single‑letter string‑variable names, the floating‑point cost) were each discovered and worked around along the way.

Credits & thanks

Huge thanks to Adam Klotblixt (NollKollTroll), creator of OpenSpand — the open‑source, all‑in‑one ZX80/ZX81 expansion this launcher is built for and could not exist without. Project home: https://codeberg.org/NollKollTroll/OpenSpand

The CONFIG/ZXPAND command conventions follow ZXpand (Charlie Robson); the snappy machine‑code navigation took inspiration from zxpand‑commander.

BASIC tokenizing courtesy of the ZX81 BASIC to P‑File Converter (maziac.zx81-bastop).

Files

File Purpose
build_menu.py Generator — source of truth (Z80 assembler + BASIC emitter)
menu.bas Generated ZX81 BASIC (zxtext2p text format)
menu.p The runnable ZX81 program — copy this to the SD card
zxserver.sh macOS/Linux zxsvr‑protocol server for serial transfer (./zxserver.sh file.p)
.vscode/tasks.json VSCode build task (Build menu.p)
LICENSE GNU GPL v3.0

License

Released under the GNU General Public License v3.0 — the same license as the OpenSpand firmware — see LICENSE.

Copyright (C) 2026  Henrik Jensen

This program is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.  It is
distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; see the
GNU General Public License for more details.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages