UnoDOS pc64

Remote control & automation (unoautomate)

A debug build of UnoDOS pc64 can open a link to the PC you develop from and stream its logs to you, take commands from you, and exchange messages in either direction - all over the network, with a simple typed command language or a Python API. This is unoautomate: the OS's remote-control and automation channel. It is how you drive, observe, and update a machine that is sitting on a bench across the room.

Armed by a person, LAN onlyThe channel ships in every build, and the gate is privilege, not a compile flag: a production OS boots with it disarmed, and only a user at the console can arm it (below). A debug OS (UNO_DEBUG=1) arms it from DEBUG.CFG with every verb allowed and no code. Either way it is meant for a trusted LAN and speaks in plaintext - the code proves who may drive the box, it does not encrypt the wire - so never expose it to an untrusted network.

Production: arming and the access code

On a production boot nothing listens and nothing dials. To hand your PC the keys, open Control Panel → Remote control… and tick what you are willing to grant - the three ticks map to the three powers every command is checked against:

TickGrants
Watchsee the screen and system state - logs, probe, screenshots
Controlmove the mouse, type, open apps
Full accessdisks, files, run code, restart

Arming mints a 6-digit code, shown on screen and never stored. The first thing a client sends is auth <code>; until then every verb answers err auth-required, and three wrong codes disarm the channel outright - the operator can re-arm at the console, an attacker on the wire cannot, which is what the code's length rests on. A verb outside the granted powers answers err denied naming the missing power. The remote path never prompts (nobody is at the machine to answer), and signing out disarms - a code can never outlive the login that made it. Grant the least you need: a monitoring script wants Watch and nothing else.

Turning it on (debug builds)

pc64's network stack makes outbound connections only, so the device dials your PC rather than the other way round. You tell it where to dial with one line in the stick's DEBUG.CFG (the Developer-options file the flasher writes) - your PC's LAN address and a port:

remote=192.168.2.43:5099

Boot a debug stick with that key and, once networking is up, it connects to the listener you run on your PC (below). If the link drops it reconnects on its own.

You do not edit DEBUG.CFG by hand. Flash a debug stick with Developer options turned on in the UnoDOS flasher (that is what selects the debug OS and writes the file), and set the remote address there. To point an existing debug stick at a different PC, use the flasher's Reconfigure button: it rewrites DEBUG.CFG in place without erasing the disk, so you are not reflashing the whole image just to change an IP.

If you would rather not hardcode an address at all, set discover instead of remote=: the device broadcasts on the LAN and any PC running the listener answers with its address, so the box finds you automatically. And when the machine has no working network - because the NIC is the very thing you are debugging - the link can ride a serial cable instead; see when the only network is the one you are debugging below.

Runs alongside everything elseThe link runs on its own network connection, so the Browser and AI apps keep working while a link is active - they are no longer mutually exclusive.

When the only network is the one you are debugging

The link normally rides TCP over the LAN - but that assumes a working NIC, and the hardest bring-up cases are exactly the ones where the machine's only network card is the one that does not work yet. For that, the same channel runs over a serial cable instead: no network required. Every command works identically; only the wire underneath changes. Arm it in DEBUG.CFG with remote-serial in place of remote=, and point the tool at the serial port:

# on the stick: URC over COM1, or name another port (2f8 = COM2, 3e8 = COM3)
remote-serial
remote-serial=3e8

# on your PC (needs pyserial):
python tools/unoauto_remote.py --serial COM3            # Windows
python tools/unoauto_remote.py --serial /dev/ttyUSB0    # Linux
Pick a non-console serial portDebug builds stay attached to firmware, and a firmware serial console can steal bytes from whichever port URC is using. Put URC on a port the firmware is not consoling - COM3 is the safe default; if unsure, disable serial console redirection in firmware setup. Serial is for interactive control (register debug, driving the desktop); do the multi-megabyte A/B pushes over TCP.

Remote logging

