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:
parent
db47d8ed10
commit
32eb8f3c17
4 changed files with 1064 additions and 78 deletions
631
src/index.ts
631
src/index.ts
|
|
@ -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
436
src/protocol/client.ts
Normal 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
2
src/protocol/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./types.js";
|
||||||
|
export * from "./client.js";
|
||||||
73
src/protocol/types.ts
Normal file
73
src/protocol/types.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue