Overview of remote control¶
The emulator supports a persistent TCP remote control endpoint that lets external processes inspect and drive a running emulator instance in real time. It is designed for automation, AI agent integration, and tooling that needs ad-hoc access without embedding a Lua script inside the emulator process.
Implementation library: Highbyte.DotNet6502.Remoting.
Key design points:
- Persistent TCP connection — one client at a time; the server accepts a new client after the previous one disconnects.
- Newline-delimited JSON — every request is a single JSON object terminated by
\n; every response is a single JSON object terminated by\n. - Platform-agnostic — the same protocol works against both the Avalonia Desktop app and the Headless app.
- Non-exclusive — user input from keyboard/joystick and remote input coexist; neither locks the other out.
- Frame-synchronized input — joystick, keyboard, and memory-write commands are queued and executed at the next frame boundary so they do not race with the CPU.
For the protocol and command reference, see TCP protocol. For automation recipes, see Examples. For the bundled CLI client, see Remote Client console app.
Starting the Emulator with Remote Control¶
Avalonia Desktop¶
Option 1 — command line (server starts automatically on launch):
# Start on a fixed port (loopback only — 127.0.0.1 — by default)
dotnet run --project src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Desktop -- --remote-port 6510
# Or via the published binary
./Highbyte.DotNet6502.App.Avalonia.Desktop --remote-port 6510
# Bind to a specific interface to accept remote connections
./Highbyte.DotNet6502.App.Avalonia.Desktop --remote-port 6510 --remote-bind-address 0.0.0.0
Option 2 — from the UI (start/stop at any time without restarting the app):
- Open the Debug & Remoting tab (via the
Viewmenu →Debug & Remoting). - In the Remote Control Server section, enter the desired bind address (default
127.0.0.1) and port number. - Click Start to begin listening.
When the server is listening, the Debug & Remoting tab shows the Remote Control Server status as Listening on 127.0.0.1:6510. When a client connects, a blue banner appears at the bottom of the window: • Remote Control Connected (port 6510).
Headless¶
# Loopback only (default)
dotnet run --project src/apps/Highbyte.DotNet6502.App.Headless -- \
--remote-port 6510 \
--system C64 --start
# Bind to all network interfaces so the server is reachable from another device
dotnet run --project src/apps/Highbyte.DotNet6502.App.Headless -- \
--remote-port 6510 --remote-bind-address 0.0.0.0 \
--system C64 --start
# Allow the emu.quit command (disabled by default on headless too unless opted in)
dotnet run --project src/apps/Highbyte.DotNet6502.App.Headless -- \
--remote-port 6510 --allow-remote-quit \
--system C64 --start
Bind address (--remote-bind-address)¶
By default the server binds to 127.0.0.1, so it is only reachable from the same machine. Pass --remote-bind-address <ip> (or set the Bind field in the Remote Control Server section of the Debug & Remoting tab) to change this.
| Value | Meaning |
|---|---|
127.0.0.1 |
Loopback only — same machine (default) |
0.0.0.0 |
Any IPv4 interface — reachable over the network |
192.168.x.y |
A specific LAN IP on the host |
::1 |
IPv6 loopback |
:: |
Any IPv6 interface |
Warning
The protocol is unauthenticated. Any client that can reach the bind address can fully drive the emulator (inject input, read/write memory, etc.). Only bind to non-loopback addresses on networks you trust.
Limitations¶
- Frame-boundary commands require the emulator to be running.
mem.write,cpu.set,keyboard.press/release/releaseall,joystick.set/press/release/releaseall, the C64 and Apple II type/load/media commands, andoric.type,oric.loadtap,oric.inserttape,oric.rewindtape, andoric.ejecttapereturn an immediate error if the emulator state isPausedorUninitialized. Useemu.stateto confirmRunningbefore sending these commands, or sendemu.startfirst. - There is no
emu.resumecommand.emu.startserves dual purpose: it starts the emulator fromUninitializedand resumes fromPaused. The existing system state is preserved on resume; useemu.resetfor a hard restart. emu.selectsystemandemu.selectvariantrequire the emulator to be stopped (Uninitialized). Callemu.stopfirst, then select, thenemu.start.emu.loadsnapshotleaves the emulator paused, andemu.runframesrequires the emulator to be stopped/paused (it is rejected whileRunning, since the real-time run loop would make the frame step non-deterministic). The typical automation flow isemu.loadsnapshot→emu.runframes→screenshot.emu.savesnapshot/emu.loadsnapshotpaths are resolved on the server (the machine the emulator runs on), not the client; relative paths use the server's shared snapshot directory.- One client at a time. A second connection attempt is accepted only after the first client disconnects.
emu.quitis disabled in Avalonia Desktop by default. It is available in headless mode when--allow-remote-quitis passed.- Loopback by default. The server binds to
127.0.0.1unless--remote-bind-address(or the Bind field in the Debug & Remoting tab) is set to a different interface. The wire protocol is unauthenticated — only bind to non-loopback addresses on trusted networks. keyboard.pressholds a key untilkeyboard.releaseorkeyboard.releaseall. The client controls press duration by choosing when to release. Keys are applied at frame boundary and remain held until released.joystick.pressholds joystick actions untiljoystick.releaseorjoystick.releaseall. Use this for ergonomic hold/release remote control.- The Apple II has one game port, so
--portis accepted and ignored there rather than rejected, and scripts written for the C64's two ports keep working. Its stick is analog: a held direction drives that axis to the end of its travel, soPDL(0)/PDL(1)read 0 or 255 and return to 127 on release, and fire is button 0 at$C061. It also acceptsfire2for the second game-port button at$C062, which the C64 has no equivalent of. - The Oric has two joystick sockets, but injected actions affect software only when its configuration selects the PASE or IJK interface. Oric sticks support
up,down,left,right, andfire;fire2is ignored. - System-prefixed commands are system-specific. A C64, Apple II, or Oric command returns an error if another system is running. Text and load/media mutations are applied at the next frame boundary.
c64.typefeeds text across frames — if the C64 keyboard buffer is full the remaining characters wait until space is available.apple2.typefeeds text across frames — the Apple II has a single-key latch instead of a buffer, so each character waits until the program has consumed the previous one (at most one per frame).oric.typefeeds text across frames — each character waits until the Atmos ROM consumes the keyboard latch, and BASIC keywords should be uppercase.- Oric TAP support is byte-level, not cassette-signal emulation.
oric.inserttapeworks with the standard Atmos ROM cassette routines; custom pulse loaders and tape recording are not supported. - Injected joystick actions from
joystick.setare not persistent — they must be resent every frame to hold a direction. Usejoystick.pressif you want stateful joystick hold/release behavior.