Files
jh.chun 1bf60d01f3 DTM TRAFFIC, SESSION EVENTS 로그 개선
- Receiver에 expected packet type 필드 제거
- 로그 아래로 쌓기
- Clear 버튼 추가
- Trasmitter 세션에서는 packet report 숨김
2026-05-20 11:05:28 +09:00

528 lines
17 KiB
JavaScript

const { DTM } = require('./DTM.cjs');
const { DEFAULT_DEVICE, DEFAULT_SWD_SPEED } = require('./DTM_transport.cjs');
const {
ChannelMode,
DtmModulationMode,
DtmPacketType,
DtmPhysicalLayer,
} = require('./dtm-types.cjs');
const { listJLinkProbes, getJLinkSetupHint } = require('./jlink/jlinkProbe.cjs');
const { hasSeggerRttStack } = require('./jlink/jlinkPaths.cjs');
const { allocateSessionPorts, JLINK_PORTS_BY_SESSION } = require('./jlink/jlinkSessionPorts.cjs');
const BLE_CHANNELS = Array.from({ length: 40 }, (_, index) => index);
const SESSION_IDS = ['receiver', 'transmitter'];
const createInitialStatus = mode => ({
running: false,
mode,
currentChannel: null,
lastReceived: new Array(40).fill(0),
txPower: 0,
message: 'Idle',
jlinkPorts: null,
});
const allocatedPortsBySession = {
receiver: null,
transmitter: null,
};
const getGloballyReservedPorts = () => {
const reserved = new Set();
for (const sessionId of SESSION_IDS) {
const ports = allocatedPortsBySession[sessionId];
if (!ports) continue;
reserved.add(ports.gdb);
reserved.add(ports.swo);
reserved.add(ports.rtt);
}
return reserved;
};
const dtmBySession = {
receiver: null,
transmitter: null,
};
const configKeyBySession = {
receiver: '',
transmitter: '',
};
const lastConfigBySession = {
receiver: null,
transmitter: null,
};
const runningBySession = {
receiver: false,
transmitter: false,
};
const startingBySession = {
receiver: false,
transmitter: false,
};
const statusBySession = {
receiver: createInitialStatus('receiver'),
transmitter: createInitialStatus('transmitter'),
};
let dtmEventSink = () => undefined;
const configKey = config =>
`${config.port}|${config.device || DEFAULT_DEVICE}|${Number(config.speed || DEFAULT_SWD_SPEED)}`;
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
const probeSerialForSession = sessionId => {
const fromConfig = lastConfigBySession[sessionId]?.port;
if (fromConfig) return String(fromConfig);
const key = configKeyBySession[sessionId];
return key ? key.split('|')[0] : '';
};
const emit = event => {
dtmEventSink(event);
};
const emitForSession = (sessionId, event) => {
emit({ sessionId, ...event });
};
const getSessionStatus = sessionId => statusBySession[sessionId];
const releaseDtm = async sessionId => {
if (!dtmBySession[sessionId]) {
allocatedPortsBySession[sessionId] = null;
getSessionStatus(sessionId).jlinkPorts = null;
return;
}
const instance = dtmBySession[sessionId];
dtmBySession[sessionId] = null;
configKeyBySession[sessionId] = '';
allocatedPortsBySession[sessionId] = null;
getSessionStatus(sessionId).jlinkPorts = null;
await instance.dispose().catch(() => {});
await wait(800);
};
const isSessionBusy = sessionId => {
const status = getSessionStatus(sessionId);
return runningBySession[sessionId] || startingBySession[sessionId] || status.running;
};
const sharesProbeSerial = (leftPort, rightPort) =>
Boolean(leftPort) && Boolean(rightPort) && String(leftPort) === String(rightPort);
const findProbeConflict = (sessionId, config) => {
if (!config.port) return null;
for (const otherSessionId of SESSION_IDS) {
if (otherSessionId === sessionId) continue;
const sameProbe = sharesProbeSerial(probeSerialForSession(otherSessionId), config.port);
if (!sameProbe) continue;
if (startingBySession[otherSessionId] || runningBySession[otherSessionId] || dtmBySession[otherSessionId]) {
return otherSessionId;
}
}
return null;
};
const assertProbeAvailable = (sessionId, config) => {
const conflict = findProbeConflict(sessionId, config);
if (!conflict) return;
throw new Error(
`동일 J-Link(SN ${config.port})는 GDB Server를 하나만 쓸 수 있습니다. ${conflict} 세션을 Stop한 뒤 ${sessionId}를 시작하세요. 서로 다른 SN이면 동시 실행이 가능합니다.`
);
};
const releaseInactiveSessionUsingProbe = async (sessionId, config) => {
const nextKey = configKey(config);
const otherSessionId = SESSION_IDS.find(id => id !== sessionId && configKeyBySession[id] === nextKey);
if (!otherSessionId) return;
if (isSessionBusy(otherSessionId) || dtmBySession[otherSessionId]) {
throw new Error(
`동일 J-Link(${config.port})는 RTT 세션을 하나만 열 수 있습니다. ${otherSessionId} 세션을 Stop한 뒤 다시 시도하세요.`
);
}
await releaseDtm(otherSessionId);
};
const attachEvents = (sessionId, instance) => {
instance.onReset(() => {
const status = getSessionStatus(sessionId);
status.currentChannel = null;
status.lastReceived = new Array(40).fill(0);
emitForSession(sessionId, { type: 'reset' });
});
instance.onStarted(event => {
const status = getSessionStatus(sessionId);
status.currentChannel = event.channel;
emitForSession(sessionId, { type: 'started', channel: event.channel, mode: event.type });
});
instance.onEnded(event => {
const status = getSessionStatus(sessionId);
status.currentChannel = null;
if (typeof event.packets === 'number') {
if (event.accumulate) {
status.lastReceived[event.channel] += event.packets;
} else {
status.lastReceived[event.channel] = Math.max(
status.lastReceived[event.channel] || 0,
event.packets
);
}
}
emitForSession(sessionId, {
type: 'ended',
channel: event.channel,
mode: event.type,
data: [...status.lastReceived],
...(typeof event.packets === 'number' ? { packets: event.packets } : {}),
});
});
instance.onPackets(event => {
const status = getSessionStatus(sessionId);
if (typeof event.packets !== 'number' || typeof event.channel !== 'number') {
return;
}
status.lastReceived[event.channel] = event.packets;
emitForSession(sessionId, {
type: 'packets',
channel: event.channel,
packets: event.packets,
data: [...status.lastReceived],
});
});
instance.onTraffic(event => {
if (sessionId === 'transmitter' && event.kind === 'packetReport') {
return;
}
emitForSession(sessionId, {
type: 'traffic',
direction: event.direction,
data: event.data,
note: event.note,
});
});
};
const ensureDtm = async (sessionId, config) => {
const nextKey = configKey(config);
await releaseInactiveSessionUsingProbe(sessionId, config);
if (!dtmBySession[sessionId] || configKeyBySession[sessionId] !== nextKey) {
await releaseDtm(sessionId);
const ports = await allocateSessionPorts(sessionId, getGloballyReservedPorts());
allocatedPortsBySession[sessionId] = ports;
getSessionStatus(sessionId).jlinkPorts = ports;
const instance = new DTM(config.port, {
device: config.device || DEFAULT_DEVICE,
speed: Number(config.speed || DEFAULT_SWD_SPEED),
gdbPort: ports.gdb,
swoPort: ports.swo,
rttPort: ports.rtt,
});
attachEvents(sessionId, instance);
await instance.connect();
dtmBySession[sessionId] = instance;
configKeyBySession[sessionId] = nextKey;
emitForSession(sessionId, {
type: 'status',
message: `RTT ports GDB ${ports.gdb} / SWO ${ports.swo} / RTT ${ports.rtt}`,
running: false,
mode: getSessionStatus(sessionId).mode,
jlinkPorts: ports,
});
}
return dtmBySession[sessionId];
};
const runSetupStep = async (sessionId, label, action) => {
const status = getSessionStatus(sessionId);
status.message = label;
emitForSession(sessionId, { type: 'status', message: label, running: true, mode: status.mode });
try {
return await action();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message === 'Timeout') {
throw new Error(
`${label} timed out. Check J-Link SN, SWD, target device (NRF52840_xxAA), DTM_TRANSPORT_RTT firmware flashed and running, and traffic log for TX without RX. Close Ozone or a second GDB Server on the same probe.`
);
}
throw new Error(`${label} failed: ${message}`);
}
};
const setupTest = async (sessionId, instance, config) => {
const status = getSessionStatus(sessionId);
await runSetupStep(sessionId, 'Resetting device', () => instance.setupReset());
if (config.testMode === 'transmitter') {
status.txPower = await runSetupStep(sessionId, 'Applying TX power', () =>
instance.setTxPower(Number(config.txPower))
);
await runSetupStep(sessionId, 'Applying packet length', () =>
instance.setupLength(Number(config.length))
);
await runSetupStep(sessionId, 'Applying modulation mode', () =>
instance.setupModulation(Number(config.modulationMode))
);
} else {
await runSetupStep(sessionId, 'Applying packet length', () =>
instance.setupLength(Number(config.length))
);
}
await runSetupStep(sessionId, 'Applying PHY mode', () => instance.setupPhy(Number(config.phy)));
};
const normalizeChannelIndexes = config => {
const single = BLE_CHANNELS.indexOf(Number(config.singleChannel));
const rangeValues = [Number(config.rangeStart), Number(config.rangeEnd)];
const indexedRange = rangeValues.map(value => BLE_CHANNELS.indexOf(value)).sort((a, b) => a - b);
return {
singleChannelIndexed: single,
rangeStart: indexedRange[0],
rangeEnd: indexedRange[1],
};
};
const startTest = async (sessionId, config, nextEmitter) => {
if (typeof nextEmitter === 'function') {
dtmEventSink = nextEmitter;
}
if (!SESSION_IDS.includes(sessionId)) {
throw new Error(`Unknown session: ${sessionId}`);
}
if (!config.port) {
throw new Error('J-Link serial number is required.');
}
if (!hasSeggerRttStack()) {
throw new Error(getJLinkSetupHint() || 'SEGGER J-Link GDB Server is required for RTT.');
}
lastConfigBySession[sessionId] = config;
startingBySession[sessionId] = true;
const status = getSessionStatus(sessionId);
status.mode = config.testMode;
status.message = 'Connecting';
status.lastReceived = new Array(40).fill(0);
emitForSession(sessionId, { type: 'status', message: status.message, running: false, mode: status.mode });
let instance;
try {
assertProbeAvailable(sessionId, config);
instance = await ensureDtm(sessionId, config);
runningBySession[sessionId] = true;
status.running = true;
emitForSession(sessionId, { type: 'status', message: status.message, running: true, mode: status.mode });
status.message = 'Setting up device';
emitForSession(sessionId, { type: 'status', message: status.message, running: true, mode: status.mode });
await setupTest(sessionId, instance, config);
startingBySession[sessionId] = false;
} catch (error) {
startingBySession[sessionId] = false;
runningBySession[sessionId] = false;
status.running = false;
await releaseDtm(sessionId).catch(() => {});
const message = error instanceof Error ? error.message : String(error);
status.message = message;
emitForSession(sessionId, { type: 'error', message });
throw error;
}
const { singleChannelIndexed, rangeStart, rangeEnd } = normalizeChannelIndexes(config);
status.message = 'Running test';
emitForSession(sessionId, {
type: 'status',
message: status.message,
running: true,
mode: status.mode,
txPower: status.txPower,
});
let testPromise;
if (config.testMode === 'transmitter' && config.channelMode === ChannelMode.single) {
testPromise = instance.singleChannelTransmitterTest(
Number(config.bitpattern),
Number(config.length),
singleChannelIndexed,
Number(config.timeoutMs)
);
} else if (config.testMode === 'transmitter') {
testPromise = instance.sweepTransmitterTest(
Number(config.bitpattern),
Number(config.length),
rangeStart,
rangeEnd,
Number(config.sweepTimeMs),
Number(config.timeoutMs)
);
} else if (config.channelMode === ChannelMode.single) {
testPromise = instance.singleChannelReceiverTest(
Number(config.bitpattern),
Number(config.length),
singleChannelIndexed,
Number(config.timeoutMs)
);
} else {
testPromise = instance.sweepReceiverTest(
Number(config.bitpattern),
Number(config.length),
rangeStart,
rangeEnd,
Number(config.sweepTimeMs),
Number(config.timeoutMs)
);
}
testPromise
.then(result => {
runningBySession[sessionId] = false;
status.running = false;
releaseDtm(sessionId).catch(() => {});
if (result.type === 'error') {
status.message = result.message;
emitForSession(sessionId, { type: 'error', message: result.message });
return;
}
if (result.type === 'receiver') {
status.lastReceived = result.receivedPerChannel;
} else {
status.lastReceived = new Array(40).fill(0);
}
status.message = 'Test finished';
emitForSession(sessionId, {
type: 'complete',
mode: result.type,
data: [...status.lastReceived],
txPower: status.txPower,
});
})
.catch(error => {
runningBySession[sessionId] = false;
status.running = false;
releaseDtm(sessionId).catch(() => {});
status.message = error.message;
emitForSession(sessionId, { type: 'error', message: error.message });
});
return { ok: true };
};
const stopTest = async sessionId => {
if (!SESSION_IDS.includes(sessionId)) {
throw new Error(`Unknown session: ${sessionId}`);
}
const instance = dtmBySession[sessionId];
const status = getSessionStatus(sessionId);
if (instance) {
await instance.endTest().catch(() => {});
}
await releaseDtm(sessionId);
startingBySession[sessionId] = false;
runningBySession[sessionId] = false;
status.running = false;
status.message = 'Stopped';
emitForSession(sessionId, { type: 'status', message: status.message, running: false, mode: status.mode });
return { ok: true };
};
const listJLinks = async () => listJLinkProbes();
const syncSessionConfig = (sessionId, config) => {
if (!SESSION_IDS.includes(sessionId)) {
throw new Error(`Unknown session: ${sessionId}`);
}
lastConfigBySession[sessionId] = config;
return { ok: true };
};
const canStartSession = async (sessionId, config) => {
try {
assertProbeAvailable(sessionId, config);
if (!dtmBySession[sessionId]) {
await allocateSessionPorts(sessionId, getGloballyReservedPorts());
}
return { ok: true };
} catch (error) {
return {
ok: false,
reason: error instanceof Error ? error.message : String(error),
};
}
};
const getStatus = async sessionId => {
if (!SESSION_IDS.includes(sessionId)) {
throw new Error(`Unknown session: ${sessionId}`);
}
return {
...getSessionStatus(sessionId),
running: runningBySession[sessionId],
starting: startingBySession[sessionId],
jlinkPorts:
allocatedPortsBySession[sessionId] ||
getSessionStatus(sessionId).jlinkPorts ||
JLINK_PORTS_BY_SESSION[sessionId],
};
};
const stopAllSessions = async () => {
await Promise.all(SESSION_IDS.map(sessionId => releaseDtm(sessionId)));
};
module.exports = {
listJLinks,
listSerialPorts: listJLinks,
startTest,
stopTest,
getStatus,
syncSessionConfig,
canStartSession,
stopAllSessions,
constants: {
ChannelMode,
DtmPhysicalLayer,
DtmModulationMode,
DtmPacketType,
DEFAULT_DEVICE,
DEFAULT_SWD_SPEED,
},
};