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