From 75e381f344b30da7fbf6bb352035a8ef3f85970f Mon Sep 17 00:00:00 2001 From: eddieoz Date: Fri, 28 Nov 2025 15:07:19 +0200 Subject: [PATCH] feat: Add configurable router density radius and active threshold, and improve report recommendations and issue presentation. --- mesh_monitor/analyzer.py | 10 +++-- mesh_monitor/reporter.py | 79 +++++++++++++++++++++++++++++++--------- sample-config.yaml | 12 ++++-- 3 files changed, 76 insertions(+), 25 deletions(-) diff --git a/mesh_monitor/analyzer.py b/mesh_monitor/analyzer.py index 8dffc3e..d334c6a 100644 --- a/mesh_monitor/analyzer.py +++ b/mesh_monitor/analyzer.py @@ -125,9 +125,10 @@ class NetworkHealthAnalyzer: # 2. Analyze Each Router for r in routers: - # A. Neighbors (2km) + # A. Neighbors (within configured radius) nearby_routers = 0 total_neighbors = 0 + radius = self.router_density_threshold for node_id, node in nodes.items(): if node_id == r['id']: continue @@ -137,7 +138,7 @@ class NetworkHealthAnalyzer: if lat and lon: dist = haversine(r['lat'], r['lon'], lat, lon) - if dist < 2000: + if dist < radius: total_neighbors += 1 # Check if it's also a router # (Simplified check, ideally we'd check against the routers list but this is O(N)) @@ -181,8 +182,9 @@ class NetworkHealthAnalyzer: 'lat': r['lat'], 'lon': r['lon'], 'role': r['role'], - 'neighbors_2km': total_neighbors, - 'routers_2km': nearby_routers, + 'neighbors': total_neighbors, + 'routers_nearby': nearby_routers, + 'radius': radius, 'ch_util': ch_util, 'relay_count': relay_count, 'status': ", ".join(status_issues) if status_issues else "OK" diff --git a/mesh_monitor/reporter.py b/mesh_monitor/reporter.py index 8c8b6e3..1dac285 100644 --- a/mesh_monitor/reporter.py +++ b/mesh_monitor/reporter.py @@ -164,6 +164,24 @@ class NetworkReporter: f.write("No significant network issues detected.\n\n") return + # Helper to clean issue strings (remove recommendations) + def clean_issue(issue): + # Topology: High Router Density + if "Best positioned seems to be" in issue: + return issue.split("Best positioned seems to be")[0].strip() + if "Consider changing" in issue: + return issue.split("Consider changing")[0].strip() + + # Network Size + if "If using" in issue: + return issue.split("If using")[0].strip() + + # Efficiency + if "Consolidate?" in issue: + return issue.split("Consolidate?")[0].strip() + + return issue + # Group issues by type congestion = [] config = [] @@ -171,16 +189,19 @@ class NetworkReporter: other = [] for issue in analysis_issues: + # Clean the issue string first + cleaned_issue = clean_issue(issue) + if "Congestion" in issue or "Spam" in issue: - congestion.append(issue) + congestion.append(cleaned_issue) elif "Config" in issue or "Role" in issue: - config.append(issue) + config.append(cleaned_issue) elif "Topology" in issue or "Density" in issue or "hops away" in issue: - topology.append(issue) + topology.append(cleaned_issue) elif "Efficiency" in issue or "Route Quality" in issue: pass # Handled in separate sections else: - other.append(issue) + other.append(cleaned_issue) if congestion: f.write("### Congestion & Airtime\n") @@ -203,7 +224,7 @@ class NetworkReporter: f.write("\n") # Separate section for Efficiency - efficiency = [i for i in analysis_issues if "Efficiency" in i] + efficiency = [clean_issue(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") @@ -213,7 +234,7 @@ class NetworkReporter: f.write("\n") # Separate section for Route Quality - quality = [i for i in analysis_issues if "Route Quality" in i] + quality = [clean_issue(i) for i in analysis_issues if "Route Quality" in i] if quality: f.write("### Route Quality Analysis\n") f.write("Analysis of path efficiency and stability.\n\n") @@ -228,11 +249,19 @@ class NetworkReporter: f.write("No routers found.\n\n") return - f.write("| Name | Role | Neighbors (2km) | Routers (2km) | ChUtil | Relayed | Status |\n") + # Get radius from first stat entry (default to 2000m if missing) + radius_m = router_stats[0].get('radius', 2000) + radius_km = radius_m / 1000.0 + + f.write(f"| Name | Role | Neighbors ({radius_km:.1f}km) | Routers ({radius_km:.1f}km) | ChUtil | Relayed | Status |\n") f.write("|---|---|---|---|---|---|---|\n") for s in router_stats: - f.write(f"| {s['name']} | {s['role']} | {s['neighbors_2km']} | {s['routers_2km']} | {s['ch_util']:.1f}% | {s['relay_count']} | {s['status']} |\n") + # Handle backward compatibility if keys are missing + neighbors = s.get('neighbors', s.get('neighbors_2km', 0)) + routers_nearby = s.get('routers_nearby', s.get('routers_2km', 0)) + + f.write(f"| {s['name']} | {s['role']} | {neighbors} | {routers_nearby} | {s['ch_util']:.1f}% | {s['relay_count']} | {s['status']} |\n") f.write("\n") @@ -347,22 +376,36 @@ class NetworkReporter: # Fallback for generic density issue if not caught above recs.append("- **Optimize Placement:** Routers are too close together. Convert redundant routers to clients.") - # 2. Congestion - if any("Congestion" in i for i in analysis_issues): - recs.append("- **Reduce Traffic:** High channel utilization detected. Identify spamming nodes or reduce broadcast frequency.") + # 2. Efficiency (Router Performance) + if any("Ineffective" in i for i in analysis_issues): + recs.append("- **Review Ineffective Routers:** Some routers have neighbors but aren't relaying packets. Consider repositioning them or checking their antenna/LOS.") + + if any("Redundant" in i for i in analysis_issues): + # This might overlap with Topology, but good to have specific advice + recs.append("- **Reduce Redundancy:** Routers marked as 'Redundant' have too many other routers nearby. Change their role to CLIENT to save airtime.") - # 3. Configuration + # 3. Congestion + if any("Congestion" in i or "Congested" in i for i in analysis_issues): + recs.append("- **Reduce Traffic:** High channel utilization detected. Identify spamming nodes, reduce broadcast frequency, or increase channel speed (if possible).") + + # 4. Configuration if any("ROUTER_CLIENT" in i for i in analysis_issues): recs.append("- **Fix Roles:** Deprecated `ROUTER_CLIENT` role detected. Change these nodes to `CLIENT` or `CLIENT_MUTE`.") if any("Network Size" in i for i in analysis_issues): recs.append("- **Adjust Presets:** Network size exceeds recommendations for the current estimated preset. Consider switching to a faster preset (e.g. LONG_MODERATE or SHORT_FAST).") - # 4. Hardware / Signal - if any("poor SNR" in i for i in analysis_issues): - recs.append("- **Check Hardware:** Nodes with poor SNR at close range may have antenna issues or bad placement.") + # 5. Route Quality / Signal + if any("poor SNR" in i or "Weak signal" in i for i in analysis_issues): + recs.append("- **Check Hardware/LOS:** Nodes with poor SNR or weak signals may have antenna issues, bad placement, or obstructions.") + + if any("Long path" in i for i in analysis_issues): + recs.append("- **Optimize Paths:** Long paths (>3 hops) detected. Consider adding a strategically placed relay to shorten the path.") + + if any("Favorite Router" in i for i in analysis_issues): + recs.append("- **Check Favorites:** Routes are using 'Favorite Router' nodes. Ensure this is intentional, as it forces specific paths.") - # 5. Connectivity (Traceroute Failures) + # 6. Connectivity (Traceroute Failures) failures = [r for r in test_results if r.get('status') != 'success'] if failures: recs.append(f"- **Investigate Connectivity:** {len(failures)} nodes failed traceroute tests. Check if they are online or if the path is broken.") @@ -370,6 +413,8 @@ class NetworkReporter: if not recs: f.write("Network looks healthy! Keep up the good work.\n") else: - for r in recs: + # Deduplicate recommendations + unique_recs = sorted(list(set(recs))) + for r in unique_recs: f.write(f"{r}\n") f.write("\n") diff --git a/sample-config.yaml b/sample-config.yaml index 5b793bb..ad829e3 100644 --- a/sample-config.yaml +++ b/sample-config.yaml @@ -1,8 +1,10 @@ # Configuration for Meshtastic Network Monitor -# List of Node IDs to prioritize for active testing (Traceroute, etc.) -# Format: "!" -priority_nodes: +# # List of Node IDs to prioritize for active testing (Traceroute, etc.) +# # If priority nodes is enabled, then only those nodes will be tested. +# # If this session is disabled, then auto discovery will run automatically +# # Format: "!" +# priority_nodes: # - "!12345678" # Logging Level [warn|info|debug] @@ -12,7 +14,8 @@ log_level: info # Analysis Mode: 'distance' (default) or 'router_clusters' analysis_mode: distance -# Radius for router cluster analysis (in meters) +# Radius for router cluster analysis (nodes close to routers in meters). +# Only used if analysis_mode is 'router_clusters' cluster_radius: 2000 # Roles to prioritize for auto-discovery @@ -50,6 +53,7 @@ thresholds: channel_utilization: 25.0 # Percent air_util_tx: 7.0 # Percent router_density_threshold: 2000 # Meters (Minimum distance between routers) + active_threshold_seconds: 7200 # 2 Hours (Nodes seen within this time are considered active) # Network Size Settings max_nodes_for_long_fast: 60