jlink-mcp
Version:
MCP server for SEGGER J-Link debug probes — LLM-driven embedded debugging with RTT, GDB server, and Trice/Pigweed support
225 lines • 10.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenOCDBackend = void 0;
const child_process_1 = require("child_process");
const net = __importStar(require("net"));
const backend_1 = require("./backend");
const logger_1 = require("../utils/logger");
const OPENOCD_PROCESS = "openocd-server";
/**
* OpenOCD backend. Supports ST-Link, CMSIS-DAP, FTDI, and many other adapters.
* Uses OpenOCD's telnet interface for commands when the server is running,
* or spawns one-shot openocd processes for individual commands.
*/
class OpenOCDBackend extends backend_1.ProbeBackend {
type = "openocd";
displayName = "OpenOCD";
config;
processManager;
gdbOutputBuffer = [];
constructor(config, processManager) {
super();
this.processManager = processManager;
this.config = {
binaryPath: config.binaryPath || "openocd",
interfaceConfig: config.interfaceConfig || "interface/stlink.cfg",
targetConfig: config.targetConfig || "target/stm32f4x.cfg",
extraConfigs: config.extraConfigs || [],
gdbPort: config.gdbPort || 3333,
telnetPort: config.telnetPort || 4444,
tclPort: config.tclPort || 6666,
};
}
/** Send a command via OpenOCD's telnet interface (when server is running) */
async telnetCommand(command) {
return new Promise((resolve, reject) => {
const socket = new net.Socket();
let response = "";
socket.connect(this.config.telnetPort, "127.0.0.1", () => {
// Wait for prompt then send command
setTimeout(() => {
socket.write(command + "\n");
setTimeout(() => {
socket.write("exit\n");
}, 500);
}, 200);
});
socket.on("data", (data) => { response += data.toString(); });
socket.on("close", () => { resolve(response); });
socket.on("error", (err) => { reject(err); });
setTimeout(() => { socket.destroy(); resolve(response); }, 5000);
});
}
/** Execute OpenOCD commands. If server is running, uses telnet. Otherwise spawns a one-shot process. */
async exec(ocdCommands) {
if (this.isGDBServerRunning()) {
// Use telnet interface
try {
const results = [];
for (const cmd of ocdCommands) {
const resp = await this.telnetCommand(cmd);
results.push(resp);
}
const output = results.join("\n");
return { success: true, rawOutput: output, output };
}
catch (err) {
return { success: false, rawOutput: "", output: "", error: `Telnet error: ${err instanceof Error ? err.message : String(err)}` };
}
}
// One-shot: spawn openocd with -c commands
const args = this.buildConfigArgs();
for (const cmd of ocdCommands) {
args.push("-c", cmd);
}
args.push("-c", "shutdown");
(0, logger_1.log)(`[OpenOCD] ${ocdCommands.join("; ")}`);
return new Promise((resolve) => {
const proc = (0, child_process_1.spawn)(this.config.binaryPath, args, { stdio: ["pipe", "pipe", "pipe"] });
let stdout = "", stderr = "";
proc.stdout?.on("data", (d) => { stdout += d.toString(); });
proc.stderr?.on("data", (d) => { stderr += d.toString(); });
proc.on("error", (err) => {
resolve({ success: false, rawOutput: stdout, output: stdout, error: `Failed to spawn openocd: ${err.message}` });
});
proc.on("exit", (code) => {
// OpenOCD outputs to stderr by default
const combined = stdout + stderr;
resolve({ success: code === 0, rawOutput: combined, output: combined, error: code !== 0 ? stderr : undefined });
});
setTimeout(() => { proc.kill("SIGTERM"); resolve({ success: false, rawOutput: stdout, output: stdout, error: "OpenOCD timed out" }); }, 30000);
});
}
buildConfigArgs() {
const args = [];
args.push("-f", this.config.interfaceConfig);
args.push("-f", this.config.targetConfig);
for (const cfg of this.config.extraConfigs) {
args.push("-f", cfg);
}
return args;
}
// ── ProbeBackend implementation ──────────────────────────────────
async getDeviceInfo() {
return this.exec(["init", "targets", "halt", "reg"]);
}
async halt() { return this.exec(["halt"]); }
async resume() { return this.exec(["resume"]); }
async reset(halt = false) {
return this.exec([halt ? "reset halt" : "reset run"]);
}
async step() { return this.exec(["step"]); }
async readMemory(address, length) {
// OpenOCD: mdw (32-bit words) or mdb (bytes)
const wordCount = Math.ceil(length / 4);
return this.exec([`mdw 0x${address.toString(16)} ${wordCount}`]);
}
async writeMemory(address, value) {
return this.exec([`mww 0x${address.toString(16)} 0x${value.toString(16)}`]);
}
async readAllRegisters() { return this.exec(["halt", "reg"]); }
async readRegister(name) { return this.exec(["halt", `reg ${name}`]); }
async flash(filePath, baseAddress) {
const addr = baseAddress !== undefined ? `0x${baseAddress.toString(16)}` : "";
const writeCmd = addr
? `flash write_image erase ${filePath} ${addr}`
: `program ${filePath} verify reset`;
return this.exec(["init", "halt", writeCmd]);
}
async erase() {
return this.exec(["init", "halt", "flash erase_sector 0 0 last"]);
}
async setBreakpoint(address) {
return this.exec([`bp 0x${address.toString(16)} 2 hw`]);
}
async clearBreakpoints() { return this.exec(["rbp all"]); }
async executeRaw(commands) { return this.exec(commands); }
// ── GDB Server ───────────────────────────────────────────────────
async startGDBServer() {
if (this.processManager.get(OPENOCD_PROCESS)) {
return { success: true, message: "OpenOCD is already running" };
}
const args = this.buildConfigArgs();
args.push("-c", `gdb_port ${this.config.gdbPort}`);
args.push("-c", `telnet_port ${this.config.telnetPort}`);
args.push("-c", `tcl_port ${this.config.tclPort}`);
try {
const managed = this.processManager.spawn(OPENOCD_PROCESS, this.config.binaryPath, args);
managed.process.stdout?.on("data", (d) => {
for (const line of d.toString().split("\n").filter(Boolean)) {
(0, logger_1.log)(`[OpenOCD] ${line}`);
this.gdbOutputBuffer.push(line);
if (this.gdbOutputBuffer.length > 1000)
this.gdbOutputBuffer.shift();
}
});
managed.process.stderr?.on("data", (d) => {
for (const line of d.toString().split("\n").filter(Boolean)) {
(0, logger_1.log)(`[OpenOCD] ${line}`); // OpenOCD uses stderr for normal output
this.gdbOutputBuffer.push(line);
if (this.gdbOutputBuffer.length > 1000)
this.gdbOutputBuffer.shift();
}
});
return { success: true, message: `OpenOCD started: GDB on port ${this.config.gdbPort}, telnet on port ${this.config.telnetPort}` };
}
catch (err) {
return { success: false, message: `Failed to start OpenOCD: ${err instanceof Error ? err.message : String(err)}` };
}
}
stopGDBServer() {
const killed = this.processManager.kill(OPENOCD_PROCESS);
this.gdbOutputBuffer = [];
return { success: true, message: killed ? "OpenOCD stopped" : "OpenOCD was not running" };
}
isGDBServerRunning() { return !!this.processManager.get(OPENOCD_PROCESS); }
getGDBServerStatus() {
return { running: this.isGDBServerRunning(), gdbPort: this.config.gdbPort, rttTelnetPort: -1 };
}
getGDBServerOutput(lines = 50) { return this.gdbOutputBuffer.slice(-lines); }
supportsRTT() { return false; }
isDeviceConfigured() {
return !!this.config.targetConfig && this.config.targetConfig !== "";
}
getDeviceName() { return this.config.targetConfig; }
setDevice(device) { this.config.targetConfig = device; }
async listDevices() {
return this.exec(["init", "targets"]);
}
dispose() { this.processManager.kill(OPENOCD_PROCESS); }
}
exports.OpenOCDBackend = OpenOCDBackend;
//# sourceMappingURL=openocd.js.map