Add VICE binary protocol layer and core debugging tools

Protocol layer (src/protocol/):
- TCP socket client with connection management
- Binary packet encoding/decoding per VICE monitor protocol
- Async response handling and request ID tracking
- Proper error handling with actionable suggestions

MCP tools implemented:
- status: Connection and emulation state
- connect/disconnect: VICE session management
- readMemory/writeMemory: Memory access with hex dump formatting
- getRegisters: CPU state with decoded flags
- step/continue: Execution control
- reset: Machine reset (soft/hard)
- setBreakpoint/deleteBreakpoint: Breakpoint management

AX patterns integrated:
- _meta block in all responses (connection state context)
- Structured output (value + hex + hint where relevant)
- Rich tool descriptions with cross-references
- Actionable error messages with suggestions
- Memory region hints for common addresses

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Simen Svale 2025-12-30 01:50:27 +01:00
parent db47d8ed10
commit 32eb8f3c17
4 changed files with 1064 additions and 78 deletions

View file

@ -3,40 +3,101 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod"; import { z } from "zod";
import { getViceClient, ViceError } from "./protocol/index.js";
const server = new McpServer({ const server = new McpServer({
name: "vice-mcp", name: "vice-mcp",
version: "0.1.0", version: "0.1.0",
}); });
// Connection state (will be managed by protocol layer) const client = getViceClient();
let connected = false;
// Helper to format tool responses with _meta context
function formatResponse(data: object) {
const state = client.getState();
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
...data,
_meta: {
connected: state.connected,
running: state.running,
...(state.connected && { host: state.host, port: state.port }),
},
},
null,
2
),
},
],
};
}
// Helper to format error responses
function formatError(error: ViceError | Error) {
const state = client.getState();
const errorData =
"code" in error
? error
: {
error: true,
code: "UNKNOWN_ERROR",
message: error.message,
};
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
...errorData,
_meta: {
connected: state.connected,
running: state.running,
},
},
null,
2
),
},
],
};
}
// Tool: status - Get current connection and emulation state // Tool: status - Get current connection and emulation state
server.registerTool( server.registerTool(
"status", "status",
{ {
description: "Get current connection and emulation state", description: `Get current VICE connection and emulation state.
Returns connection status, whether emulation is running or paused, and host/port if connected.
Use this to:
- Check if you're connected before running other commands
- See if emulation is running or stopped (e.g., at a breakpoint)
- Verify connection details
Related tools: connect, disconnect`,
}, },
async () => { async () => {
return { const state = client.getState();
content: [ return formatResponse({
{ connected: state.connected,
type: "text", running: state.running,
text: JSON.stringify( ...(state.connected && {
{ host: state.host,
connected, port: state.port,
running: false, }),
hint: connected hint: state.connected
? "Connected to VICE" ? state.running
: "Not connected. Use connect() to establish connection to VICE.", ? "VICE is running. Use step() to pause execution."
}, : "VICE is paused. Use continue() to resume."
null, : "Not connected. Use connect() to establish connection to VICE.",
2 });
),
},
],
};
} }
); );
@ -44,36 +105,45 @@ server.registerTool(
server.registerTool( server.registerTool(
"connect", "connect",
{ {
description: "Connect to a running VICE instance with binary monitor enabled", description: `Connect to a running VICE emulator instance via the binary monitor protocol.
VICE must be started with the binary monitor enabled:
x64sc -binarymonitor -binarymonitoraddress ip4://127.0.0.1:6502
Default connection: 127.0.0.1:6502
Use this first before any debugging operations. Connection persists until disconnect() is called or VICE closes.
Related tools: status, disconnect`,
inputSchema: z.object({ inputSchema: z.object({
host: z.string().optional().describe("VICE host address (default: 127.0.0.1)"), host: z
port: z.number().optional().describe("VICE binary monitor port (default: 6502)"), .string()
.optional()
.describe("VICE host address (default: 127.0.0.1)"),
port: z
.number()
.min(1)
.max(65535)
.optional()
.describe("VICE binary monitor port (default: 6502)"),
}), }),
}, },
async (args) => { async (args) => {
const host = args.host || "127.0.0.1"; const host = args.host || "127.0.0.1";
const port = args.port || 6502; const port = args.port || 6502;
// Placeholder - actual connection logic will be in protocol layer try {
return { await client.connect(host, port);
content: [ return formatResponse({
{ connected: true,
type: "text", host,
text: JSON.stringify( port,
{ message: `Successfully connected to VICE at ${host}:${port}`,
connected: false, hint: "Connection established. You can now use readMemory, getRegisters, and other debugging tools.",
error: true, });
code: "NOT_IMPLEMENTED", } catch (error) {
message: `Connection to ${host}:${port} not yet implemented`, return formatError(error as ViceError);
suggestion: }
"Protocol layer is under development. Check back soon!",
},
null,
2
),
},
],
};
} }
); );
@ -81,44 +151,449 @@ server.registerTool(
server.registerTool( server.registerTool(
"disconnect", "disconnect",
{ {
description: "Disconnect from the VICE instance", description: `Disconnect from the VICE emulator instance.
Cleanly closes the connection. Safe to call even if not connected.
Related tools: connect, status`,
}, },
async () => { async () => {
if (!connected) { const wasConnected = client.getState().connected;
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: true,
code: "NOT_CONNECTED",
message: "Not connected to VICE",
},
null,
2
),
},
],
};
}
connected = false; try {
return { await client.disconnect();
content: [ return formatResponse({
{ disconnected: true,
type: "text", wasConnected,
text: JSON.stringify( message: wasConnected
{ ? "Disconnected from VICE"
disconnected: true, : "Was not connected",
message: "Disconnected from VICE", });
}, } catch (error) {
null, return formatError(error as ViceError);
2 }
), }
);
// Tool: readMemory - Read memory from the C64
server.registerTool(
"readMemory",
{
description: `Read memory from the C64's address space.
Returns raw bytes plus hex and ASCII representations.
C64 memory map highlights:
- $0000-$00FF: Zero page (fast access, common variables)
- $0100-$01FF: Stack
- $0400-$07FF: Default screen RAM (1000 bytes)
- $D000-$D3FF: VIC-II registers (graphics)
- $D400-$D7FF: SID registers (sound)
- $D800-$DBFF: Color RAM
For screen content, consider using readScreen instead for interpreted output.
For sprite info, use readSprites for semantic data.
Related tools: writeMemory, readScreen, readSprites, readVicState`,
inputSchema: z.object({
address: z
.number()
.min(0)
.max(0xffff)
.describe("Start address (0x0000-0xFFFF)"),
length: z
.number()
.min(1)
.max(65536)
.optional()
.describe("Number of bytes to read (default: 256, max: 65536)"),
}),
},
async (args) => {
const address = args.address;
const length = Math.min(args.length || 256, 65536);
const endAddress = Math.min(address + length - 1, 0xffff);
try {
const data = await client.readMemory(address, endAddress);
// Format as hex dump
const hexLines: string[] = [];
const asciiLines: string[] = [];
for (let i = 0; i < data.length; i += 16) {
const chunk = data.subarray(i, Math.min(i + 16, data.length));
const hex = Array.from(chunk)
.map((b) => b.toString(16).padStart(2, "0"))
.join(" ");
const ascii = Array.from(chunk)
.map((b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : "."))
.join("");
hexLines.push(
`$${(address + i).toString(16).padStart(4, "0")}: ${hex}`
);
asciiLines.push(ascii);
}
return formatResponse({
address: {
value: address,
hex: `$${address.toString(16).padStart(4, "0")}`,
}, },
], length: data.length,
}; bytes: Array.from(data),
hex: hexLines.join("\n"),
ascii: asciiLines.join(""),
hint: getMemoryHint(address, endAddress),
});
} catch (error) {
return formatError(error as ViceError);
}
}
);
// Helper to provide context about memory regions
function getMemoryHint(start: number, end: number): string {
if (start <= 0xff) return "Zero page - commonly used for variables and pointers";
if (start >= 0x100 && start <= 0x1ff) return "Stack area";
if (start >= 0x400 && end <= 0x7ff) return "Default screen RAM area";
if (start >= 0xd000 && end <= 0xd3ff) return "VIC-II registers - use readVicState for interpreted data";
if (start >= 0xd400 && end <= 0xd7ff) return "SID registers - use readSidState for interpreted data";
if (start >= 0xd800 && end <= 0xdbff) return "Color RAM";
if (start >= 0xa000 && end <= 0xbfff) return "BASIC ROM (or RAM if bank switched)";
if (start >= 0xe000 && end <= 0xffff) return "KERNAL ROM (or RAM if bank switched)";
return "";
}
// Tool: writeMemory - Write memory to the C64
server.registerTool(
"writeMemory",
{
description: `Write bytes to the C64's memory.
Directly modifies memory at the specified address. Changes take effect immediately.
Common uses:
- Poke values for testing
- Patch code at runtime
- Modify screen/color RAM directly
- Change VIC/SID registers
Be careful writing to ROM areas ($A000-$BFFF, $E000-$FFFF) - you may need to bank out ROM first.
Related tools: readMemory, fillMemory`,
inputSchema: z.object({
address: z
.number()
.min(0)
.max(0xffff)
.describe("Start address (0x0000-0xFFFF)"),
bytes: z
.array(z.number().min(0).max(255))
.min(1)
.describe("Array of bytes to write (0-255 each)"),
}),
},
async (args) => {
try {
await client.writeMemory(args.address, args.bytes);
return formatResponse({
success: true,
address: {
value: args.address,
hex: `$${args.address.toString(16).padStart(4, "0")}`,
},
bytesWritten: args.bytes.length,
message: `Wrote ${args.bytes.length} byte(s) to $${args.address.toString(16).padStart(4, "0")}`,
});
} catch (error) {
return formatError(error as ViceError);
}
}
);
// Tool: getRegisters - Get CPU registers
server.registerTool(
"getRegisters",
{
description: `Get current 6502/6510 CPU register state.
Returns all CPU registers with interpreted flags.
Registers:
- A: Accumulator (arithmetic operations)
- X, Y: Index registers (addressing, loops)
- SP: Stack pointer ($100-$1FF range)
- PC: Program counter (current instruction address)
- Flags: N(egative), V(overflow), B(reak), D(ecimal), I(nterrupt), Z(ero), C(arry)
Use this to:
- Check CPU state at breakpoints
- Understand program flow
- Debug crashes (check PC, SP)
Related tools: setRegister, step, continue, status`,
},
async () => {
try {
const response = await client.getRegisters();
// Parse register response
// Format: count(2) + [id(1) + size(1) + value(size)]...
const count = response.body.readUInt16LE(0);
const registers: Record<string, number> = {};
let offset = 2;
const regNames: Record<number, string> = {
0: "A",
1: "X",
2: "Y",
3: "PC",
4: "SP",
5: "FL", // Flags
};
for (let i = 0; i < count && offset < response.body.length; i++) {
const id = response.body[offset];
const size = response.body[offset + 1];
offset += 2;
let value = 0;
if (size === 1) {
value = response.body[offset];
} else if (size === 2) {
value = response.body.readUInt16LE(offset);
}
offset += size;
const name = regNames[id] || `R${id}`;
registers[name] = value;
}
// Parse flags
const flags = registers.FL || 0;
const flagsDecoded = {
negative: !!(flags & 0x80),
overflow: !!(flags & 0x40),
break: !!(flags & 0x10),
decimal: !!(flags & 0x08),
interrupt: !!(flags & 0x04),
zero: !!(flags & 0x02),
carry: !!(flags & 0x01),
raw: flags,
};
return formatResponse({
a: { value: registers.A, hex: `$${(registers.A || 0).toString(16).padStart(2, "0")}` },
x: { value: registers.X, hex: `$${(registers.X || 0).toString(16).padStart(2, "0")}` },
y: { value: registers.Y, hex: `$${(registers.Y || 0).toString(16).padStart(2, "0")}` },
sp: {
value: registers.SP,
hex: `$${(registers.SP || 0).toString(16).padStart(2, "0")}`,
stackTop: `$01${(registers.SP || 0).toString(16).padStart(2, "0")}`,
},
pc: {
value: registers.PC,
hex: `$${(registers.PC || 0).toString(16).padStart(4, "0")}`,
},
flags: flagsDecoded,
hint:
registers.SP !== undefined && registers.SP < 0x10
? "Warning: Stack pointer very low - possible stack overflow"
: registers.SP !== undefined && registers.SP > 0xf0
? "Warning: Stack nearly empty - possible stack underflow"
: "CPU state looks normal",
});
} catch (error) {
return formatError(error as ViceError);
}
}
);
// Tool: continue - Resume execution
server.registerTool(
"continue",
{
description: `Resume C64 execution after a breakpoint or pause.
Starts the emulator running until the next breakpoint, manual stop, or error.
Related tools: step, status, setBreakpoint`,
},
async () => {
try {
await client.continue();
return formatResponse({
resumed: true,
message: "Execution resumed",
hint: "Use status() to check if execution stopped (e.g., at breakpoint)",
});
} catch (error) {
return formatError(error as ViceError);
}
}
);
// Tool: step - Single-step execution
server.registerTool(
"step",
{
description: `Execute one or more instructions, then stop.
Single-stepping is essential for understanding code flow and debugging.
Options:
- count: Number of instructions to execute (default: 1)
- stepOver: If true, treat JSR as single instruction (don't step into subroutines)
After stepping, use getRegisters to see the new CPU state.
Related tools: getRegisters, continue, setBreakpoint, status`,
inputSchema: z.object({
count: z.number().min(1).optional().describe("Number of instructions to step (default: 1)"),
stepOver: z.boolean().optional().describe("Step over JSR calls instead of into them (default: false)"),
}),
},
async (args) => {
try {
await client.step(args.count || 1, args.stepOver || false);
// Get registers after step
const regResponse = await client.getRegisters();
const count = regResponse.body.readUInt16LE(0);
let offset = 2;
let pc = 0;
for (let i = 0; i < count && offset < regResponse.body.length; i++) {
const id = regResponse.body[offset];
const size = regResponse.body[offset + 1];
offset += 2;
if (id === 3 && size === 2) {
// PC
pc = regResponse.body.readUInt16LE(offset);
}
offset += size;
}
return formatResponse({
stepped: true,
count: args.count || 1,
stepOver: args.stepOver || false,
pc: {
value: pc,
hex: `$${pc.toString(16).padStart(4, "0")}`,
},
message: `Stepped ${args.count || 1} instruction(s)`,
hint: "Use getRegisters() for full CPU state, or readMemory at PC for next instruction",
});
} catch (error) {
return formatError(error as ViceError);
}
}
);
// Tool: reset - Reset the C64
server.registerTool(
"reset",
{
description: `Reset the C64 machine.
Options:
- hard: If true, performs hard reset (like power cycle). If false, soft reset (like reset button).
A soft reset preserves some memory contents, hard reset clears everything.
Related tools: connect, status`,
inputSchema: z.object({
hard: z.boolean().optional().describe("Hard reset (true) vs soft reset (false, default)"),
}),
},
async (args) => {
try {
await client.reset(args.hard || false);
return formatResponse({
reset: true,
type: args.hard ? "hard" : "soft",
message: `${args.hard ? "Hard" : "Soft"} reset performed`,
hint: "C64 is now at startup. Use status() to check state.",
});
} catch (error) {
return formatError(error as ViceError);
}
}
);
// Tool: setBreakpoint - Set execution breakpoint
server.registerTool(
"setBreakpoint",
{
description: `Set an execution breakpoint at a memory address.
When the PC reaches this address, execution stops. Use to:
- Debug code at specific points
- Catch when routines are called
- Analyze code flow
Returns a breakpoint ID for later management.
Related tools: deleteBreakpoint, listBreakpoints, continue, step`,
inputSchema: z.object({
address: z.number().min(0).max(0xffff).describe("Address to break at (0x0000-0xFFFF)"),
enabled: z.boolean().optional().describe("Whether breakpoint is active (default: true)"),
temporary: z.boolean().optional().describe("Auto-delete after hit (default: false)"),
}),
},
async (args) => {
try {
const id = await client.setBreakpoint(args.address, {
enabled: args.enabled ?? true,
temporary: args.temporary ?? false,
});
return formatResponse({
success: true,
breakpointId: id,
address: {
value: args.address,
hex: `$${args.address.toString(16).padStart(4, "0")}`,
},
enabled: args.enabled ?? true,
temporary: args.temporary ?? false,
message: `Breakpoint ${id} set at $${args.address.toString(16).padStart(4, "0")}`,
hint: "Use continue() to run until breakpoint is hit",
});
} catch (error) {
return formatError(error as ViceError);
}
}
);
// Tool: deleteBreakpoint - Remove a breakpoint
server.registerTool(
"deleteBreakpoint",
{
description: `Delete a breakpoint by its ID.
Use listBreakpoints to see current breakpoint IDs.
Related tools: setBreakpoint, listBreakpoints`,
inputSchema: z.object({
breakpointId: z.number().describe("Breakpoint ID from setBreakpoint"),
}),
},
async (args) => {
try {
await client.deleteBreakpoint(args.breakpointId);
return formatResponse({
success: true,
deletedId: args.breakpointId,
message: `Breakpoint ${args.breakpointId} deleted`,
});
} catch (error) {
return formatError(error as ViceError);
}
} }
); );

436
src/protocol/client.ts Normal file
View file

@ -0,0 +1,436 @@
// VICE Binary Monitor Client
import { Socket } from "net";
import {
STX,
API_VERSION,
Command,
ResponseType,
ErrorCode,
MemorySpace,
ViceResponse,
ConnectionState,
} from "./types.js";
export interface ViceError {
error: true;
code: string;
message: string;
suggestion?: string;
}
export class ViceClient {
private socket: Socket | null = null;
private requestId = 0;
private responseBuffer = Buffer.alloc(0);
private pendingRequests = new Map<
number,
{
resolve: (response: ViceResponse) => void;
reject: (error: ViceError) => void;
}
>();
private state: ConnectionState = {
connected: false,
host: "",
port: 0,
running: true,
};
// Event handlers for async events (breakpoints, etc.)
public onStopped?: (response: ViceResponse) => void;
public onResumed?: (response: ViceResponse) => void;
getState(): ConnectionState {
return { ...this.state };
}
async connect(host = "127.0.0.1", port = 6502): Promise<void> {
if (this.socket) {
throw this.makeError(
"ALREADY_CONNECTED",
"Already connected to VICE",
"Use disconnect() first if you want to reconnect"
);
}
return new Promise((resolve, reject) => {
this.socket = new Socket();
const timeout = setTimeout(() => {
this.socket?.destroy();
this.socket = null;
reject(
this.makeError(
"CONNECTION_TIMEOUT",
`Connection to ${host}:${port} timed out after 5 seconds`,
"Ensure VICE is running with -binarymonitor flag: x64sc -binarymonitor -binarymonitoraddress ip4://127.0.0.1:6502"
)
);
}, 5000);
this.socket.on("connect", () => {
clearTimeout(timeout);
this.state = { connected: true, host, port, running: true };
resolve();
});
this.socket.on("error", (err) => {
clearTimeout(timeout);
this.socket?.destroy();
this.socket = null;
this.state.connected = false;
reject(
this.makeError(
"CONNECTION_FAILED",
`Failed to connect to ${host}:${port}: ${err.message}`,
"Ensure VICE is running with -binarymonitor flag: x64sc -binarymonitor -binarymonitoraddress ip4://127.0.0.1:6502"
)
);
});
this.socket.on("close", () => {
this.state.connected = false;
this.socket = null;
// Reject all pending requests
for (const [, { reject: rejectFn }] of this.pendingRequests) {
rejectFn(
this.makeError(
"CONNECTION_CLOSED",
"Connection to VICE closed unexpectedly",
"VICE may have been closed or crashed. Try reconnecting."
)
);
}
this.pendingRequests.clear();
});
this.socket.on("data", (data) => this.handleData(data));
this.socket.connect(port, host);
});
}
async disconnect(): Promise<void> {
if (!this.socket) {
return;
}
return new Promise((resolve) => {
this.socket!.once("close", () => {
this.state.connected = false;
resolve();
});
this.socket!.end();
});
}
private makeError(code: string, message: string, suggestion?: string): ViceError {
return { error: true, code, message, suggestion };
}
private nextRequestId(): number {
this.requestId = (this.requestId + 1) & 0xff;
return this.requestId;
}
private handleData(data: Buffer): void {
this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
// Process complete packets
while (this.responseBuffer.length >= 9) {
// Minimum header size
const stx = this.responseBuffer[0];
if (stx !== STX) {
// Protocol error, skip byte
this.responseBuffer = this.responseBuffer.subarray(1);
continue;
}
const bodyLength = this.responseBuffer.readUInt32LE(2);
const totalLength = 6 + bodyLength; // Header (6) + body
if (this.responseBuffer.length < totalLength) {
// Wait for more data
break;
}
// Parse complete packet
const responseType = this.responseBuffer[6] as ResponseType;
const errorCode = this.responseBuffer[7] as ErrorCode;
const requestId = this.responseBuffer[8];
const body = this.responseBuffer.subarray(9, totalLength);
const response: ViceResponse = {
responseType,
errorCode,
requestId,
body,
};
// Remove processed packet from buffer
this.responseBuffer = this.responseBuffer.subarray(totalLength);
// Handle response
this.handleResponse(response);
}
}
private handleResponse(response: ViceResponse): void {
// Check for async events
if (response.responseType === ResponseType.Stopped || response.responseType === ResponseType.CheckpointHit) {
this.state.running = false;
this.onStopped?.(response);
return;
}
if (response.responseType === ResponseType.Resumed) {
this.state.running = true;
this.onResumed?.(response);
return;
}
// Match to pending request
const pending = this.pendingRequests.get(response.requestId);
if (pending) {
this.pendingRequests.delete(response.requestId);
if (response.errorCode !== ErrorCode.Ok) {
pending.reject(
this.makeError(
`VICE_ERROR_${response.errorCode}`,
`VICE returned error code ${response.errorCode}`,
this.getErrorSuggestion(response.errorCode)
)
);
} else {
pending.resolve(response);
}
}
}
private getErrorSuggestion(code: ErrorCode): string {
switch (code) {
case ErrorCode.ObjectMissing:
return "The requested object (checkpoint, etc.) does not exist";
case ErrorCode.InvalidMemspace:
return "Invalid memory space specified. Use 0 for main CPU memory.";
case ErrorCode.InvalidCmdLength:
return "Command packet has invalid length - this is likely a protocol bug";
case ErrorCode.InvalidParameter:
return "Invalid parameter value - check address ranges (0x0000-0xFFFF for C64)";
default:
return "Check VICE console for more details";
}
}
private async sendCommand(command: Command, body: Buffer = Buffer.alloc(0)): Promise<ViceResponse> {
if (!this.socket || !this.state.connected) {
throw this.makeError(
"NOT_CONNECTED",
"Not connected to VICE",
"Use connect() first to establish connection"
);
}
const requestId = this.nextRequestId();
// Build packet: STX (1) + API (1) + Length (4) + RequestID (1) + Command (1) + Body
const header = Buffer.alloc(8);
header[0] = STX;
header[1] = API_VERSION;
header.writeUInt32LE(body.length + 2, 2); // Body length includes request ID and command
header[6] = requestId;
header[7] = command;
const packet = Buffer.concat([header, body]);
return new Promise((resolve, reject) => {
this.pendingRequests.set(requestId, { resolve, reject });
this.socket!.write(packet, (err) => {
if (err) {
this.pendingRequests.delete(requestId);
reject(
this.makeError(
"SEND_FAILED",
`Failed to send command: ${err.message}`,
"Connection may have been lost. Try reconnecting."
)
);
}
});
// Timeout for response
setTimeout(() => {
if (this.pendingRequests.has(requestId)) {
this.pendingRequests.delete(requestId);
reject(
this.makeError(
"RESPONSE_TIMEOUT",
"Timeout waiting for VICE response",
"VICE may be busy or unresponsive. Try again or reconnect."
)
);
}
}, 10000);
});
}
// High-level commands
async readMemory(
startAddress: number,
endAddress: number,
memspace: MemorySpace = MemorySpace.MainCPU
): Promise<Buffer> {
// Validate addresses
if (startAddress < 0 || startAddress > 0xffff) {
throw this.makeError(
"INVALID_ADDRESS",
`Start address 0x${startAddress.toString(16)} is outside C64 memory range`,
"C64 addresses are 16-bit (0x0000-0xFFFF)"
);
}
if (endAddress < 0 || endAddress > 0xffff) {
throw this.makeError(
"INVALID_ADDRESS",
`End address 0x${endAddress.toString(16)} is outside C64 memory range`,
"C64 addresses are 16-bit (0x0000-0xFFFF)"
);
}
if (startAddress > endAddress) {
throw this.makeError(
"INVALID_RANGE",
`Start address (0x${startAddress.toString(16)}) is greater than end address (0x${endAddress.toString(16)})`,
"Swap the addresses or check your range"
);
}
// Build request: side_effects(1) + start(2) + memspace(1) + end(2)
const body = Buffer.alloc(6);
body[0] = 0; // No side effects
body.writeUInt16LE(startAddress, 1);
body[3] = memspace;
body.writeUInt16LE(endAddress, 4);
const response = await this.sendCommand(Command.MemoryGet, body);
// Response body: length(2) + data(N)
const dataLength = response.body.readUInt16LE(0);
return response.body.subarray(2, 2 + dataLength);
}
async writeMemory(
address: number,
data: Buffer | number[],
memspace: MemorySpace = MemorySpace.MainCPU
): Promise<void> {
const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
if (address < 0 || address > 0xffff) {
throw this.makeError(
"INVALID_ADDRESS",
`Address 0x${address.toString(16)} is outside C64 memory range`,
"C64 addresses are 16-bit (0x0000-0xFFFF)"
);
}
if (dataBuffer.length === 0) {
throw this.makeError(
"INVALID_DATA",
"Cannot write empty data",
"Provide at least one byte to write"
);
}
if (address + dataBuffer.length > 0x10000) {
throw this.makeError(
"INVALID_RANGE",
`Write would extend past end of memory (0x${address.toString(16)} + ${dataBuffer.length} bytes)`,
"Reduce data length or use a lower start address"
);
}
// Build request: side_effects(1) + start(2) + memspace(1) + length-1(1) + data(N)
const body = Buffer.alloc(5 + dataBuffer.length);
body[0] = 0; // No side effects
body.writeUInt16LE(address, 1);
body[3] = memspace;
body[4] = dataBuffer.length - 1;
dataBuffer.copy(body, 5);
await this.sendCommand(Command.MemorySet, body);
}
async getRegisters(memspace: MemorySpace = MemorySpace.MainCPU): Promise<ViceResponse> {
const body = Buffer.alloc(1);
body[0] = memspace;
return this.sendCommand(Command.RegistersGet, body);
}
async continue(): Promise<void> {
await this.sendCommand(Command.Continue);
this.state.running = true;
}
async step(count = 1, stepOver = false): Promise<ViceResponse> {
const body = Buffer.alloc(3);
body[0] = stepOver ? 1 : 0;
body.writeUInt16LE(count, 1);
const response = await this.sendCommand(Command.Step, body);
this.state.running = false;
return response;
}
async reset(hard = false): Promise<void> {
const body = Buffer.alloc(1);
body[0] = hard ? 1 : 0;
await this.sendCommand(Command.Reset, body);
}
async setBreakpoint(
address: number,
options: {
enabled?: boolean;
stop?: boolean;
temporary?: boolean;
} = {}
): Promise<number> {
const { enabled = true, stop = true, temporary = false } = options;
if (address < 0 || address > 0xffff) {
throw this.makeError(
"INVALID_ADDRESS",
`Address 0x${address.toString(16)} is outside C64 memory range`,
"C64 addresses are 16-bit (0x0000-0xFFFF)"
);
}
// Build request: start(2) + end(2) + stop(1) + enabled(1) + op(1) + temp(1)
const body = Buffer.alloc(8);
body.writeUInt16LE(address, 0);
body.writeUInt16LE(address, 2);
body[4] = stop ? 1 : 0;
body[5] = enabled ? 1 : 0;
body[6] = 0x01; // Exec
body[7] = temporary ? 1 : 0;
const response = await this.sendCommand(Command.CheckpointSet, body);
return response.body.readUInt32LE(0);
}
async deleteBreakpoint(checkpointId: number): Promise<void> {
const body = Buffer.alloc(4);
body.writeUInt32LE(checkpointId, 0);
await this.sendCommand(Command.CheckpointDelete, body);
}
}
// Singleton instance
let clientInstance: ViceClient | null = null;
export function getViceClient(): ViceClient {
if (!clientInstance) {
clientInstance = new ViceClient();
}
return clientInstance;
}

