diff --git a/README.md b/README.md index 5d5a1e2..7c2e15f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,11 @@ If `priority_nodes` is empty in `config.yaml`, the monitor will automatically se * **Role Check**: Warns if the monitoring node itself is set to `ROUTER` or `ROUTER_CLIENT` (Monitoring is best done as `CLIENT`). * **Hop Limit**: Warns if the default hop limit is > 3, which can cause network congestion. -### 5. Comprehensive Reporting +### 5. Data Persistence & Regeneration +* **JSON Data**: Saves all raw data (nodes, test results, analysis) to a JSON file alongside the Markdown report. +* **Regeneration Tool**: Includes `report_generate.py` to regenerate reports from JSON files, allowing for format updates or re-analysis without re-running tests. + +### 6. Comprehensive Reporting * Generates a detailed **Markdown Report** (`report-YYYYMMDD-HHMMSS.md`) after each test cycle. * Includes: * Executive Summary @@ -82,6 +86,16 @@ python3 main.py --tcp 192.168.1.10 python3 main.py --ignore-no-position ``` +### Regenerating Reports +To regenerate a report from a saved JSON file (e.g., to apply new analysis logic): +```bash +python3 report_generate.py reports/report-YYYYMMDD-HHMMSS.json +``` +You can also specify a custom output filename: +```bash +python3 report_generate.py reports/report-YYYYMMDD-HHMMSS.json --output my_custom_report.md +``` + ## Configuration (Priority Testing) To prioritize testing specific nodes (e.g., to check if a router is reachable), add their IDs to `config.yaml`: @@ -91,15 +105,23 @@ priority_nodes: - "!12345678" - "!87654321" +# Auto-Discovery Settings +analysis_mode: router_clusters # 'distance' or 'router_clusters' +cluster_radius: 3000 # Meters + # Generate report after N full testing cycles report_cycles: 1 -# Timeout for traceroute response (in seconds) +# Active Testing Settings traceroute_timeout: 90 - -# Minimum interval between tests (in seconds) active_test_interval: 30 +# Manual Geolocation Overrides +manual_positions: + "!12345678": + lat: 59.12345 + lon: 24.12345 + # Thresholds for Analysis thresholds: channel_utilization: 25.0 # Percent @@ -140,7 +162,7 @@ INFO - Received Traceroute Packet: {...} * **Action**: Check the hop count in the response (if visible/parsed) to verify the path. ## Project Structure -* `mesh_monitor/`: Source code. +* `mesh_analyzer/`: Source code. * `monitor.py`: Main application loop. * `analyzer.py`: Health check logic. * `active_tests.py`: Traceroute logic. diff --git a/main.py b/main.py index 40f65b5..f452a19 100755 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ """ Entry point for the Meshtastic Network Monitor. """ -from mesh_monitor.monitor import main +from mesh_analyzer.monitor import main if __name__ == "__main__": main() diff --git a/mesh_monitor/__init__.py b/mesh_analyzer/__init__.py similarity index 100% rename from mesh_monitor/__init__.py rename to mesh_analyzer/__init__.py diff --git a/mesh_monitor/active_tests.py b/mesh_analyzer/active_tests.py similarity index 100% rename from mesh_monitor/active_tests.py rename to mesh_analyzer/active_tests.py diff --git a/mesh_monitor/analyzer.py b/mesh_analyzer/analyzer.py similarity index 99% rename from mesh_monitor/analyzer.py rename to mesh_analyzer/analyzer.py index aed482b..b6330a8 100644 --- a/mesh_monitor/analyzer.py +++ b/mesh_analyzer/analyzer.py @@ -276,7 +276,7 @@ class NetworkHealthAnalyzer: if not test_results: return issues - from mesh_monitor.route_analyzer import RouteAnalyzer + from mesh_analyzer.route_analyzer import RouteAnalyzer route_analyzer = RouteAnalyzer(nodes) relay_usage = route_analyzer._analyze_relay_usage( [r for r in test_results if r.get('status') == 'success'] diff --git a/mesh_monitor/monitor.py b/mesh_analyzer/monitor.py similarity index 100% rename from mesh_monitor/monitor.py rename to mesh_analyzer/monitor.py diff --git a/mesh_monitor/reporter.py b/mesh_analyzer/reporter.py similarity index 99% rename from mesh_monitor/reporter.py rename to mesh_analyzer/reporter.py index adcdbc1..1ae44b3 100644 --- a/mesh_monitor/reporter.py +++ b/mesh_analyzer/reporter.py @@ -5,7 +5,7 @@ import json from datetime import datetime from .utils import get_val, haversine, get_node_name -from mesh_monitor.route_analyzer import RouteAnalyzer +from mesh_analyzer.route_analyzer import RouteAnalyzer logger = logging.getLogger(__name__) diff --git a/mesh_monitor/route_analyzer.py b/mesh_analyzer/route_analyzer.py similarity index 100% rename from mesh_monitor/route_analyzer.py rename to mesh_analyzer/route_analyzer.py diff --git a/mesh_monitor/utils.py b/mesh_analyzer/utils.py similarity index 100% rename from mesh_monitor/utils.py rename to mesh_analyzer/utils.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8fe2f47 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt index cab5395..0da2920 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ meshtastic -meshtastic + pypubsub PyYAML diff --git a/debug_traceroute.py b/scripts/debug_traceroute.py similarity index 100% rename from debug_traceroute.py rename to scripts/debug_traceroute.py diff --git a/report_generate.py b/scripts/report_generate.py similarity index 93% rename from report_generate.py rename to scripts/report_generate.py index 75c3760..0f1733f 100755 --- a/report_generate.py +++ b/scripts/report_generate.py @@ -18,11 +18,11 @@ import os import argparse from datetime import datetime -# Add mesh_monitor to path -sys.path.insert(0, os.path.dirname(__file__)) +# Add mesh_analyzer to path (parent directory) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from mesh_monitor.reporter import NetworkReporter -from mesh_monitor.route_analyzer import RouteAnalyzer +from mesh_analyzer.reporter import NetworkReporter +from mesh_analyzer.route_analyzer import RouteAnalyzer def load_json_data(json_filepath): @@ -100,7 +100,7 @@ def generate_report_from_json(json_filepath, output_path=None): node['position']['longitude'] = pos['lon'] # Recreate analyzer and re-run analysis to populate cluster_data and ch_util_data - from mesh_monitor.analyzer import NetworkHealthAnalyzer + from mesh_analyzer.analyzer import NetworkHealthAnalyzer analyzer = NetworkHealthAnalyzer(config=config) # Re-run analysis to populate analyzer data structures AND get new issues @@ -119,13 +119,13 @@ def generate_report_from_json(json_filepath, output_path=None): # Monkey-patch the generate_report to use custom filename original_generate = reporter.generate_report - def custom_generate(nodes, test_results, analysis_issues, local_node=None, router_stats=None, analyzer=None): + def custom_generate(nodes, test_results, analysis_issues, local_node=None, router_stats=None, analyzer=None, override_timestamp=None, override_location=None, save_json=True): # Temporarily change the method to use custom filename timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") custom_filename = os.path.basename(output_path) filepath = os.path.join(report_dir, custom_filename) - from mesh_monitor.route_analyzer import RouteAnalyzer + from mesh_analyzer.route_analyzer import RouteAnalyzer route_analyzer = RouteAnalyzer(nodes) route_analysis_local = route_analyzer.analyze_routes(test_results) diff --git a/verify_parsing.py b/scripts/verify_parsing.py similarity index 100% rename from verify_parsing.py rename to scripts/verify_parsing.py diff --git a/test_report_refactoring.py b/scripts/verify_report_refactoring.py similarity index 98% rename from test_report_refactoring.py rename to scripts/verify_report_refactoring.py index b32fabd..7143775 100644 --- a/test_report_refactoring.py +++ b/scripts/verify_report_refactoring.py @@ -9,10 +9,11 @@ import os import json from datetime import datetime -# Add mesh_monitor to path +# Add mesh_analyzer to path sys.path.insert(0, os.path.dirname(__file__)) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from mesh_monitor.reporter import NetworkReporter +from mesh_analyzer.reporter import NetworkReporter def create_mock_data(): diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..833342c --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup, find_packages + +setup( + name="mesh_analyzer", + version="0.1.0", + description="LoRa Mesh Analyzer for Meshtastic networks", + packages=find_packages(), + install_requires=[ + "meshtastic", + "pypubsub", + "PyYAML", + ], + entry_points={ + "console_scripts": [ + "mesh-analyzer=mesh_analyzer.monitor:main", + ], + }, +) diff --git a/tests/mock_test.py b/tests/mock_test.py index a2ed910..8e23656 100644 --- a/tests/mock_test.py +++ b/tests/mock_test.py @@ -6,9 +6,9 @@ from unittest.mock import MagicMock, patch, mock_open # Add project root to path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from mesh_monitor.analyzer import NetworkHealthAnalyzer -from mesh_monitor.monitor import MeshMonitor -from mesh_monitor.active_tests import ActiveTester +from mesh_analyzer.analyzer import NetworkHealthAnalyzer +from mesh_analyzer.monitor import MeshMonitor +from mesh_analyzer.active_tests import ActiveTester class TestNetworkMonitor(unittest.TestCase): def setUp(self): @@ -270,7 +270,7 @@ class TestNetworkMonitor(unittest.TestCase): def test_local_config_check(self): print("\nRunning Local Config Check Test...") - from mesh_monitor.monitor import MeshMonitor + from mesh_analyzer.monitor import MeshMonitor from unittest.mock import MagicMock # Mock the interface and node @@ -301,7 +301,7 @@ class TestNetworkMonitor(unittest.TestCase): def test_auto_discovery(self): print("\nRunning Auto-Discovery Test...") - from mesh_monitor.active_tests import ActiveTester + from mesh_analyzer.active_tests import ActiveTester # Mock Interface mock_interface = MagicMock() @@ -370,7 +370,7 @@ class TestNetworkMonitor(unittest.TestCase): } issues = self.analyzer.analyze(self.mock_nodes) - density_warnings = [i for i in issues if "High Density" in i] + density_warnings = [i for i in issues if "High Router Density" in i] self.assertTrue(len(density_warnings) > 0, "Should detect high router density") print(" [Pass] Router Density Check") @@ -398,7 +398,7 @@ class TestNetworkMonitor(unittest.TestCase): def test_reporting(self): print("\nRunning Reporting Test...") - from mesh_monitor.reporter import NetworkReporter + from mesh_analyzer.reporter import NetworkReporter # Initialize self.monitor mock since setUp doesn't do it self.monitor = MagicMock() @@ -463,8 +463,8 @@ class TestNetworkMonitor(unittest.TestCase): def test_route_analysis(self): """Test the RouteAnalyzer and Reporter integration.""" print("\nRunning Route Analysis Test...") - from mesh_monitor.route_analyzer import RouteAnalyzer - from mesh_monitor.reporter import NetworkReporter + from mesh_analyzer.route_analyzer import RouteAnalyzer + from mesh_analyzer.reporter import NetworkReporter # Mock Test Results with Routes test_results = [ diff --git a/tests/test_analyzer_enhancements.py b/tests/test_analyzer_enhancements.py index af8b315..2612c6a 100644 --- a/tests/test_analyzer_enhancements.py +++ b/tests/test_analyzer_enhancements.py @@ -1,5 +1,6 @@ import unittest -from mesh_monitor.analyzer import NetworkHealthAnalyzer +from mesh_analyzer.analyzer import NetworkHealthAnalyzer +import time class TestAnalyzerEnhancements(unittest.TestCase): def setUp(self): @@ -37,7 +38,7 @@ class TestAnalyzerEnhancements(unittest.TestCase): def test_network_size(self): nodes = {} for i in range(61): - nodes[f'!{i}'] = {'user': {'id': f'!{i}'}} + nodes[f'!{i}'] = {'user': {'id': f'!{i}'}, 'lastHeard': time.time()} issues = self.analyzer.analyze(nodes) self.assertTrue(any("Network Size" in i for i in issues)) diff --git a/tests/test_hop_count.py b/tests/test_hop_count.py index 0db9b90..f060e2c 100644 --- a/tests/test_hop_count.py +++ b/tests/test_hop_count.py @@ -1,6 +1,6 @@ import unittest from unittest.mock import Mock -from mesh_monitor.active_tests import ActiveTester +from mesh_analyzer.active_tests import ActiveTester class TestHopCountCalculation(unittest.TestCase): diff --git a/tests/test_network_size.py b/tests/test_network_size.py index 6d49c34..61ab9db 100644 --- a/tests/test_network_size.py +++ b/tests/test_network_size.py @@ -1,6 +1,6 @@ import unittest import time -from mesh_monitor.analyzer import NetworkHealthAnalyzer +from mesh_analyzer.analyzer import NetworkHealthAnalyzer class TestNetworkSize(unittest.TestCase): def setUp(self): diff --git a/tests/test_route_quality.py b/tests/test_route_quality.py index b97efc9..058e649 100644 --- a/tests/test_route_quality.py +++ b/tests/test_route_quality.py @@ -1,5 +1,5 @@ import unittest -from mesh_monitor.analyzer import NetworkHealthAnalyzer +from mesh_analyzer.analyzer import NetworkHealthAnalyzer class TestRouteQuality(unittest.TestCase): def setUp(self): diff --git a/tests/test_router_clusters.py b/tests/test_router_clusters.py index 6785c07..67e98a4 100644 --- a/tests/test_router_clusters.py +++ b/tests/test_router_clusters.py @@ -1,6 +1,6 @@ import unittest from unittest.mock import MagicMock -from mesh_monitor.active_tests import ActiveTester +from mesh_analyzer.active_tests import ActiveTester class TestRouterClusters(unittest.TestCase): def setUp(self): diff --git a/tests/test_router_efficiency.py b/tests/test_router_efficiency.py index c19c744..0f82b36 100644 --- a/tests/test_router_efficiency.py +++ b/tests/test_router_efficiency.py @@ -1,5 +1,5 @@ import unittest -from mesh_monitor.analyzer import NetworkHealthAnalyzer +from mesh_analyzer.analyzer import NetworkHealthAnalyzer class TestRouterEfficiency(unittest.TestCase): def setUp(self): diff --git a/tests/verify_refactor.py b/tests/verify_refactor.py index 0b9fb24..bcda437 100644 --- a/tests/verify_refactor.py +++ b/tests/verify_refactor.py @@ -12,7 +12,7 @@ logger = logging.getLogger("Verification") def verify_utils(): logger.info("Verifying utils.py...") - from mesh_monitor.utils import haversine, get_val, get_node_name + from mesh_analyzer.utils import haversine, get_val, get_node_name # Test haversine dist = haversine(0, 0, 1, 1) @@ -49,21 +49,21 @@ def verify_modules(): interface = MockInterface() # Test Analyzer - from mesh_monitor.analyzer import NetworkHealthAnalyzer + from mesh_analyzer.analyzer import NetworkHealthAnalyzer analyzer = NetworkHealthAnalyzer() issues = analyzer.analyze({}) assert isinstance(issues, list), "Analyzer did not return list" logger.info("Analyzer instantiated and ran.") # Test ActiveTester - from mesh_monitor.active_tests import ActiveTester + from mesh_analyzer.active_tests import ActiveTester tester = ActiveTester(interface) assert hasattr(tester, 'lock'), "ActiveTester missing lock" assert isinstance(tester.lock, type(threading.Lock())), "ActiveTester lock is not a Lock" logger.info("ActiveTester instantiated and has lock.") # Test Reporter - from mesh_monitor.reporter import NetworkReporter + from mesh_analyzer.reporter import NetworkReporter reporter = NetworkReporter() logger.info("Reporter instantiated.")