While the link is up, every line the OS logs - boot, network, storage, UI, script output - streams to your PC as it happens. Instead of pulling a stick and reading CRASH\NETLOG.TXT after the fact, you watch the machine think in real time. Nothing in the OS changes to make this happen; the remote channel simply subscribes to the same log stream the on-disk logs use.

The command language

Either end can send the other a command or a free-form message. The commands your PC can run on the device are short, human-typable lines - you can even reach them with nc:

CommandWhat it does
probeA snapshot of what the system is doing: subsystems (heap/net/fs/shell), open windows, loaded apps.
volsList storage volumes: index, kind (RAM / native-FAT / firmware), whether it is writable, and its name.
key / pointerInject a keypress or a pointer event, processed exactly like a human's on the next frame.
screenGrab the desktop as a compressed image - the video feed behind the UnoRemote remote-desktop client.
apps / apps listHow many apps there are, and what they are: one id name row each. The set depends on what is installed, so a script that wants to drive one has to be able to look it up.
launch <id> / rescan / closeOpen an app by its id, pick up apps that have appeared in APPS\ since boot, or close the top window. launch still takes a slot number, but a number is this boot's ordering of whatever happens to be installed - install one app and the same number opens a different one, without failing. Use the id.
py <source>Run a line of Python on the device and get its output back.
test <suite>Run a built-in conformance suite and stream the report.
uptime / poweroff / rebootRead the uptime, or shut down / restart the machine.
put / bootnextWrite a file to a volume and pick the next boot device - the A/B update below.
guard <secs> / pet / safeArm a dead-man's switch before a risky command: if the box stops answering within the timeout, it resets itself and dials back in - the guard below.
devicesList the machine's PCI devices and which driver claimed each one (or UNCLAIMED) - what hardware is here and what has no driver yet.
hwwdt <status|arm|pet|disarm>Read or drive the chipset's PCH TCO hardware watchdog - the guard's last-resort backstop for a wedge that has interrupts disabled. status reports whether a usable one was found; the rest drive it directly.
iwl / ethPoke a live network driver's registers - Intel Wi-Fi / Realtek Ethernet - reading and writing registers or retrying its bring-up, with no rebuild.
disks / arm / prepdiskList raw disks, arm one for a destructive op (it refuses the boot disk and echoes the target's size), and partition + format it - preparing a disk to install onto.
mkdir / installCreate a directory on a volume, or clone the running OS onto a prepared disk in one armed step - the headless install below.

These are the everyday commands; the exhaustive list, with every argument and exact reply, is in pc64/REMOTE.md.

The tool on your PC

One small script, pc64/tools/unoauto_remote.py, is both a listener you talk to interactively and a Python library you script against. Run it, and it prints the device's logs as they arrive and lets you type commands back:

$ python tools/unoauto_remote.py --listen 0.0.0.0:5099
unoauto_remote listening on 0.0.0.0:5099. Set pc64 DEBUG.CFG:
    remote=<this-machine-ip>:5099   (QEMU SLIRP guest: 10.0.2.2:5099)
[SCRIPT ] remote: link up
[NET    ] eth: DHCP lease 192.168.2.157
probe
  2 0 0 4980736 heap
  2 3 1 12 shell
  1 1 1 0 Files
  ok
apps list
  control Control Panel
  files Files
  browser Browser
  ...
  ok
launch browser
  launched
  ok
py print(6*7)
  42
  ok

The same thing as a library - drive the machine, read its state, run code on it, and register handlers so the device can drive your PC in return:

from unoauto_remote import UnoAutoLink

link = UnoAutoLink(port=5099)
link.on_log(lambda chan, text: print(chan, text))   # stream the OS log
link.listen()
link.wait_connected()                                # pc64 dials in

print(link.probe())              # [{'kind':2,'name':'heap',...}, ...]
link.launch("browser")           # open an app by its id, never by number
print(link.eval("print(6*7)"))   # run Python on the device -> ['42']

# commands can go the other way too: the device can drive your PC
link.on_command("save", lambda args: "saved " + args)

On the device side, an automation script written in Python (see Writing apps) can talk back over the link with unoauto.remote_send() and unoauto.remote_recv().

Remote desktop (UnoRemote)

When you want to see and drive the machine rather than type commands at it, UnoRemote is a Windows app that gives you a live remote-desktop view over the same link: the device's screen in a window, your mouse and keyboard forwarded to it, a log pane, a clickable command bar, a raw-command box and a session recorder. It is VNC-style - the desktop streams over the screen command and input goes back over key and pointer - but tuned for UnoDOS's flat desktop: each frame sends only the tiles that changed since the last one, so the view stays responsive even at full resolution.

  1. Build it once with pc64\remote\build-remote.ps1 (it produces UnoRemote.exe), run it, set the port (default 5099) and click Listen.
  2. On the device, either point DEBUG.CFG at your PC with remote=<your-ip>:5099, or just set discover and let the device find your PC on its own (see the tip below); then boot a debug build. The UnoDOS desktop appears in the window.
  3. Click and type on the view to drive the machine. A Scale control (1×–4×) trades resolution for bandwidth on a busy or high-resolution screen.
  4. The command bar under the log runs the everyday remote actions with a click - list volumes and disks, launch or close an app, check uptime, reboot or power off (these ask first) - while the box beside it still takes any raw URC command line.

Recording. Record saves the session to Videos\UnoRemote\ - an MP4 if ffmpeg is on your PATH, otherwise a folder of PNG frames. Tick on device first to record on the machine itself: it captures at a steady frame rate independent of the network and UnoRemote pulls the finished recording when you stop, which is smoother than saving whatever trickled over a slow link.

Zero-config discoveryNo address to type. With discover set in DEBUG.CFG (instead of a remote= address) the device broadcasts on the local network, and UnoRemote - or the unoauto_remote.py tool - answers with its own address, so the device connects with nothing configured. Both ends must be on the same LAN.

Which end dials - and the Scan button. By default the device dials your PC: you click Listen and it connects in. That suits a headless box whose address you do not know - it calls home when its network is up. You can also flip it: put listen (or listen=<port>) in the device's DEBUG.CFG and the box becomes a server that waits to be dialed. Then click Scan… in UnoRemote: it broadcasts on the LAN, lists every box in listen mode by name, and dials the one you pick. That is the "browse the network and connect to a box" model - you choose the machine, with no address typed on either end. (Give a box a friendly name with name=<label> so it is easy to spot in the list.)

Networking is ready at bootThe network comes up at boot now, not only when an app first needs it: every device brings its NIC up early (skipping any that do not get a lease within a few seconds, so a dead or cableless port cannot hang boot), so a box is reachable - and discoverable - as soon as the desktop appears.
Armed channel, LAN onlyUnoRemote rides the same LAN-only URC channel as everything else on this page - it needs the channel armed (the Remote control panel on a production OS, DEBUG.CFG on a debug one) and a trusted network.

Scripting the OS, with permission (unoscript)

The uno module is an app's own sandbox: its window, its canvas, its files. unoscript is the step up from there - a surface for scripting the whole machine. Through import unoscript as u a script can move the pointer and type, read what is on screen, launch and close apps, see what is running, read and write the user's files, and - with permission - reach all the way down to memory, ports and power. It is how you automate the OS, not just write an app inside it. You reach it interactively through the py command above, or from an on-device automation app.

Every action needs a capability, and capabilities are tiered. A script begins with only the ambient tier; anything higher it must ask for with u.request("<cap>"), and the security subsystem decides - drawing the same consent sheet the login screen uses, honouring a role the signed-in user holds, or, on a developer machine, an auto-grant policy. A denied action raises PermissionError; a surface a given build does not carry raises NotImplementedError. This is the same gate the login screen and Accounts manager enforce, so a script can never quietly do more than the person running it is allowed to.

NamespaceTierWhat it scripts
u.uiambientMove / click the pointer, press keys, read the on-screen window text, and the shared clipboard.
u.appambient / userCount, launch and close apps; send an app a message (focus, close, ask its state).
u.fsuser / adminRead and write files. A plain name is the user's own home (USERS/<id>/…); an absolute /volume/path reaches elsewhere and needs the higher tier.
u.procadminList what is running - each open app as a process, with its name and which is focused.
u.mem / u.ioadmin / kernelRead and write raw memory and I/O ports - a kernel-level debugging surface, always audited.
u.sysadminPower: shut down or restart the machine.
u.hookadminWatch internal events (file writes, module loads) stream past - a debug-build observability tap.

A short session over the py command, escalating as it goes:

# ambient - no permission needed: read the screen, drive the pointer
py import unoscript as u; print(u.ui.screen())
py import unoscript as u; u.ui.click(200, 160)

# the user's own files - u.fs.read/write ask for the 'fs.user' capability first
py import unoscript as u; u.request("fs.user"); u.fs.write("notes.txt", b"hello")
py import unoscript as u; u.request("fs.user"); print(u.fs.read("notes.txt"))

# 'what is running' is an admin surface - unescalated it is refused
py import unoscript as u; print(u.proc.list())        # -> PermissionError
py import unoscript as u; u.request("proc.enum"); print(u.proc.list())
Production surface, gated by permissionThe unoscript surface itself is production - a trusted, signed automation app can use it on a normal machine, always under the permission gate. Only u.hook is debug-only (a production tap on hot internal events would cost every machine that ships), and the deep u.mem/u.io surfaces are kernel-tier: strongest escalation, every use audited. The full capability list and tiers are in UNOSCRIPT.md.

Automation apps: caps without prompts (signed manifests)

Asking for a capability with u.request() works for a script you are driving - the system can draw a consent sheet and you click Allow. But an automation app runs with nobody watching, so there is no one to answer the prompt. A trusted one instead ships a signed manifest: a small <app>.MFT file next to its .UNO that declares the caps it needs, signed by a key the machine trusts. At launch UnoDOS verifies the signature and grants exactly those caps for the life of the app - no prompts - and drops them again the moment it closes.

# once: make a signing key and the line that enrolls it on the machine
python tools/uno_manifest.py keygen --key-id acme > acme.line
#   -> prints  "acme <64 hex>"  ; append that line to \TRUST.MFK on the boot disk

# per app: sign a manifest declaring what it needs, beside the .UNO
python tools/uno_manifest.py sign --name mybot --caps proc.enum,fs.sys \
    --key-id acme --secret <64 hex> -o MYBOT.MFT
Trust is the whole pointThe manifest is a convenience for trusted apps, never a way around the gate: an unsigned or untrusted-key manifest grants nothing (the app just runs with ordinary user authority), a kiosk machine refuses all of it, and every grant is audited. Enrolling a key in TRUST.MFK is what says “I trust apps this key signs” - guard it like any signing key.

A/B updates: push a new build over the link

The headline use: iterate on the OS itself without touching a USB stick. Run two sticks - A, the machine you are working on, and B, a spare - and push only a freshly built kernel (EFI\BOOT\BOOTX64.EFI, about 4 MB) to stick B over the wire, then reboot into it. A driver change touches only that one file; everything else on the stick is untouched, and stick A stays as a known-good fallback.

# find which volume is the spare stick B (a writable "kind 2" volume)
python tools/unoauto_remote.py --listen 0.0.0.0:5099      # then type: vols

# push a freshly built kernel to stick B and reboot into it
python tools/unoauto_remote.py \
    --push 2 "EFI\BOOT\BOOTX64.EFI" build/BOOTX64.EFI --reboot

The file is streamed in chunks, staged in the device's memory, and written in one step at the end with a size check - so an interrupted transfer never leaves stick B half-written. Add --bootnext <n> to have the machine boot the other stick automatically on the next restart, without anyone touching the firmware boot menu. From the library the same flow is link.push_file(...) then link.reboot().

What these can doput and reboot are an arbitrary file write and a reset. In a production OS they sit behind the Full access power; grant it only over your trusted LAN. A single push is capped at 8 MB.

Installing UnoDOS onto a disk over the link

The A/B push writes files onto an already-formatted stick. The channel can go further and stand up a bootable disk from scratch - partition a raw disk, format it, and lay the whole OS down on it - so you can move UnoDOS off the USB stick and onto an internal drive without ever touching the machine's keyboard. The one-shot install command does all of it, cloning the running system straight onto the target:

disks                # find the writable, non-boot target (say index 1)
arm 1                # echoes the disk's name and size; refuses the boot disk
install 1            # partition + format, then clone the whole OS onto it
reboot               # writes are flushed to disk first

Because the copy is disk-to-disk on the device, none of the OS actually crosses the network. From the Python library the same thing is link.install(1), or link.install_dir(1, "build/esp") to push a freshly built tree from your PC instead of cloning the running one. The lower-level pieces are there too if you want them - prepdisk to format, mkdir + put to build the tree by hand.

Booting the installed diskA USB stick installed this way boots on its own (the firmware removable-media fallback finds it). An internal disk is made bootable the same way, but writing a permanent firmware boot entry needs the on-device Install app booted to firmware - over the link, an internal disk boots via the firmware fallback or a one-time boot-menu pick. Every destructive step is inert until you arm the specific disk, which auto-disarms after one op and always refuses the disk UnoDOS booted from.

The guard: recover a wedged box automatically

Some commands push the device into code that has never run before - driving a network card's bring-up by hand is the classic case - and when that goes wrong it can wedge the machine: the remote channel stops answering and normally the only way back is to walk over and power-cycle it. The guard turns that into automatic recovery. Arm it right before the risky command, and if the box cannot answer the channel within your timeout, it resets itself and dials back in on its own.

# arm a 15-second dead-man's switch, then run the risky command
guard 15
<the risky command>
# if the box wedges, ~15s later it resets and reconnects by itself;
# if the command returns fine, stand the guard down:
safe

"Answering the channel" means any command getting through - each one you send pushes the deadline out, so an ordinary interactive session keeps the guard happy without thinking about it. For a step that legitimately takes a while, send pet as a keep-alive. From the Python library it is one context manager that arms on the way in and stands down on the way out:

with link.guarded(15):
    link.command("iwl", "rerun")   # a wedge here resets the box, which reconnects
It fires on silence, not just crashesAn armed guard will reset a perfectly healthy machine if it simply stops hearing from you (your PC goes to sleep, the network drops) - that is the whole point, so arm it around a specific risky step and safe it afterwards, or use guarded(...), which does that for you. Debug builds only, like the rest of the channel.

The hardware backstop

The guard normally resets a wedged box from software - a heartbeat in the main loop, a timer interrupt, or the firmware watchdog. All three need the CPU to still be taking interrupts. The one failure they cannot catch is a tight loop that has turned interrupts off (or a true bus hang): nothing runs, so nothing resets the machine. For that last case UnoDOS can enlist separate silicon that does not care what the CPU is doing - the Intel PCH TCO hardware watchdog. When a usable one is present the guard arms it automatically, a little past the software timeout, so it only ever fires on the wedge software cannot reach. You can also drive it directly to check it is there:

hwwdt status      # is a usable TCO present on this chipset? which registers?
hwwdt arm 20      # arm it for ~20s; if nothing pets it, the chipset resets the box
hwwdt disarm
When it is availableThe hardware watchdog is only available where UnoDOS knows how to reach that chipset’s registers and the firmware has not locked the timer. hwwdt status reports honestly: it says present only when it has proven the watchdog can actually fire. Where it cannot (some laptop firmwares lock it), the software guard still covers every wedge except the interrupts-off one. See HWWATCHDOG.md.

Setting up a development environment

Putting the pieces together, a comfortable unoautomate workflow needs a one-time setup and then a tight loop:

  1. Two sticks. Flash two USB sticks with Developer options on: A, a known-good build you keep as a fallback, and B, the spare you push new builds to. Set remote=<your-pc-ip>:5099 on both when you flash them (or add it later with the flasher's Reconfigure button - no reflash).
  2. The bench machine boots stick B and, once its network comes up, dials your PC. The driver box builds attached (-DUNO_NO_DETACH) so its own USB stick shows up as a writable volume you can push to.
  3. Run the listener on your PC: python tools/unoauto_remote.py --listen 0.0.0.0:5099. Its logs start streaming the moment the device connects.
  4. Edit, build, push, reboot. Change a driver, rebuild BOOTX64.EFI, push it to stick B, and reboot into it - seconds, no walking to the bench. Stick A is always there if a build will not boot.

The whole loop scripts cleanly from the Python library, so you can wrap it in one keystroke:

from unoauto_remote import UnoAutoLink

link = UnoAutoLink(port=5099)
link.on_log(lambda chan, text: print(f"[{chan}] {text}"))  # watch the OS think
link.listen(); link.wait_connected()                     # stick B dials in

while True:                       # your edit / build / test loop
    input("built a new BOOTX64.EFI? press enter to push it > ")
    print(link.command("probe"))                   # inspect the live system...
    link.push_file(2, r"EFI\BOOT\BOOTX64.EFI", "build/BOOTX64.EFI")
    link.reboot()                                  # ...then boot the new build
    link.wait_connected()                          # it dials back in

What you can do with it

Driver development

This is the case unoautomate was built for. Bringing up a driver for real hardware - a NIC, a Wi-Fi chip, a storage controller - is a cycle of "change one thing, watch what the hardware does, change it again", and without a channel each turn means pulling a stick, reflashing it, walking it to the bench, booting, and reading a log file after the fact. unoautomate collapses that:

  • Watch the bring-up live. Every line the driver logs - each register write, each firmware handshake, exactly where it stalls - streams to your PC as it happens, instead of being read from CRASH\NETLOG.TXT after a hang.
  • Reflash in seconds, not minutes. The A/B push writes only the ~4 MB kernel to the spare stick and reboots into it; a driver change touches one file and the round trip is a keystroke.
  • Poke the hardware without a rebuild. Run a line of Python on the device with py / link.eval() to read back state between builds, and probe to confirm which subsystems actually came up. A debug build is exactly where you want this reach.
  • Read and write live registers. For a network card, iwl (Intel Wi-Fi) and eth (Realtek Ethernet) reach straight into the running driver - peek and poke registers, retry the bring-up sequence - so you can try a fix in seconds and only rebuild once you know it works. Pair it with a guard so a bad poke that hangs the card resets the box instead of stranding it.
  • See what hardware is actually there. devices dumps the machine's whole PCI tree - every function's location, IDs, class and which driver claimed it - so the first question of any bring-up, "what is on this box and what has no driver yet?", is answered on-device instead of guessed from a spec sheet. The same registry is a Python call away with uno.pci() for a script to filter.

The Intel Wi-Fi bring-up is the working example: the driver is iterated this way, its firmware-load sequence streamed back register by register while builds are pushed to the bench machine over the link.

Automated conformance testing

A debug OS carries built-in conformance suites (storage, system, network, frameworks, apps). Run one from your PC and stream its report, then assert on a live probe() - a real regression test you can put in CI or run after every push:

link.wait_connected()
report = link.test("network")            # run one built-in conformance suite
for line in report:
    print(line)                          # e.g.  S-NET-10 dhcp lease .......... ok
# probe the live system and assert on it
subs = {r["name"]: r for r in link.probe() if r["kind"] == 2}
assert subs["net"]["state"] == 1, "network subsystem did not come up"

Driving the desktop headlessly

Because key and pointer are injected exactly as a human's input on the next frame, you can drive the whole desktop from a script: launch an app, type into it, walk a menu, and check the result - no monitor or keyboard attached. This is how the screenshots in this manual are produced (see Regenerate the manual screenshots): the harness boots the OS and drives it entirely over the channel.

Unattended runs and remote triage

Leave a machine running a soak test on the bench, watch its log stream from your desk, and have it poweroff itself when it finishes or reboot when it wedges. Wrap a run that might hang in a guard and it recovers on its own - no one has to be in the room to power-cycle it. For an intermittent bug you no longer reproduce-then-lose-the-evidence: the log is already on your PC when it happens.

The full wire protocol and every command's exact reply are in pc64/REMOTE.md.