Implement trace packet logging and SNR display enhancements

This commit is contained in:
Lloyd
2025-10-30 00:11:04 +00:00
parent ed678af4ca
commit 85df2c5b0f
3 changed files with 222 additions and 9 deletions
+12 -4
View File
@@ -239,6 +239,18 @@ class RepeaterHandler(BaseHandler):
if len(self.recent_packets) > self.max_recent_packets:
self.recent_packets.pop(0)
def log_trace_record(self, packet_record: dict) -> None:
self.recent_packets.append(packet_record)
self.rx_count += 1
if packet_record.get("transmitted", False):
self.forwarded_count += 1
else:
self.dropped_count += 1
if len(self.recent_packets) > self.max_recent_packets:
self.recent_packets.pop(0)
def cleanup_cache(self):
now = time.time()
@@ -564,10 +576,6 @@ class RepeaterHandler(BaseHandler):
logger.error(f"Error sending periodic advert: {e}", exc_info=True)
def get_noise_floor(self) -> Optional[float]:
"""
Get the current noise floor (instantaneous RSSI) from the radio in dBm.
Returns None if radio is not available or reading fails.
"""
try:
radio = self.dispatcher.radio if self.dispatcher else None
if radio and hasattr(radio, 'get_noise_floor'):
+60 -1
View File
@@ -132,7 +132,59 @@ class RepeaterDaemon:
trace_path = parsed_data["trace_path"]
trace_path_len = len(trace_path)
if self.repeater_handler:
import time
trace_path_bytes = [f"{h:02X}" for h in trace_path[:8]]
if len(trace_path) > 8:
trace_path_bytes.append("...")
path_hash = "[" + ", ".join(trace_path_bytes) + "]"
path_snrs = []
path_snr_details = []
for i in range(packet.path_len):
if i < len(packet.path):
snr_val = packet.path[i]
snr_db = snr_val / 4.0
path_snrs.append(f"{snr_val}({snr_db:.1f}dB)")
# Create hash->SNR mapping for display
if i < len(trace_path):
path_snr_details.append({
"hash": f"{trace_path[i]:02X}",
"snr_raw": snr_val,
"snr_db": snr_db
})
packet_record = {
"timestamp": time.time(),
"type": packet.get_payload_type(), # 0x09 for trace
"route": packet.get_route_type(), # Should be direct (1)
"length": len(packet.payload or b""),
"rssi": getattr(packet, "rssi", 0),
"snr": getattr(packet, "snr", 0.0),
"score": self.repeater_handler.calculate_packet_score(
getattr(packet, "snr", 0.0),
len(packet.payload or b""),
self.repeater_handler.radio_config.get("spreading_factor", 8)
),
"tx_delay_ms": 0,
"transmitted": False,
"is_duplicate": False,
"packet_hash": packet.calculate_packet_hash().hex()[:16],
"drop_reason": "trace_received",
"path_hash": path_hash,
"src_hash": None,
"dst_hash": None,
"original_path": [f"{h:02X}" for h in trace_path],
"forwarded_path": None,
# Add trace-specific SNR path information
"path_snrs": path_snrs, # ["58(14.5dB)", "19(4.8dB)"]
"path_snr_details": path_snr_details, # [{"hash": "29", "snr_raw": 58, "snr_db": 14.5}]
"is_trace": True,
}
self.repeater_handler.log_trace_record(packet_record)
path_snrs = []
path_hashes = []
for i in range(packet.path_len):
@@ -156,6 +208,13 @@ class RepeaterDaemon:
trace_path[packet.path_len] == self.local_hash and
self.repeater_handler and not self.repeater_handler.is_duplicate(packet)):
if self.repeater_handler and hasattr(self.repeater_handler, 'recent_packets'):
packet_hash = packet.calculate_packet_hash().hex()[:16]
for record in reversed(self.repeater_handler.recent_packets):
if record.get("packet_hash") == packet_hash:
record["transmitted"] = True
record["drop_reason"] = "trace_forwarded"
break
snr_scaled = int(packet.get_snr() * 4)
snr_byte = snr_scaled & 0xFF
+150 -4
View File
@@ -229,6 +229,61 @@
<span class="signal-bars ${className}" title="Signal: ${className.replace('signal-', '')}">${'<span class="signal-bar"></span>'.repeat(4)}</span>
<span class="snr-value">${snr.toFixed(1)} dB</span>
</div>`;
}
// Helper function to display SNR for trace packets with path information
function getTraceSnrDisplay(pkt, localHash) {
if (!pkt.is_trace || !pkt.path_snr_details || pkt.path_snr_details.length === 0) {
// Regular packet or no path SNR data
return getSignalBars(pkt.snr);
}
// Build trace path SNR display
let pathSnrHtml = `<div class="trace-snr-container">`;
// Show received packet SNR first
pathSnrHtml += `<div class="rx-snr">${getSignalBars(pkt.snr)}</div>`;
// Show path SNRs if available
if (pkt.path_snr_details.length > 0) {
pathSnrHtml += `<div class="path-snrs">`;
pathSnrHtml += `<div class="path-snr-label">Path (${pkt.path_snr_details.length} hops):</div>`;
// Handle many hops - show first few and indicate if there are more
const maxDisplayHops = 4;
const hopsToShow = pkt.path_snr_details.slice(0, maxDisplayHops);
const hasMoreHops = pkt.path_snr_details.length > maxDisplayHops;
hopsToShow.forEach((pathSnr, index) => {
const isMyHash = localHash && pathSnr.hash === localHash;
const hashClass = isMyHash ? 'my-hash' : 'path-hash';
// Get signal quality class for color coding
let snrClass = 'snr-poor';
if (pathSnr.snr_db >= 10) snrClass = 'snr-excellent';
else if (pathSnr.snr_db >= 5) snrClass = 'snr-good';
else if (pathSnr.snr_db >= 0) snrClass = 'snr-fair';
pathSnrHtml += `<div class="path-snr-item">
<span class="hop-index">${index + 1}.</span>
<span class="${hashClass}">${pathSnr.hash}</span>
<span class="snr-value ${snrClass}">${pathSnr.snr_db.toFixed(1)}dB</span>
</div>`;
});
// Show indicator if there are more hops
if (hasMoreHops) {
const remainingCount = pkt.path_snr_details.length - maxDisplayHops;
pathSnrHtml += `<div class="path-snr-item">
<span class="more-hops">+${remainingCount} more hop${remainingCount > 1 ? 's' : ''}</span>
</div>`;
}
pathSnrHtml += `</div>`;
}
pathSnrHtml += `</div>`;
return pathSnrHtml;
} function updatePacketTable(packets, localHash) {
const tbody = document.getElementById('packet-table');
@@ -244,7 +299,13 @@
}
tbody.innerHTML = packets.slice(-20).map(pkt => {
const time = new Date(pkt.timestamp * 1000).toLocaleTimeString();
const time = new Date(pkt.timestamp * 1000).toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
// Match pyMC_core PAYLOAD_TYPES exactly (from constants.py)
const typeNames = {
0: 'REQ',
@@ -327,7 +388,7 @@
<td data-label="Len">${pkt.length}B</td>
<td data-label="Path/Hashes">${pathHashesHtml}</td>
<td data-label="RSSI">${pkt.rssi}</td>
<td data-label="SNR">${getSignalBars(pkt.snr)}</td>
<td data-label="SNR">${getTraceSnrDisplay(pkt, localHash)}</td>
<td data-label="Score"><span class="score">${pkt.score.toFixed(2)}</span></td>
<td data-label="TX Delay">${pkt.tx_delay_ms.toFixed(0)}ms</td>
<td data-label="Status">${statusHtml}</td>
@@ -337,7 +398,13 @@
// Add duplicate rows (always visible)
if (hasDuplicates) {
mainRow += pkt.duplicates.map(dupe => {
const dupeTime = new Date(dupe.timestamp * 1000).toLocaleTimeString();
const dupeTime = new Date(dupe.timestamp * 1000).toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
const dupeRoute = routeNames[dupe.route] || `UNKNOWN_${dupe.route}`;
// Format duplicate path/hashes - match main row format
@@ -375,7 +442,7 @@
<td data-label="Len">${dupe.length}B</td>
<td data-label="Path/Hashes">${dupePathHashesHtml}</td>
<td data-label="RSSI">${dupe.rssi}</td>
<td data-label="SNR">${getSignalBars(dupe.snr)}</td>
<td data-label="SNR">${getTraceSnrDisplay(dupe, localHash)}</td>
<td data-label="Score"><span class="score">${dupe.score.toFixed(2)}</span></td>
<td data-label="TX Delay">${dupe.tx_delay_ms.toFixed(0)}ms</td>
<td data-label="Status">${dupeStatusHtml}</td>
@@ -589,6 +656,85 @@
font-size: 0.8em;
color: #999;
white-space: nowrap;
}
/* Trace packet SNR display */
.trace-snr-container {
display: flex;
flex-direction: column;
gap: 6px;
align-items: center;
min-width: 120px;
}
.rx-snr {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 4px;
}
.path-snrs {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 0.85em;
width: 100%;
}
.path-snr-label {
font-size: 0.75em;
color: #888;
text-align: center;
margin-bottom: 2px;
font-weight: 500;
}
.path-snr-item {
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
justify-content: space-between;
}
.hop-index {
font-size: 0.7em;
color: #666;
min-width: 16px;
text-align: right;
}
.path-snr-item .path-hash {
font-family: 'Courier New', monospace;
font-size: 0.75em;
color: #dcdcaa;
background: rgba(220, 220, 170, 0.1);
padding: 1px 3px;
border-radius: 3px;
min-width: 24px;
text-align: center;
}
.path-snr-item .my-hash {
font-family: 'Courier New', monospace;
font-size: 0.75em;
background: rgba(86, 156, 214, 0.2);
color: #569cd6;
font-weight: 700;
padding: 1px 3px;
border-radius: 3px;
min-width: 24px;
text-align: center;
}
.path-snr-item .snr-value {
font-size: 0.75em;
font-weight: 500;
min-width: 48px;
text-align: right;
}
/* SNR quality color coding */
.snr-excellent { color: #4ade80; }
.snr-good { color: #4ec9b0; }
.snr-fair { color: #fbbf24; }
.snr-poor { color: #f48771; }
.more-hops {
font-size: 0.7em;
color: #888;
font-style: italic;
text-align: center;
width: 100%;
} /* Path/Hashes column layout */
.path-info {
display: flex;