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
185 lines
6.6 KiB
Python
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()
|