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, };