create heard by packet trace

This commit is contained in:
pablorevilla-meshtastic committed 2026-09-09 16:33:46 -07:00
1 parent ad1f404f4a
commit 0657899a6f
3 files changed
+121 -11

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
import subprocess
from pathlib import Path
__version__ = "3.0.8"
__version__ = "3.0.9"
__release_date__ = "2026-5-12"
+56 -10
View File
@@ -22,6 +22,18 @@ NODE_NAME_CONTROL_CHARS = re.compile(r"[\x00-\x1f\x7f]")
MQTT_DIRECT_NODE_ID = 1
def has_valid_location(lat: int | None, lon: int | None) -> bool:
return lat not in (None, 0) and lon not in (None, 0)
def node_user_id(node_id: int) -> str:
return f"!{node_id:0{8}x}"
def placeholder_short_name(node_id: int) -> str:
return node_user_id(node_id)[-4:]
def normalize_node_name(name: str) -> str:
name = name.strip()
if not name or NODE_NAME_CONTROL_CHARS.search(name):
@@ -47,6 +59,35 @@ def storage_to_node_id(packet) -> int:
return packet.to
async def ensure_node_exists(session, node_id: int | None, channel: str | None) -> Node | None:
if node_id is None:
return None
node = (await session.execute(select(Node).where(Node.node_id == node_id))).scalar_one_or_none()
now_us = int(time.time() * 1_000_000)
if node:
node.last_seen_us = now_us
if node.first_seen_us is None:
node.first_seen_us = now_us
if channel and not node.channel:
node.channel = channel
return node
display_name = str(node_id)
node = Node(
id=node_user_id(node_id),
node_id=node_id,
long_name=display_name,
short_name=placeholder_short_name(node_id),
channel=channel,
first_seen_us=now_us,
last_seen_us=now_us,
)
session.add(node)
return node
async def capture_daily_snapshot() -> None:
today = datetime.now(UTC).date()
@@ -130,6 +171,7 @@ async def process_envelope(topic, env):
).scalar_one_or_none()
now_us = int(time.time() * 1_000_000)
has_location = has_valid_location(map_report.latitude_i, map_report.longitude_i)
if node:
node.node_id = node_id
@@ -139,8 +181,9 @@ async def process_envelope(topic, env):
node.role = role
if not node.channel:
node.channel = env.channel_id
node.last_lat = map_report.latitude_i
node.last_long = map_report.longitude_i
if has_location:
node.last_lat = map_report.latitude_i
node.last_long = map_report.longitude_i
node.firmware = map_report.firmware_version
node.last_seen_us = now_us
if node.first_seen_us is None:
@@ -155,11 +198,12 @@ async def process_envelope(topic, env):
role=role,
channel=env.channel_id,
firmware=map_report.firmware_version,
last_lat=map_report.latitude_i,
last_long=map_report.longitude_i,
first_seen_us=now_us,
last_seen_us=now_us,
)
if has_location:
node.last_lat = map_report.latitude_i
node.last_long = map_report.longitude_i
session.add(node)
except Exception as e:
print(f"Error processing MAP_REPORT_APP: {e}")
@@ -172,6 +216,9 @@ async def process_envelope(topic, env):
async with mqtt_database.async_session() as session:
# --- Packet insert with ON CONFLICT DO NOTHING
from_node_id = getattr(env.packet, "from", None)
await ensure_node_exists(session, from_node_id, env.channel_id)
result = await session.execute(select(Packet).where(Packet.id == packet_id))
packet = result.scalar_one_or_none()
if not packet:
@@ -179,7 +226,7 @@ async def process_envelope(topic, env):
packet_values = {
"id": packet_id,
"portnum": env.packet.decoded.portnum,
"from_node_id": getattr(env.packet, "from"),
"from_node_id": from_node_id,
"to_node_id": storage_to_node_id(env.packet),
"payload": env.packet.SerializeToString(),
"import_time_us": now_us,
@@ -332,11 +379,10 @@ async def process_envelope(topic, env):
position = decode_payload.decode_payload(
PortNum.POSITION_APP, env.packet.decoded.payload
)
if position and position.latitude_i and position.longitude_i:
from_node_id = getattr(env.packet, "from")
node = (
await session.execute(select(Node).where(Node.node_id == from_node_id))
).scalar_one_or_none()
if position and has_valid_location(position.latitude_i, position.longitude_i):
node = await ensure_node_exists(
session, getattr(env.packet, "from", None), env.channel_id
)
if node:
now_us = int(time.time() * 1_000_000)
node.last_lat = position.latitude_i
+64
View File
@@ -37,6 +37,16 @@
animation: textPulse 1.1s ease-in-out 6;
border-color: #ff8c00;
}
.packet-seen-trace {
stroke-dasharray: 34 999;
stroke-linecap: round;
filter: drop-shadow(0 0 4px rgba(0, 183, 255, 0.85));
animation: packetTrace 6.5s cubic-bezier(0.25, 0.05, 0.15, 1) 1 forwards;
}
@keyframes packetTrace {
from { stroke-dashoffset: 0; }
to { stroke-dashoffset: -1000; }
}
@keyframes textPulse {
0% { box-shadow: 0 2px 8px rgba(0,0,0,0.25); }
50% { box-shadow: 0 4px 14px rgba(255,140,0,0.45); }
@@ -207,6 +217,7 @@ L.control.scale({
}).addTo(map);
var nodeLayer = L.layerGroup().addTo(map);
var edgeLayer = L.layerGroup().addTo(map);
var packetSeenLayer = L.layerGroup().addTo(map);
var trafficHeatLayer = L.heatLayer([], {
radius: 34,
blur: 18,
@@ -221,6 +232,7 @@ var trafficHeatLayer = L.heatLayer([], {
});
var overlayLayers = {
"Nodes": nodeLayer,
"Packet Heard By": packetSeenLayer,
"Traffic HeatMap": trafficHeatLayer
};
L.control.layers(baseLayers, overlayLayers, { collapsed: false }).addTo(map);
@@ -229,6 +241,7 @@ L.control.layers(baseLayers, overlayLayers, { collapsed: false }).addTo(map);
var nodes = [], markers = {}, markerById = {}, nodeMap = new Map();
var selectedNodeId = null;
var activeBlinks = new Map(), lastImportTime = null;
var activePacketSeenHighlights = [];
var mapInterval = 0;
var unmappedPackets = [];
var trafficHeatLoaded = false;
@@ -395,6 +408,7 @@ function fetchNewPackets(){
} else {
addUnmappedPacket(pkt, nodeData);
}
showPacketSeenNodes(pkt);
});
lastImportTime = latest;
@@ -430,6 +444,7 @@ map.on("overlayadd", e => {
map.on("overlayremove", e => {
if(e.layer === nodeLayer) clearActiveBlinks();
if(e.layer === packetSeenLayer) clearPacketSeenHighlights();
});
async function waitForConfig() {
@@ -769,6 +784,55 @@ function clearActiveBlinks(){
activeBlinks.clear();
}
function clearPacketSeenHighlights(){
activePacketSeenHighlights.forEach(timeout => clearTimeout(timeout));
activePacketSeenHighlights = [];
packetSeenLayer.clearLayers();
}
async function showPacketSeenNodes(pkt){
if(!map.hasLayer(packetSeenLayer) || pkt.id == null) return;
try {
const res = await fetch(`/api/packets_seen/${pkt.id}`);
if(!res.ok) return;
const data = await res.json();
const seen = data.seen || [];
const sourceId = pkt.from_node_id;
const sourceNode = nodeMap.get(sourceId);
const sourceLatLng = sourceNode && !isInvalidCoord(sourceNode)
? getNodeLatLng(sourceNode)
: null;
seen.forEach(row => {
const node = nodeMap.get(row.node_id);
if(!node || isInvalidCoord(node)) return;
const isSource = row.node_id === sourceId;
if(!sourceLatLng || isSource) return;
const markerLatLng = markerById[row.node_id]?.getLatLng();
const lat = markerLatLng?.lat ?? node.lat;
const lng = markerLatLng?.lng ?? node.long;
const trace = L.polyline([[sourceLatLng.lat, sourceLatLng.lng], [lat, lng]], {
color: "#26d9ff",
weight: 6,
opacity: 0.9,
className: "packet-seen-trace",
interactive: false
}).addTo(packetSeenLayer);
const timeout = setTimeout(() => {
packetSeenLayer.removeLayer(trace);
}, 6700);
activePacketSeenHighlights.push(timeout);
});
} catch(err) {
console.error("Failed to load packet seen nodes", pkt.id, err);
}
}
function blinkNode(marker,longName,portnum,payload){
if(!isNodeVisibleForPackets(marker)) return;