TCP protocol¶
The remote control endpoint speaks newline-delimited JSON over a single persistent TCP connection. Every request is a single JSON object terminated by \n; every response is a single JSON object terminated by \n.
For how to start the server, see Overview. For automation patterns, see Examples.
Request format¶
id is optional. If supplied it is echoed back in the response, which lets clients correlate responses when pipelining multiple requests.
Response format¶
Success:
Failure:
Null-valued fields are omitted from the response.
Command Reference¶
server.info¶
Cheap, read-only query returning the server's host app name and its release-stamped app version. Used by the client's --check-server-version preflight, but usable directly. Works in any emulator state.
Request
Response fields
| Field | Type | Description |
|---|---|---|
app |
string | Host app name, e.g. Avalonia or Headless |
appversion |
string | Raw AssemblyInformationalVersion (e.g. 0.42.0-alpha+<hash>); 1.0.0… for an unstamped development build. Omitted if unavailable. The client normalizes and compares it. |
Response example
emu.state¶
Returns the current emulator state and selected system name.
Request
Response fields
| Field | Type | Description |
|---|---|---|
state |
string | Uninitialized, Running, or Paused |
system |
string | Currently selected system, e.g. C64 or Generic |
variant |
string | Currently selected configuration variant, e.g. C64NTSC |
Response example
emu.start¶
Starts the emulator. Equivalent to clicking the Start button in the UI.
When the emulator is Paused, this command resumes it rather than reinitializing — the existing system runner is reused. Use emu.reset if you want a fresh start from the Paused state.
There is no separate
emu.resumecommand;emu.startserves both roles.
emu.stop¶
Stops the emulator and resets it to the Uninitialized state.
emu.pause¶
Pauses emulation without resetting state.
emu.reset¶
Stops and immediately restarts the emulator.
emu.quit¶
Terminates the host application. Headless only — requires --allow-remote-quit. Returns an error in the Avalonia Desktop app.
emu.systems¶
Returns the names of all available systems the emulator supports.
Response fields
| Field | Type | Description |
|---|---|---|
data |
array of string | Available system names |
emu.selectsystem¶
Selects the active system. The emulator must be stopped (Uninitialized state) before calling this command; use emu.stop first if needed.
Parameters
| Parameter | Type | Description |
|---|---|---|
name |
string | System name, e.g. C64 |
After selecting a system, call emu.variants to see which configuration variants are available, then emu.selectvariant to pick one before starting with emu.start.
emu.variants¶
Returns the available configuration variants for the currently selected system (e.g. C64NTSC, C64PAL).
Response fields
| Field | Type | Description |
|---|---|---|
data |
array of string | Configuration variant names |
emu.selectvariant¶
Selects a configuration variant for the current system. The emulator must be stopped (Uninitialized state).
Parameters
| Parameter | Type | Description |
|---|---|---|
name |
string | Variant name, e.g. C64NTSC |
emu.storagepaths¶
Returns the effective storage paths used by the emulator host. This is a read-only diagnostics command; it does not start or stop the emulator.
{
"id": 10,
"ok": true,
"data": {
"hostName": "Headless",
"userContentRoot": "/Users/me/Documents/Highbyte/DotNet6502",
"scriptsDirectory": "/Users/me/Documents/Highbyte/DotNet6502/scripts",
"snapshotsDirectory": "/Users/me/Documents/Highbyte/DotNet6502/snapshots",
"userSettingsFile": "/Users/me/.local/share/Highbyte/DotNet6502/Headless/appsettings.user.json",
"cacheRoot": "/Users/me/.local/share/Highbyte/DotNet6502/cache",
"downloadCacheDirectory": "/Users/me/.local/share/Highbyte/DotNet6502/cache/downloads",
"systems": [
{ "name": "C64", "romDirectory": "/Users/me/Documents/Highbyte/DotNet6502/roms/C64" }
]
}
}
emu.savesnapshot¶
Saves the current full emulator state to a .d6502snap snapshot file. The path is resolved on the machine the emulator runs on (the server), not the client. Absolute paths are used as-is; relative paths are resolved from the server's shared snapshot directory. The selected system must support snapshots.
Parameters
| Parameter | Type | Description |
|---|---|---|
path |
string | Server-side destination path for the snapshot. Relative paths use the server's shared snapshot directory. |
emu.loadsnapshot¶
Restores full emulator state from a .d6502snap snapshot file (read from the server-side path). Absolute paths are used as-is; relative paths are resolved from the server's shared snapshot directory. The snapshot's manifest determines the system, so no emu.selectsystem is needed first. The emulator is left paused after restore — send emu.start to resume.
Parameters
| Parameter | Type | Description |
|---|---|---|
path |
string | Server-side path of the snapshot to load. Relative paths use the server's shared snapshot directory. |
emu.runframes¶
Deterministically advances the emulator by a fixed number of frames and renders the result (so a following screenshot reflects it). Intended for automation: load a snapshot (paused), step a known number of frames, then screenshot. The emulator must be stopped/paused — stepping is rejected while it is Running, because the real-time run loop would make the step non-deterministic.
Parameters
| Parameter | Type | Description |
|---|---|---|
count |
int | Number of frames to step (default 1) |
cpu.get¶
Returns all CPU registers. The emulator must be running or paused.
Response fields
| Field | Type | Description |
|---|---|---|
pc |
string | Program Counter as 4-digit hex |
a |
int | Accumulator |
x |
int | X register |
y |
int | Y register |
sp |
int | Stack Pointer |
flags |
string | 8-char processor status: each position is the flag letter (N, V, U, B, D, I, Z, C) or - when clear |
cpu.set¶
Sets one or more CPU registers. At least one parameter must be supplied. Executed at the next frame boundary, so the emulator must be Running.
Parameters (all optional; omitted registers are left unchanged)
| Parameter | Type | Description |
|---|---|---|
pc |
string | Program Counter as a hex string, e.g. C000 |
a |
int | Accumulator (0–255) |
x |
int | X register (0–255) |
y |
int | Y register (0–255) |
sp |
int | Stack Pointer (0–255) |
flags |
string | Processor status — 8-char string in NVUBDIZC format, same as cpu.get output |
For flags, each character is the flag letter when set or - when clear. Example: "----I---" sets only the InterruptDisable flag. You can copy the flags value directly from a cpu.get response.
Set only the program counter:
mem.read¶
Reads bytes from the system's address space. The emulator must be running or paused.
Parameters
| Parameter | Type | Description |
|---|---|---|
addr |
string | Start address as a hex string (e.g. C000) |
len |
int | Number of bytes to read (1–4096) |
mem.write¶
Writes bytes into the system's address space. Executed at the next frame boundary.
Parameters
| Parameter | Type | Description |
|---|---|---|
addr |
string | Start address as a hex string |
data |
array of int | Byte values to write (0–255 each) |
mem.loadbin¶
Loads raw binary data into the system's address space at a specified address. Unlike mem.write (which takes a JSON integer array), this command accepts a base64-encoded byte string — convenient for larger payloads and binary files. Bytes are written directly with no header interpretation. Executed at the next frame boundary.
For C64 PRG files that include a 2-byte load-address header, use c64.loadprg instead.
Parameters
| Parameter | Type | Description |
|---|---|---|
addr |
string | Start address as a hex string (e.g. 0801) |
data |
string | Base64-encoded raw bytes |
When using dotnet-6502-remote, pass --file <path> and the client reads and encodes the file:
joystick.set¶
Sets joystick direction and fire button state for the next frame. Any combination of directions may be active simultaneously. Executed at the next frame boundary.
Parameters
| Parameter | Type | Description |
|---|---|---|
port |
int | Joystick port: 1 or 2 |
up |
bool | Up direction |
down |
bool | Down direction |
left |
bool | Left direction |
right |
bool | Right direction |
fire |
bool | Fire button |
Only the fields you include are changed; omitted fields are not touched.
When using dotnet-6502-remote, you can explicitly clear an action with --no-up, --no-down, --no-left, --no-right, or --no-fire. The client also accepts --up false style boolean values. This is mainly useful when multiple joystick.set updates are queued before the same frame boundary; across frames, joystick.set is already non-persistent and must be resent every frame to remain active.
joystick.press¶
Presses and holds joystick actions on the selected port. Held joystick actions stay active across frames until explicitly released with joystick.release or joystick.releaseall. Executed at the next frame boundary.
Parameters
| Parameter | Type | Description |
|---|---|---|
port |
int | Joystick port: 1 or 2 |
up |
bool | Hold Up direction when true |
down |
bool | Hold Down direction when true |
left |
bool | Hold Left direction when true |
right |
bool | Hold Right direction when true |
fire |
bool | Hold Fire button when true |
Only fields explicitly set to true are applied; omitted or false fields are ignored.
joystick.release¶
Releases held joystick actions on the selected port. Executed at the next frame boundary.
Parameters
| Parameter | Type | Description |
|---|---|---|
port |
int | Joystick port: 1 or 2 |
up |
bool | Release Up direction when true |
down |
bool | Release Down direction when true |
left |
bool | Release Left direction when true |
right |
bool | Release Right direction when true |
fire |
bool | Release Fire button when true |
Only fields explicitly set to true are applied; omitted or false fields are ignored.
joystick.releaseall¶
Releases all held joystick actions on one port. Executed at the next frame boundary.
Parameters
| Parameter | Type | Description |
|---|---|---|
port |
int | Joystick port: 1 or 2 |
keyboard.press¶
Holds down a named key. The key stays down until keyboard.release or keyboard.releaseall is sent. Executed at the next frame boundary.
Parameters
| Parameter | Type | Description |
|---|---|---|
key |
string | Key name (case-insensitive), e.g. space, return, a |
keyboard.release¶
Releases a previously pressed key. Executed at the next frame boundary.
Parameters
| Parameter | Type | Description |
|---|---|---|
key |
string | Key name |
keyboard.releaseall¶
Releases all injected keys at once. Useful for cleanup after automation. Executed at the next frame boundary.
keyboard.iskeydown¶
Returns whether a key is currently pressed (by the user or by a prior keyboard.press).
Parameters
| Parameter | Type | Description |
|---|---|---|
key |
string | Key name |
Response fields
| Field | Type | Description |
|---|---|---|
isdown |
bool | true if the key is down |
keyboard.getall¶
Returns the list of all valid key names for the currently running system.
Response fields
| Field | Type | Description |
|---|---|---|
data |
array of string | Valid key names for this system |
C64 key name reference¶
Call keyboard.getall at runtime for the authoritative list. Common C64 key names:
| Key name | Physical key |
|---|---|
space |
Space bar |
return |
Return (Enter) |
a–z |
Letter keys A–Z |
0–9 |
Digit keys |
+ - * / : ; = . , @ |
Punctuation keys |
lira |
Pound sign (£) |
leftarrow |
← left-arrow key (top-left of keyboard) |
rightarrow |
↑ up-arrow key |
stop |
RUN/STOP |
cbm |
Commodore key |
ctrl |
CTRL |
lshift / rshift |
Left / Right Shift |
home |
CLR/HOME |
delete |
INST/DEL |
crsrdown |
Cursor Down |
crsrright |
Cursor Right |
f1 f3 f5 f7 |
Function keys F1 / F3 / F5 / F7 |
Cursor Up =
crsrdown+lshiftheld. Cursor Left =crsrright+lshiftheld. F2/F4/F6/F8 = corresponding F-key +lshiftheld.
Oric key name reference¶
Call keyboard.getall at runtime for the authoritative list. Oric Atmos key names are:
| Key name | Atmos key |
|---|---|
a–z, 0–9 |
Letter and digit keys |
space, return, esc, backspace |
Space, RETURN, ESC and DEL |
left, right, up, down |
Cursor keys |
shift, lshift, rshift |
Shift (shift selects left Shift) |
ctrl, lctrl, rctrl |
Control (ctrl selects left Control) |
funct, alt |
FUNCT |
- = [ ] \ ; ' , . / |
Punctuation keys |
The Atmos has no function keys. In particular, f12 is not an Oric key name because F12 remains
the emulator's machine-code monitor shortcut.
screenshot¶
Captures the current display as a Base64-encoded PNG. Works in both Avalonia and headless hosts (composited from the current system's render frame); returns an error if no system is running.
Response fields
| Field | Type | Description |
|---|---|---|
format |
string | Always "png" |
data |
string | Base64-encoded PNG bytes |
ui.message¶
Displays a message in the emulator UI. In the Avalonia Desktop app it appears in the Log tab prefixed with [Remote]. In headless mode it is written to stdout.
Parameters
| Parameter | Type | Description |
|---|---|---|
text |
string | Message text |
level |
string | info (default), warning, or error |
c64.type¶
Pastes a string of text into the C64's keyboard buffer character by character. Characters are converted from ASCII to PETSCII automatically and fed into the buffer across frames — the C64 BASIC interpreter processes them exactly as if the user typed them. Newline (\n) maps to C64 Return.
C64 only. Returns an error on other systems.
Parameters
| Parameter | Type | Description |
|---|---|---|
text |
string | Text to paste, e.g. load"*",8,1\nrun\n |
PETSCII case mapping¶
Warning
Use lowercase letters in the text parameter to produce uppercase letters on the C64 screen.
The C64 boots into upper/graphics character mode. In this mode the PETSCII code range $41–$5A (produced by lowercase a–z input) renders as the uppercase Latin alphabet A–Z, while $C1–$DA (produced by uppercase A–Z input) renders as graphics characters.
| Input character | PETSCII code | Displayed on C64 screen |
|---|---|---|
a–z (lowercase) |
$41–$5A |
A–Z (uppercase letters) |
A–Z (uppercase) |
$C1–$DA |
Graphics / symbols |
0–9, punctuation |
Same as ASCII | Digits and punctuation |
Practical rule: write all BASIC keywords and commands in lowercase in the text value.
# Correct — displays LOAD"*",8,1 on the C64 screen
dotnet-6502-remote c64.type --text 'load"*",8,1'
# Wrong — each letter maps to a graphics character, BASIC gives SYNTAX ERROR
dotnet-6502-remote c64.type --text 'LOAD"*",8,1'
# Correct — displays SYS 49152 and executes the machine code at $C000
dotnet-6502-remote c64.type --text "sys 49152"
Newline (\n) in the text is equivalent to pressing Return on the C64 keyboard.
c64.loadprg¶
Loads a Commodore 64 PRG file into memory. The first two bytes of the data are the little-endian load address; the remaining bytes are written to memory starting at that address. Executed at the next frame boundary, so the emulator must be Running.
C64 only. Returns an error on other systems.
Parameters
| Parameter | Type | Description |
|---|---|---|
data |
string | Base64-encoded PRG file bytes (address + payload) |
When using dotnet-6502-remote, pass --file <path.prg> and the client reads and encodes the file for you:
c64.isbasicstarted¶
Returns whether C64 BASIC has finished initializing. Checks the TXTAB pointer at $002B–$002C; it equals $0801 once BASIC is ready. Use this to poll before sending c64.type commands.
C64 only.
Response fields
| Field | Type | Description |
|---|---|---|
isbasicstarted |
bool | true once BASIC initialization is done |
c64.getbasicsource¶
Returns the current BASIC program in memory as a human-readable string with line numbers. Returns an empty string if BASIC has not initialized yet.
C64 only.
Response fields
| Field | Type | Description |
|---|---|---|
data |
string | BASIC source text with line numbers |
apple2.type¶
Types a string of text into the Apple II by feeding characters through the keyboard latch. The machine has no keyboard buffer — pacing is driven by consumption: the next character is latched only after software has read the previous one and cleared the strobe (at most one character per frame). Letters are typed as uppercase (the II Plus has no lowercase); newline (\n) maps to RETURN ($0D); characters the Apple II keyboard cannot produce are dropped with a log warning.
Apple II only. Returns an error on other systems.
Parameters
| Parameter | Type | Description |
|---|---|---|
text |
string | Text to type, e.g. 10 PRINT "HI"\nRUN\n |
Unlike c64.type, letter case in text does not matter — everything is typed uppercase.
apple2.isbasicstarted¶
Returns whether Applesoft BASIC has finished initializing (the machine has booted to the ] prompt). Use this to poll before sending apple2.type commands.
Apple II only.
Response fields
| Field | Type | Description |
|---|---|---|
isbasicstarted |
bool | true once BASIC initialization is done |
apple2.getbasicsource¶
Returns the current Applesoft BASIC program in memory as a human-readable string with line numbers (keywords detokenized, LIST-style spacing). Returns an empty string if BASIC has not initialized yet.
Apple II only.
Response fields
| Field | Type | Description |
|---|---|---|
data |
string | BASIC source text with line numbers |
apple2.loadbasic¶
Loads a tokenized Applesoft BASIC program into memory at $0801 and initialises the
Applesoft zero-page pointers so RUN and LIST work. The data is bare tokenized bytes —
no length header (the in-memory/SAVEd layout, the same format the sidebar's Load Basic
uses). Executed at the next frame boundary, so the emulator must be Running.
Apple II only. Returns an error on other systems.
Parameters
| Parameter | Type | Description |
|---|---|---|
data |
string | Base64-encoded bare tokenized Applesoft program bytes |
When using dotnet-6502-remote, pass --file <path.bas> and the client reads and encodes
the file for you:
dotnet-6502-remote apple2.loadbasic --file /path/to/program.bas
dotnet-6502-remote apple2.type --text "RUN"
apple2.insertdisk¶
Puts a DOS 3.3 disk image (140 KB, DOS sector order) in the emulated Disk II drive 1 without
disturbing the running machine — a diskette swap, which resident DOS picks up on its next access.
Booting from it is the separate apple2.bootdisk. Executed at the next frame boundary, so the
emulator must be Running.
Apple II only. Returns an error on other systems.
Parameters
| Parameter | Type | Description |
|---|---|---|
data |
string | Base64-encoded 140 KB DOS-ordered disk image |
apple2.bootdisk¶
Boots the machine from the disk in drive 1, the equivalent of typing PR#6. Requires the optional
disk2 boot ROM and a disk in the drive. Applesoft has no disk commands of its own, so this is
also how DOS gets loaded in the first place; see
Apple II Disk II, which also covers why booting takes a while.
apple2.ejectdisk¶
Removes the disk image from drive 1, leaving the machine running.
apple2.diskstatus¶
Reports the drive state, which is useful for telling "still loading" from "stuck": the read counter keeps climbing while software is reading the disk.
Response
| Field | Type | Description |
|---|---|---|
data |
string | inserted, bootRom, motor, spinning, track and reads as text |
{"id": 25, "ok": true, "data": "inserted=True, bootRom=True, motor=False, spinning=False, track=3, reads=81091"}
oric.type¶
Types text through the Atmos ROM keyboard latch. The next character is submitted only after the
ROM consumes the previous one, at most one character per frame. Newline (\n) maps to RETURN.
BASIC keywords should be uppercase, as on a real Atmos.
Oric only. Executed at the next frame boundary.
| Parameter | Type | Description |
|---|---|---|
text |
string | Text to type, for example LIST\n |
oric.isbasicstarted¶
Returns whether the Atmos has reached a READY prompt. Use this after start or reset before
sending BASIC input with oric.type.
oric.getbasicsource¶
Detokenizes the current Extended BASIC program using the program pointers in zero page. This
query does not require READY to remain visible, so it also works after a program changes the
screen.
oric.loadtap¶
Direct-loads the first record in a byte-level Oric .tap image. BASIC pointers are initialized
for a BASIC record; a machine-code record is copied to its header address. The TAP autorun flag is
honored (RUN is queued for BASIC, or the PC is set to a machine-code record's start address).
This fast host convenience bypasses the cassette ROM routines and is therefore best for a
single-file image. Executed at the next frame boundary.
| Parameter | Type | Description |
|---|---|---|
data |
string | Base64-encoded Oric .tap bytes |
With the command-line client:
oric.inserttape¶
Validates, inserts and rewinds a byte-level .tap image without loading it. This is the authentic
ROM path and preserves multi-file tape sequencing. After insertion, type CLOAD"" (or a named
CLOAD) with oric.type; standard Atmos ROM tape routines consume bytes from the virtual tape.
dotnet-6502-remote oric.inserttape --file /path/to/game.tap
dotnet-6502-remote oric.type --text $'CLOAD""\n'
oric.rewindtape¶
Moves the inserted virtual tape to byte position zero without changing the running machine.
oric.ejecttape¶
Ejects the virtual tape and clears its file list and position.
oric.tapestatus¶
Returns virtual tape transport state plus metadata for every parsed record.
{
"id": 33,
"ok": true,
"data": {
"inserted": true,
"position": 0,
"length": 12345,
"atend": false,
"files": [
{"name": "GAME", "type": "machinecode", "autorun": true, "start": "0500", "end": "2FFF"}
]
}
}
Architecture¶
┌─────────────────────────────────────────────────────────┐
│ External process (AI agent, script, dotnet-6502-remote) │
└──────────────────────┬──────────────────────────────────┘
│ TCP (newline-delimited JSON)
▼
┌──────────────────────────────────────────┐
│ Highbyte.DotNet6502.Remoting (library) │
│ ┌─────────────────────────────────────┐ │
│ │ RemoteControlController │ │ ← owns server + session
│ │ RemoteCommandDispatcher │ │ ← routes commands
│ │ IRemoteControlEnvironment │ │ ← platform abstraction
│ │ IRemotableHostApp │ │ ← host app contract
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Tcp/ │ │
│ │ TcpRemoteControlServer │ │ ← listens on configurable bind address
│ │ RemoteControlSession │ │ ← one per client
│ └─────────────────────────────────────┘ │
└───────────┬──────────────────────────────┘
│
┌───────┴────────┐
│ │
▼ ▼
Avalonia Headless
Desktop App
Dispatch paths:
| Command type | Execution thread |
|---|---|
Read-only queries (emu.state, emu.systems, emu.variants, cpu.get, mem.read, screenshot, ui.message, BASIC-source/readiness queries, apple2.diskstatus, oric.tapestatus) |
Session thread (direct) |
emu.start/stop/pause/reset/quit, emu.selectsystem, emu.selectvariant, emu.savesnapshot, emu.loadsnapshot, emu.runframes |
UI thread via dispatcher |
mem.write, mem.loadbin, cpu.set, system load/media/type commands, joystick.set/press/release/releaseall, keyboard.press/release/releaseall |
Frame boundary via action queue |
keyboard.iskeydown, keyboard.getall |
Session thread (direct read) |