390 lines
10 KiB
JavaScript
390 lines
10 KiB
JavaScript
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,
|
|
};
|