Skip to content

carledwards/u64ctl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

u64ctl

A control tool for the Ultimate 64 over its REST API. Two faces over one shared core:

  • a CLI for interactive/scripted use — read & disassemble memory, write bytes, query info, control the machine;
  • an MCP server (the mcp subcommand) exposing the same operations to an MCP host such as Claude, over a Streamable HTTP transport.

Disassembly is powered by go6asm.

The core is a public, structured library (ops returns typed data; render formats it for terminals) so every face binds to the same logic — including a WebAssembly build for the browser:

              ┌─ u64ctl read/dis/write ...     (CLI)
   ops  ──────┤  u64ctl mcp  → MCP server   →  Claude
 (typed core) │  cmd/u64wasm → globalThis.u64ctl  (browser)
  + render    │
        │     └─ render → display text (CLI + MCP)
        │
        └─ u64 → HTTP /v1/... → Ultimate 64

Build

make build        # -> bin/u64ctl
make install      # go install (puts u64ctl on your PATH)
make test vet

The device is selected with -host (or U64_HOST) and an optional -password (or U64_PASSWORD). Hexadecimal addresses need a $ or 0x prefix ($C000, 0xC000); an unprefixed number is decimal. Prefer 0xC000 on the command line — a shell would expand the unquoted $C000.

CLI

export U64_HOST=192.168.1.64

u64ctl info                       # device + API info
u64ctl read  0xC000 64            # hex/ASCII dump of 64 bytes
u64ctl dis   0xC000 64            # 6502 disassembly
u64ctl write 0xC000 "A9 00 8D 20 D0"
u64ctl asm   hello.s -o hello.prg # assemble locally (no device needed)
u64ctl run   hello.s              # assemble → upload → run on the device
u64ctl run   demo.prg             # upload & run a prebuilt .prg as-is
u64ctl run   game.crt             # upload & start a .crt cartridge image
u64ctl run   kickman.bin          # wrap a raw cart ROM as a .crt, then start it
u64ctl run   diamond.t64          # extract a program from a .t64 tape & run it
u64ctl type  'RUN{enter}'         # inject keystrokes ({enter} = RETURN)
u64ctl stop                       # send RUN/STOP to break a BASIC program
u64ctl screen                     # read the 40x25 text screen as text
u64ctl reset                      # also: reboot | pause | resume | menu | poweroff

-host/-password work on every command too: u64ctl read -host 192.168.1.64 0xC000. Add -v to any device command to log the exact HTTP requests hit on the U64:

$ u64ctl read -v 0xC000 16
u64ctl: GET http://192.168.1.64/v1/machine:readmem?address=0xc000&length=16 -> 200

Assemble & run

asm/run use go6asm to assemble ca65-compatible 6502 source into a C64 .prg, then upload it via the device's run_prg runner. asm is fully offline; run needs a device. Source can set its own .org; otherwise the load address defaults to $0801 (override with -org). run -load loads without starting.

run picks how to handle a file by extension (and content), all over the device's runner endpoints:

Input What run does
.s assemble with go6asm, then run_prg
.prg upload as-is (load address from the 2-byte header), then run_prg
.crt start as-is via run_crt (also taken if the bytes carry the C64 CARTRIDGE signature)
.bin / .rom raw cartridge ROM — wrap in a .crt and run_crt. Mapping auto-detects from size + reset vector (8K $E000 reset → Ultimax, else $8000); override with -cart-mode ultimax8k|std8k|std16k
.t64 tape image — extract a program (-entry n for multi-program tapes; an out-of-range n prints the directory) and run_prg it

A .t64 is an emulator tape archive, not a real tape recording: a small directory of one or more .prg-style programs (each with its own load address). It belongs to the .prg family — run pulls the chosen program out and starts it like any other .prg. A raw cartridge ROM (.bin) carries no mapping metadata, so run reconstructs the .crt container (header + CHIP packet with the right load address and EXROM/GAME lines) the device needs to drive the cartridge banking — the faithful way to run a cart.

Examples

examples/ holds ready-to-build C64 source:

