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`).
|
* **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.
|
* **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.
|
* Generates a detailed **Markdown Report** (`report-YYYYMMDD-HHMMSS.md`) after each test cycle.
|
||||||
* Includes:
|
* Includes:
|
||||||
* Executive Summary
|
* Executive Summary
|
||||||
@@ -82,6 +86,16 @@ python3 main.py --tcp 192.168.1.10
|
|||||||
python3 main.py --ignore-no-position
|
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)
|
## Configuration (Priority Testing)
|
||||||
|
|
||||||
To prioritize testing specific nodes (e.g., to check if a router is reachable), add their IDs to `config.yaml`:
|
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"
|
- "!12345678"
|
||||||
- "!87654321"
|
- "!87654321"
|
||||||
|
|
||||||
|
# Auto-Discovery Settings
|
||||||
|
analysis_mode: router_clusters # 'distance' or 'router_clusters'
|
||||||
|
cluster_radius: 3000 # Meters
|
||||||
|
|
||||||
# Generate report after N full testing cycles
|
# Generate report after N full testing cycles
|
||||||
report_cycles: 1
|
report_cycles: 1
|
||||||
|
|
||||||
# Timeout for traceroute response (in seconds)
|
# Active Testing Settings
|
||||||
traceroute_timeout: 90
|
traceroute_timeout: 90
|
||||||
|
|
||||||
# Minimum interval between tests (in seconds)
|
|
||||||
active_test_interval: 30
|
active_test_interval: 30
|
||||||
|
|
||||||
|
# Manual Geolocation Overrides
|
||||||
|
manual_positions:
|
||||||
|
"!12345678":
|
||||||
|
lat: 59.12345
|
||||||
|
lon: 24.12345
|
||||||
|
|
||||||
# Thresholds for Analysis
|
# Thresholds for Analysis
|
||||||
thresholds:
|
thresholds:
|
||||||
channel_utilization: 25.0 # Percent
|
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.
|
* **Action**: Check the hop count in the response (if visible/parsed) to verify the path.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
* `mesh_monitor/`: Source code.
|
* `mesh_analyzer/`: Source code.
|
||||||
* `monitor.py`: Main application loop.
|
* `monitor.py`: Main application loop.
|
||||||
* `analyzer.py`: Health check logic.
|
* `analyzer.py`: Health check logic.
|
||||||
* `active_tests.py`: Traceroute logic.
|
* `active_tests.py`: Traceroute logic.
|
||||||
|
|||||||
2
main.py
2
main.py
@@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
Entry point for the Meshtastic Network Monitor.
|
Entry point for the Meshtastic Network Monitor.
|
||||||
"""
|
"""
|
||||||
from mesh_monitor.monitor import main
|
from mesh_analyzer.monitor import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ class NetworkHealthAnalyzer:
|
|||||||
if not test_results:
|
if not test_results:
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
from mesh_monitor.route_analyzer import RouteAnalyzer
|
from mesh_analyzer.route_analyzer import RouteAnalyzer
|
||||||
route_analyzer = RouteAnalyzer(nodes)
|
route_analyzer = RouteAnalyzer(nodes)
|
||||||
relay_usage = route_analyzer._analyze_relay_usage(
|
relay_usage = route_analyzer._analyze_relay_usage(
|
||||||
[r for r in test_results if r.get('status') == 'success']
|
[r for r in test_results if r.get('status') == 'success']
|
||||||
@@ -5,7 +5,7 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from .utils import get_val, haversine, get_node_name
|
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__)
|
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
|
||||||
meshtastic
|
|
||||||
pypubsub
|
pypubsub
|
||||||
PyYAML
|
PyYAML
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import os
|
|||||||
import argparse
|
import argparse
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Add mesh_monitor to path
|
# Add mesh_analyzer to path (parent directory)
|
||||||
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
|
||||||
from mesh_monitor.route_analyzer import RouteAnalyzer
|
from mesh_analyzer.route_analyzer import RouteAnalyzer
|
||||||
|
|
||||||
|
|
||||||
def load_json_data(json_filepath):
|
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']
|
node['position']['longitude'] = pos['lon']
|
||||||
|
|
||||||
# Recreate analyzer and re-run analysis to populate cluster_data and ch_util_data
|
# 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)
|
analyzer = NetworkHealthAnalyzer(config=config)
|
||||||
|
|
||||||
# Re-run analysis to populate analyzer data structures AND get new issues
|
# 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
|
# Monkey-patch the generate_report to use custom filename
|
||||||
original_generate = reporter.generate_report
|
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
|
# Temporarily change the method to use custom filename
|
||||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
custom_filename = os.path.basename(output_path)
|
custom_filename = os.path.basename(output_path)
|
||||||
filepath = os.path.join(report_dir, custom_filename)
|
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_analyzer = RouteAnalyzer(nodes)
|
||||||
route_analysis_local = route_analyzer.analyze_routes(test_results)
|
route_analysis_local = route_analyzer.analyze_routes(test_results)
|
||||||
|
|
||||||
@@ -9,10 +9,11 @@ import os
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime
|
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.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():
|
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
|
# Add project root to path
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
|
|
||||||
from mesh_monitor.analyzer import NetworkHealthAnalyzer
|
from mesh_analyzer.analyzer import NetworkHealthAnalyzer
|
||||||
from mesh_monitor.monitor import MeshMonitor
|
from mesh_analyzer.monitor import MeshMonitor
|
||||||
from mesh_monitor.active_tests import ActiveTester
|
from mesh_analyzer.active_tests import ActiveTester
|
||||||
|
|
||||||
class TestNetworkMonitor(unittest.TestCase):
|
class TestNetworkMonitor(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -270,7 +270,7 @@ class TestNetworkMonitor(unittest.TestCase):
|
|||||||
|
|
||||||
def test_local_config_check(self):
|
def test_local_config_check(self):
|
||||||
print("\nRunning Local Config Check Test...")
|
print("\nRunning Local Config Check Test...")
|
||||||
from mesh_monitor.monitor import MeshMonitor
|
from mesh_analyzer.monitor import MeshMonitor
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
# Mock the interface and node
|
# Mock the interface and node
|
||||||
@@ -301,7 +301,7 @@ class TestNetworkMonitor(unittest.TestCase):
|
|||||||
|
|
||||||
def test_auto_discovery(self):
|
def test_auto_discovery(self):
|
||||||
print("\nRunning Auto-Discovery Test...")
|
print("\nRunning Auto-Discovery Test...")
|
||||||
from mesh_monitor.active_tests import ActiveTester
|
from mesh_analyzer.active_tests import ActiveTester
|
||||||
|
|
||||||
# Mock Interface
|
# Mock Interface
|
||||||
mock_interface = MagicMock()
|
mock_interface = MagicMock()
|
||||||
@@ -370,7 +370,7 @@ class TestNetworkMonitor(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
issues = self.analyzer.analyze(self.mock_nodes)
|
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")
|
self.assertTrue(len(density_warnings) > 0, "Should detect high router density")
|
||||||
print(" [Pass] Router Density Check")
|
print(" [Pass] Router Density Check")
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ class TestNetworkMonitor(unittest.TestCase):
|
|||||||
|
|
||||||
def test_reporting(self):
|
def test_reporting(self):
|
||||||
print("\nRunning Reporting Test...")
|
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
|
# Initialize self.monitor mock since setUp doesn't do it
|
||||||
self.monitor = MagicMock()
|
self.monitor = MagicMock()
|
||||||
@@ -463,8 +463,8 @@ class TestNetworkMonitor(unittest.TestCase):
|
|||||||
def test_route_analysis(self):
|
def test_route_analysis(self):
|
||||||
"""Test the RouteAnalyzer and Reporter integration."""
|
"""Test the RouteAnalyzer and Reporter integration."""
|
||||||
print("\nRunning Route Analysis Test...")
|
print("\nRunning Route Analysis Test...")
|
||||||
from mesh_monitor.route_analyzer import RouteAnalyzer
|
from mesh_analyzer.route_analyzer import RouteAnalyzer
|
||||||
from mesh_monitor.reporter import NetworkReporter
|
from mesh_analyzer.reporter import NetworkReporter
|
||||||
|
|
||||||
# Mock Test Results with Routes
|
# Mock Test Results with Routes
|
||||||
test_results = [
|
test_results = [
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from mesh_monitor.analyzer import NetworkHealthAnalyzer
|
from mesh_analyzer.analyzer import NetworkHealthAnalyzer
|
||||||
|
import time
|
||||||
|
|
||||||
class TestAnalyzerEnhancements(unittest.TestCase):
|
class TestAnalyzerEnhancements(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -37,7 +38,7 @@ class TestAnalyzerEnhancements(unittest.TestCase):
|
|||||||
def test_network_size(self):
|
def test_network_size(self):
|
||||||
nodes = {}
|
nodes = {}
|
||||||
for i in range(61):
|
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)
|
issues = self.analyzer.analyze(nodes)
|
||||||
self.assertTrue(any("Network Size" in i for i in issues))
|
self.assertTrue(any("Network Size" in i for i in issues))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
from mesh_monitor.active_tests import ActiveTester
|
from mesh_analyzer.active_tests import ActiveTester
|
||||||
|
|
||||||
|
|
||||||
class TestHopCountCalculation(unittest.TestCase):
|
class TestHopCountCalculation(unittest.TestCase):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
import time
|
import time
|
||||||
from mesh_monitor.analyzer import NetworkHealthAnalyzer
|
from mesh_analyzer.analyzer import NetworkHealthAnalyzer
|
||||||
|
|
||||||
class TestNetworkSize(unittest.TestCase):
|
class TestNetworkSize(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from mesh_monitor.analyzer import NetworkHealthAnalyzer
|
from mesh_analyzer.analyzer import NetworkHealthAnalyzer
|
||||||
|
|
||||||
class TestRouteQuality(unittest.TestCase):
|
class TestRouteQuality(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
from mesh_monitor.active_tests import ActiveTester
|
from mesh_analyzer.active_tests import ActiveTester
|
||||||
|
|
||||||
class TestRouterClusters(unittest.TestCase):
|
class TestRouterClusters(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from mesh_monitor.analyzer import NetworkHealthAnalyzer
|
from mesh_analyzer.analyzer import NetworkHealthAnalyzer
|
||||||
|
|
||||||
class TestRouterEfficiency(unittest.TestCase):
|
class TestRouterEfficiency(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ logger = logging.getLogger("Verification")
|
|||||||
|
|
||||||
def verify_utils():
|
def verify_utils():
|
||||||
logger.info("Verifying utils.py...")
|
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
|
# Test haversine
|
||||||
dist = haversine(0, 0, 1, 1)
|
dist = haversine(0, 0, 1, 1)
|
||||||
@@ -49,21 +49,21 @@ def verify_modules():
|
|||||||
interface = MockInterface()
|
interface = MockInterface()
|
||||||
|
|
||||||
# Test Analyzer
|
# Test Analyzer
|
||||||
from mesh_monitor.analyzer import NetworkHealthAnalyzer
|
from mesh_analyzer.analyzer import NetworkHealthAnalyzer
|
||||||
analyzer = NetworkHealthAnalyzer()
|
analyzer = NetworkHealthAnalyzer()
|
||||||
issues = analyzer.analyze({})
|
issues = analyzer.analyze({})
|
||||||
assert isinstance(issues, list), "Analyzer did not return list"
|
assert isinstance(issues, list), "Analyzer did not return list"
|
||||||
logger.info("Analyzer instantiated and ran.")
|
logger.info("Analyzer instantiated and ran.")
|
||||||
|
|
||||||
# Test ActiveTester
|
# Test ActiveTester
|
||||||
from mesh_monitor.active_tests import ActiveTester
|
from mesh_analyzer.active_tests import ActiveTester
|
||||||
tester = ActiveTester(interface)
|
tester = ActiveTester(interface)
|
||||||
assert hasattr(tester, 'lock'), "ActiveTester missing lock"
|
assert hasattr(tester, 'lock'), "ActiveTester missing lock"
|
||||||
assert isinstance(tester.lock, type(threading.Lock())), "ActiveTester lock is not a Lock"
|
assert isinstance(tester.lock, type(threading.Lock())), "ActiveTester lock is not a Lock"
|
||||||
logger.info("ActiveTester instantiated and has lock.")
|
logger.info("ActiveTester instantiated and has lock.")
|
||||||
|
|
||||||
# Test Reporter
|
# Test Reporter
|
||||||
from mesh_monitor.reporter import NetworkReporter
|
from mesh_analyzer.reporter import NetworkReporter
|
||||||
reporter = NetworkReporter()
|
reporter = NetworkReporter()
|
||||||
logger.info("Reporter instantiated.")
|
logger.info("Reporter instantiated.")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user