mirror of
https://github.com/eddieoz/LoRa-Mesh-Analyzer.git
synced 2026-03-28 17:42:59 +01:00
feat: Add HTML report generation with configurable output formats and improve packet processing robustness.
This commit is contained in:
@@ -189,7 +189,8 @@ class MeshMonitor:
|
|||||||
def on_receive(self, packet: dict, interface) -> None:
|
def on_receive(self, packet: dict, interface) -> None:
|
||||||
"""
|
"""
|
||||||
Callback for received packets.
|
Callback for received packets.
|
||||||
""" # We need: id, fromId, hopLimit (if available)
|
"""
|
||||||
|
try:
|
||||||
pkt_info = {
|
pkt_info = {
|
||||||
'id': packet.get('id'),
|
'id': packet.get('id'),
|
||||||
'fromId': packet.get('fromId'),
|
'fromId': packet.get('fromId'),
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ from .utils import get_val, haversine, get_node_name
|
|||||||
|
|
||||||
from mesh_analyzer.route_analyzer import RouteAnalyzer
|
from mesh_analyzer.route_analyzer import RouteAnalyzer
|
||||||
|
|
||||||
|
import io
|
||||||
|
import markdown
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class NetworkReporter:
|
class NetworkReporter:
|
||||||
@@ -19,7 +22,7 @@ class NetworkReporter:
|
|||||||
|
|
||||||
def generate_report(self, nodes: dict, test_results: list, analysis_issues: list, local_node: dict = None, router_stats: list = None, analyzer: object = None, override_timestamp: str = None, override_location: str = None, save_json: bool = True, output_filename: str = None) -> str:
|
def generate_report(self, nodes: dict, test_results: list, analysis_issues: list, local_node: dict = None, router_stats: list = None, analyzer: object = None, override_timestamp: str = None, override_location: str = None, save_json: bool = True, output_filename: str = None) -> str:
|
||||||
"""
|
"""
|
||||||
Generates a Markdown report based on collected data.
|
Generates a Markdown and/or HTML report based on collected data.
|
||||||
Also persists all raw data to JSON format.
|
Also persists all raw data to JSON format.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -41,58 +44,99 @@ class NetworkReporter:
|
|||||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
report_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
report_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
# Use custom filename if provided, otherwise use timestamp-based name
|
# Determine base filename
|
||||||
if output_filename:
|
if output_filename:
|
||||||
filename = output_filename if output_filename.endswith('.md') else f"{output_filename}.md"
|
base_name = output_filename.replace('.md', '').replace('.html', '')
|
||||||
# Extract base name without extension for JSON
|
|
||||||
base_name = output_filename.replace('.md', '')
|
|
||||||
json_filename = f"{base_name}.json"
|
|
||||||
else:
|
else:
|
||||||
filename = f"report-{timestamp}.md"
|
base_name = f"report-{timestamp}"
|
||||||
json_filename = f"report-{timestamp}.json"
|
|
||||||
|
|
||||||
filepath = os.path.join(self.report_dir, filename)
|
json_filename = f"{base_name}.json"
|
||||||
json_filepath = os.path.join(self.report_dir, json_filename)
|
json_filepath = os.path.join(self.report_dir, json_filename)
|
||||||
|
|
||||||
logger.info(f"Generating network report: {filepath}")
|
logger.info(f"Generating network report: {base_name}")
|
||||||
|
|
||||||
# Run Route Analysis
|
# Run Route Analysis
|
||||||
route_analyzer = RouteAnalyzer(nodes)
|
route_analyzer = RouteAnalyzer(nodes)
|
||||||
route_analysis = route_analyzer.analyze_routes(test_results)
|
route_analysis = route_analyzer.analyze_routes(test_results)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# --- Generate Markdown Report ---
|
# --- Generate Report Content ---
|
||||||
with open(filepath, "w") as f:
|
# We build the markdown content in memory first
|
||||||
# Header
|
f = io.StringIO()
|
||||||
f.write(f"# Meshtastic Network Report\n")
|
|
||||||
f.write(f"**Date:** {report_date}\n\n")
|
# Header
|
||||||
|
f.write(f"# Meshtastic Network Report\n")
|
||||||
|
f.write(f"**Date:** {report_date}\n\n")
|
||||||
|
|
||||||
# Calculate location if not overridden
|
# Calculate location if not overridden
|
||||||
if override_location:
|
if override_location:
|
||||||
test_location = override_location
|
test_location = override_location
|
||||||
else:
|
else:
|
||||||
test_location = self._get_location_string(nodes, local_node)
|
test_location = self._get_location_string(nodes, local_node)
|
||||||
|
|
||||||
# 1. Executive Summary
|
# 1. Executive Summary
|
||||||
self._write_executive_summary(f, nodes, test_results, analysis_issues, test_location)
|
self._write_executive_summary(f, nodes, test_results, analysis_issues, test_location)
|
||||||
|
|
||||||
# 2. Network Health (Analysis Findings)
|
# 2. Network Health (Analysis Findings)
|
||||||
self._write_network_health(f, analysis_issues, analyzer)
|
self._write_network_health(f, analysis_issues, analyzer)
|
||||||
|
|
||||||
|
# 2.1 Router Performance Table (New)
|
||||||
|
if router_stats:
|
||||||
|
self._write_router_performance_table(f, router_stats)
|
||||||
|
|
||||||
|
# 3. Route Analysis (New Section)
|
||||||
|
self._write_route_analysis(f, route_analysis)
|
||||||
|
|
||||||
|
# 4. Traceroute Results
|
||||||
|
self._write_traceroute_results(f, test_results, nodes, local_node)
|
||||||
|
|
||||||
|
# 5. Recommendations
|
||||||
|
self._write_recommendations(f, analysis_issues, test_results, analyzer)
|
||||||
|
|
||||||
|
# Get the full markdown content
|
||||||
|
markdown_content = f.getvalue()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# --- Output to Files ---
|
||||||
|
output_formats = self.config.get('report_output_formats', ['markdown'])
|
||||||
|
generated_files = []
|
||||||
|
|
||||||
|
# 1. Markdown Output
|
||||||
|
if 'markdown' in output_formats:
|
||||||
|
md_filepath = os.path.join(self.report_dir, f"{base_name}.md")
|
||||||
|
with open(md_filepath, "w") as md_file:
|
||||||
|
md_file.write(markdown_content)
|
||||||
|
generated_files.append(md_filepath)
|
||||||
|
logger.info(f"Report generated: {md_filepath}")
|
||||||
|
|
||||||
|
# 2. HTML Output
|
||||||
|
if 'html' in output_formats:
|
||||||
|
html_filepath = os.path.join(self.report_dir, f"{base_name}.html")
|
||||||
|
|
||||||
# 2.1 Router Performance Table (New)
|
# Basic CSS for better readability
|
||||||
if router_stats:
|
css = """
|
||||||
self._write_router_performance_table(f, router_stats)
|
<style>
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max_width: 960px; margin: 0 auto; padding: 20px; }
|
||||||
# 3. Route Analysis (New Section)
|
h1, h2, h3 { color: #2c3e50; margin-top: 1.5em; }
|
||||||
self._write_route_analysis(f, route_analysis)
|
h1 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
||||||
|
h2 { border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
||||||
# 4. Traceroute Results
|
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
|
||||||
self._write_traceroute_results(f, test_results, nodes, local_node)
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||||
|
th { background-color: #f8f9fa; font-weight: bold; }
|
||||||
# 5. Recommendations
|
tr:nth-child(even) { background-color: #f9f9f9; }
|
||||||
self._write_recommendations(f, analysis_issues, test_results, analyzer)
|
code { background-color: #f5f5f5; padding: 2px 4px; border-radius: 3px; font-family: monospace; }
|
||||||
|
ul { padding-left: 20px; }
|
||||||
logger.info(f"Report generated successfully: {filepath}")
|
li { margin-bottom: 5px; }
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
|
||||||
|
html_content = markdown.markdown(markdown_content, extensions=['tables', 'fenced_code'])
|
||||||
|
full_html = f"<!DOCTYPE html>\n<html>\n<head>\n<meta charset='utf-8'>\n<title>Meshtastic Network Report - {report_date}</title>\n{css}\n</head>\n<body>\n{html_content}\n</body>\n</html>"
|
||||||
|
|
||||||
|
with open(html_filepath, "w") as html_file:
|
||||||
|
html_file.write(full_html)
|
||||||
|
generated_files.append(html_filepath)
|
||||||
|
logger.info(f"Report generated: {html_filepath}")
|
||||||
|
|
||||||
# --- Persist Raw Data to JSON ---
|
# --- Persist Raw Data to JSON ---
|
||||||
if save_json:
|
if save_json:
|
||||||
@@ -112,7 +156,10 @@ class NetworkReporter:
|
|||||||
except Exception as json_e:
|
except Exception as json_e:
|
||||||
logger.error(f"Failed to save JSON data: {json_e}")
|
logger.error(f"Failed to save JSON data: {json_e}")
|
||||||
|
|
||||||
return filepath
|
# Return the primary file path (prefer markdown if available, else first generated)
|
||||||
|
if generated_files:
|
||||||
|
return generated_files[0]
|
||||||
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to generate report: {e}")
|
logger.error(f"Failed to generate report: {e}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ meshtastic
|
|||||||
|
|
||||||
pypubsub
|
pypubsub
|
||||||
PyYAML
|
PyYAML
|
||||||
|
markdown
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ auto_discovery_limit: 5
|
|||||||
# Generate report after N full testing cycles
|
# Generate report after N full testing cycles
|
||||||
report_cycles: 1
|
report_cycles: 1
|
||||||
|
|
||||||
|
# Report Output Formats
|
||||||
|
# Options: 'markdown', 'html'
|
||||||
|
report_output_formats:
|
||||||
|
- markdown
|
||||||
|
|
||||||
# Active Testing Settings
|
# Active Testing Settings
|
||||||
# Timeout for traceroute response (in seconds)
|
# Timeout for traceroute response (in seconds)
|
||||||
traceroute_timeout: 90
|
traceroute_timeout: 90
|
||||||
|
|||||||
Reference in New Issue
Block a user