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:
@@ -24,7 +24,7 @@ set -e
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
VERSION="1.1.0"
|
VERSION="2.1.0"
|
||||||
OUTPUT_DIR="$SCRIPT_DIR"
|
OUTPUT_DIR="$SCRIPT_DIR"
|
||||||
PACKAGE_NAME="dbus-raymarine-publisher"
|
PACKAGE_NAME="dbus-raymarine-publisher"
|
||||||
|
|
||||||
|
|||||||
@@ -255,9 +255,10 @@ def main():
|
|||||||
sensor_data=sensor_data,
|
sensor_data=sensor_data,
|
||||||
interface_ip=args.interface,
|
interface_ip=args.interface,
|
||||||
on_decode=on_decode if args.debug else None,
|
on_decode=on_decode if args.debug else None,
|
||||||
|
min_decode_interval=0.05,
|
||||||
)
|
)
|
||||||
listener.start()
|
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)
|
# Create NMEA TCP server (broadcasts all NMEA sentences, more than D-Bus)
|
||||||
nmea_tcp_server = None
|
nmea_tcp_server = None
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
VERSION="2.1.0"
|
VERSION="2.2.0"
|
||||||
OUTPUT_DIR="$SCRIPT_DIR"
|
OUTPUT_DIR="$SCRIPT_DIR"
|
||||||
PACKAGE_NAME="dbus-anchor-alarm"
|
PACKAGE_NAME="dbus-anchor-alarm"
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ FIRMWARE_VERSION = 0
|
|||||||
CONNECTED = 1
|
CONNECTED = 1
|
||||||
|
|
||||||
# Version
|
# Version
|
||||||
VERSION = '2.1.0'
|
VERSION = '2.2.0'
|
||||||
|
|
||||||
# Timing
|
# Timing
|
||||||
MAIN_LOOP_INTERVAL_MS = 1000
|
MAIN_LOOP_INTERVAL_MS = 1000
|
||||||
|
|||||||
@@ -53,13 +53,27 @@ def _unwrap(v):
|
|||||||
|
|
||||||
|
|
||||||
class SensorReader:
|
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):
|
def __init__(self, bus):
|
||||||
self._bus = bus
|
self._bus = bus
|
||||||
self._gps_available = False
|
self._gps_available = False
|
||||||
self._proxy_cache = {}
|
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):
|
def _get_proxy(self, service_name, path):
|
||||||
"""Return a cached D-Bus proxy, creating it only once per (service, path)."""
|
"""Return a cached D-Bus proxy, creating it only once per (service, path)."""
|
||||||
key = (service_name, path)
|
key = (service_name, path)
|
||||||
@@ -80,6 +94,93 @@ class SensorReader:
|
|||||||
self._proxy_cache.pop((service_name, path), None)
|
self._proxy_cache.pop((service_name, path), None)
|
||||||
logger.debug('D-Bus read failed: %s %s -- %s', service_name, path, e)
|
logger.debug('D-Bus read failed: %s %s -- %s', service_name, path, e)
|
||||||
return None
|
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
|
@property
|
||||||
def connected(self):
|
def connected(self):
|
||||||
@@ -87,45 +188,20 @@ class SensorReader:
|
|||||||
return self._gps_available
|
return self._gps_available
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
"""Read all sensors and return a SensorSnapshot.
|
"""Return a SensorSnapshot from cached signal values.
|
||||||
|
|
||||||
Each field is None if the corresponding D-Bus read fails.
|
No D-Bus GetValue() calls - all values updated via signals.
|
||||||
Speed is converted from m/s to knots; depth from meters to feet.
|
|
||||||
"""
|
"""
|
||||||
lat = self._read_dbus_value(GPS_SERVICE, '/Position/Latitude')
|
speed = float(self._speed_ms) * MS_TO_KNOTS if self._speed_ms is not None else None
|
||||||
lon = self._read_dbus_value(GPS_SERVICE, '/Position/Longitude')
|
course = float(self._course) if self._course is not None else None
|
||||||
fix = self._read_dbus_value(GPS_SERVICE, '/Fix')
|
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
|
||||||
self._gps_available = (
|
wind_speed = float(self._wind_speed) if self._wind_speed is not None else None
|
||||||
lat is not None and lon is not None
|
wind_direction = float(self._wind_direction) if self._wind_direction is not None else 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)
|
|
||||||
|
|
||||||
return SensorSnapshot(
|
return SensorSnapshot(
|
||||||
latitude=float(lat) if lat is not None else None,
|
latitude=float(self._latitude) if self._latitude is not None else None,
|
||||||
longitude=float(lon) if lon is not None else None,
|
longitude=float(self._longitude) if self._longitude is not None else None,
|
||||||
speed=speed,
|
speed=speed,
|
||||||
course=course,
|
course=course,
|
||||||
heading=heading,
|
heading=heading,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ set -e
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
VERSION="2.1.0"
|
VERSION="2.2.0"
|
||||||
OUTPUT_DIR="$SCRIPT_DIR"
|
OUTPUT_DIR="$SCRIPT_DIR"
|
||||||
PACKAGE_NAME="dbus-generator-ramp"
|
PACKAGE_NAME="dbus-generator-ramp"
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ from ramp_controller import RampController
|
|||||||
|
|
||||||
|
|
||||||
# Version
|
# Version
|
||||||
VERSION = '2.1.0'
|
VERSION = '2.2.0'
|
||||||
|
|
||||||
# D-Bus service name for our addon
|
# D-Bus service name for our addon
|
||||||
SERVICE_NAME = 'com.victronenergy.generatorramp'
|
SERVICE_NAME = 'com.victronenergy.generatorramp'
|
||||||
@@ -122,6 +122,9 @@ class GeneratorRampController:
|
|||||||
self._last_run_energy_wh = 0.0
|
self._last_run_energy_wh = 0.0
|
||||||
self._energy_last_time = None
|
self._energy_last_time = None
|
||||||
|
|
||||||
|
# Signal subscription tracking
|
||||||
|
self._power_subscriptions_active = False
|
||||||
|
|
||||||
# D-Bus connection
|
# D-Bus connection
|
||||||
self.bus = dbus.SystemBus()
|
self.bus = dbus.SystemBus()
|
||||||
|
|
||||||
@@ -595,9 +598,110 @@ class GeneratorRampController:
|
|||||||
self._read_ac_state()
|
self._read_ac_state()
|
||||||
self._read_current_limit()
|
self._read_current_limit()
|
||||||
|
|
||||||
|
# Set up signal subscriptions
|
||||||
|
self._setup_signal_subscriptions()
|
||||||
|
|
||||||
except dbus.exceptions.DBusException as e:
|
except dbus.exceptions.DBusException as e:
|
||||||
self.logger.error(f"D-Bus initialization failed: {e}")
|
self.logger.error(f"D-Bus initialization failed: {e}")
|
||||||
raise
|
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):
|
def _get_proxy(self, service, path):
|
||||||
"""Return a cached D-Bus proxy, creating it only once per (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:
|
if not self.enabled:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Read current states from D-Bus
|
# Conditional D-Bus reading based on state
|
||||||
self._read_generator_state()
|
if self.state == self.STATE_IDLE:
|
||||||
self._read_ac_state()
|
self._read_generator_state()
|
||||||
self._read_power()
|
self._unsubscribe_power_readings()
|
||||||
self._read_output_power()
|
else:
|
||||||
self._read_current_limit()
|
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
|
# Accumulate energy while generator is providing AC power
|
||||||
if self.ac_connected and self.generator_state in [
|
if self.ac_connected and self.generator_state in [
|
||||||
|
|||||||
Reference in New Issue
Block a user