feat: add router efficiency and route quality analysis, integrating it into the monitor, report, and adding new tests.

This commit is contained in:
eddieoz
2025-11-27 16:56:24 +02:00
parent 34066691b2
commit 992745b50c
4 changed files with 250 additions and 0 deletions

View File

@@ -79,6 +79,135 @@ class NetworkHealthAnalyzer:
return issues
def check_router_efficiency(self, nodes, test_results=None):
"""
Analyzes router placement and efficiency.
Returns a list of issue strings.
"""
issues = []
routers = []
# 1. Identify Routers
for node_id, node in nodes.items():
user = get_val(node, 'user', {})
role = get_val(user, 'role')
is_router = False
if isinstance(role, int):
if role in [2, 3]: # ROUTER, ROUTER_CLIENT (REPEATER is 4, usually dumb)
is_router = True
elif role in ['ROUTER', 'ROUTER_CLIENT']:
is_router = True
if is_router:
pos = get_val(node, 'position', {})
lat = get_val(pos, 'latitude')
lon = get_val(pos, 'longitude')
if lat is not None and lon is not None:
routers.append({
'id': node_id,
'name': get_node_name(node, node_id),
'lat': lat,
'lon': lon,
'metrics': get_val(node, 'deviceMetrics', {})
})
# 2. Analyze Each Router
for r in routers:
# A. Check Density (Redundancy)
nearby_routers = 0
for other in routers:
if r['id'] == other['id']: continue
dist = haversine(r['lat'], r['lon'], other['lat'], other['lon'])
if dist < 2000: # 2km radius
nearby_routers += 1
if nearby_routers >= 2:
issues.append(f"Efficiency: Router '{r['name']}' is Redundant. Has {nearby_routers} other routers within 2km. Consolidate?")
# B. Check Congestion
ch_util = get_val(r['metrics'], 'channelUtilization', 0)
if ch_util > 20:
issues.append(f"Efficiency: Router '{r['name']}' is Congested (ChUtil {ch_util:.1f}% > 20%).")
# C. Check Relay Efficiency (if we have test results)
if test_results:
# Count how many times this router was used as a relay
relay_count = 0
for res in test_results:
route = res.get('route', [])
# route is list of IDs (int or hex string? usually int in packet, but we need to match)
# Let's normalize to check
r_id_num = r['id'].replace('!', '')
try:
r_id_int = int(r_id_num, 16)
except:
r_id_int = 0
if r_id_int in route:
relay_count += 1
# Check for "Ineffective" (High Density of Neighbors but Low Relay Count)
# Count ALL neighbors (clients + routers)
total_neighbors = 0
for node_id, node in nodes.items():
if node_id == r['id']: continue
pos = get_val(node, 'position', {})
lat = get_val(pos, 'latitude')
lon = get_val(pos, 'longitude')
if lat and lon:
dist = haversine(r['lat'], r['lon'], lat, lon)
if dist < 2000:
total_neighbors += 1
if total_neighbors > 5 and relay_count == 0:
issues.append(f"Efficiency: Router '{r['name']}' is Ineffective. Has {total_neighbors} neighbors but relayed 0 packets in tests.")
return issues
def check_route_quality(self, nodes, test_results):
"""
Analyzes the quality of routes found in traceroute tests.
Checks for Hop Efficiency and Favorite Router usage.
"""
issues = []
if not test_results:
return issues
for res in test_results:
node_id = res.get('node_id')
node = nodes.get(node_id, {})
node_name = get_node_name(node, node_id)
# 1. Hop Efficiency
hops_to = res.get('hops_to')
if isinstance(hops_to, int):
if hops_to > 3:
issues.append(f"Route Quality: Long path to '{node_name}' ({hops_to} hops). Latency risk.")
# 2. Favorite Router Usage
route = res.get('route', [])
used_favorite = False
for hop_id in route:
# Normalize ID
hop_hex = f"!{hop_id:08x}" if isinstance(hop_id, int) else hop_id
hop_node = nodes.get(hop_hex)
if hop_node:
is_fav = get_val(hop_node, 'is_favorite', False)
if is_fav:
used_favorite = True
fav_name = get_node_name(hop_node, hop_hex)
issues.append(f"Route Quality: Route to '{node_name}' uses Favorite Router '{fav_name}'. Range Extended.")
# 3. SNR Check
snr = res.get('snr')
if snr is not None and snr < -10:
issues.append(f"Route Quality: Weak signal to '{node_name}' (SNR {snr}dB). Link unstable.")
return list(set(issues))
def check_duplication(self, history, nodes):
"""
Detects if the same message ID is being received multiple times.

View File

@@ -307,6 +307,12 @@ class MeshMonitor:
# Run Analysis
issues = self.analyzer.analyze(nodes, packet_history=self.packet_history, my_node=my_node)
# Run Router Efficiency Analysis (using accumulated test results if available)
if self.active_tester:
issues.extend(self.analyzer.check_router_efficiency(nodes, test_results=self.active_tester.test_results))
else:
issues.extend(self.analyzer.check_router_efficiency(nodes))
# Report Issues
if issues:
logger.warning(f"Found {len(issues)} potential issues:")

View File

@@ -160,6 +160,18 @@ class NetworkReporter:
for i in other: f.write(f"- {i}\n")
f.write("\n")
# Separate section for Efficiency
efficiency = [i for i in analysis_issues if "Efficiency" in i]
if efficiency:
f.write("### Router Efficiency Analysis\n")
f.write("Analysis of router placement, congestion, and relay performance.\n\n")
for i in efficiency:
# Format: Efficiency: Router 'Name' is Issue. Details.
# Let's make it a bit cleaner
clean_msg = i.replace("Efficiency: ", "")
f.write(f"- {clean_msg}\n")
f.write("\n")
def _write_traceroute_results(self, f, test_results, nodes, local_node=None):
f.write("## 3. Traceroute Results\n")
if not test_results:

View File

@@ -0,0 +1,103 @@
import unittest
from mesh_monitor.analyzer import NetworkHealthAnalyzer
class TestRouterEfficiency(unittest.TestCase):
def setUp(self):
self.analyzer = NetworkHealthAnalyzer()
def test_redundant_routers(self):
# Create 3 routers very close to each other
nodes = {
'!11111111': {
'user': {'id': '!11111111', 'longName': 'Router1', 'role': 'ROUTER'},
'position': {'latitude': 40.0, 'longitude': -74.0},
'deviceMetrics': {'channelUtilization': 10}
},
'!22222222': {
'user': {'id': '!22222222', 'longName': 'Router2', 'role': 'ROUTER'},
'position': {'latitude': 40.001, 'longitude': -74.001}, # Very close
'deviceMetrics': {'channelUtilization': 10}
},
'!33333333': {
'user': {'id': '!33333333', 'longName': 'Router3', 'role': 'ROUTER'},
'position': {'latitude': 40.002, 'longitude': -74.002}, # Very close
'deviceMetrics': {'channelUtilization': 10}
}
}
issues = self.analyzer.check_router_efficiency(nodes)
print("\nRedundancy Issues:", issues)
# All 3 should be flagged as redundant (each has 2 neighbors)
self.assertTrue(any("Router1" in i and "Redundant" in i for i in issues))
self.assertTrue(any("Router2" in i and "Redundant" in i for i in issues))
self.assertTrue(any("Router3" in i and "Redundant" in i for i in issues))
def test_congested_router(self):
nodes = {
'!44444444': {
'user': {'id': '!44444444', 'longName': 'BusyRouter', 'role': 'ROUTER'},
'position': {'latitude': 41.0, 'longitude': -75.0},
'deviceMetrics': {'channelUtilization': 50} # High Util
}
}
issues = self.analyzer.check_router_efficiency(nodes)
print("\nCongestion Issues:", issues)
self.assertTrue(any("BusyRouter" in i and "Congested" in i for i in issues))
def test_ineffective_router(self):
# Router surrounded by many nodes but not relaying
nodes = {
'!55555555': {
'user': {'id': '!55555555', 'longName': 'LazyRouter', 'role': 'ROUTER'},
'position': {'latitude': 42.0, 'longitude': -76.0},
'deviceMetrics': {'channelUtilization': 5}
}
}
# Add 6 neighbors
for i in range(6):
nodes[f'!neighbor{i}'] = {
'user': {'id': f'!neighbor{i}', 'role': 'CLIENT'},
'position': {'latitude': 42.001, 'longitude': -76.001}
}
# Test results showing NO relaying by LazyRouter
test_results = [
{'node_id': '!neighbor0', 'route': [12345, 67890]} # Random IDs, not LazyRouter
]
issues = self.analyzer.check_router_efficiency(nodes, test_results)
print("\nIneffective Issues:", issues)
self.assertTrue(any("LazyRouter" in i and "Ineffective" in i for i in issues))
def test_effective_router(self):
# Router surrounded by nodes AND relaying
nodes = {
'!66666666': {
'user': {'id': '!66666666', 'longName': 'GoodRouter', 'role': 'ROUTER'},
'position': {'latitude': 43.0, 'longitude': -77.0},
'deviceMetrics': {'channelUtilization': 5}
}
}
# Add 6 neighbors
for i in range(6):
nodes[f'!neighbor{i}'] = {
'user': {'id': f'!neighbor{i}', 'role': 'CLIENT'},
'position': {'latitude': 43.001, 'longitude': -77.001}
}
# Test results showing relaying by GoodRouter (ID !66666666 -> 0x66666666)
# 0x66666666 = 1717986918
test_results = [
{'node_id': '!neighbor0', 'route': [1717986918]}
]
issues = self.analyzer.check_router_efficiency(nodes, test_results)
print("\nEffective Issues (Should be empty):", issues)
self.assertFalse(any("GoodRouter" in i for i in issues))
if __name__ == '__main__':
unittest.main()