mirror of
https://github.com/eddieoz/LoRa-Mesh-Analyzer.git
synced 2026-03-28 17:42:59 +01:00
refactor: Rename core package to mesh_analyzer, formalize project setup, and introduce report regeneration with new configuration options.
This commit is contained in:
32
README.md
32
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.
|
||||
|
||||
2
main.py
2
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()
|
||||
|
||||
@@ -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']
|
||||
@@ -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__)
|
||||
|
||||
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=42", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
@@ -1,4 +1,4 @@
|
||||
meshtastic
|
||||
meshtastic
|
||||
|
||||
pypubsub
|
||||
PyYAML
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
18
setup.py
Normal file
18
setup.py
Normal file
@@ -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",
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user