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