initial commit
This commit is contained in:
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user