278 lines
8.2 KiB
JavaScript
278 lines
8.2 KiB
JavaScript
const net = require('net');
|
|
const { spawn } = require('child_process');
|
|
|
|
const { getJLinkGdbServerExe } = require('./jlinkPaths.cjs');
|
|
const { withGdbSpawn } = require('./jlinkGdbSpawnLock.cjs');
|
|
const { waitUntilTcpPortFree } = require('./jlinkPortCheck.cjs');
|
|
|
|
const RTT_CONFIG_PREFIX = '$$SEGGER_TELNET_ConfigStr=';
|
|
const RTT_CONFIG_SUFFIX = '$$';
|
|
|
|
const buildRttConfigString = channel =>
|
|
`${RTT_CONFIG_PREFIX}RTTCh;${channel}${RTT_CONFIG_SUFFIX}`;
|
|
|
|
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
const waitForGdbTargetReady = async (readLog, timeoutMs = 12000) => {
|
|
const startedAt = Date.now();
|
|
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
const log = readLog();
|
|
if (/connected to target|cpu is running|resetting target|listening on tcp/i.test(log)) {
|
|
await wait(250);
|
|
return;
|
|
}
|
|
await wait(100);
|
|
}
|
|
};
|
|
|
|
const waitForTcpPort = async (host, port, timeoutMs = 25000, context = {}) => {
|
|
const startedAt = Date.now();
|
|
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
const socket = net.createConnection({ host, port }, () => {
|
|
socket.end();
|
|
resolve();
|
|
});
|
|
socket.on('error', reject);
|
|
});
|
|
return;
|
|
} catch {
|
|
await wait(200);
|
|
}
|
|
}
|
|
|
|
const bootTail = (context.getBootLog?.() || '').trim().slice(-700);
|
|
const serial = context.serialNumber ? `J-Link SN ${context.serialNumber}, ` : '';
|
|
const gdbPortHint = /Failed to open listener port/i.test(bootTail)
|
|
? 'A GDB/SWO port is still in use — Stop both sessions, end all JLinkGDBServerCL in Task Manager, wait a few seconds, then Start again. '
|
|
: '';
|
|
throw new Error(
|
|
`Timed out waiting for RTT server on ${host}:${port} (${serial}port ${port}). ` +
|
|
(bootTail ? `GDB log: ${bootTail} ` : '') +
|
|
gdbPortHint +
|
|
'Check SWD, DTM RTT firmware, and that no stale JLinkGDBServerCL holds this port. ' +
|
|
'With two probes, start the other session only after the first shows Running.'
|
|
);
|
|
};
|
|
|
|
class JlinkRttSession {
|
|
#serialNumber;
|
|
#device;
|
|
#speed;
|
|
#gdbPort;
|
|
#swoPort;
|
|
#rttPort;
|
|
#rttChannel;
|
|
#gdbServer;
|
|
#socket;
|
|
#dataListeners = new Set();
|
|
#started = false;
|
|
#bootLog = '';
|
|
|
|
constructor({ serialNumber, device, speed, gdbPort, swoPort, rttPort, rttChannel = 0 }) {
|
|
this.#serialNumber = serialNumber;
|
|
this.#device = device;
|
|
this.#speed = speed;
|
|
this.#gdbPort = gdbPort;
|
|
this.#swoPort = swoPort;
|
|
this.#rttPort = rttPort;
|
|
this.#rttChannel = rttChannel;
|
|
}
|
|
|
|
get gdbPort() {
|
|
return this.#gdbPort;
|
|
}
|
|
|
|
get rttPort() {
|
|
return this.#rttPort;
|
|
}
|
|
|
|
get bootLog() {
|
|
return this.#bootLog;
|
|
}
|
|
|
|
async start() {
|
|
if (this.#started) return;
|
|
|
|
return withGdbSpawn(async () => {
|
|
if (this.#started) return;
|
|
|
|
await this.#startGdbServer();
|
|
});
|
|
}
|
|
|
|
async #startGdbServer() {
|
|
const gdbServerExe = getJLinkGdbServerExe();
|
|
if (!gdbServerExe) {
|
|
throw new Error(
|
|
'J-Link GDB Server was not found. Install SEGGER J-Link software or set SEGGER_JLINK_PATH.'
|
|
);
|
|
}
|
|
|
|
await waitUntilTcpPortFree('127.0.0.1', this.#gdbPort, 12000);
|
|
await waitUntilTcpPortFree('127.0.0.1', this.#swoPort, 12000);
|
|
await waitUntilTcpPortFree('127.0.0.1', this.#rttPort, 12000);
|
|
|
|
const args = [
|
|
'-select',
|
|
`USB=${this.#serialNumber}`,
|
|
'-device',
|
|
this.#device,
|
|
'-if',
|
|
'SWD',
|
|
'-speed',
|
|
String(this.#speed),
|
|
'-port',
|
|
String(this.#gdbPort),
|
|
'-swoport',
|
|
String(this.#swoPort),
|
|
'-RTTTelnetPort',
|
|
String(this.#rttPort),
|
|
'-nogui',
|
|
'1',
|
|
// DTM RTT firmware must keep running; default GDB Server halts on connect.
|
|
'-nohalt',
|
|
'-noir',
|
|
];
|
|
|
|
try {
|
|
this.#bootLog = '';
|
|
this.#gdbServer = spawn(gdbServerExe, args, {
|
|
windowsHide: true,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
const appendLog = chunk => {
|
|
this.#bootLog += chunk.toString();
|
|
if (this.#bootLog.length > 16000) {
|
|
this.#bootLog = this.#bootLog.slice(-16000);
|
|
}
|
|
};
|
|
|
|
this.#gdbServer.stdout.on('data', appendLog);
|
|
this.#gdbServer.stderr.on('data', appendLog);
|
|
|
|
this.#gdbServer.on('exit', code => {
|
|
if (!this.#started) return;
|
|
const detail = this.#bootLog.trim() ? `: ${this.#bootLog.trim().slice(-400)}` : '';
|
|
for (const listener of this.#dataListeners) {
|
|
listener(
|
|
Buffer.from([]),
|
|
new Error(`J-Link GDB Server exited (${code ?? 'unknown'})${detail}`)
|
|
);
|
|
}
|
|
});
|
|
|
|
await waitForTcpPort('127.0.0.1', this.#rttPort, 25000, {
|
|
getBootLog: () => this.#bootLog,
|
|
serialNumber: this.#serialNumber,
|
|
});
|
|
await waitForGdbTargetReady(() => this.#bootLog);
|
|
await this.#openRttSocket();
|
|
// Allow DTM main (SEGGER_RTT_Init + dtm_init) to finish after target runs.
|
|
await wait(600);
|
|
this.#started = true;
|
|
} catch (error) {
|
|
await this.close().catch(() => {});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async #openRttSocket() {
|
|
this.#socket = await new Promise((resolve, reject) => {
|
|
const socket = net.createConnection(
|
|
{ host: '127.0.0.1', port: this.#rttPort },
|
|
() => resolve(socket)
|
|
);
|
|
socket.on('error', reject);
|
|
});
|
|
|
|
this.#socket.setNoDelay(true);
|
|
this.#socket.on('data', chunk => {
|
|
for (const listener of this.#dataListeners) {
|
|
listener(chunk);
|
|
}
|
|
});
|
|
|
|
this.#socket.on('error', error => {
|
|
for (const listener of this.#dataListeners) {
|
|
listener(Buffer.alloc(0), error);
|
|
}
|
|
});
|
|
|
|
const configString = buildRttConfigString(this.#rttChannel);
|
|
await new Promise((resolve, reject) => {
|
|
this.#socket.write(configString, error => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
await wait(50);
|
|
}
|
|
|
|
onData(listener) {
|
|
this.#dataListeners.add(listener);
|
|
return () => this.#dataListeners.delete(listener);
|
|
}
|
|
|
|
async write(buffer) {
|
|
if (!this.#socket) {
|
|
throw new Error('RTT socket is not connected.');
|
|
}
|
|
|
|
await new Promise((resolve, reject) => {
|
|
this.#socket.write(buffer, error => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
async close() {
|
|
this.#started = false;
|
|
this.#dataListeners.clear();
|
|
|
|
if (this.#socket) {
|
|
await new Promise(resolve => {
|
|
this.#socket.end(() => resolve());
|
|
this.#socket.destroy();
|
|
});
|
|
this.#socket = null;
|
|
}
|
|
|
|
const gdbPort = this.#gdbPort;
|
|
const swoPort = this.#swoPort;
|
|
const rttPort = this.#rttPort;
|
|
|
|
if (this.#gdbServer && !this.#gdbServer.killed) {
|
|
const child = this.#gdbServer;
|
|
child.kill();
|
|
await new Promise(resolve => {
|
|
child.once('exit', () => resolve());
|
|
setTimeout(resolve, 2500);
|
|
});
|
|
this.#gdbServer = null;
|
|
}
|
|
|
|
for (const port of [gdbPort, swoPort, rttPort]) {
|
|
await waitUntilTcpPortFree('127.0.0.1', port, 8000).catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
JlinkRttSession,
|
|
buildRttConfigString,
|
|
RTT_CHANNEL_DTM: 0,
|
|
};
|