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