From cf537628cf4ea6aa4c2642eb4b91e250f7923dfd Mon Sep 17 00:00:00 2001 From: MarekWo Date: Wed, 18 Feb 2026 08:26:43 +0100 Subject: [PATCH] feat: Add MeshCore Analyzer link button to channel messages Compute packet_hash from pkt_payload (SHA-256 of type byte + payload) and generate analyzer.letsmesh.net links. Button appears on both sent and received messages when echo data is available. Co-Authored-By: Claude Opus 4.6 --- app/routes/api.py | 21 +++++++++++++++++++++ app/static/js/app.js | 10 ++++++++++ meshcore-bridge/bridge.py | 8 +++++--- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/routes/api.py b/app/routes/api.py index 4e3188e..23396f3 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -2,6 +2,7 @@ REST API endpoints for mc-webui """ +import hashlib import logging import json import re @@ -33,6 +34,20 @@ _contacts_detailed_cache_timestamp = 0 CONTACTS_DETAILED_CACHE_TTL = 60 # seconds +ANALYZER_BASE_URL = 'https://analyzer.letsmesh.net/packets?packet_hash=' +GRP_TXT_TYPE_BYTE = 0x05 + + +def compute_analyzer_url(pkt_payload): + """Compute MeshCore Analyzer URL from a hex-encoded pkt_payload.""" + try: + raw = bytes([GRP_TXT_TYPE_BYTE]) + bytes.fromhex(pkt_payload) + packet_hash = hashlib.sha256(raw).hexdigest()[:16].upper() + return f"{ANALYZER_BASE_URL}{packet_hash}" + except (ValueError, TypeError): + return None + + def get_channels_cached(force_refresh=False): """ Get channels with caching to reduce USB/meshcli calls. @@ -321,6 +336,9 @@ def get_messages(): abs(msg['timestamp'] - ec['timestamp']) < 5): msg['echo_count'] = ec['count'] msg['echo_paths'] = ec.get('paths', []) + pkt = ec.get('pkt_payload') + if pkt: + msg['analyzer_url'] = compute_analyzer_url(pkt) break # Merge incoming paths into received messages @@ -342,6 +360,9 @@ def get_messages(): best_delta = delta if best_match: msg['path'] = best_match['path'] + pkt = best_match.get('pkt_payload') + if pkt: + msg['analyzer_url'] = compute_analyzer_url(pkt) except Exception as e: logger.debug(f"Echo data fetch failed (non-critical): {e}") diff --git a/app/static/js/app.js b/app/static/js/app.js index 28f1be8..8edf304 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -746,6 +746,11 @@ function createMessageElement(msg) {
${processMessageContent(msg.content)}
${echoDisplay} + ${msg.analyzer_url ? ` + + ` : ''} @@ -785,6 +790,11 @@ function createMessageElement(msg) { ` : ''} + ${msg.analyzer_url ? ` + + ` : ''}
diff --git a/meshcore-bridge/bridge.py b/meshcore-bridge/bridge.py index 8f2a2e7..f6c3115 100644 --- a/meshcore-bridge/bridge.py +++ b/meshcore-bridge/bridge.py @@ -1274,11 +1274,11 @@ def get_echo_counts(): { "success": true, "echo_counts": [ - {"timestamp": 1706500000.123, "channel_idx": 0, "count": 3, "paths": ["5e", "d1", "a3"]}, + {"timestamp": 1706500000.123, "channel_idx": 0, "count": 3, "paths": ["5e", "d1", "a3"], "pkt_payload": "abcd..."}, ... ], "incoming_paths": [ - {"timestamp": 1706500000.456, "path": "8a40a605", "path_len": 4, "snr": 11.0}, + {"timestamp": 1706500000.456, "path": "8a40a605", "path_len": 4, "snr": 11.0, "pkt_payload": "efgh..."}, ... ] } @@ -1293,7 +1293,8 @@ def get_echo_counts(): 'timestamp': data['timestamp'], 'channel_idx': data['channel_idx'], 'count': len(data['paths']), - 'paths': list(data['paths']) + 'paths': list(data['paths']), + 'pkt_payload': pkt_payload, }) incoming = [] @@ -1303,6 +1304,7 @@ def get_echo_counts(): 'path': data['path'], 'path_len': data.get('path_len'), 'snr': data.get('snr'), + 'pkt_payload': pkt_payload, }) return jsonify({