From 85df2c5b0f9b55ae5d0644aa52a09e04d2a2fa07 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Thu, 30 Oct 2025 00:11:04 +0000 Subject: [PATCH] Implement trace packet logging and SNR display enhancements --- repeater/engine.py | 16 +++- repeater/main.py | 61 +++++++++++- repeater/templates/dashboard.html | 154 +++++++++++++++++++++++++++++- 3 files changed, 222 insertions(+), 9 deletions(-) diff --git a/repeater/engine.py b/repeater/engine.py index d6252dc..3d58c76 100644 --- a/repeater/engine.py +++ b/repeater/engine.py @@ -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'): diff --git a/repeater/main.py b/repeater/main.py index 1835ec5..234585f 100644 --- a/repeater/main.py +++ b/repeater/main.py @@ -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 diff --git a/repeater/templates/dashboard.html b/repeater/templates/dashboard.html index 50946f9..da558db 100644 --- a/repeater/templates/dashboard.html +++ b/repeater/templates/dashboard.html @@ -229,6 +229,61 @@ ${''.repeat(4)} ${snr.toFixed(1)} dB `; + } + + // 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 = `
`; + + // Show received packet SNR first + pathSnrHtml += `
${getSignalBars(pkt.snr)}
`; + + // Show path SNRs if available + if (pkt.path_snr_details.length > 0) { + pathSnrHtml += `
`; + pathSnrHtml += `
Path (${pkt.path_snr_details.length} hops):
`; + + // 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 += `
+ ${index + 1}. + ${pathSnr.hash} + ${pathSnr.snr_db.toFixed(1)}dB +
`; + }); + + // Show indicator if there are more hops + if (hasMoreHops) { + const remainingCount = pkt.path_snr_details.length - maxDisplayHops; + pathSnrHtml += `
+ +${remainingCount} more hop${remainingCount > 1 ? 's' : ''} +
`; + } + + pathSnrHtml += `
`; + } + + pathSnrHtml += `
`; + 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 @@ ${pkt.length}B ${pathHashesHtml} ${pkt.rssi} - ${getSignalBars(pkt.snr)} + ${getTraceSnrDisplay(pkt, localHash)} ${pkt.score.toFixed(2)} ${pkt.tx_delay_ms.toFixed(0)}ms ${statusHtml} @@ -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 @@ ${dupe.length}B ${dupePathHashesHtml} ${dupe.rssi} - ${getSignalBars(dupe.snr)} + ${getTraceSnrDisplay(dupe, localHash)} ${dupe.score.toFixed(2)} ${dupe.tx_delay_ms.toFixed(0)}ms ${dupeStatusHtml} @@ -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;