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