DTM TRAFFIC, SESSION EVENTS 로그 개선

- Receiver에 expected packet type 필드 제거
- 로그 아래로 쌓기
- Clear 버튼 추가
- Trasmitter 세션에서는 packet report 숨김
This commit is contained in:
2026-05-20 11:05:28 +09:00
parent fadeba40cc
commit 1bf60d01f3
4 changed files with 250 additions and 125 deletions
+34 -24
View File
@@ -68,19 +68,32 @@ const DTM_SETUP_CONTROL = {
5: 'TX/RX select', 5: 'TX/RX select',
}; };
const DTM_PKT_TYPE = {
0: 'PRBS9',
1: '11110000',
2: '10101010',
3: 'constant carrier',
};
const dtmChannelLabel = byte0 => { const dtmChannelLabel = byte0 => {
const channel = byte0 & 0x3f; const channel = byte0 & 0x3f;
const mhz = 2402 + 2 * channel; const mhz = 2402 + 2 * channel;
return `ch ${channel} (${mhz} MHz)`; return `ch ${channel} (${mhz} MHz)`;
}; };
const DTM_PAYLOAD_LABEL = {
0: 'PRBS9',
1: '11110000',
2: '10101010',
3: 'Constant carrier',
};
const dtmTransmitterTestNote = (b0, b1) => {
const channel = b0 & 0x3f;
const mhz = 2402 + 2 * channel;
const lengthField = (b1 >> 2) & 0x3f;
const pattern = DTM_PAYLOAD_LABEL[b1 & 0x03] ?? `type ${b1 & 0x03}`;
return `Transmitter test - transmit on ch ${channel}(${mhz}MHz), len ${lengthField}, ${pattern}`;
};
const dtmPacketReportNote = count => {
const noun = count === 1 ? 'packet' : 'packets';
return `Receiver report - ${count} ${noun} received on active RX channel (LE DTM event)`;
};
const dtmTxNote = buffer => { const dtmTxNote = buffer => {
const b0 = buffer[0]; const b0 = buffer[0];
const b1 = buffer[1]; const b1 = buffer[1];
@@ -96,15 +109,11 @@ const dtmTxNote = buffer => {
} }
if (cmdType === 1) { if (cmdType === 1) {
const length = (b1 >> 2) & 0x3f; return `Receiver test - listen on ${dtmChannelLabel(b0)}`;
const pkt = DTM_PKT_TYPE[b1 & 0x03] ?? `type ${b1 & 0x03}`;
return `Receiver test — listen on ${dtmChannelLabel(b0)}, len ${length}, ${pkt}`;
} }
if (cmdType === 2) { if (cmdType === 2) {
const length = (b1 >> 2) & 0x3f; return dtmTransmitterTestNote(b0, b1);
const pkt = DTM_PKT_TYPE[b1 & 0x03] ?? `type ${b1 & 0x03}`;
return `Transmitter test — transmit on ${dtmChannelLabel(b0)}, len ${length}, ${pkt}`;
} }
if (cmdType === 3) { if (cmdType === 3) {
@@ -125,11 +134,6 @@ const dtmRxNote = buffer => {
const b0 = buffer[0]; const b0 = buffer[0];
const b1 = buffer[1]; const b1 = buffer[1];
const packetCount = parsePacketReportCount(buffer);
if (packetCount !== null) {
return `Packet report — ${packetCount} packet(s) received on active RX channel (LE DTM event)`;
}
if ((b1 & 0x01) !== 0) { if ((b1 & 0x01) !== 0) {
const rawDbm = (b1 & 0xfe) >> 1; const rawDbm = (b1 & 0xfe) >> 1;
const modifier = b0 & 0x01 ? -128 : 0; const modifier = b0 & 0x01 ? -128 : 0;
@@ -198,15 +202,21 @@ class DTMTransport {
this.#dataBuffer = this.#dataBuffer =
this.#dataBuffer.length > 2 ? this.#dataBuffer.subarray(2) : undefined; this.#dataBuffer.length > 2 ? this.#dataBuffer.subarray(2) : undefined;
this.#eventEmitter.emit('traffic', {
direction: 'rx',
data: Array.from(response),
note: dtmRxNote(response),
});
const packetCount = parsePacketReportCount(response); const packetCount = parsePacketReportCount(response);
if (packetCount !== null) { if (packetCount !== null) {
this.#eventEmitter.emit('packetReport', packetCount); this.#eventEmitter.emit('packetReport', packetCount);
this.#eventEmitter.emit('traffic', {
direction: 'rx',
data: Array.from(response),
note: dtmPacketReportNote(packetCount),
kind: 'packetReport',
});
} else {
this.#eventEmitter.emit('traffic', {
direction: 'rx',
data: Array.from(response),
note: dtmRxNote(response),
});
} }
if (this.#callback) { if (this.#callback) {
+4 -1
View File
@@ -189,9 +189,9 @@ const attachEvents = (sessionId, instance) => {
emitForSession(sessionId, { emitForSession(sessionId, {
type: 'ended', type: 'ended',
channel: event.channel, channel: event.channel,
packets: event.packets ?? 0,
mode: event.type, mode: event.type,
data: [...status.lastReceived], data: [...status.lastReceived],
...(typeof event.packets === 'number' ? { packets: event.packets } : {}),
}); });
}); });
@@ -210,6 +210,9 @@ const attachEvents = (sessionId, instance) => {
}); });
instance.onTraffic(event => { instance.onTraffic(event => {
if (sessionId === 'transmitter' && event.kind === 'packetReport') {
return;
}
emitForSession(sessionId, { emitForSession(sessionId, {
type: 'traffic', type: 'traffic',
direction: event.direction, direction: event.direction,
+182 -100
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import packageJson from '../package.json'; import packageJson from '../package.json';
@@ -72,14 +72,22 @@ type TrafficEntry = {
const parseTrafficBytes = (hex: string) => const parseTrafficBytes = (hex: string) =>
hex === '--' ? [] : hex.split(' ').map(part => parseInt(part, 16)).filter(n => !Number.isNaN(n)); hex === '--' ? [] : hex.split(' ').map(part => parseInt(part, 16)).filter(n => !Number.isNaN(n));
/** RF-related DTM traffic: RX/TX test commands and LE Packet Report events (not setup/status). */ /** RTT direction: host→board (TX) vs board→host (RX), not over-the-air RF. */
const trafficDirLabel = (direction: Direction) => {
if (direction === 'tx') return 'TX';
if (direction === 'rx') return 'RX';
if (direction === 'error') return 'ERR';
return 'INFO';
};
/** RF-related DTM traffic: test commands and packet-report events (not setup/status). */
const isRfTrafficEntry = (entry: TrafficEntry) => { const isRfTrafficEntry = (entry: TrafficEntry) => {
if (entry.direction === 'info' || entry.direction === 'error') return false; if (entry.direction === 'info' || entry.direction === 'error') return false;
if ( if (
entry.note.startsWith('Packet report') || entry.note.startsWith('Receiver test -') ||
entry.note.startsWith('Receiver test') || entry.note.startsWith('Transmitter test -') ||
entry.note.startsWith('Transmitter test') entry.note.startsWith('Receiver report -')
) { ) {
return true; return true;
} }
@@ -95,6 +103,108 @@ const isRfTrafficEntry = (entry: TrafficEntry) => {
return cmdType === 0b01 || cmdType === 0b10; return cmdType === 0b01 || cmdType === 0b10;
}; };
const SCROLL_BOTTOM_THRESHOLD_PX = 12;
/** Append-newest-at-bottom lists: auto-scroll only while the user is already at the bottom. */
function useStickToBottomOnAppend(items: unknown[]) {
const scrollRef = useRef<HTMLDivElement>(null);
const stickToBottomRef = useRef(true);
const onScroll = () => {
const el = scrollRef.current;
if (!el) {
return;
}
stickToBottomRef.current =
el.scrollHeight - el.scrollTop - el.clientHeight <= SCROLL_BOTTOM_THRESHOLD_PX;
};
useLayoutEffect(() => {
const el = scrollRef.current;
if (!el) {
return;
}
if (items.length === 0) {
el.scrollTop = 0;
stickToBottomRef.current = true;
return;
}
if (stickToBottomRef.current) {
el.scrollTop = el.scrollHeight;
}
}, [items]);
return { scrollRef, onScroll };
}
function TrafficLogList({
sessionId,
trafficLog,
trafficRfOnly,
}: {
sessionId: SessionId;
trafficLog: TrafficEntry[];
trafficRfOnly: boolean;
}) {
const visibleTraffic = useMemo(
() => (trafficRfOnly ? trafficLog.filter(isRfTrafficEntry) : trafficLog),
[trafficLog, trafficRfOnly]
);
const { scrollRef, onScroll } = useStickToBottomOnAppend(visibleTraffic);
return (
<div ref={scrollRef} onScroll={onScroll} className="traffic-list scroll-panel">
{visibleTraffic.length === 0 ? (
<div className="empty-state">
{trafficLog.length === 0 ? 'No traffic yet.' : 'No RF traffic (setup/status hidden).'}
</div>
) : (
visibleTraffic.map((entry, index) => (
<div
key={`${sessionId}-traffic-${entry.time}-${index}`}
className={`traffic-item ${entry.direction}`}
>
<span className="traffic-time">{entry.time}</span>
<span className="traffic-dir">{trafficDirLabel(entry.direction)}</span>
<span className="traffic-hex">{entry.hex}</span>
<span className="traffic-note">{entry.note}</span>
</div>
))
)}
</div>
);
}
function SessionEventLogList({
sessionId,
eventLog,
}: {
sessionId: SessionId;
eventLog: LogEntry[];
}) {
const { scrollRef, onScroll } = useStickToBottomOnAppend(eventLog);
return (
<div ref={scrollRef} onScroll={onScroll} className="log-list scroll-panel">
{eventLog.length === 0 ? (
<div className="empty-state">No events yet.</div>
) : (
eventLog.map((entry, index) => (
<div
key={`${sessionId}-event-${entry.time}-${index}`}
className="log-item"
>
<span className="log-time">{entry.time}</span>
<span className="log-message">{entry.message}</span>
</div>
))
)}
</div>
);
}
type JlinkPorts = { type JlinkPorts = {
gdb: number; gdb: number;
swo: number; swo: number;
@@ -169,8 +279,8 @@ const formatLogTime = (date = new Date()) => {
return `${hours}:${minutes}:${seconds}.${millis}`; return `${hours}:${minutes}:${seconds}.${millis}`;
}; };
const prependEvent = (message: string, eventLog: LogEntry[]) => const appendEvent = (message: string, eventLog: LogEntry[]) =>
[{ time: formatLogTime(), message }, ...eventLog].slice(0, PANEL_LIMIT); [...eventLog, { time: formatLogTime(), message }].slice(-PANEL_LIMIT);
const formatSessionMessage = (message: string) => const formatSessionMessage = (message: string) =>
message message
@@ -288,7 +398,7 @@ function App() {
const nextState: SessionState = { const nextState: SessionState = {
...current, ...current,
trafficLog: [entry, ...current.trafficLog].slice(0, TRAFFIC_LIMIT), trafficLog: [...current.trafficLog, entry].slice(-TRAFFIC_LIMIT),
lastTxHex: entry.direction === 'tx' ? entry.hex : current.lastTxHex, lastTxHex: entry.direction === 'tx' ? entry.hex : current.lastTxHex,
lastRxHex: entry.direction === 'rx' ? entry.hex : current.lastRxHex, lastRxHex: entry.direction === 'rx' ? entry.hex : current.lastRxHex,
}; };
@@ -316,7 +426,7 @@ function App() {
received: new Array(40).fill(0), received: new Array(40).fill(0),
txVisited: new Array(40).fill(false), txVisited: new Array(40).fill(false),
currentChannel: null, currentChannel: null,
eventLog: prependEvent('Reset test state', current.eventLog), eventLog: appendEvent('Reset test state', current.eventLog),
}; };
} }
@@ -331,7 +441,7 @@ function App() {
...current, ...current,
currentChannel: channel, currentChannel: channel,
txVisited, txVisited,
eventLog: prependEvent(`Started ${String(event.mode)} on ch ${channel}`, current.eventLog), eventLog: appendEvent(`Started ${String(event.mode)} on ch ${channel}`, current.eventLog),
}; };
} }
@@ -345,14 +455,17 @@ function App() {
if (nextType === 'ended') { if (nextType === 'ended') {
const data = Array.isArray(event.data) ? (event.data as number[]) : current.received; const data = Array.isArray(event.data) ? (event.data as number[]) : current.received;
const channel = String(event.channel);
const endedMessage =
event.mode === 'receiver' && typeof event.packets === 'number'
? `Ended receiver on ch ${channel} · ${event.packets} packets received`
: `Ended ${String(event.mode)} on ch ${channel}`;
return { return {
...current, ...current,
received: data, received: data,
currentChannel: null, currentChannel: null,
eventLog: prependEvent( eventLog: appendEvent(endedMessage, current.eventLog),
`Ended ${String(event.mode)} on ch ${String(event.channel)} packets=${String(event.packets ?? 0)}`,
current.eventLog
),
}; };
} }
@@ -368,7 +481,7 @@ function App() {
txPowerFeedback: txPowerFeedback:
typeof event.txPower === 'number' ? Number(event.txPower) : current.txPowerFeedback, typeof event.txPower === 'number' ? Number(event.txPower) : current.txPowerFeedback,
message: 'Test finished', message: 'Test finished',
eventLog: prependEvent('Test finished', current.eventLog), eventLog: appendEvent('Test finished', current.eventLog),
}; };
} }
@@ -399,7 +512,7 @@ function App() {
starting: false, starting: false,
currentChannel: null, currentChannel: null,
message: String(event.message ?? 'Unknown error'), message: String(event.message ?? 'Unknown error'),
eventLog: prependEvent( eventLog: appendEvent(
`Error: ${String(event.message ?? 'Unknown error')}`, `Error: ${String(event.message ?? 'Unknown error')}`,
current.eventLog current.eventLog
), ),
@@ -498,7 +611,7 @@ function App() {
running: false, running: false,
starting: false, starting: false,
message: canStart.reason || 'Cannot start', message: canStart.reason || 'Cannot start',
eventLog: prependEvent(canStart.reason || 'Cannot start', current.eventLog), eventLog: appendEvent(canStart.reason || 'Cannot start', current.eventLog),
})); }));
return; return;
} }
@@ -514,7 +627,7 @@ function App() {
trafficLog: [], trafficLog: [],
lastTxHex: '--', lastTxHex: '--',
lastRxHex: '--', lastRxHex: '--',
eventLog: prependEvent('Requested start', current.eventLog), eventLog: appendEvent('Requested start', current.eventLog),
})); }));
try { try {
@@ -537,7 +650,7 @@ function App() {
starting: false, starting: false,
currentChannel: null, currentChannel: null,
message: 'Stopped', message: 'Stopped',
eventLog: prependEvent('Requested stop', current.eventLog), eventLog: appendEvent('Requested stop', current.eventLog),
})); }));
}; };
@@ -638,28 +751,6 @@ function App() {
<div <div
className={`radio-param-grid ${isReceiver ? 'radio-param-grid--rx' : 'radio-param-grid--tx'}`} className={`radio-param-grid ${isReceiver ? 'radio-param-grid--rx' : 'radio-param-grid--tx'}`}
> >
{isReceiver && (
<div className="field">
<label>Expected packet type</label>
<div className="field-control-wrap">
<select
className="field-control"
value={config.bitpattern}
onChange={event =>
updateConfig(sessionId, 'bitpattern', Number(event.target.value))
}
>
{PACKET_OPTIONS.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<span className="field-hint">Start Transmitter </span>
</div>
)}
{!isReceiver && ( {!isReceiver && (
<> <>
<div className="field"> <div className="field">
@@ -1036,70 +1127,61 @@ function App() {
<section className="panel dock-panel"> <section className="panel dock-panel">
<div className="panel-title"> <div className="panel-title">
<span>DTM Traffic</span> <span>DTM Traffic</span>
<button <div className="panel-title-actions">
type="button" <button
className={ type="button"
session.trafficRfOnly className={
? 'traffic-filter-btn active' session.trafficRfOnly
: 'traffic-filter-btn' ? 'traffic-filter-btn active'
} : 'traffic-filter-btn'
onClick={() => }
setSession(sessionId, current => ({ onClick={() =>
...current, setSession(sessionId, current => ({
trafficRfOnly: !current.trafficRfOnly, ...current,
})) trafficRfOnly: !current.trafficRfOnly,
} }))
> }
RF only >
</button> RF only
</div> </button>
<div className="traffic-list scroll-panel"> <button
{(() => { type="button"
const visibleTraffic = session.trafficRfOnly className="traffic-filter-btn"
? session.trafficLog.filter(isRfTrafficEntry) onClick={() =>
: session.trafficLog; setSession(sessionId, current => ({
...current,
if (visibleTraffic.length === 0) { trafficLog: [],
return ( }))
<div className="empty-state"> }
{session.trafficLog.length === 0 >
? 'No traffic yet.' Clear
: 'No RF traffic (setup/status hidden).'} </button>
</div> </div>
);
}
return visibleTraffic.map((entry, index) => (
<div
key={`${sessionId}-traffic-${entry.time}-${index}`}
className={`traffic-item ${entry.direction}`}
>
<span className="traffic-time">{entry.time}</span>
<span className="traffic-dir">{entry.direction.toUpperCase()}</span>
<span className="traffic-hex">{entry.hex}</span>
<span className="traffic-note">{entry.note}</span>
</div>
));
})()}
</div> </div>
<TrafficLogList
sessionId={sessionId}
trafficLog={session.trafficLog}
trafficRfOnly={session.trafficRfOnly}
/>
</section> </section>
<section className="panel dock-panel"> <section className="panel dock-panel">
<div className="panel-title">Session Events</div> <div className="panel-title">
<div className="log-list scroll-panel"> <span>Session Events</span>
{session.eventLog.length === 0 && ( <button
<div className="empty-state">No events yet.</div> type="button"
)} className="traffic-filter-btn"
{session.eventLog.map((entry, index) => ( onClick={() =>
<div setSession(sessionId, current => ({
key={`${sessionId}-event-${entry.time}-${index}`} ...current,
className="log-item" eventLog: [],
> }))
<span className="log-time">{entry.time}</span> }
<span className="log-message">{entry.message}</span> >
</div> Clear
))} </button>
</div> </div>
<SessionEventLogList sessionId={sessionId} eventLog={session.eventLog} />
</section> </section>
</div> </div>
</div> </div>
+30
View File
@@ -1017,6 +1017,36 @@ input:focus {
align-content: start; align-content: start;
} }
.panel-title--traffic {
flex-wrap: wrap;
gap: 6px 10px;
align-items: flex-start;
}
.panel-title--traffic .panel-title-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.traffic-legend {
font-size: 9px;
font-weight: 500;
color: var(--text-dim);
line-height: 1.35;
letter-spacing: 0;
text-transform: none;
}
.panel-title-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.traffic-filter-btn { .traffic-filter-btn {
padding: 2px 8px; padding: 2px 8px;
border-radius: 4px; border-radius: 4px;