Files
venus/axiom-nmea/examples/dbus-raymarine-publisher/venus_publisher.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

426 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,
)
listener.start()
logger.info("Multicast listener started")
# 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()