2
src/protocol/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from "./types.js";
export * from "./client.js";

73
src/protocol/types.ts Normal file
View file

@ -0,0 +1,73 @@
// VICE Binary Monitor Protocol Types
// API Constants
export const STX = 0x02;
export const API_VERSION = 0x02;
// Command codes
export enum Command {
MemoryGet = 0x01,
MemorySet = 0x02,
CheckpointSet = 0x11,
CheckpointDelete = 0x13,
RegistersGet = 0x22,
Continue = 0x31,
Step = 0x32,
Reset = 0x43,
Exit = 0x71,
}
// Response types
export enum ResponseType {
Invalid = 0x00,
Ok = 0x01,
Object = 0x02,
Stopped = 0x11,
Resumed = 0x12,
MemoryGet = 0x31,
RegisterInfo = 0x62,
CheckpointHit = 0x63,
}
// Memory spaces
export enum MemorySpace {
MainCPU = 0,
Drive8 = 1,
Drive9 = 2,
Drive10 = 3,
Drive11 = 4,
}
// Checkpoint (breakpoint) operation types
export enum CheckpointOp {
Exec = 0x01,
Load = 0x02,
Store = 0x04,
}
// Error codes
export enum ErrorCode {
Ok = 0x00,
ObjectMissing = 0x01,
InvalidMemspace = 0x02,
InvalidCmdLength = 0x80,
InvalidParameterLength = 0x81,
InvalidAPI = 0x82,
InvalidCmdType = 0x83,
InvalidTarget = 0x84,
InvalidParameter = 0x85,
}
export interface ViceResponse {
responseType: ResponseType;
errorCode: ErrorCode;
requestId: number;
body: Buffer;
}
export interface ConnectionState {
connected: boolean;
host: string;
port: number;
running: boolean;
}