497 lines
16 KiB
JavaScript
497 lines
16 KiB
JavaScript
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 };
|