mirror of
https://github.com/eddieoz/LoRa-Mesh-Analyzer.git
synced 2026-03-28 17:42:59 +01:00
242 lines
9.6 KiB
Python
242 lines
9.6 KiB
Python
import time
|
|
import sys
|
|
import threading
|
|
import logging
|
|
from pubsub import pub
|
|
import meshtastic.serial_interface
|
|
import meshtastic.tcp_interface
|
|
import meshtastic.util
|
|
from .analyzer import NetworkHealthAnalyzer
|
|
from .active_tests import ActiveTester
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger(__name__)
|
|
|
|
import yaml
|
|
import os
|
|
|
|
# ... imports ...
|
|
|
|
class MeshMonitor:
|
|
def __init__(self, interface_type='serial', hostname=None, ignore_no_position=False, config_file='config.yaml'):
|
|
self.interface = None
|
|
self.interface_type = interface_type
|
|
self.hostname = hostname
|
|
self.analyzer = NetworkHealthAnalyzer(ignore_no_position=ignore_no_position)
|
|
self.active_tester = None
|
|
self.running = False
|
|
self.config = self.load_config(config_file)
|
|
self.packet_history = [] # List of recent packets for duplication check
|
|
|
|
# Configure Log Level
|
|
log_level_str = self.config.get('log_level', 'info').upper()
|
|
log_level = getattr(logging, log_level_str, logging.INFO)
|
|
logger.setLevel(log_level)
|
|
logging.getLogger().setLevel(log_level) # Set root logger too to capture lib logs if needed
|
|
logger.info(f"Log level set to: {log_level_str}")
|
|
|
|
def load_config(self, config_file):
|
|
if os.path.exists(config_file):
|
|
try:
|
|
with open(config_file, 'r') as f:
|
|
return yaml.safe_load(f) or {}
|
|
except Exception as e:
|
|
logger.error(f"Error loading config file: {e}")
|
|
return {}
|
|
|
|
def start(self):
|
|
logger.info(f"Connecting to Meshtastic node via {self.interface_type}...")
|
|
try:
|
|
# ... interface init ...
|
|
if self.interface_type == 'serial':
|
|
self.interface = meshtastic.serial_interface.SerialInterface()
|
|
elif self.interface_type == 'tcp':
|
|
if not self.hostname:
|
|
raise ValueError("Hostname required for TCP interface")
|
|
self.interface = meshtastic.tcp_interface.TCPInterface(self.hostname)
|
|
else:
|
|
raise ValueError(f"Unknown interface type: {self.interface_type}")
|
|
|
|
# Check local config
|
|
self.check_local_config()
|
|
|
|
priority_nodes = self.config.get('priority_nodes', [])
|
|
if priority_nodes:
|
|
logger.info(f"Loaded {len(priority_nodes)} priority nodes for active testing.")
|
|
|
|
self.active_tester = ActiveTester(self.interface, priority_nodes=priority_nodes)
|
|
|
|
# ... subscriptions ...
|
|
pub.subscribe(self.on_receive, "meshtastic.receive")
|
|
pub.subscribe(self.on_connection, "meshtastic.connection.established")
|
|
pub.subscribe(self.on_node_info, "meshtastic.node.updated")
|
|
|
|
logger.info("Connected to node.")
|
|
self.running = True
|
|
self.main_loop()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to connect or run: {e}")
|
|
self.stop()
|
|
|
|
def check_local_config(self):
|
|
"""
|
|
Analyzes the local node's configuration and warns about non-optimal settings.
|
|
"""
|
|
logger.info("Checking local node configuration...")
|
|
try:
|
|
# Wait a moment for node to populate if needed (though interface init usually does it)
|
|
node = None
|
|
if hasattr(self.interface, 'localNode'):
|
|
node = self.interface.localNode
|
|
|
|
if not node:
|
|
logger.warning("Could not access local node information.")
|
|
return
|
|
|
|
# 1. Check Role
|
|
# We access the protobuf config directly
|
|
try:
|
|
# Note: node.config might be a property of the node object
|
|
# In some versions, it's node.localConfig
|
|
# Let's try to access it safely
|
|
if hasattr(node, 'config'):
|
|
config = node.config
|
|
elif hasattr(node, 'localConfig'):
|
|
config = node.localConfig
|
|
else:
|
|
logger.warning("Could not find config attribute on local node.")
|
|
return
|
|
|
|
from meshtastic.protobuf import config_pb2
|
|
role = config.device.role
|
|
role_name = config_pb2.Config.DeviceConfig.Role.Name(role)
|
|
|
|
if role_name in ['ROUTER', 'ROUTER_CLIENT', 'REPEATER']:
|
|
logger.warning(f" [!] Local Node Role is '{role_name}'.")
|
|
logger.warning(" Recommended for monitoring: 'CLIENT' or 'CLIENT_MUTE'.")
|
|
logger.warning(" (Active monitoring works best when the monitor itself isn't a router)")
|
|
else:
|
|
logger.info(f"Local Node Role: {role_name} (OK)")
|
|
except Exception as e:
|
|
logger.warning(f"Could not verify role: {e}")
|
|
|
|
# 2. Check Hop Limit
|
|
try:
|
|
if hasattr(node, 'config'):
|
|
config = node.config
|
|
elif hasattr(node, 'localConfig'):
|
|
config = node.localConfig
|
|
|
|
hop_limit = config.lora.hop_limit
|
|
if hop_limit > 3:
|
|
logger.warning(f" [!] Local Node Hop Limit is {hop_limit}.")
|
|
logger.warning(" Recommended: 3. High hop limits can cause network congestion.")
|
|
else:
|
|
logger.info(f"Local Node Hop Limit: {hop_limit} (OK)")
|
|
except Exception as e:
|
|
logger.warning(f"Could not verify hop limit: {e}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to check local config: {e}")
|
|
|
|
def stop(self):
|
|
self.running = False
|
|
if self.interface:
|
|
self.interface.close()
|
|
|
|
def on_receive(self, packet, interface):
|
|
try:
|
|
# Store packet for analysis
|
|
# We need: id, fromId, hopLimit (if available)
|
|
pkt_info = {
|
|
'id': packet.get('id'),
|
|
'fromId': packet.get('fromId'),
|
|
'toId': packet.get('toId'),
|
|
'rxTime': packet.get('rxTime', time.time()),
|
|
'hopLimit': packet.get('hopLimit'), # Might be in 'decoded' depending on packet type
|
|
'decoded': packet.get('decoded', {})
|
|
}
|
|
|
|
# Keep history manageable (e.g., last 100 packets or last minute)
|
|
self.packet_history.append(pkt_info)
|
|
# Prune old packets (older than 60s)
|
|
current_time = time.time()
|
|
self.packet_history = [p for p in self.packet_history if current_time - p['rxTime'] < 60]
|
|
|
|
if packet.get('decoded', {}).get('portnum') == 'ROUTING_APP':
|
|
# This might be a traceroute response
|
|
pass
|
|
|
|
# Log interesting packets
|
|
portnum = packet.get('decoded', {}).get('portnum')
|
|
if portnum == 'TEXT_MESSAGE_APP':
|
|
text = packet.get('decoded', {}).get('text', '')
|
|
logger.info(f"Received Message: {text}")
|
|
elif portnum == 'TRACEROUTE_APP':
|
|
logger.info(f"Received Traceroute Packet: {packet}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing packet: {e}")
|
|
|
|
def on_connection(self, interface, topic=pub.AUTO_TOPIC):
|
|
logger.info("Connection established signal received.")
|
|
|
|
def on_node_info(self, node, interface):
|
|
# logger.debug(f"Node info updated: {node}")
|
|
pass
|
|
|
|
def main_loop(self):
|
|
logger.info("Starting monitoring loop...")
|
|
while self.running:
|
|
try:
|
|
logger.info("--- Running Network Analysis ---")
|
|
nodes = self.interface.nodes
|
|
|
|
# Get local node info for distance calculations
|
|
my_node = None
|
|
if hasattr(self.interface, 'localNode'):
|
|
my_node = self.interface.localNode
|
|
|
|
# Run Analysis
|
|
issues = self.analyzer.analyze(nodes, packet_history=self.packet_history, my_node=my_node)
|
|
|
|
# Report Issues
|
|
if issues:
|
|
logger.warning(f"Found {len(issues)} potential issues:")
|
|
for issue in issues:
|
|
logger.warning(f" - {issue}")
|
|
else:
|
|
logger.info("No critical issues found in current scan.")
|
|
|
|
# Run Active Tests
|
|
if self.active_tester:
|
|
self.active_tester.run_next_test()
|
|
|
|
# Wait for next scan
|
|
time.sleep(60)
|
|
# ... exceptions ...
|
|
except KeyboardInterrupt:
|
|
logger.info("Stopping monitor...")
|
|
self.stop()
|
|
break
|
|
except Exception as e:
|
|
logger.error(f"Error in main loop: {e}")
|
|
time.sleep(10)
|
|
|
|
if __name__ == "__main__":
|
|
# Simple CLI for testing
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description='Meshtastic Network Monitor')
|
|
parser.add_argument('--tcp', help='Hostname for TCP connection (e.g. 192.168.1.10)')
|
|
parser.add_argument('--ignore-no-position', action='store_true', help='Ignore routers without position')
|
|
args = parser.parse_args()
|
|
|
|
if args.tcp:
|
|
monitor = MeshMonitor(interface_type='tcp', hostname=args.tcp, ignore_no_position=args.ignore_no_position)
|
|
else:
|
|
monitor = MeshMonitor(interface_type='serial', ignore_no_position=args.ignore_no_position)
|
|
|
|
monitor.start()
|