Files
2026-05-18 17:36:05 +09:00

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