427 lines
13 KiB
Python
427 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Venus OS D-Bus Publisher for Raymarine Sensor Data.
|
|
|
|
This script reads sensor data from Raymarine LightHouse multicast
|
|
and publishes it to Venus OS via D-Bus, making it available to
|
|
the Victron ecosystem. It also runs an NMEA TCP server on port 10110
|
|
for integration with navigation apps and charting software.
|
|
|
|
Published D-Bus services:
|
|
- com.victronenergy.gps.raymarine_0
|
|
GPS position, speed, and course
|
|
|
|
- com.victronenergy.meteo.raymarine_0
|
|
Wind direction and speed, air temperature, barometric pressure
|
|
|
|
- com.victronenergy.tank.raymarine_tank{N}_0
|
|
Tank levels for each configured tank
|
|
|
|
- com.victronenergy.battery.raymarine_bat{N}_0
|
|
Battery voltage for each configured battery
|
|
|
|
- com.victronenergy.navigation.raymarine_0
|
|
Heading, depth, and water temperature
|
|
|
|
NMEA TCP Server (port 10110):
|
|
The NMEA TCP server broadcasts ALL available NMEA 0183 sentences,
|
|
which includes more data than what Venus OS can display via D-Bus:
|
|
- GPS: GGA, GLL, RMC
|
|
- Navigation: HDG, HDT, VTG, VHW
|
|
- Wind: MWV (apparent & true), MWD
|
|
- Depth: DPT, DBT
|
|
- Temperature: MTW, MTA
|
|
- Transducers: XDR (tanks, batteries, pressure)
|
|
|
|
Compatible with Navionics, iSailor, OpenCPN, SignalK, and other
|
|
NMEA 0183 TCP clients.
|
|
|
|
Usage:
|
|
# Basic usage (listens on 198.18.5.5 VLAN interface)
|
|
python3 venus_publisher.py
|
|
|
|
# Specify interface IP
|
|
python3 venus_publisher.py --interface 198.18.5.5
|
|
|
|
# Enable debug logging
|
|
python3 venus_publisher.py --debug
|
|
|
|
# Disable specific services
|
|
python3 venus_publisher.py --no-tanks --no-batteries
|
|
|
|
# Use a different NMEA TCP port
|
|
python3 venus_publisher.py --nmea-tcp-port 2000
|
|
|
|
# Disable NMEA TCP server (D-Bus only)
|
|
python3 venus_publisher.py --no-nmea-tcp
|
|
|
|
Installation on Venus OS:
|
|
1. Build: ./build-package.sh
|
|
2. Copy to device: scp dbus-raymarine-publisher-*.tar.gz root@<device-ip>:/data/
|
|
3. Extract: cd /data && tar -xzf dbus-raymarine-publisher-*.tar.gz
|
|
4. Install: bash /data/dbus-raymarine-publisher/install.sh
|
|
|
|
Requirements:
|
|
- Venus OS (or GLib + dbus-python for testing)
|
|
- Network access to Raymarine LightHouse multicast (VLAN interface)
|
|
- raymarine-nmea library
|
|
|
|
Testing without Venus OS:
|
|
The script will start but D-Bus services won't register without
|
|
velib_python. Use --dry-run to test the listener without D-Bus.
|
|
|
|
Author: Axiom NMEA Project
|
|
License: MIT
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
import os
|
|
import time
|
|
|
|
# Add script directory to path (raymarine_nmea is bundled alongside this script
|
|
# in deployed packages, or two levels up in the source tree)
|
|
_script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, _script_dir)
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(_script_dir)))
|
|
|
|
from raymarine_nmea import (
|
|
RaymarineDecoder,
|
|
SensorData,
|
|
MulticastListener,
|
|
NMEATcpServer,
|
|
TANK_CONFIG,
|
|
BATTERY_CONFIG,
|
|
)
|
|
from raymarine_nmea.venus_dbus import VenusPublisher
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.StreamHandler(sys.stdout),
|
|
]
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def parse_args():
|
|
"""Parse command line arguments."""
|
|
parser = argparse.ArgumentParser(
|
|
description='Publish Raymarine sensor data to Venus OS D-Bus',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__,
|
|
)
|
|
|
|
# Network settings
|
|
parser.add_argument(
|
|
'--interface', '-i',
|
|
default='198.18.5.5',
|
|
help='VLAN interface IP for Raymarine multicast (default: 198.18.5.5)',
|
|
)
|
|
|
|
# Service enable/disable
|
|
parser.add_argument(
|
|
'--no-gps',
|
|
action='store_true',
|
|
help='Disable GPS service',
|
|
)
|
|
parser.add_argument(
|
|
'--no-meteo',
|
|
action='store_true',
|
|
help='Disable Meteo (wind) service',
|
|
)
|
|
parser.add_argument(
|
|
'--no-tanks',
|
|
action='store_true',
|
|
help='Disable Tank services',
|
|
)
|
|
parser.add_argument(
|
|
'--no-batteries',
|
|
action='store_true',
|
|
help='Disable Battery services',
|
|
)
|
|
parser.add_argument(
|
|
'--no-navigation',
|
|
action='store_true',
|
|
help='Disable Navigation service (heading, depth, water temp)',
|
|
)
|
|
|
|
# Specific IDs
|
|
parser.add_argument(
|
|
'--tank-ids',
|
|
type=str,
|
|
help='Comma-separated tank IDs to publish (default: all configured)',
|
|
)
|
|
parser.add_argument(
|
|
'--battery-ids',
|
|
type=str,
|
|
help='Comma-separated battery IDs to publish (default: all configured)',
|
|
)
|
|
|
|
# Update interval
|
|
parser.add_argument(
|
|
'--update-interval',
|
|
type=int,
|
|
default=1000,
|
|
help='D-Bus update interval in milliseconds (default: 1000)',
|
|
)
|
|
|
|
# NMEA TCP server settings
|
|
parser.add_argument(
|
|
'--nmea-tcp-port',
|
|
type=int,
|
|
default=10110,
|
|
help='NMEA TCP server port (default: 10110)',
|
|
)
|
|
parser.add_argument(
|
|
'--no-nmea-tcp',
|
|
action='store_true',
|
|
help='Disable NMEA TCP server',
|
|
)
|
|
|
|
# Debugging
|
|
parser.add_argument(
|
|
'--debug',
|
|
action='store_true',
|
|
help='Enable debug logging',
|
|
)
|
|
parser.add_argument(
|
|
'--dry-run',
|
|
action='store_true',
|
|
help='Start listener but don\'t register D-Bus services',
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
args = parse_args()
|
|
|
|
# Configure logging level
|
|
if args.debug:
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
logging.getLogger('raymarine_nmea').setLevel(logging.DEBUG)
|
|
|
|
# Parse tank and battery IDs if specified
|
|
tank_ids = None
|
|
if args.tank_ids:
|
|
tank_ids = [int(x.strip()) for x in args.tank_ids.split(',')]
|
|
|
|
battery_ids = None
|
|
if args.battery_ids:
|
|
battery_ids = [int(x.strip()) for x in args.battery_ids.split(',')]
|
|
|
|
# Log configuration
|
|
logger.info("=" * 60)
|
|
logger.info("Venus OS D-Bus Publisher for Raymarine Sensor Data")
|
|
logger.info("=" * 60)
|
|
logger.info(f"Interface IP: {args.interface}")
|
|
logger.info(f"GPS enabled: {not args.no_gps}")
|
|
logger.info(f"Meteo enabled: {not args.no_meteo}")
|
|
logger.info(f"Navigation enabled: {not args.no_navigation}")
|
|
logger.info(f"Tanks enabled: {not args.no_tanks}")
|
|
if not args.no_tanks:
|
|
ids = tank_ids or list(TANK_CONFIG.keys())
|
|
logger.info(f" Tank IDs: {ids}")
|
|
logger.info(f"Batteries enabled: {not args.no_batteries}")
|
|
if not args.no_batteries:
|
|
ids = battery_ids or list(BATTERY_CONFIG.keys())
|
|
logger.info(f" Battery IDs: {ids}")
|
|
logger.info(f"Update interval: {args.update_interval}ms")
|
|
logger.info(f"NMEA TCP server enabled: {not args.no_nmea_tcp}")
|
|
if not args.no_nmea_tcp:
|
|
logger.info(f" NMEA TCP port: {args.nmea_tcp_port}")
|
|
logger.info("=" * 60)
|
|
|
|
# Create components
|
|
decoder = RaymarineDecoder()
|
|
sensor_data = SensorData()
|
|
|
|
# Callback to log decoded data in debug mode
|
|
def on_decode(decoded):
|
|
if args.debug and decoded.has_data():
|
|
logger.debug(f"Decoded: lat={decoded.latitude}, lon={decoded.longitude}, "
|
|
f"twd={decoded.twd_deg}, tanks={decoded.tanks}, "
|
|
f"batteries={decoded.batteries}")
|
|
|
|
# Start multicast listener
|
|
logger.info(f"Starting multicast listener on {args.interface}...")
|
|
listener = MulticastListener(
|
|
decoder=decoder,
|
|
sensor_data=sensor_data,
|
|
interface_ip=args.interface,
|
|
on_decode=on_decode if args.debug else None,
|
|
min_decode_interval=0.05,
|
|
)
|
|
listener.start()
|
|
logger.info("Multicast listener started (20Hz decode rate)")
|
|
|
|
# Create NMEA TCP server (broadcasts all NMEA sentences, more than D-Bus)
|
|
nmea_tcp_server = None
|
|
if not args.no_nmea_tcp:
|
|
nmea_tcp_server = NMEATcpServer(
|
|
sensor_data=sensor_data,
|
|
port=args.nmea_tcp_port,
|
|
)
|
|
if nmea_tcp_server.start():
|
|
logger.info(f"NMEA TCP server started on port {args.nmea_tcp_port}")
|
|
else:
|
|
logger.warning("Failed to start NMEA TCP server, continuing without it")
|
|
nmea_tcp_server = None
|
|
|
|
# Dry run mode - just listen and print data
|
|
if args.dry_run:
|
|
logger.info("Dry run mode - press Ctrl+C to stop")
|
|
try:
|
|
while True:
|
|
# Broadcast NMEA sentences if TCP server is running
|
|
if nmea_tcp_server:
|
|
nmea_tcp_server.broadcast()
|
|
|
|
time.sleep(args.update_interval / 1000.0)
|
|
data = sensor_data.to_dict()
|
|
tcp_clients = nmea_tcp_server.client_count if nmea_tcp_server else 0
|
|
logger.info(f"Position: {data['position']}")
|
|
logger.info(f"Wind: {data['wind']}")
|
|
logger.info(f"Tanks: {data['tanks']}")
|
|
logger.info(f"Batteries: {data['batteries']}")
|
|
logger.info(f"NMEA TCP clients: {tcp_clients}")
|
|
logger.info("-" * 40)
|
|
except KeyboardInterrupt:
|
|
logger.info("Stopping...")
|
|
if nmea_tcp_server:
|
|
nmea_tcp_server.stop()
|
|
listener.stop()
|
|
return
|
|
|
|
# Create Venus publisher
|
|
logger.info("Creating Venus OS D-Bus publisher...")
|
|
publisher = VenusPublisher(
|
|
sensor_data=sensor_data,
|
|
enable_gps=not args.no_gps,
|
|
enable_meteo=not args.no_meteo,
|
|
enable_navigation=not args.no_navigation,
|
|
enable_tanks=not args.no_tanks,
|
|
enable_batteries=not args.no_batteries,
|
|
tank_ids=tank_ids,
|
|
battery_ids=battery_ids,
|
|
update_interval_ms=args.update_interval,
|
|
)
|
|
|
|
# Log service status
|
|
for service in publisher.services:
|
|
logger.info(f" Service: {service.service_name}")
|
|
|
|
# Run publisher with integrated NMEA TCP broadcasting
|
|
try:
|
|
logger.info("Starting D-Bus publisher...")
|
|
_run_with_nmea_tcp(publisher, nmea_tcp_server, args.update_interval)
|
|
except RuntimeError as e:
|
|
logger.error(f"Failed to start publisher: {e}")
|
|
logger.info("Falling back to NMEA TCP only mode (no D-Bus)")
|
|
|
|
# Fall back to just NMEA TCP broadcasting
|
|
try:
|
|
while True:
|
|
if nmea_tcp_server:
|
|
nmea_tcp_server.broadcast()
|
|
time.sleep(args.update_interval / 1000.0)
|
|
data = sensor_data.to_dict()
|
|
tcp_clients = nmea_tcp_server.client_count if nmea_tcp_server else 0
|
|
logger.info(f"GPS: {data['position']}")
|
|
logger.info(f"Wind: {data['wind']}")
|
|
logger.info(f"Stats: {data['stats']}")
|
|
logger.info(f"NMEA TCP clients: {tcp_clients}")
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
logger.info("Stopping services...")
|
|
if nmea_tcp_server:
|
|
nmea_tcp_server.stop()
|
|
listener.stop()
|
|
logger.info("Shutdown complete")
|
|
|
|
|
|
def _run_with_nmea_tcp(publisher: VenusPublisher, nmea_tcp_server, update_interval_ms: int):
|
|
"""Run the Venus publisher with integrated NMEA TCP broadcasting.
|
|
|
|
This combines D-Bus publishing with NMEA TCP broadcasting in the same
|
|
GLib main loop, ensuring both are updated at the same interval.
|
|
|
|
Args:
|
|
publisher: VenusPublisher instance
|
|
nmea_tcp_server: NMEATcpServer instance (or None to skip)
|
|
update_interval_ms: Update interval in milliseconds
|
|
"""
|
|
import signal
|
|
|
|
# Try to import GLib
|
|
try:
|
|
from gi.repository import GLib
|
|
except ImportError:
|
|
raise RuntimeError(
|
|
"GLib is required to run VenusPublisher. "
|
|
"Install PyGObject or use --dry-run mode."
|
|
)
|
|
|
|
# Set up D-Bus main loop
|
|
try:
|
|
from dbus.mainloop.glib import DBusGMainLoop
|
|
DBusGMainLoop(set_as_default=True)
|
|
except ImportError:
|
|
raise RuntimeError(
|
|
"dbus-python with GLib support is required. "
|
|
"Install python3-dbus on Venus OS."
|
|
)
|
|
|
|
# Start D-Bus services
|
|
if not publisher.start():
|
|
logger.error("Failed to start VenusPublisher")
|
|
return
|
|
|
|
# Main loop reference for signal handler
|
|
mainloop = None
|
|
|
|
# Set up signal handlers for graceful shutdown
|
|
def signal_handler(signum, frame):
|
|
logger.info(f"Received signal {signum}, stopping...")
|
|
if mainloop:
|
|
mainloop.quit()
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
# Combined update callback: D-Bus + NMEA TCP
|
|
def update_callback():
|
|
# Update D-Bus services
|
|
if not publisher.update():
|
|
return False
|
|
|
|
# Broadcast NMEA sentences to TCP clients
|
|
if nmea_tcp_server:
|
|
nmea_tcp_server.broadcast()
|
|
|
|
return True
|
|
|
|
# Set up periodic updates
|
|
GLib.timeout_add(update_interval_ms, update_callback)
|
|
|
|
# Run main loop
|
|
logger.info("Publisher running, press Ctrl+C to stop")
|
|
mainloop = GLib.MainLoop()
|
|
|
|
try:
|
|
mainloop.run()
|
|
except Exception as e:
|
|
logger.error(f"Main loop error: {e}")
|
|
finally:
|
|
publisher.stop()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|