initial commit

This commit is contained in:
2026-05-18 17:36:05 +09:00
commit fadeba40cc
24 changed files with 12173 additions and 0 deletions
+496
View File
@@ -0,0 +1,496 @@
const EventEmitter = require('events');
const {
DTMTransport,
DTM_CONTROL,
DTM_DC,
DTM_EVENT,
DTM_PARAMETER,
} = require('./DTM_transport.cjs');
const validate0x09Command = res => {
if (!Array.isArray(res) || res.length !== 2 || (res[1] & 0x01) !== 0) {
throw new Error(`Invalid result: ${JSON.stringify(res)}`);
}
const rawDbmValue = (res[1] & 0xfe) >> 1;
const modifier = res[0] & 0x01 ? -128 : 0;
return modifier + rawDbmValue;
};
const validateResult = res => {
if (!Array.isArray(res) || res.length !== 2 || (res[0] !== 0 && res[1] !== 0)) {
throw new Error('Invalid result');
}
return res;
};
const DEFAULT_COMMAND_TIMEOUT_MS = 3000;
const RESET_COMMAND_TIMEOUT_MS = 8000;
const channelToFrequency = channel => 2402 + 2 * channel;
const reportSuccess = report => (report && report[0] & 0x01) === 0;
const RECEIVER_LIVE_POLL_MS = 400;
const packetCountFromResponse = response => {
if (!Array.isArray(response) || response.length < 2) {
return 0;
}
if (((response[0] & 0x80) >> 7) !== DTM_EVENT.LE_PACKET_REPORT_EVENT) {
return 0;
}
return ((response[0] & 0x3f) << 8) | response[1];
};
class DTM {
#dtmTransport;
#lengthPayload = 1;
#modulationPayload = 0;
#phyPayload = 0x01;
#dbmPayload = 0;
#isTransmitting = false;
#isReceiving = false;
#timedOut = false;
#sweepTimedOut = false;
#timeoutEvent;
#onEndEvent;
#activeReceiverChannel = null;
#receiverHopAccumulate = false;
#receiverLiveTotal = 0;
#receiverPollTimer = null;
#receiverPollInFlight = false;
#receiverPollParams = null;
#eventEmitter = new EventEmitter();
constructor(jlinkSerial, transportOptions = {}) {
this.#dtmTransport = new DTMTransport(jlinkSerial, transportOptions);
this.#dtmTransport.onPacketReport(count => {
if (
!this.#isReceiving ||
this.#activeReceiverChannel === null ||
this.#receiverPollInFlight ||
this.#receiverPollTimer
) {
return;
}
this.#eventEmitter.emit('packets', {
type: 'receiver',
channel: this.#activeReceiverChannel,
packets: count,
});
});
}
async connect() {
await this.#dtmTransport.open();
}
onReset(listener) {
this.#eventEmitter.on('reset', listener);
return () => this.#eventEmitter.removeListener('reset', listener);
}
onStarted(listener) {
this.#eventEmitter.on('started', listener);
return () => this.#eventEmitter.removeListener('started', listener);
}
onEnded(listener) {
this.#eventEmitter.on('ended', listener);
return () => this.#eventEmitter.removeListener('ended', listener);
}
onTraffic(listener) {
return this.#dtmTransport.onTraffic(listener);
}
onPackets(listener) {
this.#eventEmitter.on('packets', listener);
return () => this.#eventEmitter.removeListener('packets', listener);
}
#resetEvent() {
this.#eventEmitter.emit('reset');
}
#startedTransmitterEvent(channel) {
this.#eventEmitter.emit('started', { type: 'transmitter', channel });
return () => this.#eventEmitter.emit('ended', { type: 'transmitter', channel });
}
#startedReceiverEvent(channel, accumulate = false) {
this.#activeReceiverChannel = channel;
this.#receiverHopAccumulate = accumulate;
this.#eventEmitter.emit('started', { type: 'receiver', channel });
return packets => {
this.#activeReceiverChannel = null;
this.#eventEmitter.emit('ended', {
type: 'receiver',
channel,
packets,
accumulate,
});
};
}
startTimeoutEvent(rxtxFlag, timeout) {
let timeoutEvent;
this.#timedOut = false;
if (timeout > 0) {
timeoutEvent = setTimeout(() => {
this.#timedOut = true;
if (rxtxFlag() && !this.#sweepTimedOut) {
this.endCurrentTest().catch(() => undefined);
}
}, timeout);
}
return timeoutEvent;
}
startSweepTimeoutEvent(rxtxFlag, timeout) {
let timeoutEvent;
this.#sweepTimedOut = false;
if (timeout > 0) {
timeoutEvent = setTimeout(() => {
this.#sweepTimedOut = true;
if (rxtxFlag() && !this.#timedOut) {
this.endCurrentTest().catch(() => undefined);
}
}, timeout);
}
return timeoutEvent;
}
endEventDataReceived() {
return new Promise(done => {
this.#onEndEvent = received => {
this.#onEndEvent = undefined;
done({ received });
};
});
}
async endCurrentTest() {
const response = await this.#dtmTransport.sendCMD(DTMTransport.createEndCMD());
const receivedPackets = packetCountFromResponse(response);
if (this.#onEndEvent) {
this.#onEndEvent(receivedPackets);
}
return response;
}
#emitReceiverPacketTotal(channel, total) {
this.#eventEmitter.emit('packets', {
type: 'receiver',
channel,
packets: total,
});
}
startReceiverLivePoll(channel, bitpattern, length) {
this.stopReceiverLivePoll();
this.#receiverLiveTotal = 0;
this.#receiverPollParams = { channel, bitpattern, length };
this.#receiverPollTimer = setInterval(() => {
this.#pollReceiverLiveCount().catch(() => undefined);
}, RECEIVER_LIVE_POLL_MS);
}
stopReceiverLivePoll() {
if (this.#receiverPollTimer) {
clearInterval(this.#receiverPollTimer);
this.#receiverPollTimer = null;
}
this.#receiverPollParams = null;
}
async #pollReceiverLiveCount() {
if (
this.#receiverPollInFlight ||
!this.#isReceiving ||
this.#timedOut ||
!this.#receiverPollParams
) {
return;
}
const { channel, bitpattern, length } = this.#receiverPollParams;
this.#receiverPollInFlight = true;
try {
const endResponse = await this.#dtmTransport.sendCMD(DTMTransport.createEndCMD());
const hopCount = packetCountFromResponse(endResponse);
this.#receiverLiveTotal += hopCount;
this.#emitReceiverPacketTotal(channel, this.#receiverLiveTotal);
if (!this.#isReceiving || this.#timedOut) {
return;
}
const rxResponse = await this.#dtmTransport.sendCMD(
DTMTransport.createReceiverCMD(channelToFrequency(channel), length, bitpattern)
);
if (!reportSuccess(rxResponse)) {
this.#isReceiving = false;
}
} finally {
this.#receiverPollInFlight = false;
}
}
static carrierTestCMD(frequency, length, bitpattern) {
let lengthParam = length & 0x3f;
if (bitpattern === 0x03) {
lengthParam = 0;
}
return DTMTransport.createTransmitterCMD(frequency, lengthParam, bitpattern);
}
async setTxPower(dbm = this.#dbmPayload) {
this.#dbmPayload = dbm;
const cmd = DTMTransport.createTxPowerCMD(dbm);
return validate0x09Command(await this.#dtmTransport.sendCMD(cmd));
}
async setupReset() {
const cmd = DTMTransport.createSetupCMD(
DTM_CONTROL.RESET,
DTM_PARAMETER.DEFAULT,
DTM_DC.DEFAULT
);
return validateResult(await this.#dtmTransport.sendCMD(cmd, RESET_COMMAND_TIMEOUT_MS));
}
async setupLength(length = this.#lengthPayload) {
this.#lengthPayload = length;
const lengthBits = length >> 6;
const cmd = DTMTransport.createSetupCMD(
DTM_CONTROL.ENABLE_LENGTH,
lengthBits,
DTM_DC.DEFAULT
);
return validateResult(await this.#dtmTransport.sendCMD(cmd, DEFAULT_COMMAND_TIMEOUT_MS));
}
async setupPhy(phy = this.#phyPayload) {
this.#phyPayload = phy;
const cmd = DTMTransport.createSetupCMD(DTM_CONTROL.PHY, phy, DTM_DC.DEFAULT);
return validateResult(await this.#dtmTransport.sendCMD(cmd));
}
async setupModulation(modulation = this.#modulationPayload) {
this.#modulationPayload = modulation;
const cmd = DTMTransport.createSetupCMD(
DTM_CONTROL.MODULATION,
modulation,
DTM_DC.DEFAULT
);
return validateResult(await this.#dtmTransport.sendCMD(cmd));
}
async singleChannelTransmitterTest(bitpattern, length, channel, timeout = 0) {
this.#resetEvent();
this.#isTransmitting = true;
this.#timeoutEvent = this.startTimeoutEvent(() => this.#isTransmitting, timeout);
this.#sweepTimedOut = false;
this.#timedOut = false;
const response = await this.#dtmTransport.sendCMD(
DTM.carrierTestCMD(channelToFrequency(channel), length, bitpattern)
);
if (!reportSuccess(response)) {
this.#isTransmitting = false;
clearTimeout(this.#timeoutEvent);
return { type: 'error', message: 'Could not start transmission.' };
}
const endEvent = this.#startedTransmitterEvent(channel);
await this.endEventDataReceived();
this.#isTransmitting = false;
endEvent();
clearTimeout(this.#timeoutEvent);
return { type: 'transmitter' };
}
async sweepTransmitterTest(
bitpattern,
length,
channelLow,
channelHigh,
sweepTime = 1000,
timeout = 0
) {
this.#resetEvent();
this.#isTransmitting = true;
this.#timeoutEvent = this.startTimeoutEvent(() => this.#isTransmitting, timeout);
let currentChannelIdx = 0;
do {
const channel = channelLow + currentChannelIdx;
this.#sweepTimedOut = false;
this.#isTransmitting = false;
if (this.#timedOut) continue;
const endEventDataReceivedEvt = this.endEventDataReceived();
const sendCMDPromise = this.#dtmTransport
.sendCMD(DTM.carrierTestCMD(channelToFrequency(channel), length, bitpattern))
.catch(() => undefined);
if (this.#timedOut) continue;
this.#isTransmitting = true;
const response = await sendCMDPromise;
if (!reportSuccess(response)) {
this.#isTransmitting = false;
clearTimeout(this.#timeoutEvent);
return { type: 'error', message: 'Could not start transmission.' };
}
const endEvent = this.#startedTransmitterEvent(channel);
const sweepTimeoutEvent = this.startSweepTimeoutEvent(
() => this.#isTransmitting,
sweepTime
);
if (this.#timedOut) {
this.endCurrentTest().catch(() => undefined);
}
await endEventDataReceivedEvt;
clearTimeout(sweepTimeoutEvent);
endEvent();
currentChannelIdx = (currentChannelIdx + 1) % (channelHigh - channelLow + 1);
} while (this.#isTransmitting && !this.#timedOut);
this.#isTransmitting = false;
clearTimeout(this.#timeoutEvent);
return { type: 'transmitter' };
}
async singleChannelReceiverTest(bitpattern, length, channel, timeout = 0) {
this.#resetEvent();
this.#isReceiving = true;
this.#timeoutEvent = this.startTimeoutEvent(() => this.#isReceiving, timeout);
this.#timedOut = false;
this.#sweepTimedOut = false;
const endEventDataReceivedEvt = this.endEventDataReceived();
const response = await this.#dtmTransport
.sendCMD(DTMTransport.createReceiverCMD(channelToFrequency(channel), length, bitpattern))
.catch(() => undefined);
if (!reportSuccess(response)) {
this.#isReceiving = false;
clearTimeout(this.#timeoutEvent);
return { type: 'error', message: 'Could not start receiver.' };
}
const endEvent = this.#startedReceiverEvent(channel, false);
this.startReceiverLivePoll(channel, bitpattern, length);
let status;
try {
status = await endEventDataReceivedEvt;
} finally {
this.stopReceiverLivePoll();
}
this.#isReceiving = false;
this.#activeReceiverChannel = null;
clearTimeout(this.#timeoutEvent);
const totalReceived = this.#receiverLiveTotal + status.received;
this.#receiverLiveTotal = 0;
endEvent(totalReceived);
const receivedPerChannel = new Array(40).fill(0);
receivedPerChannel[channel] = totalReceived;
return { type: 'receiver', receivedPerChannel };
}
async sweepReceiverTest(
bitpattern,
length,
channelLow,
channelHigh,
sweepTime = 1000,
timeout = 0
) {
this.#resetEvent();
this.#isReceiving = true;
const packetsReceivedForChannel = new Array(40).fill(0);
this.#timeoutEvent = this.startTimeoutEvent(() => this.#isReceiving, timeout);
let currentChannelIdx = 0;
do {
const channel = channelLow + currentChannelIdx;
this.#sweepTimedOut = false;
this.#isReceiving = false;
if (this.#timedOut) continue;
const endEventDataReceivedEvt = this.endEventDataReceived();
const responseEvent = this.#dtmTransport
.sendCMD(DTMTransport.createReceiverCMD(channelToFrequency(channel), length, bitpattern))
.catch(() => undefined);
if (this.#timedOut) continue;
this.#isReceiving = true;
const response = await responseEvent;
if (!reportSuccess(response)) {
this.#isReceiving = false;
clearTimeout(this.#timeoutEvent);
return { type: 'error', message: 'Could not start receiver.' };
}
const endEvent = this.#startedReceiverEvent(channel, true);
const sweepTimeoutEvent = this.startSweepTimeoutEvent(
() => this.#isReceiving,
sweepTime
);
const status = await endEventDataReceivedEvt;
clearTimeout(sweepTimeoutEvent);
packetsReceivedForChannel[channel] += status.received;
endEvent(status.received);
currentChannelIdx = (currentChannelIdx + 1) % (channelHigh - channelLow + 1);
} while (this.#isReceiving && !this.#timedOut);
this.#isReceiving = false;
this.#activeReceiverChannel = null;
clearTimeout(this.#timeoutEvent);
return { type: 'receiver', receivedPerChannel: packetsReceivedForChannel };
}
async endTest() {
if (this.#timedOut) return;
this.#timedOut = true;
clearTimeout(this.#timeoutEvent);
this.stopReceiverLivePoll();
if (!this.#sweepTimedOut && (this.#isTransmitting || this.#isReceiving)) {
await this.endCurrentTest();
}
this.#isTransmitting = false;
this.#isReceiving = false;
this.#activeReceiverChannel = null;
}
async dispose() {
this.#eventEmitter.removeAllListeners();
await this.endTest().catch(() => {});
await this.#dtmTransport.dispose();
}
}
module.exports = { DTM };
+389
View File
@@ -0,0 +1,389 @@
const EventEmitter = require('events');
const { JlinkRttSession, RTT_CHANNEL_DTM } = require('./jlink/jlinkRttSession.cjs');
const DtmPacketType = {
PRBS9: 0x00,
_11110000: 0x01,
_10101010: 0x02,
'Constant carrier': 0x03,
};
const DTM_CMD = {
TEST_SETUP: '00',
RECEIVER_TEST: '01',
TRANSMITTER_TEST: '10',
TEST_END: '11',
};
const DTM_CONTROL = {
RESET: 0x00,
ENABLE_LENGTH: 0x01,
PHY: 0x02,
MODULATION: 0x03,
FEATURES: 0x04,
TXRX: 0x05,
END: 0x00,
};
const DTM_PARAMETER = {
DEFAULT: 0x00,
};
const DTM_DC = {
DEFAULT: '00',
};
const DTM_EVENT = {
LE_TEST_STATUS_EVENT: 0,
LE_PACKET_REPORT_EVENT: 1,
};
const DTM_FREQUENCY = frequency =>
((frequency - 2402) / 2).toString(2).padStart(6, '0');
const toTwosComplementBitString = data => {
const absTwosComplementValue = (data < 0 ? 128 : 0) + data;
const negativeBit = data < 0 ? 128 : 0;
return (negativeBit + absTwosComplementValue).toString(2).padStart(8, '0');
};
const toBitString = (data, length = 6) => data.toString(2).padStart(length, '0');
const DTM_CMD_FORMAT = cmd => {
const firstByte = parseInt(cmd.substring(0, 8), 2);
const secondByte = parseInt(cmd.substring(8, 16), 2);
return Buffer.from([firstByte, secondByte]);
};
const DEFAULT_DEVICE = 'NRF52840_xxAA';
const DEFAULT_SWD_SPEED = 4000;
const DTM_SETUP_CONTROL = {
0: 'reset',
1: 'packet length',
2: 'PHY',
3: 'modulation',
4: 'features',
5: 'TX/RX select',
};
const DTM_PKT_TYPE = {
0: 'PRBS9',
1: '11110000',
2: '10101010',
3: 'constant carrier',
};
const dtmChannelLabel = byte0 => {
const channel = byte0 & 0x3f;
const mhz = 2402 + 2 * channel;
return `ch ${channel} (${mhz} MHz)`;
};
const dtmTxNote = buffer => {
const b0 = buffer[0];
const b1 = buffer[1];
const cmdType = (b0 >> 6) & 0x03;
if (cmdType === 0) {
const control = b0 & 0x3f;
if (control === 9) {
return `DTM setup — set TX power (param 0x${b1.toString(16).toUpperCase().padStart(2, '0')})`;
}
const label = DTM_SETUP_CONTROL[control] ?? `control ${control}`;
return `DTM setup — ${label} (param 0x${b1.toString(16).toUpperCase().padStart(2, '0')})`;
}
if (cmdType === 1) {
const length = (b1 >> 2) & 0x3f;
const pkt = DTM_PKT_TYPE[b1 & 0x03] ?? `type ${b1 & 0x03}`;
return `Receiver test — listen on ${dtmChannelLabel(b0)}, len ${length}, ${pkt}`;
}
if (cmdType === 2) {
const length = (b1 >> 2) & 0x3f;
const pkt = DTM_PKT_TYPE[b1 & 0x03] ?? `type ${b1 & 0x03}`;
return `Transmitter test — transmit on ${dtmChannelLabel(b0)}, len ${length}, ${pkt}`;
}
if (cmdType === 3) {
return 'Test end — stop current receiver/transmitter test';
}
return 'DTM command';
};
const parsePacketReportCount = buffer => {
if (!buffer || buffer.length < 2 || (buffer[0] & 0x80) === 0) {
return null;
}
return ((buffer[0] & 0x3f) << 8) | buffer[1];
};
const dtmRxNote = buffer => {
const b0 = buffer[0];
const b1 = buffer[1];
const packetCount = parsePacketReportCount(buffer);
if (packetCount !== null) {
return `Packet report — ${packetCount} packet(s) received on active RX channel (LE DTM event)`;
}
if ((b1 & 0x01) !== 0) {
const rawDbm = (b1 & 0xfe) >> 1;
const modifier = b0 & 0x01 ? -128 : 0;
return `DTM status — applied TX power ${modifier + rawDbm} dBm (firmware feedback)`;
}
if (b0 === 0 && b1 === 0) {
return 'DTM status — OK (command accepted)';
}
if ((b0 & 0x01) !== 0) {
return 'DTM status — command failed (status bit set)';
}
return 'DTM status — response from firmware';
};
class DTMTransport {
#session;
#dataBuffer;
#callback;
#sendQueue = [];
#isProcessing = false;
#isOpen = false;
#eventEmitter = new EventEmitter();
#detachDataListener;
constructor(jlinkSerial, options = {}) {
const {
device = DEFAULT_DEVICE,
speed = DEFAULT_SWD_SPEED,
gdbPort = 29021,
swoPort = 29022,
rttPort = 19021,
rttChannel = RTT_CHANNEL_DTM,
} = options;
this.#session = new JlinkRttSession({
serialNumber: jlinkSerial,
device,
speed,
gdbPort,
swoPort,
rttPort,
rttChannel,
});
}
addListeners() {
this.#detachDataListener = this.#session.onData((data, error) => {
if (error) {
this.#eventEmitter.emit('traffic', {
direction: 'error',
data: [],
note: error.message,
});
return;
}
this.#dataBuffer = this.#dataBuffer
? Buffer.concat([this.#dataBuffer, data])
: Buffer.from(data);
while (this.#dataBuffer && this.#dataBuffer.length >= 2) {
const response = this.#dataBuffer.subarray(0, 2);
this.#dataBuffer =
this.#dataBuffer.length > 2 ? this.#dataBuffer.subarray(2) : undefined;
this.#eventEmitter.emit('traffic', {
direction: 'rx',
data: Array.from(response),
note: dtmRxNote(response),
});
const packetCount = parsePacketReportCount(response);
if (packetCount !== null) {
this.#eventEmitter.emit('packetReport', packetCount);
}
if (this.#callback) {
this.#callback(response);
}
}
});
}
onTraffic(listener) {
this.#eventEmitter.on('traffic', listener);
return () => this.#eventEmitter.removeListener('traffic', listener);
}
onPacketReport(listener) {
this.#eventEmitter.on('packetReport', listener);
return () => this.#eventEmitter.removeListener('packetReport', listener);
}
open() {
return new Promise(async (resolve, reject) => {
try {
if (!this.#detachDataListener) {
this.addListeners();
}
await this.#session.start();
this.#isOpen = true;
this.#dataBuffer = undefined;
this.#eventEmitter.emit('traffic', {
direction: 'info',
data: [],
note: `RTT connected on localhost:${this.#session.rttPort} (channel ${RTT_CHANNEL_DTM}, target running)`,
});
resolve();
} catch (error) {
reject(error);
}
});
}
close() {
return new Promise(async (resolve, reject) => {
try {
await this.#session.close();
this.#isOpen = false;
resolve();
} catch (error) {
reject(error);
}
});
}
static #createCMD(cmdType, arg2, arg3, arg4 = '') {
return DTM_CMD_FORMAT(cmdType + arg2 + arg3 + arg4);
}
static createSetupCMD(
control = DTM_CONTROL.RESET,
parameter = DTM_PARAMETER.DEFAULT,
dc = DTM_DC.DEFAULT
) {
return DTMTransport.#createCMD(
DTM_CMD.TEST_SETUP,
toBitString(control),
toBitString(parameter),
dc
);
}
static createEndCMD() {
return DTMTransport.#createCMD(
DTM_CMD.TEST_END,
toBitString(DTM_CONTROL.END),
toBitString(DTM_PARAMETER.DEFAULT),
DTM_DC.DEFAULT
);
}
static createTransmitterCMD(frequency = 2402, length = 0, pkt = DtmPacketType.PRBS9) {
return DTMTransport.#createCMD(
DTM_CMD.TRANSMITTER_TEST,
DTM_FREQUENCY(frequency),
toBitString(length),
toBitString(pkt, 2)
);
}
static createReceiverCMD(frequency = 2402, length = 0, pkt = DtmPacketType.PRBS9) {
return DTMTransport.#createCMD(
DTM_CMD.RECEIVER_TEST,
DTM_FREQUENCY(frequency),
toBitString(length),
toBitString(pkt, 2)
);
}
static createTxPowerCMD(dbm) {
return DTMTransport.#createCMD(
DTM_CMD.TEST_SETUP,
toBitString(9, 6),
toTwosComplementBitString(dbm)
);
}
#processQueue() {
if (this.#isProcessing || this.#sendQueue.length === 0) {
return;
}
this.#isProcessing = true;
const item = this.#sendQueue.shift();
if (!item) return;
const { cmd, resolve, reject, timeoutMs } = item;
const responseTimeout = setTimeout(() => {
this.#callback = undefined;
this.#dataBuffer = undefined;
this.#isProcessing = false;
reject(new Error('Timeout'));
this.#processQueue();
}, timeoutMs);
this.#callback = data => {
this.#callback = undefined;
clearTimeout(responseTimeout);
this.#isProcessing = false;
resolve(Array.from(data));
this.#processQueue();
};
this.#eventEmitter.emit('traffic', {
direction: 'tx',
data: Array.from(cmd),
note: dtmTxNote(cmd),
});
this.#session.write(cmd).catch(error => {
clearTimeout(responseTimeout);
this.#callback = undefined;
this.#isProcessing = false;
reject(error);
this.#processQueue();
});
}
async sendCMD(cmd, timeoutMs = 3000) {
if (!this.#isOpen) {
await this.open();
}
return new Promise((resolve, reject) => {
this.#sendQueue.push({ cmd, resolve, reject, timeoutMs });
this.#processQueue();
});
}
async dispose() {
this.#callback = undefined;
this.#sendQueue = [];
this.#isProcessing = false;
if (this.#detachDataListener) {
this.#detachDataListener();
this.#detachDataListener = undefined;
}
if (this.#isOpen) {
await this.close().catch(() => {});
}
}
}
module.exports = {
DTMTransport,
DTM_CONTROL,
DTM_DC,
DTM_PARAMETER,
DTM_EVENT,
DEFAULT_DEVICE,
DEFAULT_SWD_SPEED,
};
+30
View File
@@ -0,0 +1,30 @@
const ChannelMode = {
single: 'single',
sweep: 'sweep',
};
const DtmPhysicalLayer = {
'LE 1Mbps': 0x01,
'LE 2Mbps': 0x02,
'LE Coded S8': 0x03,
'LE Coded S2': 0x04,
};
const DtmModulationMode = {
Standard: 0x00,
Stable: 0x01,
};
const DtmPacketType = {
PRBS9: 0x00,
_11110000: 0x01,
_10101010: 0x02,
'Constant carrier': 0x03,
};
module.exports = {
ChannelMode,
DtmPhysicalLayer,
DtmModulationMode,
DtmPacketType,
};
+524
View File
@@ -0,0 +1,524 @@
const { DTM } = require('./DTM.cjs');
const { DEFAULT_DEVICE, DEFAULT_SWD_SPEED } = require('./DTM_transport.cjs');
const {
ChannelMode,
DtmModulationMode,
DtmPacketType,
DtmPhysicalLayer,
} = require('./dtm-types.cjs');
const { listJLinkProbes, getJLinkSetupHint } = require('./jlink/jlinkProbe.cjs');
const { hasSeggerRttStack } = require('./jlink/jlinkPaths.cjs');
const { allocateSessionPorts, JLINK_PORTS_BY_SESSION } = require('./jlink/jlinkSessionPorts.cjs');
const BLE_CHANNELS = Array.from({ length: 40 }, (_, index) => index);
const SESSION_IDS = ['receiver', 'transmitter'];
const createInitialStatus = mode => ({
running: false,
mode,
currentChannel: null,
lastReceived: new Array(40).fill(0),
txPower: 0,
message: 'Idle',
jlinkPorts: null,
});
const allocatedPortsBySession = {
receiver: null,
transmitter: null,
};
const getGloballyReservedPorts = () => {
const reserved = new Set();
for (const sessionId of SESSION_IDS) {
const ports = allocatedPortsBySession[sessionId];
if (!ports) continue;
reserved.add(ports.gdb);
reserved.add(ports.swo);
reserved.add(ports.rtt);
}
return reserved;
};
const dtmBySession = {
receiver: null,
transmitter: null,
};
const configKeyBySession = {
receiver: '',
transmitter: '',
};
const lastConfigBySession = {
receiver: null,
transmitter: null,
};
const runningBySession = {
receiver: false,
transmitter: false,
};
const startingBySession = {
receiver: false,
transmitter: false,
};
const statusBySession = {
receiver: createInitialStatus('receiver'),
transmitter: createInitialStatus('transmitter'),
};
let dtmEventSink = () => undefined;
const configKey = config =>
`${config.port}|${config.device || DEFAULT_DEVICE}|${Number(config.speed || DEFAULT_SWD_SPEED)}`;
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
const probeSerialForSession = sessionId => {
const fromConfig = lastConfigBySession[sessionId]?.port;
if (fromConfig) return String(fromConfig);
const key = configKeyBySession[sessionId];
return key ? key.split('|')[0] : '';
};
const emit = event => {
dtmEventSink(event);
};
const emitForSession = (sessionId, event) => {
emit({ sessionId, ...event });
};
const getSessionStatus = sessionId => statusBySession[sessionId];
const releaseDtm = async sessionId => {
if (!dtmBySession[sessionId]) {
allocatedPortsBySession[sessionId] = null;
getSessionStatus(sessionId).jlinkPorts = null;
return;
}
const instance = dtmBySession[sessionId];
dtmBySession[sessionId] = null;
configKeyBySession[sessionId] = '';
allocatedPortsBySession[sessionId] = null;
getSessionStatus(sessionId).jlinkPorts = null;
await instance.dispose().catch(() => {});
await wait(800);
};
const isSessionBusy = sessionId => {
const status = getSessionStatus(sessionId);
return runningBySession[sessionId] || startingBySession[sessionId] || status.running;
};
const sharesProbeSerial = (leftPort, rightPort) =>
Boolean(leftPort) && Boolean(rightPort) && String(leftPort) === String(rightPort);
const findProbeConflict = (sessionId, config) => {
if (!config.port) return null;
for (const otherSessionId of SESSION_IDS) {
if (otherSessionId === sessionId) continue;
const sameProbe = sharesProbeSerial(probeSerialForSession(otherSessionId), config.port);
if (!sameProbe) continue;
if (startingBySession[otherSessionId] || runningBySession[otherSessionId] || dtmBySession[otherSessionId]) {
return otherSessionId;
}
}
return null;
};
const assertProbeAvailable = (sessionId, config) => {
const conflict = findProbeConflict(sessionId, config);
if (!conflict) return;
throw new Error(
`동일 J-Link(SN ${config.port})는 GDB Server를 하나만 쓸 수 있습니다. ${conflict} 세션을 Stop한 뒤 ${sessionId}를 시작하세요. 서로 다른 SN이면 동시 실행이 가능합니다.`
);
};
const releaseInactiveSessionUsingProbe = async (sessionId, config) => {
const nextKey = configKey(config);
const otherSessionId = SESSION_IDS.find(id => id !== sessionId && configKeyBySession[id] === nextKey);
if (!otherSessionId) return;
if (isSessionBusy(otherSessionId) || dtmBySession[otherSessionId]) {
throw new Error(
`동일 J-Link(${config.port})는 RTT 세션을 하나만 열 수 있습니다. ${otherSessionId} 세션을 Stop한 뒤 다시 시도하세요.`
);
}
await releaseDtm(otherSessionId);
};
const attachEvents = (sessionId, instance) => {
instance.onReset(() => {
const status = getSessionStatus(sessionId);
status.currentChannel = null;
status.lastReceived = new Array(40).fill(0);
emitForSession(sessionId, { type: 'reset' });
});
instance.onStarted(event => {
const status = getSessionStatus(sessionId);
status.currentChannel = event.channel;
emitForSession(sessionId, { type: 'started', channel: event.channel, mode: event.type });
});
instance.onEnded(event => {
const status = getSessionStatus(sessionId);
status.currentChannel = null;
if (typeof event.packets === 'number') {
if (event.accumulate) {
status.lastReceived[event.channel] += event.packets;
} else {
status.lastReceived[event.channel] = Math.max(
status.lastReceived[event.channel] || 0,
event.packets
);
}
}
emitForSession(sessionId, {
type: 'ended',
channel: event.channel,
packets: event.packets ?? 0,
mode: event.type,
data: [...status.lastReceived],
});
});
instance.onPackets(event => {
const status = getSessionStatus(sessionId);
if (typeof event.packets !== 'number' || typeof event.channel !== 'number') {
return;
}
status.lastReceived[event.channel] = event.packets;
emitForSession(sessionId, {
type: 'packets',
channel: event.channel,
packets: event.packets,
data: [...status.lastReceived],
});
});
instance.onTraffic(event => {
emitForSession(sessionId, {
type: 'traffic',
direction: event.direction,
data: event.data,
note: event.note,
});
});
};
const ensureDtm = async (sessionId, config) => {
const nextKey = configKey(config);
await releaseInactiveSessionUsingProbe(sessionId, config);
if (!dtmBySession[sessionId] || configKeyBySession[sessionId] !== nextKey) {
await releaseDtm(sessionId);
const ports = await allocateSessionPorts(sessionId, getGloballyReservedPorts());
allocatedPortsBySession[sessionId] = ports;
getSessionStatus(sessionId).jlinkPorts = ports;
const instance = new DTM(config.port, {
device: config.device || DEFAULT_DEVICE,
speed: Number(config.speed || DEFAULT_SWD_SPEED),
gdbPort: ports.gdb,
swoPort: ports.swo,
rttPort: ports.rtt,
});
attachEvents(sessionId, instance);
await instance.connect();
dtmBySession[sessionId] = instance;
configKeyBySession[sessionId] = nextKey;
emitForSession(sessionId, {
type: 'status',
message: `RTT ports GDB ${ports.gdb} / SWO ${ports.swo} / RTT ${ports.rtt}`,
running: false,
mode: getSessionStatus(sessionId).mode,
jlinkPorts: ports,
});
}
return dtmBySession[sessionId];
};
const runSetupStep = async (sessionId, label, action) => {
const status = getSessionStatus(sessionId);
status.message = label;
emitForSession(sessionId, { type: 'status', message: label, running: true, mode: status.mode });
try {
return await action();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message === 'Timeout') {
throw new Error(
`${label} timed out. Check J-Link SN, SWD, target device (NRF52840_xxAA), DTM_TRANSPORT_RTT firmware flashed and running, and traffic log for TX without RX. Close Ozone or a second GDB Server on the same probe.`
);
}
throw new Error(`${label} failed: ${message}`);
}
};
const setupTest = async (sessionId, instance, config) => {
const status = getSessionStatus(sessionId);
await runSetupStep(sessionId, 'Resetting device', () => instance.setupReset());
if (config.testMode === 'transmitter') {
status.txPower = await runSetupStep(sessionId, 'Applying TX power', () =>
instance.setTxPower(Number(config.txPower))
);
await runSetupStep(sessionId, 'Applying packet length', () =>
instance.setupLength(Number(config.length))
);
await runSetupStep(sessionId, 'Applying modulation mode', () =>
instance.setupModulation(Number(config.modulationMode))
);
} else {
await runSetupStep(sessionId, 'Applying packet length', () =>
instance.setupLength(Number(config.length))
);
}
await runSetupStep(sessionId, 'Applying PHY mode', () => instance.setupPhy(Number(config.phy)));
};
const normalizeChannelIndexes = config => {
const single = BLE_CHANNELS.indexOf(Number(config.singleChannel));
const rangeValues = [Number(config.rangeStart), Number(config.rangeEnd)];
const indexedRange = rangeValues.map(value => BLE_CHANNELS.indexOf(value)).sort((a, b) => a - b);
return {
singleChannelIndexed: single,
rangeStart: indexedRange[0],
rangeEnd: indexedRange[1],
};
};
const startTest = async (sessionId, config, nextEmitter) => {
if (typeof nextEmitter === 'function') {
dtmEventSink = nextEmitter;
}
if (!SESSION_IDS.includes(sessionId)) {
throw new Error(`Unknown session: ${sessionId}`);
}
if (!config.port) {
throw new Error('J-Link serial number is required.');
}
if (!hasSeggerRttStack()) {
throw new Error(getJLinkSetupHint() || 'SEGGER J-Link GDB Server is required for RTT.');
}
lastConfigBySession[sessionId] = config;
startingBySession[sessionId] = true;
const status = getSessionStatus(sessionId);
status.mode = config.testMode;
status.message = 'Connecting';
status.lastReceived = new Array(40).fill(0);
emitForSession(sessionId, { type: 'status', message: status.message, running: false, mode: status.mode });
let instance;
try {
assertProbeAvailable(sessionId, config);
instance = await ensureDtm(sessionId, config);
runningBySession[sessionId] = true;
status.running = true;
emitForSession(sessionId, { type: 'status', message: status.message, running: true, mode: status.mode });
status.message = 'Setting up device';
emitForSession(sessionId, { type: 'status', message: status.message, running: true, mode: status.mode });
await setupTest(sessionId, instance, config);
startingBySession[sessionId] = false;
} catch (error) {
startingBySession[sessionId] = false;
runningBySession[sessionId] = false;
status.running = false;
await releaseDtm(sessionId).catch(() => {});
const message = error instanceof Error ? error.message : String(error);
status.message = message;
emitForSession(sessionId, { type: 'error', message });
throw error;
}
const { singleChannelIndexed, rangeStart, rangeEnd } = normalizeChannelIndexes(config);
status.message = 'Running test';
emitForSession(sessionId, {
type: 'status',
message: status.message,
running: true,
mode: status.mode,
txPower: status.txPower,
});
let testPromise;
if (config.testMode === 'transmitter' && config.channelMode === ChannelMode.single) {
testPromise = instance.singleChannelTransmitterTest(
Number(config.bitpattern),
Number(config.length),
singleChannelIndexed,
Number(config.timeoutMs)
);
} else if (config.testMode === 'transmitter') {
testPromise = instance.sweepTransmitterTest(
Number(config.bitpattern),
Number(config.length),
rangeStart,
rangeEnd,
Number(config.sweepTimeMs),
Number(config.timeoutMs)
);
} else if (config.channelMode === ChannelMode.single) {
testPromise = instance.singleChannelReceiverTest(
Number(config.bitpattern),
Number(config.length),
singleChannelIndexed,
Number(config.timeoutMs)
);
} else {
testPromise = instance.sweepReceiverTest(
Number(config.bitpattern),
Number(config.length),
rangeStart,
rangeEnd,
Number(config.sweepTimeMs),
Number(config.timeoutMs)
);
}
testPromise
.then(result => {
runningBySession[sessionId] = false;
status.running = false;
releaseDtm(sessionId).catch(() => {});
if (result.type === 'error') {
status.message = result.message;
emitForSession(sessionId, { type: 'error', message: result.message });
return;
}
if (result.type === 'receiver') {
status.lastReceived = result.receivedPerChannel;
} else {
status.lastReceived = new Array(40).fill(0);
}
status.message = 'Test finished';
emitForSession(sessionId, {
type: 'complete',
mode: result.type,
data: [...status.lastReceived],
txPower: status.txPower,
});
})
.catch(error => {
runningBySession[sessionId] = false;
status.running = false;
releaseDtm(sessionId).catch(() => {});
status.message = error.message;
emitForSession(sessionId, { type: 'error', message: error.message });
});
return { ok: true };
};
const stopTest = async sessionId => {
if (!SESSION_IDS.includes(sessionId)) {
throw new Error(`Unknown session: ${sessionId}`);
}
const instance = dtmBySession[sessionId];
const status = getSessionStatus(sessionId);
if (instance) {
await instance.endTest().catch(() => {});
}
await releaseDtm(sessionId);
startingBySession[sessionId] = false;
runningBySession[sessionId] = false;
status.running = false;
status.message = 'Stopped';
emitForSession(sessionId, { type: 'status', message: status.message, running: false, mode: status.mode });
return { ok: true };
};
const listJLinks = async () => listJLinkProbes();
const syncSessionConfig = (sessionId, config) => {
if (!SESSION_IDS.includes(sessionId)) {
throw new Error(`Unknown session: ${sessionId}`);
}
lastConfigBySession[sessionId] = config;
return { ok: true };
};
const canStartSession = async (sessionId, config) => {
try {
assertProbeAvailable(sessionId, config);
if (!dtmBySession[sessionId]) {
await allocateSessionPorts(sessionId, getGloballyReservedPorts());
}
return { ok: true };
} catch (error) {
return {
ok: false,
reason: error instanceof Error ? error.message : String(error),
};
}
};
const getStatus = async sessionId => {
if (!SESSION_IDS.includes(sessionId)) {
throw new Error(`Unknown session: ${sessionId}`);
}
return {
...getSessionStatus(sessionId),
running: runningBySession[sessionId],
starting: startingBySession[sessionId],
jlinkPorts:
allocatedPortsBySession[sessionId] ||
getSessionStatus(sessionId).jlinkPorts ||
JLINK_PORTS_BY_SESSION[sessionId],
};
};
const stopAllSessions = async () => {
await Promise.all(SESSION_IDS.map(sessionId => releaseDtm(sessionId)));
};
module.exports = {
listJLinks,
listSerialPorts: listJLinks,
startTest,
stopTest,
getStatus,
syncSessionConfig,
canStartSession,
stopAllSessions,
constants: {
ChannelMode,
DtmPhysicalLayer,
DtmModulationMode,
DtmPacketType,
DEFAULT_DEVICE,
DEFAULT_SWD_SPEED,
},
};
@@ -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 };
+208
View File
@@ -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,
};
+180
View File
@@ -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,
};
+277
View File
@@ -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,
};