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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-02-18 08:26:43 +01:00
parent 4bb33a7346
commit cf537628cf
3 changed files with 36 additions and 3 deletions

View File

@@ -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}")

View File

@@ -746,6 +746,11 @@ function createMessageElement(msg) {
<div class="message-content">${processMessageContent(msg.content)}</div>
<div class="message-actions justify-content-end">
${echoDisplay}
${msg.analyzer_url ? `
<button class="btn btn-outline-secondary btn-msg-action" onclick="window.open('${msg.analyzer_url}', 'meshcore-analyzer')" title="View in Analyzer">
<i class="bi bi-search"></i>
</button>
` : ''}
<button class="btn btn-outline-secondary btn-msg-action" onclick='resendMessage(${JSON.stringify(msg.content)})' title="Resend">
<i class="bi bi-arrow-repeat"></i>
</button>
@@ -785,6 +790,11 @@ function createMessageElement(msg) {
<i class="bi bi-geo-alt"></i>
</button>
` : ''}
${msg.analyzer_url ? `
<button class="btn btn-outline-secondary btn-msg-action" onclick="window.open('${msg.analyzer_url}', 'meshcore-analyzer')" title="View in Analyzer">
<i class="bi bi-search"></i>
</button>
` : ''}
</div>
</div>
</div>

View File

@@ -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({