Files
venus/axiom-nmea/examples/sensor-monitor/sensor_monitor.py
dev 9756538f16 Initial commit: Venus OS boat addons monorepo
Organizes 11 projects for Cerbo GX/Venus OS into a single repository:
- axiom-nmea: Raymarine LightHouse protocol decoder
- dbus-generator-ramp: Generator current ramp controller
- dbus-lightning: Blitzortung lightning monitor
- dbus-meteoblue-forecast: Meteoblue weather forecast
- dbus-no-foreign-land: noforeignland.com tracking
- dbus-tides: Tide prediction from depth + harmonics
- dbus-vrm-history: VRM cloud history proxy
- dbus-windy-station: Windy.com weather upload
- mfd-custom-app: MFD app deployment package
- venus-html5-app: Custom Victron HTML5 app fork
- watermaker: Watermaker PLC control UI

Adds root README, .gitignore, project template, and per-project
.gitignore files. Sensitive config files excluded via .gitignore
with .example templates provided.

Made-with: Cursor
2026-03-16 17:04:16 +00:00

185 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""
Raymarine Sensor Update Monitor
Displays real-time frequency of updates from each sensor type on the Raymarine network.
Useful for diagnosing gaps or inconsistent data delivery.
Usage:
python sensor_monitor.py -i 198.18.5.5
"""
import argparse
import logging
import sys
import time
from datetime import datetime
from pathlib import Path
# Add repo root to path for development
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from raymarine_nmea import RaymarineDecoder, MulticastListener, SensorData
def main():
parser = argparse.ArgumentParser(description="Monitor Raymarine sensor update frequency")
parser.add_argument('-i', '--interface', required=True,
help='Interface IP for Raymarine multicast (e.g., 198.18.5.5)')
parser.add_argument('--interval', type=float, default=1.0,
help='Display refresh interval in seconds (default: 1.0)')
parser.add_argument('--debug', action='store_true',
help='Enable debug logging to see raw packets')
args = parser.parse_args()
# Enable debug logging if requested
if args.debug:
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
else:
logging.basicConfig(level=logging.WARNING)
sensor_data = SensorData()
decoder = RaymarineDecoder()
listener = MulticastListener(
decoder=decoder,
sensor_data=sensor_data,
interface_ip=args.interface,
)
print(f"Starting Raymarine sensor monitor on interface {args.interface}...")
if args.debug:
print("Debug mode enabled - check logs for packet details")
listener.start()
# Track update times for each sensor
last_values = {}
update_counts = {}
last_update_time = {}
max_gaps = {}
start_time = time.time()
sensor_fields = [
('GPS', 'gps', lambda d: (d.latitude, d.longitude)),
('Heading', 'heading', lambda d: d.heading_deg),
('COG', 'heading', lambda d: d.cog_deg),
('SOG', 'heading', lambda d: d.sog_kts),
('Depth', 'depth', lambda d: d.depth_m),
('Water Temp', 'temp', lambda d: d.water_temp_c),
('Air Temp', 'temp', lambda d: d.air_temp_c),
('Wind (Apparent)', 'wind', lambda d: (d.awa_deg, d.aws_kts)),
('Wind (True)', 'wind', lambda d: (d.twd_deg, d.tws_kts)),
('Pressure', 'pressure', lambda d: d.pressure_mbar),
('Tanks', 'tank', lambda d: dict(d.tanks) if d.tanks else None),
('Batteries', 'battery', lambda d: dict(d.batteries) if d.batteries else None),
]
for name, _, _ in sensor_fields:
update_counts[name] = 0
last_update_time[name] = None
max_gaps[name] = 0
last_values[name] = None
try:
while True:
if not args.debug:
# Clear screen only in non-debug mode
print("\033[2J\033[H", end="")
print("=" * 80)
print(f"RAYMARINE SENSOR UPDATE MONITOR - {datetime.now().strftime('%H:%M:%S')}")
print("=" * 80)
elapsed = time.time() - start_time
print(f"Monitoring for: {elapsed:.1f}s")
print(f"Packets: {sensor_data.packet_count} | Decoded: {sensor_data.decode_count}")
print()
print(f"{'Sensor':<18} {'Value':<25} {'Age':>8} {'Count':>7} {'Avg':>8} {'MaxGap':>8}")
print("-" * 80)
# Get age mapping (must be outside lock since get_age() also locks)
age_values = {}
for age_type in ['gps', 'heading', 'depth', 'temp', 'wind', 'pressure', 'tank', 'battery']:
age_values[age_type] = sensor_data.get_age(age_type)
with sensor_data._lock:
for name, age_type, getter in sensor_fields:
try:
value = getter(sensor_data)
except Exception:
value = None
# Check if value changed
if value != last_values[name] and value is not None:
now = time.time()
if last_update_time[name] is not None:
gap = now - last_update_time[name]
if gap > max_gaps[name]:
max_gaps[name] = gap
last_update_time[name] = now
update_counts[name] += 1
last_values[name] = value
# Get age (from pre-fetched values)
age = age_values.get(age_type)
# Format value for display
if value is None:
val_str = "-"
elif isinstance(value, tuple):
parts = [f"{v:.1f}" if v is not None else "-" for v in value]
val_str = ", ".join(parts)
elif isinstance(value, dict):
val_str = f"{len(value)} items"
elif isinstance(value, float):
val_str = f"{value:.2f}"
else:
val_str = str(value)[:25]
# Truncate value string
if len(val_str) > 25:
val_str = val_str[:22] + "..."
# Color based on age
if age is None:
color = "\033[90m" # Gray
age_str = "-"
elif age > 5:
color = "\033[91m" # Red
age_str = f"{age:.1f}s"
elif age > 2:
color = "\033[93m" # Yellow
age_str = f"{age:.1f}s"
else:
color = "\033[92m" # Green
age_str = f"{age:.1f}s"
reset = "\033[0m"
count = update_counts[name]
avg_str = f"{count/elapsed:.2f}/s" if elapsed > 0 and count > 0 else "-"
max_gap_str = f"{max_gaps[name]:.1f}s" if max_gaps[name] > 0 else "-"
print(f"{color}{name:<18} {val_str:<25} {age_str:>8} {count:>7} {avg_str:>8} {max_gap_str:>8}{reset}")
print()
print("=" * 80)
print("Press Ctrl+C to exit")
if args.debug:
print("\n--- Debug output follows ---\n")
time.sleep(args.interval)
except KeyboardInterrupt:
print("\nStopping monitor...")
listener.stop()
if __name__ == "__main__":
main()