Examples¶
End-to-end automation patterns for typical 6502 system tasks. The first section uses the dotnet-6502-remote CLI; the following sections show equivalent recipes using raw TCP from Bash and PowerShell.
For the protocol and full command reference, see TCP protocol.
Common automation workflows¶
These show end-to-end patterns using the dotnet-6502-remote CLI. The same command sequence translates directly to JSON requests over the raw TCP connection.
Preflight: check the server version¶
As a best practice, start a scripted control session by confirming the client and the emulator it drives are the same release. --check-server-version exits 0 on a match and 3 on a mismatch (printing the update commands to stderr), so a script can bail out early:
# Abort the script if the endpoint version doesn't match this client
dotnet-6502-remote --port 6510 --check-server-version || exit 1
# ... rest of the automation ...
This is a version-mismatch warning only, not full protocol compatibility handling. See Server version preflight.
Typing a BASIC command and running it¶
Poll c64.isbasicstarted before sending text — the C64 BASIC ROM takes several frames to initialize after emu.start.
dotnet-6502-remote emu.start
# Poll until BASIC is ready (repeat until isbasicstarted is true)
dotnet-6502-remote c64.isbasicstarted
# → {"ok":true,"isbasicstarted":true}
# Type the BASIC command in LOWERCASE (lowercase input → uppercase on C64 screen)
dotnet-6502-remote c64.type --text 'load"*",8,1'
dotnet-6502-remote keyboard.press --key return
dotnet-6502-remote keyboard.release --key return
Shell poll loop:
until dotnet-6502-remote c64.isbasicstarted | grep -q '"isbasicstarted":true'; do
sleep 0.5
done
dotnet-6502-remote c64.type --text "list"
dotnet-6502-remote keyboard.press --key return
dotnet-6502-remote keyboard.release --key return
Loading an Oric TAP image¶
For a single-file TAP image, direct loading is the shortest route. It loads the first record at the address in its header and honors its autorun flag:
For multi-file software, use the Atmos ROM loader so later CLOAD operations continue from the
same virtual tape. Poll readiness first because the ROM needs several frames to boot:
until dotnet-6502-remote oric.isbasicstarted | grep -q '"isbasicstarted":true'; do
sleep 0.5
done
dotnet-6502-remote oric.inserttape --file /path/to/game.tap
dotnet-6502-remote oric.type --text $'CLOAD""\n'
dotnet-6502-remote oric.tapestatus
oric.rewindtape returns the image to its first record; oric.ejecttape removes it.
Writing and running machine code¶
There are two ways to jump to machine code. Setting the PC directly is simpler and works on any system.
Method 1: Set PC directly (recommended for agents)¶
cpu.set --pc sets the program counter at the next frame boundary. No BASIC, no keyboard input, no PETSCII — just write code and point the CPU at it.
dotnet-6502-remote emu.start
# Write machine code to $C000 as comma-separated decimal bytes
# Example: LDA #$42 (169,66), STA $D020 (141,32,208), RTS (96)
dotnet-6502-remote mem.write --addr C000 --data 169,66,141,32,208,96
# Verify the bytes were written
dotnet-6502-remote mem.read --addr C000 --len 6
# Jump to the routine — takes effect at next frame boundary
dotnet-6502-remote cpu.set --pc C000
# Confirm the CPU is executing inside the routine
dotnet-6502-remote cpu.get
# Capture the result visually
dotnet-6502-remote screenshot --output /tmp/result.png
Note
cpu.set --pc does not push a return address. If the routine ends with RTS, it will pop whatever is on the stack (potentially crashing). Use this method for programs that loop forever, or end with a JMP or BRK. Use the SYS method below when the routine is designed to return to BASIC via RTS.
Method 2: Via BASIC SYS command (C64 only)¶
This approach uses the C64 BASIC interpreter to call the routine, so BASIC must be running and the routine must end with RTS to return cleanly to BASIC.
dotnet-6502-remote emu.start
dotnet-6502-remote mem.write --addr C000 --data 169,66,141,32,208,96
dotnet-6502-remote mem.read --addr C000 --len 6
# Wait until BASIC is ready before using the keyboard buffer
dotnet-6502-remote c64.isbasicstarted # repeat until "isbasicstarted":true
# Type SYS in LOWERCASE — "sys 49152" displays as SYS 49152 on the C64 screen
dotnet-6502-remote c64.type --text "sys 49152"
dotnet-6502-remote keyboard.press --key return
dotnet-6502-remote keyboard.release --key return
dotnet-6502-remote cpu.get
dotnet-6502-remote screenshot --output /tmp/result.png
Restoring a snapshot, stepping, and screenshotting¶
Deterministically restore a saved state, advance a known number of frames, and capture the result. Pair this with starting the host paused — e.g. --load-snapshot state.d6502snap --remote-port 6510 (no --start), or a fresh --remote-port server you emu.loadsnapshot into. Snapshot paths are resolved on the emulator host; relative paths use the host's shared snapshot directory.
# Restore full machine state (manifest picks the system); leaves the emulator paused
dotnet-6502-remote emu.loadsnapshot --path state.d6502snap
# Advance exactly one frame and render it (rejected if the emulator is Running)
dotnet-6502-remote emu.runframes --count 1
# Capture the rendered frame (works on both Avalonia and headless hosts)
dotnet-6502-remote screenshot --output /tmp/result.png
To save a snapshot of the current state instead:
Discovering valid key names at runtime¶
dotnet-6502-remote keyboard.getall
# → {"ok":true,"data":["space","return","a","b",...,"f7","crsrdown","crsrright","stop",...]}
Use this before calling keyboard.press to confirm the exact spelling of a key for the currently running system. Key names are case-insensitive in the protocol.
Bash / shell script examples¶
These examples use nc (netcat) and standard Unix tools. On macOS, nc is available by default; on Linux, install netcat-openbsd.
Single command¶
On Linux (where -G is not available):
Multi-command session (one connection, multiple requests)¶
(
echo '{"id":1,"cmd":"emu.start"}'
sleep 2
echo '{"id":2,"cmd":"emu.state"}'
echo '{"id":3,"cmd":"cpu.get"}'
echo '{"id":4,"cmd":"mem.read","addr":"0400","len":8}'
sleep 0.1
) | nc localhost 6510
Parse response with jq¶
# Print just the state field
echo '{"id":1,"cmd":"emu.state"}' | nc -G2 localhost 6510 | jq -r '.state'
# Read memory and format as hex
echo '{"id":1,"cmd":"mem.read","addr":"0400","len":40}' \
| nc -G2 localhost 6510 \
| jq '.data | map(. | tostring) | join(",")'
# Save screenshot to PNG
echo '{"id":1,"cmd":"screenshot"}' \
| nc -G2 localhost 6510 \
| jq -r '.data' \
| base64 --decode > /tmp/screen.png
Automation loop: poll state until emulator is running¶
#!/usr/bin/env bash
PORT=6510
HOST=localhost
echo "Starting emulator..."
echo '{"cmd":"emu.start"}' | nc -G2 "$HOST" "$PORT" > /dev/null
for i in $(seq 1 30); do
STATE=$(echo '{"cmd":"emu.state"}' | nc -G2 "$HOST" "$PORT" | jq -r '.state')
echo " state: $STATE"
if [ "$STATE" = "Running" ]; then
echo "Emulator is running."
break
fi
sleep 0.5
done
Write a POKE and verify it¶
# Write 1 to address $D020 (border color)
echo '{"id":1,"cmd":"mem.write","addr":"D020","data":[1]}' \
| nc -G2 localhost 6510
# Read it back
echo '{"id":2,"cmd":"mem.read","addr":"D020","len":1}' \
| nc -G2 localhost 6510 \
| jq '.data[0]'
# → 1
Persistent helper function¶
Add this to your ~/.bashrc or ~/.zshrc:
emu() {
local port="${EMU_PORT:-6510}"
local host="${EMU_HOST:-localhost}"
echo "$1" | nc -G2 "$host" "$port"
}
# Usage:
emu '{"cmd":"emu.state"}'
emu '{"cmd":"mem.read","addr":"C000","len":16}'
PowerShell examples¶
Single command¶
$json = '{"id":1,"cmd":"emu.state"}'
$tcp = [System.Net.Sockets.TcpClient]::new("127.0.0.1", 6510)
$stream = $tcp.GetStream()
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json + "`n")
$stream.Write($bytes, 0, $bytes.Length)
$reader = [System.IO.StreamReader]::new($stream, [System.Text.Encoding]::UTF8)
$response = $reader.ReadLine()
$tcp.Close()
$response | ConvertFrom-Json
Reusable helper function¶
function Invoke-EmuCommand {
param(
[string]$Command,
[string]$Host = "127.0.0.1",
[int]$Port = 6510
)
$tcp = [System.Net.Sockets.TcpClient]::new($Host, $Port)
$stream = $tcp.GetStream()
try {
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Command + "`n")
$stream.Write($bytes, 0, $bytes.Length)
$reader = [System.IO.StreamReader]::new($stream)
return $reader.ReadLine() | ConvertFrom-Json
}
finally {
$tcp.Close()
}
}
# Examples
Invoke-EmuCommand '{"id":1,"cmd":"emu.state"}'
Invoke-EmuCommand '{"id":2,"cmd":"emu.start"}'
Invoke-EmuCommand '{"id":3,"cmd":"cpu.get"}'
Invoke-EmuCommand '{"id":4,"cmd":"mem.read","addr":"C000","len":4}'
Multi-command session (one TCP connection)¶
function Invoke-EmuSession {
param(
[string[]]$Commands,
[string]$Host = "127.0.0.1",
[int]$Port = 6510
)
$tcp = [System.Net.Sockets.TcpClient]::new($Host, $Port)
$stream = $tcp.GetStream()
$reader = [System.IO.StreamReader]::new($stream)
try {
foreach ($cmd in $Commands) {
$bytes = [System.Text.Encoding]::UTF8.GetBytes($cmd + "`n")
$stream.Write($bytes, 0, $bytes.Length)
$response = $reader.ReadLine() | ConvertFrom-Json
Write-Output $response
}
}
finally {
$tcp.Close()
}
}
# Example: start the emulator, wait, then read memory
$cmds = @(
'{"id":1,"cmd":"emu.start"}',
'{"id":2,"cmd":"emu.state"}',
'{"id":3,"cmd":"mem.read","addr":"0400","len":8}'
)
Invoke-EmuSession -Commands $cmds
Screenshot to file¶
function Save-EmuScreenshot {
param(
[string]$OutputPath,
[string]$Host = "127.0.0.1",
[int]$Port = 6510
)
$resp = Invoke-EmuCommand '{"id":1,"cmd":"screenshot"}' -Host $Host -Port $Port
if (-not $resp.ok) { throw "Screenshot failed: $($resp.error)" }
$bytes = [Convert]::FromBase64String($resp.data)
[System.IO.File]::WriteAllBytes($OutputPath, $bytes)
Write-Host "Screenshot saved to $OutputPath"
}
Save-EmuScreenshot -OutputPath "$env:TEMP\screen.png"