mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-10-30 18:51:57 +01:00
create heard by packet trace
This commit is contained in:
1 parent
ad1f404f4a
commit
0657899a6f
3 files changed
+121
-11
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user