Files
venus/dbus-anchor-alarm/anchor_alarm.py
2026-03-26 14:15:02 +00:00

753 lines
29 KiB
Python
Executable File

#!/usr/bin/env python3
"""
dbus-anchor-alarm -- Anchor Watch & Drag Detection for Venus OS
v2: Memory-safe, SD-safe, calibrated.
- Zero SD writes in normal operation (in-memory TrackBuffer)
- Optional DebugLogger (SQLite, default off)
- Decimated tracker (1/15s slow path), 1 Hz fast path for sensors/drag
- GPS-to-bow offset geometry
- Anchor state persisted to localsettings
- RSS memory watchdog
"""
import json
import logging
import math
import os
import signal
import sys
import time
from collections import deque
sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'ext', 'velib_python'))
sys.path.insert(1, '/opt/victronenergy/velib_python')
try:
from gi.repository import GLib
except ImportError:
print("ERROR: GLib not available. This script must run on Venus OS.")
sys.exit(1)
try:
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from vedbus import VeDbusService
from ve_utils import exit_on_error
from settingsdevice import SettingsDevice
except ImportError as e:
print("ERROR: Required module not available: %s" % e)
print("This script must run on Venus OS.")
sys.exit(1)
from config import (
SERVICE_NAME,
DEVICE_INSTANCE,
PRODUCT_NAME,
PRODUCT_ID,
FIRMWARE_VERSION,
CONNECTED,
MAIN_LOOP_INTERVAL_MS,
LOG_LEVEL,
VERSION,
CHAIN_WEIGHT_LB_PER_FT,
ANCHOR_WEIGHT_LB,
VESSEL_WEIGHT_LB,
FREEBOARD_HEIGHT_FT,
WINDAGE_AREA_SQFT,
DRAG_COEFFICIENT,
DEFAULT_ALARM_RADIUS_FT,
GPS_TO_BOW_FT,
)
from sensor_reader import SensorReader
from catenary import (
wind_force_lbs,
catenary_distance,
scope_ratio,
recommended_chain_length,
dynamic_load,
)
from anchor_tracker import AnchorTracker
from drag_detector import DragDetector
from track_buffer import TrackBuffer
from debug_logger import DebugLogger
logger = logging.getLogger('dbus-anchor-alarm')
_PEAK_LOAD_WINDOW_SEC = 60.0
_JSON_UPDATE_INTERVAL_SEC = 5.0
_SLOW_TICK_DIVISOR = 15
_WATCHDOG_INTERVAL_SEC = 60.0
_WATCHDOG_WARN_MB = 25
_WATCHDOG_KILL_MB = 40
def _bearing_between(lat1, lon1, lat2, lon2):
"""Bearing from point 1 to point 2 in degrees (0-360)."""
rlat1 = math.radians(lat1)
rlat2 = math.radians(lat2)
dlon = math.radians(lon2 - lon1)
x = math.sin(dlon) * math.cos(rlat2)
y = (math.cos(rlat1) * math.sin(rlat2)
- math.sin(rlat1) * math.cos(rlat2) * math.cos(dlon))
return math.degrees(math.atan2(x, y)) % 360.0
def _signed_angle_diff(a, b):
"""Signed angle from a to b in degrees, normalised to -180..180."""
d = (b - a) % 360.0
return d if d <= 180.0 else d - 360.0
def _get_rss_mb():
"""Read VmRSS from /proc/self/status, return MB or None."""
try:
with open('/proc/self/status', 'r') as f:
for line in f:
if line.startswith('VmRSS:'):
return int(line.split()[1]) / 1024.0
except (OSError, ValueError, IndexError):
pass
return None
class AnchorAlarmService:
"""Main anchor alarm service with full D-Bus path publishing."""
def __init__(self):
self._running = False
self._dbusservice = None
self._settings = None
self._reader = None
self._tracker = None
self._detector = None
self._track_buffer = None
self._debug_logger = None
self._tick_count = 0
self._peak_load = 0.0
self._peak_load_history = deque()
self._last_json_update = 0.0
self._last_watchdog = 0.0
self._prev_heading = None
self._prev_heading_delta = None
self._last_persisted = (None, None, None, None, None)
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
def start(self):
"""Initialize D-Bus service and start the main loop."""
bus = dbus.SystemBus()
max_retries = 5
retry_delay = 1.0
for attempt in range(max_retries):
try:
self._dbusservice = VeDbusService(
SERVICE_NAME, bus=bus, register=False)
break
except dbus.exceptions.NameExistsException:
if attempt < max_retries - 1:
logger.warning(
"D-Bus name exists, retrying in %.1fs (%d/%d)",
retry_delay, attempt + 1, max_retries)
time.sleep(retry_delay)
retry_delay *= 2
else:
raise
svc = self._dbusservice
cb = self._on_path_changed
# Mandatory management paths
svc.add_path('/Mgmt/ProcessName', __file__)
svc.add_path('/Mgmt/ProcessVersion', VERSION)
svc.add_path('/Mgmt/Connection', 'local')
svc.add_path('/DeviceInstance', DEVICE_INSTANCE)
svc.add_path('/ProductId', PRODUCT_ID)
svc.add_path('/ProductName', PRODUCT_NAME)
svc.add_path('/FirmwareVersion', FIRMWARE_VERSION)
svc.add_path('/Connected', CONNECTED)
# -- Anchor state --
svc.add_path('/Anchor/Set', 0)
svc.add_path('/Anchor/Marked/Latitude', None)
svc.add_path('/Anchor/Marked/Longitude', None)
svc.add_path('/Anchor/Estimated/Latitude', None)
svc.add_path('/Anchor/Estimated/Longitude', None)
svc.add_path('/Anchor/UncertaintyRadius', 0.0)
svc.add_path('/Anchor/Drift', 0.0)
svc.add_path('/Anchor/EstimatedHistory/Json', '[]')
# -- Chain & scope --
svc.add_path('/Anchor/ChainLength', 0.0)
svc.add_path('/Anchor/ScopeRatio', 0.0)
svc.add_path('/Anchor/RecommendedScope', 0.0)
svc.add_path('/Anchor/EstimatedDistance', 0.0)
svc.add_path('/Anchor/SteadyStateLoad', 0.0)
svc.add_path('/Anchor/PeakLoad', 0.0)
svc.add_path('/Anchor/SwingRate', 0.0)
# -- Alarm --
svc.add_path('/Alarm/Active', 0)
svc.add_path('/Alarm/Type', '')
svc.add_path('/Alarm/Radius', 0.0)
svc.add_path('/Alarm/Message', '')
# -- Vessel (republished sensor data) --
svc.add_path('/Vessel/Latitude', 0.0)
svc.add_path('/Vessel/Longitude', 0.0)
svc.add_path('/Vessel/Speed', 0.0)
svc.add_path('/Vessel/Course', 0.0)
svc.add_path('/Vessel/Heading', 0.0)
svc.add_path('/Vessel/Depth', 0.0)
svc.add_path('/Vessel/WindSpeed', 0.0)
svc.add_path('/Vessel/WindDirection', 0.0)
# -- Track --
svc.add_path('/Track/Json', '[]')
svc.add_path('/Track/PointCount', 0)
# -- Writable settings (frontend <-> service) --
svc.add_path('/Settings/Enabled', 1,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/ChainLength', 0.0,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/AlarmRadius', DEFAULT_ALARM_RADIUS_FT,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/ChainWeight', CHAIN_WEIGHT_LB_PER_FT,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/AnchorWeight', ANCHOR_WEIGHT_LB,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/VesselWeight', VESSEL_WEIGHT_LB,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/FreeboardHeight', FREEBOARD_HEIGHT_FT,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/WindageArea', WINDAGE_AREA_SQFT,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/DragCoefficient', DRAG_COEFFICIENT,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/BowOffset', GPS_TO_BOW_FT,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/DebugLogging', 0,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/Anchor/Latitude', None,
writeable=True, onchangecallback=cb)
svc.add_path('/Settings/Anchor/Longitude', None,
writeable=True, onchangecallback=cb)
# -- Action paths (frontend commands) --
svc.add_path('/Action/DropAnchor', 0,
writeable=True, onchangecallback=cb)
svc.add_path('/Action/AfterDrop', '',
writeable=True, onchangecallback=cb)
svc.add_path('/Action/WeighAnchor', 0,
writeable=True, onchangecallback=cb)
# Persistent settings via Venus OS localsettings
settings_path = '/Settings/AnchorAlarm'
supported_settings = {
'Enabled': [settings_path + '/Enabled', 1, 0, 1],
'ChainLength': [settings_path + '/ChainLength', 150, 0, 1000],
'AlarmRadius': [settings_path + '/AlarmRadius',
DEFAULT_ALARM_RADIUS_FT, 0, 5000],
'ChainWeight': [settings_path + '/ChainWeight',
CHAIN_WEIGHT_LB_PER_FT, 0, 50],
'AnchorWeight': [settings_path + '/AnchorWeight',
ANCHOR_WEIGHT_LB, 0, 2000],
'VesselWeight': [settings_path + '/VesselWeight',
VESSEL_WEIGHT_LB, 0, 500000],
'FreeboardHeight': [settings_path + '/FreeboardHeight',
FREEBOARD_HEIGHT_FT, 0, 50],
'WindageArea': [settings_path + '/WindageArea',
WINDAGE_AREA_SQFT, 0, 2000],
'DragCoefficient': [settings_path + '/DragCoefficient',
DRAG_COEFFICIENT, 0, 5],
'BowOffset': [settings_path + '/BowOffset',
GPS_TO_BOW_FT, 0, 200],
'DebugLogging': [settings_path + '/DebugLogging', 0, 0, 1],
'AnchorSet': [settings_path + '/AnchorSet', 0, 0, 1],
'EstimatedLat': [settings_path + '/EstimatedLat', 0, -90, 90],
'EstimatedLon': [settings_path + '/EstimatedLon', 0, -180, 180],
'MarkedLat': [settings_path + '/MarkedLat', 0, -90, 90],
'MarkedLon': [settings_path + '/MarkedLon', 0, -180, 180],
}
self._settings = SettingsDevice(
bus, supported_settings, self._on_setting_changed,
)
# Seed D-Bus paths with persisted values
for key in ('Enabled', 'ChainLength', 'AlarmRadius', 'ChainWeight',
'AnchorWeight', 'VesselWeight', 'FreeboardHeight',
'WindageArea', 'DragCoefficient', 'BowOffset',
'DebugLogging'):
try:
svc['/Settings/' + key] = self._settings[key]
except Exception:
pass
# Create subsystem modules
bow_offset = float(self._settings['BowOffset'])
self._reader = SensorReader(bus)
self._tracker = AnchorTracker(bow_offset_ft=bow_offset)
self._detector = DragDetector()
self._track_buffer = TrackBuffer()
self._debug_logger = DebugLogger()
# Restore debug logging state
if int(self._settings['DebugLogging']):
self._debug_logger.enable()
# Restore persisted anchor state
self._restore_anchor_state()
svc.register()
logger.info('Service registered on D-Bus as %s', SERVICE_NAME)
self._running = True
GLib.timeout_add(MAIN_LOOP_INTERVAL_MS, exit_on_error, self._update)
def stop(self):
"""Clean shutdown."""
self._running = False
if self._debug_logger and self._debug_logger.active:
self._debug_logger.disable()
logger.info('Service stopped')
# ------------------------------------------------------------------
# State persistence
# ------------------------------------------------------------------
def _persist_anchor_state(self):
"""Write current anchor state to localsettings, only if changed."""
current = (
int(self._tracker.anchor_set),
self._tracker.estimated_lat,
self._tracker.estimated_lon,
self._tracker.marked_lat,
self._tracker.marked_lon,
)
if current == self._last_persisted:
return
try:
self._settings['AnchorSet'] = current[0]
if self._tracker.anchor_set:
if current[1] is not None:
self._settings['EstimatedLat'] = current[1]
self._settings['EstimatedLon'] = current[2]
if current[3] is not None:
self._settings['MarkedLat'] = current[3]
self._settings['MarkedLon'] = current[4]
self._last_persisted = current
except Exception:
logger.exception('Failed to persist anchor state')
def _restore_anchor_state(self):
"""Restore anchor state from localsettings on startup."""
try:
if int(self._settings['AnchorSet']):
marked_lat = float(self._settings['MarkedLat'])
marked_lon = float(self._settings['MarkedLon'])
est_lat = float(self._settings['EstimatedLat'])
est_lon = float(self._settings['EstimatedLon'])
if marked_lat != 0.0 and marked_lon != 0.0:
self._tracker.marked_lat = marked_lat
self._tracker.marked_lon = marked_lon
self._tracker.estimated_lat = est_lat
self._tracker.estimated_lon = est_lon
self._tracker.anchor_set = True
self._tracker.uncertainty_radius_ft = 100.0
logger.info(
'Restored anchor state: marked=%.6f,%.6f est=%.6f,%.6f',
marked_lat, marked_lon, est_lat, est_lon,
)
except Exception:
logger.exception('Failed to restore anchor state')
# ------------------------------------------------------------------
# Main loop
# ------------------------------------------------------------------
def _update(self):
"""Called every MAIN_LOOP_INTERVAL_MS. Return True to keep running."""
if not self._running:
return False
try:
self._do_update()
except Exception:
logger.exception('Error in update loop')
return True
def _do_update(self):
snapshot = self._reader.read()
svc = self._dbusservice
now = time.time()
self._tick_count += 1
is_slow_tick = (self._tick_count % _SLOW_TICK_DIVISOR == 0)
chain_length = float(self._settings['ChainLength'])
alarm_radius = float(self._settings['AlarmRadius'])
chain_weight = float(self._settings['ChainWeight'])
vessel_weight = float(self._settings['VesselWeight'])
freeboard = float(self._settings['FreeboardHeight'])
windage_area = float(self._settings['WindageArea'])
drag_coeff = float(self._settings['DragCoefficient'])
svc['/Anchor/ChainLength'] = chain_length
svc['/Alarm/Radius'] = alarm_radius
if snapshot.latitude is not None and snapshot.longitude is not None:
# Republish vessel state (every tick — 1 Hz)
svc['/Vessel/Latitude'] = snapshot.latitude
svc['/Vessel/Longitude'] = snapshot.longitude
svc['/Vessel/Speed'] = snapshot.speed or 0.0
svc['/Vessel/Course'] = snapshot.course or 0.0
svc['/Vessel/Heading'] = snapshot.heading or 0.0
svc['/Vessel/Depth'] = snapshot.depth or 0.0
svc['/Vessel/WindSpeed'] = snapshot.wind_speed or 0.0
svc['/Vessel/WindDirection'] = snapshot.wind_direction or 0.0
depth = snapshot.depth or 0.0
wind_spd = snapshot.wind_speed or 0.0
# Steady-state wind force
f_wind = wind_force_lbs(wind_spd, windage_area, drag_coeff)
# Catenary geometry
cat = catenary_distance(
chain_length, depth, freeboard, f_wind, chain_weight,
)
# Swing-reversal detection (signed heading rate) — 1 Hz
is_reversal = False
if snapshot.heading is not None:
if self._prev_heading is not None:
delta = _signed_angle_diff(
self._prev_heading, snapshot.heading,
)
if self._prev_heading_delta is not None:
if ((self._prev_heading_delta > 1.0 and delta < -1.0)
or (self._prev_heading_delta < -1.0
and delta > 1.0)):
is_reversal = True
self._prev_heading_delta = delta
self._prev_heading = snapshot.heading
# Lateral speed component for dynamic-load calculation
lateral_speed = 0.0
if (self._tracker.marked_lat is not None
and snapshot.course is not None
and snapshot.speed is not None):
bearing = _bearing_between(
snapshot.latitude, snapshot.longitude,
self._tracker.marked_lat, self._tracker.marked_lon,
)
angle = abs(_signed_angle_diff(snapshot.course, bearing))
lateral_speed = snapshot.speed * math.sin(math.radians(angle))
# Dynamic anchor load
dyn = dynamic_load(
f_wind, vessel_weight, lateral_speed,
chain_length, is_reversal,
)
# Peak load over rolling 60-second window
self._peak_load_history.append((dyn.total_force_lbs, now))
cutoff = now - _PEAK_LOAD_WINDOW_SEC
while (self._peak_load_history
and self._peak_load_history[0][1] < cutoff):
self._peak_load_history.popleft()
self._peak_load = max(
(ld for ld, _ in self._peak_load_history), default=0.0)
# Scope & recommended chain
scp = scope_ratio(chain_length, depth, freeboard)
rec = recommended_chain_length(depth, freeboard)
# Drag detector tick — 1 Hz (safety-critical)
self._detector.update(
snapshot, self._tracker, alarm_radius, chain_length,
)
# --- Slow path (1/15s) ---
if is_slow_tick:
# Tracker tick
self._tracker.update(
snapshot, chain_length, depth, freeboard,
windage_area, drag_coeff, chain_weight,
)
# Track buffer tick
self._track_buffer.add_if_moved(snapshot)
# Debug logger tick (if enabled)
if self._debug_logger.active:
self._debug_logger.log(snapshot, self._tracker)
# Persist anchor state
self._persist_anchor_state()
# Publish computed values
svc['/Anchor/ScopeRatio'] = round(scp, 2)
svc['/Anchor/RecommendedScope'] = round(rec, 1)
svc['/Anchor/EstimatedDistance'] = round(cat.total_distance_ft, 1)
svc['/Anchor/SteadyStateLoad'] = round(f_wind, 1)
svc['/Anchor/PeakLoad'] = round(self._peak_load, 1)
svc['/Anchor/SwingRate'] = round(
self._detector.swing_rate_deg_per_sec, 3,
)
# Anchor & alarm state (always update)
svc['/Anchor/Set'] = int(self._tracker.anchor_set)
svc['/Anchor/Marked/Latitude'] = self._tracker.marked_lat
svc['/Anchor/Marked/Longitude'] = self._tracker.marked_lon
svc['/Anchor/Estimated/Latitude'] = self._tracker.estimated_lat
svc['/Anchor/Estimated/Longitude'] = self._tracker.estimated_lon
svc['/Anchor/UncertaintyRadius'] = round(
self._tracker.uncertainty_radius_ft, 1,
)
svc['/Anchor/Drift'] = round(self._tracker.drift_ft, 1)
svc['/Alarm/Active'] = int(self._detector.alarm_active)
svc['/Alarm/Type'] = self._detector.alarm_type
svc['/Alarm/Message'] = self._detector.alarm_message
svc['/Track/PointCount'] = self._track_buffer.get_point_count()
# Throttled JSON serialisation (every 5 s)
if now - self._last_json_update >= _JSON_UPDATE_INTERVAL_SEC:
self._last_json_update = now
try:
svc['/Track/Json'] = json.dumps(
self._track_buffer.get_display_points_json(),
)
except (TypeError, ValueError):
svc['/Track/Json'] = '[]'
try:
svc['/Anchor/EstimatedHistory/Json'] = json.dumps(
self._tracker.get_estimated_history_json(),
)
except (TypeError, ValueError):
svc['/Anchor/EstimatedHistory/Json'] = '[]'
# Memory watchdog (every 60 s)
if now - self._last_watchdog >= _WATCHDOG_INTERVAL_SEC:
self._last_watchdog = now
self._check_memory()
# ------------------------------------------------------------------
# Memory watchdog
# ------------------------------------------------------------------
def _check_memory(self):
rss = _get_rss_mb()
if rss is None:
return
uptime_min = self._tick_count / 60.0
track_pts = self._track_buffer.get_point_count()
arc_pts = len(self._tracker._arc_points)
hdg_ests = len(self._tracker._heading_estimates)
proxies = len(self._reader._proxy_cache)
logger.info(
'WATCHDOG rss=%.1fMB up=%.0fm track=%d arc=%d hdg=%d proxies=%d',
rss, uptime_min, track_pts, arc_pts, hdg_ests, proxies,
)
if rss > _WATCHDOG_KILL_MB:
logger.error(
'RSS %.1f MB exceeds %d MB — clearing buffers and proxy cache',
rss, _WATCHDOG_KILL_MB,
)
self._track_buffer.clear()
self._tracker._arc_points.clear()
self._reader._proxy_cache.clear()
elif rss > _WATCHDOG_WARN_MB:
logger.warning('RSS %.1f MB exceeds %d MB warning threshold',
rss, _WATCHDOG_WARN_MB)
# ------------------------------------------------------------------
# D-Bus path-change callbacks
# ------------------------------------------------------------------
_SETTINGS_KEY_MAP = {
'/Settings/Enabled': 'Enabled',
'/Settings/ChainLength': 'ChainLength',
'/Settings/AlarmRadius': 'AlarmRadius',
'/Settings/ChainWeight': 'ChainWeight',
'/Settings/AnchorWeight': 'AnchorWeight',
'/Settings/VesselWeight': 'VesselWeight',
'/Settings/FreeboardHeight': 'FreeboardHeight',
'/Settings/WindageArea': 'WindageArea',
'/Settings/DragCoefficient': 'DragCoefficient',
'/Settings/BowOffset': 'BowOffset',
}
def _on_path_changed(self, path, value):
"""Handle writable D-Bus path changes from the frontend."""
logger.info('Path %s written: %s', path, value)
# --- Action: Drop Anchor ---
if path == '/Action/DropAnchor':
if value == 1:
lat = self._dbusservice['/Vessel/Latitude']
lon = self._dbusservice['/Vessel/Longitude']
if lat and lon:
self._tracker.drop_anchor(lat, lon)
logger.info('Action: drop anchor at %.6f, %.6f', lat, lon)
self._dbusservice['/Action/DropAnchor'] = 0
return True
# --- Action: After Drop ---
if path == '/Action/AfterDrop':
if value:
try:
params = json.loads(str(value))
except (json.JSONDecodeError, TypeError):
logger.warning('Invalid AfterDrop JSON: %s', value)
self._dbusservice['/Action/AfterDrop'] = ''
return True
snapshot = self._reader.read()
if snapshot.latitude is None or snapshot.longitude is None:
logger.warning('AfterDrop ignored — no GPS fix')
self._dbusservice['/Action/AfterDrop'] = ''
return True
cl = float(params.get(
'chain_length', self._settings['ChainLength'],
))
self._tracker.after_drop(
snapshot.latitude, snapshot.longitude,
snapshot.heading or 0.0,
cl,
snapshot.depth or 0.0,
float(self._settings['FreeboardHeight']),
snapshot.wind_speed or 0.0,
float(self._settings['WindageArea']),
float(self._settings['DragCoefficient']),
float(self._settings['ChainWeight']),
)
if 'chain_length' in params:
try:
self._settings['ChainLength'] = cl
except Exception:
pass
self._dbusservice['/Action/AfterDrop'] = ''
return True
# --- Action: Weigh Anchor ---
if path == '/Action/WeighAnchor':
if value == 1:
self._tracker.weigh_anchor()
self._detector.reset()
self._track_buffer.clear()
self._peak_load_history.clear()
self._peak_load = 0.0
self._prev_heading = None
self._prev_heading_delta = None
self._persist_anchor_state()
logger.info('Action: anchor weighed — all state reset')
self._dbusservice['/Action/WeighAnchor'] = 0
return True
# --- Joystick anchor-position adjustment ---
if path == '/Settings/Anchor/Latitude':
if self._tracker.anchor_set and value is not None:
self._tracker.marked_lat = float(value)
return True
if path == '/Settings/Anchor/Longitude':
if self._tracker.anchor_set and value is not None:
self._tracker.marked_lon = float(value)
return True
# --- Bow offset live update ---
if path == '/Settings/BowOffset':
try:
self._tracker.bow_offset_ft = float(value)
self._settings['BowOffset'] = float(value)
except Exception:
logger.exception('Failed to update bow offset')
return True
# --- Debug logging toggle ---
if path == '/Settings/DebugLogging':
try:
self._settings['DebugLogging'] = int(value)
except Exception:
pass
if int(value):
self._debug_logger.enable()
else:
self._debug_logger.disable()
return True
# --- Settings -> persist to localsettings ---
key = self._SETTINGS_KEY_MAP.get(path)
if key is not None:
try:
self._settings[key] = value
except Exception:
logger.exception('Failed to persist setting %s', path)
return True
return True
def _on_setting_changed(self, setting, old, new):
"""Called when a Venus OS localsetting changes (external write)."""
logger.info('Setting %s changed: %s -> %s', setting, old, new)
try:
self._dbusservice['/Settings/' + setting] = new
except Exception:
pass
# ----------------------------------------------------------------------
# Entry point
# ----------------------------------------------------------------------
def main():
DBusGMainLoop(set_as_default=True)
logging.basicConfig(
level=getattr(logging, LOG_LEVEL, logging.INFO),
format='%(asctime)s %(name)s %(levelname)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
logger.info('Starting dbus-anchor-alarm v%s', VERSION)
service = AnchorAlarmService()
mainloop = None
def shutdown(signum, _frame):
try:
sig_name = signal.Signals(signum).name
except ValueError:
sig_name = str(signum)
logger.info('Received %s, shutting down...', sig_name)
service.stop()
if mainloop is not None:
mainloop.quit()
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
try:
service.start()
mainloop = GLib.MainLoop()
mainloop.run()
except KeyboardInterrupt:
print("\nShutdown requested")
service.stop()
except Exception as e:
logging.error("Fatal error: %s", e, exc_info=True)
sys.exit(1)
finally:
logging.info("Service stopped")
if __name__ == '__main__':
main()