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
291 lines
8.7 KiB
Python
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
|
|
}
|