cpu savings. reverted to faster polling for raymarine nmea decoding. using Signal Subscriptions vs polling to reduce load on dbus service.

This commit is contained in:
2026-03-26 18:37:57 +00:00
parent 6028fd37e0
commit 6a504cdbc1
7 changed files with 237 additions and 51 deletions

View File

@@ -24,7 +24,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
VERSION="1.1.0"
VERSION="2.1.0"
OUTPUT_DIR="$SCRIPT_DIR"
PACKAGE_NAME="dbus-raymarine-publisher"

View File

@@ -255,9 +255,10 @@ def main():
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")
logger.info("Multicast listener started (20Hz decode rate)")
# Create NMEA TCP server (broadcasts all NMEA sentences, more than D-Bus)
nmea_tcp_server = None

View File

@@ -11,7 +11,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="2.1.0"
VERSION="2.2.0"
OUTPUT_DIR="$SCRIPT_DIR"
PACKAGE_NAME="dbus-anchor-alarm"

View File

@@ -11,7 +11,7 @@ FIRMWARE_VERSION = 0
CONNECTED = 1
# Version
VERSION = '2.1.0'
VERSION = '2.2.0'
# Timing
MAIN_LOOP_INTERVAL_MS = 1000

View File

@@ -53,13 +53,27 @@ def _unwrap(v):
class SensorReader:
"""Reads navigation sensor data from Venus OS D-Bus services."""
"""Reads navigation sensor data from Venus OS D-Bus services using signal subscriptions."""
def __init__(self, bus):
self._bus = bus
self._gps_available = False
self._proxy_cache = {}
self._latitude = None
self._longitude = None
self._fix = None
self._speed_ms = None
self._course = None
self._heading = None
self._depth_m = None
self._wind_speed = None
self._wind_direction = None
self._last_update = time.time()
self._poll_initial_values()
self._setup_signal_subscriptions()
def _get_proxy(self, service_name, path):
"""Return a cached D-Bus proxy, creating it only once per (service, path)."""
key = (service_name, path)
@@ -80,6 +94,93 @@ class SensorReader:
self._proxy_cache.pop((service_name, path), None)
logger.debug('D-Bus read failed: %s %s -- %s', service_name, path, e)
return None
def _poll_initial_values(self):
"""Poll initial values once at startup."""
self._latitude = self._read_dbus_value(GPS_SERVICE, '/Position/Latitude')
self._longitude = self._read_dbus_value(GPS_SERVICE, '/Position/Longitude')
self._fix = self._read_dbus_value(GPS_SERVICE, '/Fix')
self._speed_ms = self._read_dbus_value(GPS_SERVICE, '/Speed')
self._course = self._read_dbus_value(GPS_SERVICE, '/Course')
self._heading = self._read_dbus_value(NAVIGATION_SERVICE, '/Heading')
self._depth_m = self._read_dbus_value(NAVIGATION_SERVICE, '/Depth')
self._wind_speed = self._read_dbus_value(METEO_SERVICE, '/WindSpeed')
self._wind_direction = self._read_dbus_value(METEO_SERVICE, '/WindDirection')
self._update_gps_available()
def _setup_signal_subscriptions(self):
"""Subscribe to all sensor value changes."""
subscriptions = [
(GPS_SERVICE, '/Position/Latitude', '_on_latitude_changed'),
(GPS_SERVICE, '/Position/Longitude', '_on_longitude_changed'),
(GPS_SERVICE, '/Fix', '_on_fix_changed'),
(GPS_SERVICE, '/Speed', '_on_speed_changed'),
(GPS_SERVICE, '/Course', '_on_course_changed'),
(NAVIGATION_SERVICE, '/Heading', '_on_heading_changed'),
(NAVIGATION_SERVICE, '/Depth', '_on_depth_changed'),
(METEO_SERVICE, '/WindSpeed', '_on_wind_speed_changed'),
(METEO_SERVICE, '/WindDirection', '_on_wind_direction_changed'),
]
for service, path, handler_name in subscriptions:
try:
self._bus.add_signal_receiver(
getattr(self, handler_name),
signal_name='PropertiesChanged',
dbus_interface='com.victronenergy.BusItem',
bus_name=service,
path=path
)
except dbus.exceptions.DBusException as e:
logger.debug(f'Failed to subscribe to {service}{path}: {e}')
def _on_latitude_changed(self, changes):
if 'Value' in changes:
self._latitude = _unwrap(changes['Value'])
self._last_update = time.time()
def _on_longitude_changed(self, changes):
if 'Value' in changes:
self._longitude = _unwrap(changes['Value'])
self._last_update = time.time()
def _on_fix_changed(self, changes):
if 'Value' in changes:
self._fix = _unwrap(changes['Value'])
self._update_gps_available()
def _on_speed_changed(self, changes):
if 'Value' in changes:
self._speed_ms = _unwrap(changes['Value'])
def _on_course_changed(self, changes):
if 'Value' in changes:
self._course = _unwrap(changes['Value'])
def _on_heading_changed(self, changes):
if 'Value' in changes:
self._heading = _unwrap(changes['Value'])
def _on_depth_changed(self, changes):
if 'Value' in changes:
self._depth_m = _unwrap(changes['Value'])
def _on_wind_speed_changed(self, changes):
if 'Value' in changes:
self._wind_speed = _unwrap(changes['Value'])
def _on_wind_direction_changed(self, changes):
if 'Value' in changes:
self._wind_direction = _unwrap(changes['Value'])
def _update_gps_available(self):
"""Update GPS availability status."""
self._gps_available = (
self._latitude is not None and
self._longitude is not None and
self._fix is not None and
int(self._fix) >= 1
)
@property
def connected(self):
@@ -87,45 +188,20 @@ class SensorReader:
return self._gps_available
def read(self):
"""Read all sensors and return a SensorSnapshot.
Each field is None if the corresponding D-Bus read fails.
Speed is converted from m/s to knots; depth from meters to feet.
"""Return a SensorSnapshot from cached signal values.
No D-Bus GetValue() calls - all values updated via signals.
"""
lat = self._read_dbus_value(GPS_SERVICE, '/Position/Latitude')
lon = self._read_dbus_value(GPS_SERVICE, '/Position/Longitude')
fix = self._read_dbus_value(GPS_SERVICE, '/Fix')
self._gps_available = (
lat is not None and lon is not None
and fix is not None and int(fix) >= 1
)
speed_ms = self._read_dbus_value(GPS_SERVICE, '/Speed')
speed = float(speed_ms) * MS_TO_KNOTS if speed_ms is not None else None
course = self._read_dbus_value(GPS_SERVICE, '/Course')
if course is not None:
course = float(course)
heading = self._read_dbus_value(NAVIGATION_SERVICE, '/Heading')
if heading is not None:
heading = float(heading)
depth_m = self._read_dbus_value(NAVIGATION_SERVICE, '/Depth')
depth = float(depth_m) * METERS_TO_FEET if depth_m is not None else None
wind_speed = self._read_dbus_value(METEO_SERVICE, '/WindSpeed')
if wind_speed is not None:
wind_speed = float(wind_speed)
wind_direction = self._read_dbus_value(METEO_SERVICE, '/WindDirection')
if wind_direction is not None:
wind_direction = float(wind_direction)
speed = float(self._speed_ms) * MS_TO_KNOTS if self._speed_ms is not None else None
course = float(self._course) if self._course is not None else None
heading = float(self._heading) if self._heading is not None else None
depth = float(self._depth_m) * METERS_TO_FEET if self._depth_m is not None else None
wind_speed = float(self._wind_speed) if self._wind_speed is not None else None
wind_direction = float(self._wind_direction) if self._wind_direction is not None else None
return SensorSnapshot(
latitude=float(lat) if lat is not None else None,
longitude=float(lon) if lon is not None else None,
latitude=float(self._latitude) if self._latitude is not None else None,
longitude=float(self._longitude) if self._longitude is not None else None,
speed=speed,
course=course,
heading=heading,

View File

@@ -29,7 +29,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default values
VERSION="2.1.0"
VERSION="2.2.0"
OUTPUT_DIR="$SCRIPT_DIR"
PACKAGE_NAME="dbus-generator-ramp"

View File

@@ -55,7 +55,7 @@ from ramp_controller import RampController
# Version
VERSION = '2.1.0'
VERSION = '2.2.0'
# D-Bus service name for our addon
SERVICE_NAME = 'com.victronenergy.generatorramp'
@@ -122,6 +122,9 @@ class GeneratorRampController:
self._last_run_energy_wh = 0.0
self._energy_last_time = None
# Signal subscription tracking
self._power_subscriptions_active = False
# D-Bus connection
self.bus = dbus.SystemBus()
@@ -595,9 +598,110 @@ class GeneratorRampController:
self._read_ac_state()
self._read_current_limit()
# Set up signal subscriptions
self._setup_signal_subscriptions()
except dbus.exceptions.DBusException as e:
self.logger.error(f"D-Bus initialization failed: {e}")
raise
def _setup_signal_subscriptions(self):
"""Subscribe to D-Bus property changes instead of polling."""
try:
self.bus.add_signal_receiver(
self._on_generator_state_changed,
signal_name='PropertiesChanged',
dbus_interface='com.victronenergy.BusItem',
bus_name=self.generator_service,
path='/State'
)
self.logger.debug(f"Subscribed to generator state changes")
self.bus.add_signal_receiver(
self._on_ac_state_changed,
signal_name='PropertiesChanged',
dbus_interface='com.victronenergy.BusItem',
bus_name=self.vebus_service,
path='/Ac/ActiveIn/Connected'
)
self.logger.debug(f"Subscribed to AC connection changes")
except dbus.exceptions.DBusException as e:
self.logger.warning(f"Failed to set up signal subscriptions: {e}")
def _subscribe_power_readings(self):
"""Subscribe to power readings when generator is running."""
if self._power_subscriptions_active:
return
paths = [
'/Ac/ActiveIn/L1/P',
'/Ac/ActiveIn/L2/P',
'/Ac/Out/L1/P',
'/Ac/Out/L2/P',
'/Ac/In/1/CurrentLimit',
]
try:
for path in paths:
self.bus.add_signal_receiver(
lambda changes, p=path: self._on_power_changed(p, changes),
signal_name='PropertiesChanged',
dbus_interface='com.victronenergy.BusItem',
bus_name=self.vebus_service,
path=path
)
self._power_subscriptions_active = True
self.logger.debug("Subscribed to power readings")
except dbus.exceptions.DBusException as e:
self.logger.warning(f"Failed to subscribe to power readings: {e}")
def _unsubscribe_power_readings(self):
"""Mark power readings as unsubscribed (signals ignored via flag)."""
if self._power_subscriptions_active:
self._power_subscriptions_active = False
self.logger.debug("Power reading signals now ignored")
def _on_generator_state_changed(self, changes):
"""Handle generator state change signal."""
if 'Value' in changes:
self.generator_state = int(changes['Value'])
self.dbus_service['/Generator/State'] = self.generator_state
self.logger.debug(f"Generator state changed: {self.generator_state}")
def _on_ac_state_changed(self, changes):
"""Handle AC connection state change signal."""
if 'Value' in changes:
self.ac_connected = bool(changes['Value'])
self.dbus_service['/AcInput/Connected'] = 1 if self.ac_connected else 0
self.logger.debug(f"AC connected: {self.ac_connected}")
def _on_power_changed(self, path, changes):
"""Handle power reading change signal."""
if not self._power_subscriptions_active:
return
if 'Value' in changes:
value = float(changes['Value'])
if path == '/Ac/ActiveIn/L1/P':
self.current_l1_power = value
self.dbus_service['/Power/L1'] = value
elif path == '/Ac/ActiveIn/L2/P':
self.current_l2_power = value
self.dbus_service['/Power/L2'] = value
self.dbus_service['/Power/Total'] = self.current_l1_power + self.current_l2_power
elif path == '/Ac/Out/L1/P':
self.output_l1_power = value
self.dbus_service['/OutputPower/L1'] = value
elif path == '/Ac/Out/L2/P':
self.output_l2_power = value
self.dbus_service['/OutputPower/L2'] = value
total = self.output_l1_power + self.output_l2_power
self.dbus_service['/OutputPower/Total'] = total
self.ramp_controller.set_output_power(total)
elif path == '/Ac/In/1/CurrentLimit':
self.current_limit_setting = value
self.dbus_service['/CurrentLimit'] = value
def _get_proxy(self, service, path):
"""Return a cached D-Bus proxy, creating it only once per (service, path)."""
@@ -752,12 +856,17 @@ class GeneratorRampController:
if not self.enabled:
return True
# Read current states from D-Bus
self._read_generator_state()
self._read_ac_state()
self._read_power()
self._read_output_power()
self._read_current_limit()
# Conditional D-Bus reading based on state
if self.state == self.STATE_IDLE:
self._read_generator_state()
self._unsubscribe_power_readings()
else:
self._subscribe_power_readings()
if self.state == self.STATE_WARMUP and (now - self.state_enter_time) < 0.1:
self._read_ac_state()
self._read_power()
self._read_output_power()
self._read_current_limit()
# Accumulate energy while generator is providing AC power
if self.ac_connected and self.generator_state in [