DTM TRAFFIC, SESSION EVENTS 로그 개선
- Receiver에 expected packet type 필드 제거 - 로그 아래로 쌓기 - Clear 버튼 추가 - Trasmitter 세션에서는 packet report 숨김
This commit is contained in:
@@ -68,19 +68,32 @@ const DTM_SETUP_CONTROL = {
|
||||
5: 'TX/RX select',
|
||||
};
|
||||
|
||||
const DTM_PKT_TYPE = {
|
||||
0: 'PRBS9',
|
||||
1: '11110000',
|
||||
2: '10101010',
|
||||
3: 'constant carrier',
|
||||
};
|
||||
|
||||
const dtmChannelLabel = byte0 => {
|
||||
const channel = byte0 & 0x3f;
|
||||
const mhz = 2402 + 2 * channel;
|
||||
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 b0 = buffer[0];
|
||||
const b1 = buffer[1];
|
||||
@@ -96,15 +109,11 @@ const dtmTxNote = buffer => {
|
||||
}
|
||||
|
||||
if (cmdType === 1) {
|
||||
const length = (b1 >> 2) & 0x3f;
|
||||
const pkt = DTM_PKT_TYPE[b1 & 0x03] ?? `type ${b1 & 0x03}`;
|
||||
return `Receiver test — listen on ${dtmChannelLabel(b0)}, len ${length}, ${pkt}`;
|
||||
return `Receiver test - listen on ${dtmChannelLabel(b0)}`;
|
||||
}
|
||||
|
||||
if (cmdType === 2) {
|
||||
const length = (b1 >> 2) & 0x3f;
|
||||
const pkt = DTM_PKT_TYPE[b1 & 0x03] ?? `type ${b1 & 0x03}`;
|
||||
return `Transmitter test — transmit on ${dtmChannelLabel(b0)}, len ${length}, ${pkt}`;
|
||||
return dtmTransmitterTestNote(b0, b1);
|
||||
}
|
||||
|
||||
if (cmdType === 3) {
|
||||
@@ -125,11 +134,6 @@ const dtmRxNote = buffer => {
|
||||
const b0 = buffer[0];
|
||||
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) {
|
||||
const rawDbm = (b1 & 0xfe) >> 1;
|
||||
const modifier = b0 & 0x01 ? -128 : 0;
|
||||
@@ -198,15 +202,21 @@ class DTMTransport {
|
||||
this.#dataBuffer =
|
||||
this.#dataBuffer.length > 2 ? this.#dataBuffer.subarray(2) : undefined;
|
||||
|
||||
const packetCount = parsePacketReportCount(response);
|
||||
if (packetCount !== null) {
|
||||
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),
|
||||
});
|
||||
|
||||
const packetCount = parsePacketReportCount(response);
|
||||
if (packetCount !== null) {
|
||||
this.#eventEmitter.emit('packetReport', packetCount);
|
||||
}
|
||||
|
||||
if (this.#callback) {
|
||||
|
||||
@@ -189,9 +189,9 @@ const attachEvents = (sessionId, instance) => {
|
||||
emitForSession(sessionId, {
|
||||
type: 'ended',
|
||||
channel: event.channel,
|
||||
packets: event.packets ?? 0,
|
||||
mode: event.type,
|
||||
data: [...status.lastReceived],
|
||||
...(typeof event.packets === 'number' ? { packets: event.packets } : {}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -210,6 +210,9 @@ const attachEvents = (sessionId, instance) => {
|
||||
});
|
||||
|
||||
instance.onTraffic(event => {
|
||||
if (sessionId === 'transmitter' && event.kind === 'packetReport') {
|
||||
return;
|
||||
}
|
||||
emitForSession(sessionId, {
|
||||
type: 'traffic',
|
||||
direction: event.direction,
|
||||
|
||||
+162
-80
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import packageJson from '../package.json';
|
||||
|
||||
@@ -72,14 +72,22 @@ type TrafficEntry = {
|
||||
const parseTrafficBytes = (hex: string) =>
|
||||
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) => {
|
||||
if (entry.direction === 'info' || entry.direction === 'error') return false;
|
||||
|
||||
if (
|
||||
entry.note.startsWith('Packet report') ||
|
||||
entry.note.startsWith('Receiver test') ||
|
||||
entry.note.startsWith('Transmitter test')
|
||||
entry.note.startsWith('Receiver test -') ||
|
||||
entry.note.startsWith('Transmitter test -') ||
|
||||
entry.note.startsWith('Receiver report -')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -95,6 +103,108 @@ const isRfTrafficEntry = (entry: TrafficEntry) => {
|
||||
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 = {
|
||||
gdb: number;
|
||||
swo: number;
|
||||
@@ -169,8 +279,8 @@ const formatLogTime = (date = new Date()) => {
|
||||
return `${hours}:${minutes}:${seconds}.${millis}`;
|
||||
};
|
||||
|
||||
const prependEvent = (message: string, eventLog: LogEntry[]) =>
|
||||
[{ time: formatLogTime(), message }, ...eventLog].slice(0, PANEL_LIMIT);
|
||||
const appendEvent = (message: string, eventLog: LogEntry[]) =>
|
||||
[...eventLog, { time: formatLogTime(), message }].slice(-PANEL_LIMIT);
|
||||
|
||||
const formatSessionMessage = (message: string) =>
|
||||
message
|
||||
@@ -288,7 +398,7 @@ function App() {
|
||||
|
||||
const nextState: SessionState = {
|
||||
...current,
|
||||
trafficLog: [entry, ...current.trafficLog].slice(0, TRAFFIC_LIMIT),
|
||||
trafficLog: [...current.trafficLog, entry].slice(-TRAFFIC_LIMIT),
|
||||
lastTxHex: entry.direction === 'tx' ? entry.hex : current.lastTxHex,
|
||||
lastRxHex: entry.direction === 'rx' ? entry.hex : current.lastRxHex,
|
||||
};
|
||||
@@ -316,7 +426,7 @@ function App() {
|
||||
received: new Array(40).fill(0),
|
||||
txVisited: new Array(40).fill(false),
|
||||
currentChannel: null,
|
||||
eventLog: prependEvent('Reset test state', current.eventLog),
|
||||
eventLog: appendEvent('Reset test state', current.eventLog),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -331,7 +441,7 @@ function App() {
|
||||
...current,
|
||||
currentChannel: channel,
|
||||
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') {
|
||||
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 {
|
||||
...current,
|
||||
received: data,
|
||||
currentChannel: null,
|
||||
eventLog: prependEvent(
|
||||
`Ended ${String(event.mode)} on ch ${String(event.channel)} packets=${String(event.packets ?? 0)}`,
|
||||
current.eventLog
|
||||
),
|
||||
eventLog: appendEvent(endedMessage, current.eventLog),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -368,7 +481,7 @@ function App() {
|
||||
txPowerFeedback:
|
||||
typeof event.txPower === 'number' ? Number(event.txPower) : current.txPowerFeedback,
|
||||
message: 'Test finished',
|
||||
eventLog: prependEvent('Test finished', current.eventLog),
|
||||
eventLog: appendEvent('Test finished', current.eventLog),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -399,7 +512,7 @@ function App() {
|
||||
starting: false,
|
||||
currentChannel: null,
|
||||
message: String(event.message ?? 'Unknown error'),
|
||||
eventLog: prependEvent(
|
||||
eventLog: appendEvent(
|
||||
`Error: ${String(event.message ?? 'Unknown error')}`,
|
||||
current.eventLog
|
||||
),
|
||||
@@ -498,7 +611,7 @@ function App() {
|
||||
running: false,
|
||||
starting: false,
|
||||
message: canStart.reason || 'Cannot start',
|
||||
eventLog: prependEvent(canStart.reason || 'Cannot start', current.eventLog),
|
||||
eventLog: appendEvent(canStart.reason || 'Cannot start', current.eventLog),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
@@ -514,7 +627,7 @@ function App() {
|
||||
trafficLog: [],
|
||||
lastTxHex: '--',
|
||||
lastRxHex: '--',
|
||||
eventLog: prependEvent('Requested start', current.eventLog),
|
||||
eventLog: appendEvent('Requested start', current.eventLog),
|
||||
}));
|
||||
|
||||
try {
|
||||
@@ -537,7 +650,7 @@ function App() {
|
||||
starting: false,
|
||||
currentChannel: null,
|
||||
message: 'Stopped',
|
||||
eventLog: prependEvent('Requested stop', current.eventLog),
|
||||
eventLog: appendEvent('Requested stop', current.eventLog),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -638,28 +751,6 @@ function App() {
|
||||
<div
|
||||
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 && (
|
||||
<>
|
||||
<div className="field">
|
||||
@@ -1036,6 +1127,7 @@ function App() {
|
||||
<section className="panel dock-panel">
|
||||
<div className="panel-title">
|
||||
<span>DTM Traffic</span>
|
||||
<div className="panel-title-actions">
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
@@ -1052,54 +1144,44 @@ function App() {
|
||||
>
|
||||
RF only
|
||||
</button>
|
||||
</div>
|
||||
<div className="traffic-list scroll-panel">
|
||||
{(() => {
|
||||
const visibleTraffic = session.trafficRfOnly
|
||||
? session.trafficLog.filter(isRfTrafficEntry)
|
||||
: session.trafficLog;
|
||||
|
||||
if (visibleTraffic.length === 0) {
|
||||
return (
|
||||
<div className="empty-state">
|
||||
{session.trafficLog.length === 0
|
||||
? 'No traffic yet.'
|
||||
: 'No RF traffic (setup/status hidden).'}
|
||||
</div>
|
||||
);
|
||||
<button
|
||||
type="button"
|
||||
className="traffic-filter-btn"
|
||||
onClick={() =>
|
||||
setSession(sessionId, current => ({
|
||||
...current,
|
||||
trafficLog: [],
|
||||
}))
|
||||
}
|
||||
|
||||
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>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
<TrafficLogList
|
||||
sessionId={sessionId}
|
||||
trafficLog={session.trafficLog}
|
||||
trafficRfOnly={session.trafficRfOnly}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="panel dock-panel">
|
||||
<div className="panel-title">Session Events</div>
|
||||
<div className="log-list scroll-panel">
|
||||
{session.eventLog.length === 0 && (
|
||||
<div className="empty-state">No events yet.</div>
|
||||
)}
|
||||
{session.eventLog.map((entry, index) => (
|
||||
<div
|
||||
key={`${sessionId}-event-${entry.time}-${index}`}
|
||||
className="log-item"
|
||||
<div className="panel-title">
|
||||
<span>Session Events</span>
|
||||
<button
|
||||
type="button"
|
||||
className="traffic-filter-btn"
|
||||
onClick={() =>
|
||||
setSession(sessionId, current => ({
|
||||
...current,
|
||||
eventLog: [],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<span className="log-time">{entry.time}</span>
|
||||
<span className="log-message">{entry.message}</span>
|
||||
</div>
|
||||
))}
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<SessionEventLogList sessionId={sessionId} eventLog={session.eventLog} />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1017,6 +1017,36 @@ input:focus {
|
||||
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 {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
Reference in New Issue
Block a user