jlink-mcp
Version:
MCP server for SEGGER J-Link debug probes — LLM-driven embedded debugging with RTT, GDB server, and Trice/Pigweed support
235 lines • 8.37 kB
JavaScript
"use strict";
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.RTTClient = void 0;
const net = __importStar(require("net"));
const events_1 = require("events");
const logger_1 = require("../utils/logger");
/** Strip ANSI escape sequences from text */
function stripAnsi(text) {
return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\[0m/g, "");
}
/** Check if a line is SEGGER RTT header boilerplate */
function isRttHeader(line) {
return (line.startsWith("SEGGER J-Link") ||
line.startsWith("Process: JLink") ||
line.trim() === "");
}
/** Parse a Zephyr-style log line: [HH:MM:SS.mmm,uuu] <level> module: message */
function parseZephyrLog(line) {
const match = line.match(/^\[(\d{2}:\d{2}:\d{2}\.\d{3},?\d{0,3})\]\s*<(\w+)>\s*(\w[\w._-]*):\s*(.*)$/);
if (match) {
return {
deviceTime: match[1],
level: match[2],
module: match[3],
message: match[4],
raw: line,
};
}
return { deviceTime: null, level: null, module: null, message: line, raw: line };
}
/**
* Connects to RTT via telnet (when JLinkGDBServer is running).
* JLinkGDBServer exposes RTT channel 0 on a configurable telnet port (default 19021).
*
* Automatically strips ANSI escape codes and SEGGER banners.
* Parses Zephyr-style log lines into structured fields.
*/
class RTTClient extends events_1.EventEmitter {
socket = null;
host;
port;
messages = [];
/** Flat buffer of all parsed log lines for searching */
allLines = [];
maxMessages = 5000;
maxLines = 20000;
reconnectTimer = null;
connected = false;
lineBuffer = "";
constructor(host = "localhost", port = 19021) {
super();
this.host = host;
this.port = port;
}
/** Connect to RTT telnet port */
connect() {
return new Promise((resolve, reject) => {
if (this.connected) {
resolve();
return;
}
this.socket = new net.Socket();
this.socket.on("connect", () => {
(0, logger_1.log)(`RTT Client connected to ${this.host}:${this.port}`);
this.connected = true;
this.emit("connected");
resolve();
});
this.socket.on("data", (data) => {
const raw = data.toString();
// Clean and split into lines, handling partial lines
const cleaned = stripAnsi(raw);
this.lineBuffer += cleaned;
const parts = this.lineBuffer.split("\n");
// Last part might be incomplete
this.lineBuffer = parts.pop() || "";
const parsedLines = [];
for (const part of parts) {
const trimmed = part.trim();
if (!trimmed || isRttHeader(trimmed))
continue;
parsedLines.push(parseZephyrLog(trimmed));
}
if (parsedLines.length > 0) {
const msg = {
channel: 0,
timestamp: new Date(),
rawData: raw,
lines: parsedLines,
};
this.messages.push(msg);
if (this.messages.length > this.maxMessages) {
this.messages.shift();
}
for (const line of parsedLines) {
this.allLines.push(line);
}
while (this.allLines.length > this.maxLines) {
this.allLines.shift();
}
this.emit("data", msg);
}
});
this.socket.on("close", () => {
(0, logger_1.log)("RTT Client disconnected");
this.connected = false;
this.emit("disconnected");
});
this.socket.on("error", (err) => {
(0, logger_1.logError)("RTT Client error", err);
this.connected = false;
reject(err);
});
this.socket.connect(this.port, this.host);
});
}
/** Disconnect */
disconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.socket) {
this.socket.destroy();
this.socket = null;
}
this.connected = false;
this.lineBuffer = "";
}
/** Send data to RTT down-channel (host → device) */
send(data) {
if (!this.socket || !this.connected)
return false;
this.socket.write(data);
return true;
}
/** Get recent log lines as formatted text */
getLines(count = 50) {
const lines = this.allLines.slice(-count);
return lines.map((l) => {
if (l.deviceTime && l.level && l.module) {
return `[${l.deviceTime}] <${l.level}> ${l.module}: ${l.message}`;
}
return l.message;
});
}
/** Search/filter log lines */
search(opts) {
let results = [...this.allLines];
if (opts.level) {
const lvl = opts.level.toLowerCase();
results = results.filter((l) => l.level?.toLowerCase() === lvl);
}
if (opts.module) {
const mod = opts.module.toLowerCase();
results = results.filter((l) => l.module?.toLowerCase().includes(mod));
}
if (opts.pattern) {
try {
const re = new RegExp(opts.pattern, "i");
results = results.filter((l) => re.test(l.message) || re.test(l.raw));
}
catch {
// fall back to simple string match
const pat = opts.pattern.toLowerCase();
results = results.filter((l) => l.message.toLowerCase().includes(pat) || l.raw.toLowerCase().includes(pat));
}
}
if (opts.count) {
results = results.slice(-opts.count);
}
return results;
}
/** Get messages (for backward compat / resource access) */
getMessages(count) {
if (count)
return this.messages.slice(-count);
return [...this.messages];
}
/** Clear all buffers */
clearBuffer() {
this.messages = [];
this.allLines = [];
this.lineBuffer = "";
}
/** Check connection status */
isConnected() {
return this.connected;
}
/** Get buffer stats */
getStats() {
return {
connected: this.connected,
lineCount: this.allLines.length,
messageCount: this.messages.length,
host: this.host,
port: this.port,
};
}
}
exports.RTTClient = RTTClient;
//# sourceMappingURL=rtt-client.js.map