Files
venus/axiom-nmea/raymarine_nmea/venus_dbus/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

291 lines
8.7 KiB
Python

"""
Venus OS D-Bus Publisher.
This module provides the main VenusPublisher class that coordinates
all D-Bus services for publishing Raymarine sensor data to Venus OS.
"""
import logging
import signal
import sys
from typing import List, Optional, Set
from ..data.store import SensorData
from ..sensors import TANK_CONFIG, BATTERY_CONFIG
from .gps import GpsService
from .meteo import MeteoService
from .navigation import NavigationService
from .tank import TankService, create_tank_services
from .battery import BatteryService, create_battery_services
from .service import VeDbusServiceBase
logger = logging.getLogger(__name__)
# Try to import GLib for the main loop
try:
from gi.repository import GLib
HAS_GLIB = True
except ImportError:
HAS_GLIB = False
logger.warning("GLib not available. VenusPublisher.run() will not work.")
class VenusPublisher:
"""Coordinator for all Venus OS D-Bus services.
This class manages the lifecycle of GPS, Meteo, Navigation, Tank,
and Battery D-Bus services, handling registration, updates, and cleanup.
Example:
from raymarine_nmea import SensorData, RaymarineDecoder, MulticastListener
from raymarine_nmea.venus_dbus import VenusPublisher
# Create sensor data store
sensor_data = SensorData()
decoder = RaymarineDecoder()
# Start multicast listener
listener = MulticastListener(
decoder=decoder,
sensor_data=sensor_data,
interface_ip="198.18.5.5",
)
listener.start()
# Start Venus OS publisher
publisher = VenusPublisher(sensor_data)
publisher.run() # Blocks until stopped
For more control over the main loop:
publisher = VenusPublisher(sensor_data)
publisher.start() # Non-blocking, registers services
# Your own main loop here
# Call publisher.update() periodically
publisher.stop() # Cleanup
"""
# Default update interval in milliseconds
DEFAULT_UPDATE_INTERVAL_MS = 1000
def __init__(
self,
sensor_data: SensorData,
enable_gps: bool = True,
enable_meteo: bool = True,
enable_navigation: bool = True,
enable_tanks: bool = True,
enable_batteries: bool = True,
tank_ids: Optional[List[int]] = None,
battery_ids: Optional[List[int]] = None,
update_interval_ms: int = DEFAULT_UPDATE_INTERVAL_MS,
):
"""Initialize Venus Publisher.
Args:
sensor_data: SensorData instance to read values from
enable_gps: Enable GPS service (default: True)
enable_meteo: Enable Meteo/wind service (default: True)
enable_navigation: Enable Navigation service (default: True)
enable_tanks: Enable Tank services (default: True)
enable_batteries: Enable Battery services (default: True)
tank_ids: Specific tank IDs to publish (default: all configured)
battery_ids: Specific battery IDs to publish (default: all configured)
update_interval_ms: Update interval in milliseconds (default: 1000)
"""
self._sensor_data = sensor_data
self._update_interval_ms = update_interval_ms
self._services: List[VeDbusServiceBase] = []
self._running = False
self._mainloop = None
self._timer_id = None
# Create enabled services
if enable_gps:
self._services.append(GpsService(sensor_data))
if enable_meteo:
self._services.append(MeteoService(sensor_data))
if enable_navigation:
self._services.append(NavigationService(sensor_data))
if enable_tanks:
self._services.extend(
create_tank_services(sensor_data, tank_ids)
)
if enable_batteries:
self._services.extend(
create_battery_services(sensor_data, battery_ids)
)
logger.info(f"VenusPublisher initialized with {len(self._services)} services")
def start(self) -> bool:
"""Register all D-Bus services.
Returns:
True if at least one service registered successfully
"""
if self._running:
logger.warning("VenusPublisher already running")
return True
registered = 0
for service in self._services:
if service.register():
registered += 1
else:
logger.warning(f"Failed to register {service.service_name}")
self._running = registered > 0
if self._running:
logger.info(f"VenusPublisher started: {registered}/{len(self._services)} services registered")
else:
logger.error("VenusPublisher failed to start: no services registered")
return self._running
def stop(self) -> None:
"""Stop and unregister all D-Bus services."""
if not self._running:
return
# Stop timer if running in GLib main loop
if self._timer_id is not None and HAS_GLIB:
GLib.source_remove(self._timer_id)
self._timer_id = None
# Quit main loop if running
if self._mainloop is not None:
self._mainloop.quit()
self._mainloop = None
# Unregister all services
for service in self._services:
service.unregister()
self._running = False
logger.info("VenusPublisher stopped")
def update(self) -> bool:
"""Update all D-Bus services.
Call this periodically to refresh values.
Returns:
True to continue updates, False if all services failed
"""
if not self._running:
return False
success = 0
for service in self._services:
if service.update():
success += 1
return success > 0
def run(self) -> None:
"""Run the publisher with a GLib main loop.
This method blocks until the publisher is stopped via stop()
or a SIGINT/SIGTERM signal is received.
Raises:
RuntimeError: If GLib is not available
"""
if not HAS_GLIB:
raise RuntimeError(
"GLib is required to run VenusPublisher. "
"Install PyGObject or use start()/update()/stop() manually."
)
# 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 services
if not self.start():
logger.error("Failed to start VenusPublisher")
return
# Set up signal handlers for graceful shutdown
def signal_handler(signum, frame):
logger.info(f"Received signal {signum}, stopping...")
self.stop()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Set up periodic updates
def update_callback():
if not self._running:
return False
return self.update()
self._timer_id = GLib.timeout_add(
self._update_interval_ms,
update_callback
)
# Run main loop
logger.info("VenusPublisher running, press Ctrl+C to stop")
self._mainloop = GLib.MainLoop()
try:
self._mainloop.run()
except Exception as e:
logger.error(f"Main loop error: {e}")
finally:
self.stop()
@property
def services(self) -> List[VeDbusServiceBase]:
"""Get list of all managed services."""
return self._services.copy()
@property
def is_running(self) -> bool:
"""Check if publisher is running."""
return self._running
def add_service(self, service: VeDbusServiceBase) -> bool:
"""Add a custom service to the publisher.
Args:
service: Service instance to add
Returns:
True if added (and registered if already running)
"""
self._services.append(service)
if self._running:
return service.register()
return True
def get_service_status(self) -> dict:
"""Get status of all services.
Returns:
Dict with service names and their registration status
"""
return {
service.service_name: {
'registered': service._registered,
'type': service.service_type,
'product': service.product_name,
}
for service in self._services
}