mirror of
https://github.com/eddieoz/LoRa-Mesh-Analyzer.git
synced 2026-03-28 17:42:59 +01:00
feat: add router efficiency and route quality analysis, integrating it into the monitor, report, and adding new tests.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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:")
|
||||
|
||||
@@ -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:
|
||||
|
||||
103
tests/test_router_efficiency.py
Normal file
103
tests/test_router_efficiency.py
Normal 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()
|
||||
Reference in New Issue
Block a user