initial commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
/** Serialize J-Link GDB Server starts so two probes on USB do not race. */
|
||||
let spawnChain = Promise.resolve();
|
||||
|
||||
const withGdbSpawn = async fn => {
|
||||
const run = spawnChain.then(() => fn());
|
||||
spawnChain = run
|
||||
.then(() => wait(450))
|
||||
.catch(() => wait(450));
|
||||
return run;
|
||||
};
|
||||
|
||||
module.exports = { withGdbSpawn };
|
||||
@@ -0,0 +1,208 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const STATIC_INSTALL_DIRS = [
|
||||
process.env.SEGGER_JLINK_PATH,
|
||||
process.env.JLINK_PATH,
|
||||
'C:\\Program Files\\SEGGER\\JLink',
|
||||
'C:\\Program Files\\SEGGER\\JLink_V942',
|
||||
'C:\\Program Files (x86)\\SEGGER\\JLink',
|
||||
'C:\\Program Files (x86)\\SEGGER\\JLink_V942',
|
||||
path.join(process.env.ProgramFiles || '', 'SEGGER', 'JLink'),
|
||||
path.join(process.env.ProgramFiles || '', 'SEGGER', 'JLink_V942'),
|
||||
path.join(process.env['ProgramFiles(x86)'] || '', 'SEGGER', 'JLink'),
|
||||
path.join(process.env['ProgramFiles(x86)'] || '', 'SEGGER', 'JLink_V942'),
|
||||
'C:\\nrfutil\\bin',
|
||||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'SEGGER', 'JLink'),
|
||||
].filter(Boolean);
|
||||
|
||||
const SEGGER_ROOTS = [
|
||||
path.join(process.env.ProgramFiles || 'C:\\Program Files', 'SEGGER'),
|
||||
path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'SEGGER'),
|
||||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'SEGGER'),
|
||||
];
|
||||
|
||||
/** SEGGER installer default: Program Files\SEGGER\JLink_Vxxx */
|
||||
const discoverSeggerJLinkDirs = () => {
|
||||
const discovered = [];
|
||||
|
||||
for (const root of SEGGER_ROOTS) {
|
||||
if (!root || !fs.existsSync(root)) continue;
|
||||
|
||||
let entries = [];
|
||||
try {
|
||||
entries = fs.readdirSync(root, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (!/^JLink/i.test(entry.name)) continue;
|
||||
discovered.push(path.join(root, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
return discovered;
|
||||
};
|
||||
|
||||
const parseJLinkFolderVersion = directory => {
|
||||
const name = path.basename(directory);
|
||||
const match = name.match(/^JLink(?:_V)?(.+)$/i);
|
||||
if (!match) {
|
||||
return { sortKey: 0, label: name };
|
||||
}
|
||||
|
||||
const token = match[1];
|
||||
const numeric = Number.parseInt(token, 10) || 0;
|
||||
const suffix = token.slice(String(numeric).length);
|
||||
|
||||
return {
|
||||
sortKey: numeric * 1000 + (suffix ? suffix.charCodeAt(0) : 0),
|
||||
label: name,
|
||||
};
|
||||
};
|
||||
|
||||
/** Prefer newest SEGGER pack (e.g. JLink_V942 over JLink_V898). */
|
||||
const sortInstallDirsNewestFirst = directories =>
|
||||
[...directories].sort((left, right) => {
|
||||
const leftVersion = parseJLinkFolderVersion(left);
|
||||
const rightVersion = parseJLinkFolderVersion(right);
|
||||
if (rightVersion.sortKey !== leftVersion.sortKey) {
|
||||
return rightVersion.sortKey - leftVersion.sortKey;
|
||||
}
|
||||
|
||||
try {
|
||||
return fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const getInstallDirectories = () => {
|
||||
const envDirs = [process.env.SEGGER_JLINK_PATH, process.env.JLINK_PATH].filter(Boolean);
|
||||
const discovered = sortInstallDirsNewestFirst(discoverSeggerJLinkDirs());
|
||||
const staticDirs = STATIC_INSTALL_DIRS.filter(
|
||||
directory => directory && !envDirs.includes(directory)
|
||||
);
|
||||
|
||||
return [...new Set([...envDirs, ...discovered, ...staticDirs])];
|
||||
};
|
||||
|
||||
const findExecutable = (name, directories = getInstallDirectories()) => {
|
||||
for (const directory of directories) {
|
||||
const candidate = path.join(directory, name);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const findOnPath = name => {
|
||||
const pathEntries = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
||||
|
||||
for (const entry of pathEntries) {
|
||||
const candidate = path.join(entry, name);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getNrfjprogExe = () => findExecutable('nrfjprog.exe') || findOnPath('nrfjprog.exe');
|
||||
|
||||
const getJLinkExe = () => {
|
||||
const direct = findExecutable('JLink.exe') || findOnPath('JLink.exe');
|
||||
if (direct) return direct;
|
||||
|
||||
const nrfjprogExe = getNrfjprogExe();
|
||||
if (nrfjprogExe) {
|
||||
const sibling = path.join(path.dirname(nrfjprogExe), 'JLink.exe');
|
||||
if (fs.existsSync(sibling)) return sibling;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getJLinkGdbServerExe = () => {
|
||||
const candidates = ['JLinkGDBServerCL.exe', 'JLinkGDBServer.exe'];
|
||||
for (const name of candidates) {
|
||||
const direct = findExecutable(name) || findOnPath(name);
|
||||
if (direct) return direct;
|
||||
}
|
||||
|
||||
const nrfjprogExe = getNrfjprogExe();
|
||||
if (nrfjprogExe) {
|
||||
const directory = path.dirname(nrfjprogExe);
|
||||
for (const name of candidates) {
|
||||
const sibling = path.join(directory, name);
|
||||
if (fs.existsSync(sibling)) return sibling;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const hasSeggerRttStack = () => Boolean(getJLinkGdbServerExe());
|
||||
|
||||
let cachedDiagnostics;
|
||||
|
||||
const clearJLinkDiagnosticsCache = () => {
|
||||
cachedDiagnostics = null;
|
||||
};
|
||||
|
||||
const getJLinkDiagnostics = () => {
|
||||
if (cachedDiagnostics) return cachedDiagnostics;
|
||||
|
||||
let nrfjprogIds = [];
|
||||
const nrfjprogExe = getNrfjprogExe();
|
||||
const jlinkExe = getJLinkExe();
|
||||
const gdbServerExe = getJLinkGdbServerExe();
|
||||
|
||||
if (nrfjprogExe) {
|
||||
try {
|
||||
const output = execFileSync(nrfjprogExe, ['--ids'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 10000,
|
||||
windowsHide: true,
|
||||
});
|
||||
nrfjprogIds = output
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(line => /^\d+$/.test(line));
|
||||
} catch {
|
||||
nrfjprogIds = [];
|
||||
}
|
||||
}
|
||||
|
||||
cachedDiagnostics = {
|
||||
nrfjprogExe,
|
||||
jlinkExe,
|
||||
gdbServerExe,
|
||||
gdbServerDir: gdbServerExe ? path.dirname(gdbServerExe) : '',
|
||||
nrfjprogIds,
|
||||
hasRttStack: Boolean(gdbServerExe),
|
||||
installDirectories: getInstallDirectories(),
|
||||
};
|
||||
|
||||
return cachedDiagnostics;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
STATIC_INSTALL_DIRS,
|
||||
getInstallDirectories,
|
||||
discoverSeggerJLinkDirs,
|
||||
findExecutable,
|
||||
findOnPath,
|
||||
getNrfjprogExe,
|
||||
getJLinkExe,
|
||||
getJLinkGdbServerExe,
|
||||
hasSeggerRttStack,
|
||||
getJLinkDiagnostics,
|
||||
clearJLinkDiagnosticsCache,
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
const net = require('net');
|
||||
|
||||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const probeTcpPort = (host, port, timeoutMs = 500) =>
|
||||
new Promise(resolve => {
|
||||
const socket = net.createConnection({ host, port }, () => {
|
||||
socket.end();
|
||||
resolve(true);
|
||||
});
|
||||
socket.on('error', () => resolve(false));
|
||||
socket.setTimeout(timeoutMs, () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
/** Something is accepting connections on this port. */
|
||||
const isTcpPortBusy = (host, port) => probeTcpPort(host, port);
|
||||
|
||||
/** True if we can bind — more reliable than connect probe before GDB Server starts. */
|
||||
const canBindTcpPort = (host, port) =>
|
||||
new Promise(resolve => {
|
||||
const server = net.createServer();
|
||||
server.once('error', () => resolve(false));
|
||||
server.listen({ host, port }, () => {
|
||||
server.close(() => resolve(true));
|
||||
});
|
||||
});
|
||||
|
||||
const waitUntilTcpPortFree = async (host, port, timeoutMs = 10000) => {
|
||||
const startedAt = Date.now();
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
if ((await canBindTcpPort(host, port)) && !(await isTcpPortBusy(host, port))) {
|
||||
return;
|
||||
}
|
||||
await wait(200);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`TCP 포트 ${port}(${host})이 아직 사용 중입니다. JLinkGDBServerCL을 종료한 뒤 몇 초 기다려 주세요.`
|
||||
);
|
||||
};
|
||||
|
||||
const assertTcpPortFree = async (host, port, label = 'RTT') => {
|
||||
if (await canBindTcpPort(host, port)) return;
|
||||
|
||||
throw new Error(
|
||||
`${label} 포트 ${port}(${host})이 이미 사용 중입니다. Receiver/Transmitter를 Stop한 뒤 몇 초 기다리거나, 작업 관리자에서 JLinkGDBServerCL 프로세스를 종료하세요.`
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
isTcpPortBusy,
|
||||
canBindTcpPort,
|
||||
waitUntilTcpPortFree,
|
||||
assertTcpPortFree,
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFile } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const {
|
||||
getJLinkExe,
|
||||
getNrfjprogExe,
|
||||
getJLinkDiagnostics,
|
||||
clearJLinkDiagnosticsCache,
|
||||
} = require('./jlinkPaths.cjs');
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const parseShowEmuList = output => {
|
||||
const probes = [];
|
||||
const patterns = [
|
||||
/Serial number:\s*(\d+).*?(?:ProductName|Product name):\s*([^,\r\n]+).*?(?:Connection):\s*([^,\r\n]+)/i,
|
||||
/Serial number:\s*(\d+).*?(?:ProductName|Product name):\s*([^,\r\n]+)/i,
|
||||
/Serial number:\s*(\d+)/i,
|
||||
];
|
||||
|
||||
for (const line of output.split(/\r?\n/)) {
|
||||
if (!/serial number/i.test(line)) continue;
|
||||
|
||||
let serialNumber;
|
||||
let productName = 'J-Link';
|
||||
let connection = 'USB';
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = line.match(pattern);
|
||||
if (!match) continue;
|
||||
serialNumber = match[1];
|
||||
if (match[2]) productName = match[2].trim();
|
||||
if (match[3]) connection = match[3].trim();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!serialNumber) continue;
|
||||
|
||||
probes.push({
|
||||
path: serialNumber,
|
||||
serialNumber,
|
||||
friendlyName: productName,
|
||||
manufacturer: 'SEGGER',
|
||||
connection,
|
||||
source: 'jlink',
|
||||
});
|
||||
}
|
||||
|
||||
return probes;
|
||||
};
|
||||
|
||||
const parseNrfjprogIds = output =>
|
||||
output
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(line => /^\d+$/.test(line))
|
||||
.map(serialNumber => ({
|
||||
path: serialNumber,
|
||||
serialNumber,
|
||||
friendlyName: `J-Link (${serialNumber})`,
|
||||
manufacturer: 'SEGGER',
|
||||
connection: 'USB',
|
||||
source: 'nrfjprog',
|
||||
}));
|
||||
|
||||
const listViaJLinkExe = async jlinkExe => {
|
||||
const scriptPath = path.join(os.tmpdir(), `dtm-jlink-emu-${process.pid}.jlink`);
|
||||
await fs.promises.writeFile(scriptPath, 'ShowEmuList\nexit\n', 'utf8');
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
jlinkExe,
|
||||
['-CommandFile', scriptPath, '-nogui', '1'],
|
||||
{
|
||||
timeout: 15000,
|
||||
windowsHide: true,
|
||||
maxBuffer: 1024 * 1024,
|
||||
}
|
||||
);
|
||||
|
||||
return parseShowEmuList(`${stdout}\n${stderr}`);
|
||||
} finally {
|
||||
await fs.promises.unlink(scriptPath).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const listViaNrfjprog = async nrfjprogExe => {
|
||||
const { stdout, stderr } = await execFileAsync(nrfjprogExe, ['--ids'], {
|
||||
timeout: 10000,
|
||||
windowsHide: true,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
return parseNrfjprogIds(`${stdout}\n${stderr}`);
|
||||
};
|
||||
|
||||
const listJLinkProbes = async () => {
|
||||
clearJLinkDiagnosticsCache();
|
||||
const jlinkExe = getJLinkExe();
|
||||
const nrfjprogExe = getNrfjprogExe();
|
||||
const errors = [];
|
||||
|
||||
if (jlinkExe) {
|
||||
try {
|
||||
const probes = await listViaJLinkExe(jlinkExe);
|
||||
if (probes.length > 0) {
|
||||
return probes;
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (nrfjprogExe) {
|
||||
try {
|
||||
const probes = await listViaNrfjprog(nrfjprogExe);
|
||||
if (probes.length > 0) {
|
||||
return probes;
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const diagnostics = getJLinkDiagnostics();
|
||||
|
||||
if (!jlinkExe && !nrfjprogExe) {
|
||||
throw new Error(
|
||||
'nrfjprog and JLink.exe were not found. Install nRF Command Line Tools or SEGGER J-Link software, or set SEGGER_JLINK_PATH.'
|
||||
);
|
||||
}
|
||||
|
||||
if (diagnostics.nrfjprogIds.length > 0) {
|
||||
return parseNrfjprogIds(diagnostics.nrfjprogIds.join('\n'));
|
||||
}
|
||||
|
||||
const detail = errors.length > 0 ? ` (${errors.join('; ')})` : '';
|
||||
|
||||
throw new Error(
|
||||
`No J-Link responded on USB. Plug in the probe, close Ozone or RTT Viewer, then refresh.${detail}`
|
||||
);
|
||||
};
|
||||
|
||||
const getJLinkSetupHint = () => {
|
||||
const diagnostics = getJLinkDiagnostics();
|
||||
|
||||
if (!diagnostics.gdbServerExe) {
|
||||
const ids =
|
||||
diagnostics.nrfjprogIds.length > 0
|
||||
? ` Detected probe SN: ${diagnostics.nrfjprogIds.join(', ')}.`
|
||||
: '';
|
||||
const searched =
|
||||
diagnostics.installDirectories?.length > 0
|
||||
? ` Searched: ${diagnostics.installDirectories.slice(0, 4).join('; ')}…`
|
||||
: '';
|
||||
|
||||
return (
|
||||
'RTT needs JLinkGDBServerCL.exe (e.g. C:\\Program Files\\SEGGER\\JLink_V942). Install or set SEGGER_JLINK_PATH.' +
|
||||
ids +
|
||||
searched +
|
||||
' nrfjprog alone is not enough for RTT.'
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
listJLinkProbes,
|
||||
parseShowEmuList,
|
||||
parseNrfjprogIds,
|
||||
getJLinkSetupHint,
|
||||
};
|
||||
@@ -0,0 +1,277 @@
|
||||
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,
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
const { canBindTcpPort } = require('./jlinkPortCheck.cjs');
|
||||
|
||||
/** Preferred ports per session (dynamic fallback if busy). */
|
||||
const JLINK_PORTS_BY_SESSION = {
|
||||
receiver: {
|
||||
gdb: 29021,
|
||||
swo: 29022,
|
||||
rtt: 19021,
|
||||
},
|
||||
transmitter: {
|
||||
gdb: 29031,
|
||||
swo: 29032,
|
||||
rtt: 19022,
|
||||
},
|
||||
};
|
||||
|
||||
const GDB_PORT_RANGE = { min: 29000, max: 29199 };
|
||||
const SWO_PORT_RANGE = { min: 29000, max: 29199 };
|
||||
const RTT_PORT_RANGE = { min: 19020, max: 19199 };
|
||||
|
||||
const portCandidates = (preferred, min, max) => {
|
||||
const ordered = [preferred];
|
||||
for (let port = min; port <= max; port++) {
|
||||
if (port !== preferred) ordered.push(port);
|
||||
}
|
||||
return ordered;
|
||||
};
|
||||
|
||||
const pickTcpPort = async (preferred, range, reserved) => {
|
||||
for (const port of portCandidates(preferred, range.min, range.max)) {
|
||||
if (reserved.has(port)) continue;
|
||||
if (await canBindTcpPort('127.0.0.1', port)) {
|
||||
reserved.add(port);
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`사용 가능한 TCP 포트가 없습니다 (선호 ${preferred}, 범위 ${range.min}-${range.max}). JLinkGDBServerCL을 모두 종료한 뒤 다시 시도하세요.`
|
||||
);
|
||||
};
|
||||
|
||||
const allocateSessionPorts = async (sessionId, reserved = new Set()) => {
|
||||
const preferred = JLINK_PORTS_BY_SESSION[sessionId];
|
||||
if (!preferred) {
|
||||
throw new Error(`Unknown session for port allocation: ${sessionId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
gdb: await pickTcpPort(preferred.gdb, GDB_PORT_RANGE, reserved),
|
||||
swo: await pickTcpPort(preferred.swo, SWO_PORT_RANGE, reserved),
|
||||
rtt: await pickTcpPort(preferred.rtt, RTT_PORT_RANGE, reserved),
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
JLINK_PORTS_BY_SESSION,
|
||||
allocateSessionPorts,
|
||||
};
|
||||
Reference in New Issue
Block a user