diff --git a/mesh_monitor/analyzer.py b/mesh_monitor/analyzer.py index 94fa307..0cc30c7 100644 --- a/mesh_monitor/analyzer.py +++ b/mesh_monitor/analyzer.py @@ -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. diff --git a/mesh_monitor/monitor.py b/mesh_monitor/monitor.py index baa3fc0..23e052a 100644 --- a/mesh_monitor/monitor.py +++ b/mesh_monitor/monitor.py @@ -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:") diff --git a/mesh_monitor/reporter.py b/mesh_monitor/reporter.py index 2d99074..f52158c 100644 --- a/mesh_monitor/reporter.py +++ b/mesh_monitor/reporter.py @@ -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: diff --git a/tests/test_router_efficiency.py b/tests/test_router_efficiency.py new file mode 100644 index 0000000..7b5bcdd --- /dev/null +++ b/tests/test_router_efficiency.py @@ -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()