mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 15:14:45 +02:00
feat: Show hop count and GPS distance in node detail and single-gw listings
- Added hops_away to Connectivity section in node detail - Added nearest infra distance after Position in node detail - Added distance from reference infra to single-gw client listings - Added _haversine_km and _format_distance helper functions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6ac21d5f0e
commit
99c952b432
1 changed files with 59 additions and 1 deletions
|
|
@ -4,6 +4,7 @@ Refactored to consume MeshDataStore and UnifiedNode directly.
|
|||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
|
@ -150,6 +151,24 @@ def _is_valid_temperature(temp_c: Optional[float]) -> bool:
|
|||
return -50 <= temp_c <= 60
|
||||
|
||||
|
||||
def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
||||
"""Calculate distance between two GPS points in km."""
|
||||
R = 6371 # Earth radius in km
|
||||
dlat = math.radians(lat2 - lat1)
|
||||
dlon = math.radians(lon2 - lon1)
|
||||
a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2)**2
|
||||
c = 2 * math.asin(math.sqrt(a))
|
||||
return R * c
|
||||
|
||||
|
||||
def _format_distance(km: float) -> str:
|
||||
"""Format distance as km/miles."""
|
||||
miles = km * 0.621371
|
||||
if km < 1:
|
||||
return f"{km*1000:.0f}m ({miles*5280:.0f}ft)"
|
||||
return f"{km:.1f}km ({miles:.1f}mi)"
|
||||
|
||||
|
||||
class MeshReporter:
|
||||
"""Builds text blocks for mesh health prompt injection."""
|
||||
|
||||
|
|
@ -356,11 +375,36 @@ class MeshReporter:
|
|||
lines.append(f" INFRA at risk: {sgn_name} - only 1 gateway{src_info}")
|
||||
if single_clients > 0:
|
||||
single_client_nodes = [n for n in single_gw_nodes if not n.is_infrastructure]
|
||||
# Find nearest infra with GPS for distance reference
|
||||
ref_node = None
|
||||
for inf in single_infra if single_infra else []:
|
||||
if inf.latitude and inf.longitude:
|
||||
ref_node = inf
|
||||
break
|
||||
if not ref_node:
|
||||
for nid_str in region.node_ids:
|
||||
try:
|
||||
nid = int(nid_str)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
inf = health.nodes.get(nid)
|
||||
if inf and inf.is_infrastructure and inf.latitude and inf.longitude:
|
||||
ref_node = inf
|
||||
break
|
||||
|
||||
lines.append(f" Single-gw clients ({single_clients}):")
|
||||
for scn in single_client_nodes[:10]:
|
||||
scn_name = _node_display_name(scn.long_name, scn.short_name, str(scn.node_num))
|
||||
src_info = f" via {scn.sources[0]}" if len(scn.sources) == 1 else ""
|
||||
lines.append(f" {scn_name}{src_info}")
|
||||
|
||||
dist_str = ""
|
||||
if scn.latitude and scn.longitude and ref_node:
|
||||
km = _haversine_km(scn.latitude, scn.longitude, ref_node.latitude, ref_node.longitude)
|
||||
dist_str = f" ~{_format_distance(km)} from {ref_node.short_name}"
|
||||
elif scn.hops_away is not None:
|
||||
dist_str = f" {scn.hops_away} hops"
|
||||
|
||||
lines.append(f" {scn_name}{src_info}{dist_str}")
|
||||
if len(single_client_nodes) > 10:
|
||||
lines.append(f" ...and {len(single_client_nodes) - 10} more")
|
||||
|
||||
|
|
@ -922,6 +966,18 @@ class MeshReporter:
|
|||
if node.latitude and node.longitude:
|
||||
lines.append(f"Position: {node.latitude:.4f}, {node.longitude:.4f}")
|
||||
|
||||
# Distance from nearest infra node
|
||||
nearest_infra = None
|
||||
nearest_dist = float('inf')
|
||||
for n in self.health_engine.mesh_health.nodes.values():
|
||||
if n.is_infrastructure and n.latitude and n.longitude and n.node_num != node.node_num:
|
||||
d = _haversine_km(node.latitude, node.longitude, n.latitude, n.longitude)
|
||||
if d < nearest_dist:
|
||||
nearest_dist = d
|
||||
nearest_infra = n
|
||||
if nearest_infra and nearest_dist < 500:
|
||||
lines.append(f" Nearest infra: {nearest_infra.short_name} at {_format_distance(nearest_dist)}")
|
||||
|
||||
age = _format_age(node.last_heard)
|
||||
status = "Online" if node.is_online else "OFFLINE"
|
||||
lines.append(f"Last Seen: {age} ({status})")
|
||||
|
|
@ -992,6 +1048,8 @@ class MeshReporter:
|
|||
# Connectivity
|
||||
lines.append("")
|
||||
lines.append("Connectivity:")
|
||||
if node.hops_away is not None:
|
||||
lines.append(f" Hops: {node.hops_away}")
|
||||
lines.append(f" MQTT Uplink: {'Enabled' if node.uplink_enabled else 'Disabled'}")
|
||||
|
||||
# Coverage
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue