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