File What it does
border-flash.s Hijacks the KERNAL IRQ vector to cycle the border color in the background while BASIC keeps running. A complete, deployable demo of the install → IRQ-handler → chain-to-KERNAL pattern.
sprite-bounce.s Builds on the same IRQ trick with a sprite: two independent tasks (horizontal bounce, vertical bob + size pulse) each gated to its own frame rate. Shows sprite setup, a bitmap copy into VIC RAM, and a lookup-table animation.
sprite-anim.s A true two-frame shape animation: two bitmaps in two VIC blocks, with the IRQ flipping sprite 0's data pointer ($07F8) between them so the face blinks while it bounces.
u64ctl asm examples/border-flash.s -o /tmp/border.prg   # build it
u64ctl run -host 192.168.1.64 examples/border-flash.s   # build + run on the U64

Stop the flasher from the C64 with POKE 788,49 : POKE 789,234 (restores the stock vector).

MCP server

u64ctl mcp -host 192.168.1.64           # daemon, listens on :8080/mcp

It prints the connect command on startup. Register it with Claude Code:

claude mcp add --transport http ultimate64 http://localhost:8080/mcp

MCP flags: -addr (default :8080), -path (default /mcp), -v (verbose).

Tools exposed: u64_info, u64_read_memory, u64_write_memory, u64_disassemble, u64_run_source (assemble & run 6502 source), u64_type_text (inject keystrokes), u64_stop (RUN/STOP), u64_read_screen (text screen → text), u64_machine_control.

⚠️ The MCP server has no auth and binds all interfaces by default. Keep it on a trusted LAN, or bind localhost only (-addr 127.0.0.1:8080) and reach it via SSH tunnel.

Logging

All logs go to stderr. The daemon logs the connecting client on initialize and every tool call with arguments, outcome, and timing; on Ctrl+C it prints the claude mcp remove cleanup command. Add -v to also log every JSON-RPC method.

u64ctl 22:10:55 initialize: claude-code 1.2.3 (protocol 2025-06-18)
u64ctl 22:10:55 tool u64_read_memory({"address":"$C000","length":16}) -> ok in 12ms (89 bytes)

WebAssembly library

The core compiles to a browser WASM module — a third face over the same ops/ render packages. Go's net/http maps to the browser fetch API, so a page can drive the device directly with no extra server.

make wasm        # -> bin/u64.wasm (+ bin/wasm_exec.js loader)

A host page loads wasm_exec.js, instantiates u64.wasm, and finds the API on globalThis.u64ctl. Every method takes (host, password, …) and returns a Promise resolving to a parsed object:

const screen = await u64ctl.readScreen("192.168.1.64", "");
console.log(screen.text);
await u64ctl.typeText("192.168.1.64", "", "RUN\n");

Methods: readMemory, writeMemory, disassemble, info, machine, typeText, stop, readScreen, runSource. CORS applies at runtime — the page must be same-origin with the device (or the device must send CORS headers).

Layout

main.go                  subcommand dispatcher: CLI verbs + `mcp`
ops/                     public typed core: device verbs + address/hex parsing
render/                  public display formatting (CLI + MCP)
u64/                     public thin Ultimate 64 REST client (/v1 routes)
cmd/u64wasm/             WebAssembly face: ops/render exposed to JavaScript
internal/mcpserver/      hand-rolled MCP JSON-RPC over Streamable HTTP
internal/tools/          MCP tool definitions (thin adapters over ops + render)
examples/                ready-to-build C64 assembly sources

go6asm dependency

During local development go6asm is resolved via a replace directive in go.mod pointing at ../go6asm (and the local go.work). Once stable, drop the replace and pin the published tag (go6asm v0.1.1 is the current require).

Roadmap

  • u64_screenshot — capture a VIC video frame → PNG via /v1/streams/<stream>:start (needs wired Ethernet)
  • screen-base autodetect for read_screen (VIC bank via CIA2 $DD00 + $D018)
  • annotated disassembly via go6asm symbols/comments (KERNAL/BASIC labels)
  • more runners/endpoints: sidplay/modplay, drives:*, files:*, configs
  • a browser UI on top of the WASM library

About

CLI, MCP server, and WASM library for the Ultimate 64 — control it over its REST API: read/write/disassemble memory, assemble & run 6502, run .prg/.crt/.bin/.t64, and machine control.

Topics

Resources

Stars

Watchers

Forks

Packages