diff --git a/config.yaml.example b/config.yaml.example index 397d67d..ee1bcaf 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -126,64 +126,87 @@ repeater: # Controls how long users stay logged in before needing to re-authenticate jwt_expiry_minutes: 60 -# Local GPS diagnostics. When enabled, the daemon reads NMEA sentences from the -# configured source and exposes parsed attributes at /api/gps. +# Local GPS receiver. When enabled, the daemon reads NMEA sentences from the +# configured source and exposes parsed data at /api/gps. gps: enabled: false - # Use repeater.latitude/repeater.longitude as the displayed receiver location - # until the GPS has a valid non-stale fix. This prevents early no-fix GPS - # estimates from replacing the configured site location in API clients. - # The default 0,0 repeater location is treated as unset. - use_manual_location_until_fix: true - - # Opt-in: use GPS coordinates for repeater-originated location fields - # (for example flood adverts). When disabled, repeater.latitude/longitude - # from config are always used. - use_gps_for_repeater_location: false - - # Optional privacy/obfuscation control when GPS is used for repeater - # location. If set, coordinates are rounded to this many decimal places - # before use (0-8). Leave null for full reported precision. - repeater_location_precision_digits: null + # --------------------------------------------------------------------------- + # Source + # --------------------------------------------------------------------------- # Source type: # serial = read directly from an attached GPS module - # file = read NMEA lines from source_path, useful for gpsd/sidecar bridges + # file = read NMEA lines from source_path (useful for gpsd/sidecar bridges) source: serial - # Serial source settings + # Serial source settings (used when source: serial) device: "/dev/serial0" baud_rate: 9600 read_timeout_seconds: 1.0 reconnect_interval_seconds: 5.0 - # File source settings. The file can contain raw NMEA lines or JSON with a - # "sentences" list / "last_sentence" field. + # File source settings (used when source: file) + # The file may contain raw NMEA lines or JSON with a "sentences" list / + # "last_sentence" field. source_path: "/var/lib/pymc_repeater/gps_nmea.txt" poll_interval_seconds: 2.0 - # Diagnostics behavior + # --------------------------------------------------------------------------- + # Location behaviour + # Three independent controls — read the comments carefully, they do + # different things: + # + # api_fallback_to_config_location — what the API *displays* before a fix + # advertise_gps_location — what the mesh *advertises* in adverts + # persist_gps_fix_to_config — whether the fix is *written* to config + # --------------------------------------------------------------------------- + + # API display: while GPS has no valid fix, show repeater.latitude/longitude + # from config in the /api/gps effective position instead of null/0,0. + # The default 0,0 repeater location is treated as unset (no fallback shown). + # Has no effect on mesh adverts or config persistence. + api_fallback_to_config_location: true + + # Mesh adverts: use GPS coordinates in repeater-originated location fields + # (flood adverts, etc.). When false, repeater.latitude/longitude from config + # are always used for outgoing mesh packets. + advertise_gps_location: false + + # Config persistence: write a valid GPS fix back into repeater.latitude/ + # repeater.longitude so adverts location details follow the + # receiver across restarts. Updates are throttled to avoid rewriting config + # on every NMEA sentence. location_precision_digits is applied before saving. + persist_gps_fix_to_config: false + persist_gps_fix_interval_seconds: 600.0 + + # Optional privacy/obfuscation: round coordinates to this many decimal places + # before they are used for advertising or persisted to config (0–8). + # Leave null for full precision. Affects both advertise_gps_location and + # persist_gps_fix_to_config. + location_precision_digits: null + + # --------------------------------------------------------------------------- + # Diagnostics + # --------------------------------------------------------------------------- + stale_after_seconds: 10.0 retain_sentences: 25 validate_checksum: true require_checksum: false - # Automatically set the Linux system clock from GPS UTC time once the receiver - # has a valid non-stale fix. The systemd service grants CAP_SYS_TIME for this. + # --------------------------------------------------------------------------- + # Time sync + # --------------------------------------------------------------------------- + + # Automatically set the Linux system clock from GPS UTC time once the + # receiver has a valid non-stale fix. The systemd service grants + # CAP_SYS_TIME for this. time_sync_enabled: true time_sync_interval_seconds: 3600.0 time_sync_min_offset_seconds: 1.0 time_sync_min_valid_year: 2020 - # Opt-in: feed a valid GPS fix back into repeater.latitude/repeater.longitude - # so pyMC Repeater adverts and pyMC Console location details follow the - # receiver. repeater_location_precision_digits is applied before saving so - # deployments can fuzz the stored/advertised location. Updates are throttled - # to avoid rewriting config on every NMEA sentence. - update_repeater_location_from_fix: false - location_update_interval_seconds: 600.0 - # Mesh Network Configuration mesh: # Unscoped flood policy - controls whether the repeater allows or denies unscoped flooding diff --git a/repeater/config.py b/repeater/config.py index bbdec1a..d4fd12d 100644 --- a/repeater/config.py +++ b/repeater/config.py @@ -82,9 +82,9 @@ def load_config(config_path: Optional[str] = None) -> Dict[str, Any]: if "gps" not in config: config["gps"] = { "enabled": False, - "use_manual_location_until_fix": True, - "use_gps_for_repeater_location": False, - "repeater_location_precision_digits": None, + "api_fallback_to_config_location": True, + "advertise_gps_location": False, + "location_precision_digits": None, "source": "serial", "device": "/dev/serial0", "baud_rate": 9600, @@ -98,8 +98,8 @@ def load_config(config_path: Optional[str] = None) -> Dict[str, Any]: "time_sync_interval_seconds": 3600.0, "time_sync_min_offset_seconds": 1.0, "time_sync_min_valid_year": 2020, - "update_repeater_location_from_fix": False, - "location_update_interval_seconds": 600.0, + "persist_gps_fix_to_config": False, + "persist_gps_fix_interval_seconds": 600.0, } # Ensure repeater.security exists with defaults for upgrades from older configs diff --git a/repeater/data_acquisition/gps_service.py b/repeater/data_acquisition/gps_service.py index e53355b..1be4442 100644 --- a/repeater/data_acquisition/gps_service.py +++ b/repeater/data_acquisition/gps_service.py @@ -591,14 +591,14 @@ class GPSService: repeater_config = config.get("repeater", {}) if isinstance(config, dict) else {} self.config = gps_config self.enabled = bool(gps_config.get("enabled", False)) - self.use_manual_location_until_fix = bool( - gps_config.get("use_manual_location_until_fix", True) + self.api_fallback_to_config_location = bool( + gps_config.get("api_fallback_to_config_location", True) ) - self.use_gps_for_repeater_location = bool( - gps_config.get("use_gps_for_repeater_location", False) + self.advertise_gps_location = bool( + gps_config.get("advertise_gps_location", False) ) - self.repeater_location_precision_digits = _normalize_precision_digits( - gps_config.get("repeater_location_precision_digits") + self.location_precision_digits = _normalize_precision_digits( + gps_config.get("location_precision_digits") ) self.source = str(gps_config.get("source", "serial")).lower() self.device = gps_config.get("device", "/dev/serial0") @@ -618,24 +618,24 @@ class GPSService: self.time_sync_min_valid_year = int(gps_config.get("time_sync_min_valid_year", 2020)) self._clock_setter = clock_setter or _set_system_clock_from_datetime self._time_provider = time_provider or time.time - self.location_update_enabled = bool( - gps_config.get("update_repeater_location_from_fix", False) + self.persist_gps_fix_enabled = bool( + gps_config.get("persist_gps_fix_to_config", False) ) - self.location_update_interval_seconds = max( - 1.0, float(gps_config.get("location_update_interval_seconds", 600.0)) + self.persist_gps_fix_interval_seconds = max( + 1.0, float(gps_config.get("persist_gps_fix_interval_seconds", 600.0)) ) self._location_update_callback = location_update_callback self._location_update_lock = threading.RLock() self._last_location_update_monotonic: Optional[float] = None self._location_update_status: Dict[str, Any] = { - "enabled": self.location_update_enabled, - "state": "disabled" if not self.location_update_enabled else "waiting_for_fix", + "enabled": self.persist_gps_fix_enabled, + "state": "disabled" if not self.persist_gps_fix_enabled else "waiting_for_fix", "last_attempt": None, "last_success": None, "last_error": None, "last_latitude": None, "last_longitude": None, - "interval_seconds": self.location_update_interval_seconds, + "interval_seconds": self.persist_gps_fix_interval_seconds, } self._time_sync_lock = threading.RLock() self._last_time_sync_monotonic: Optional[float] = None @@ -718,9 +718,9 @@ class GPSService: def _apply_precision(self, value: Optional[float]) -> Optional[float]: if value is None: return None - if self.repeater_location_precision_digits is None: + if self.location_precision_digits is None: return value - return round(value, self.repeater_location_precision_digits) + return round(value, self.location_precision_digits) def _resolve_repeater_location(self, snapshot: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: fallback_lat = _to_float(self.repeater_config.get("latitude")) @@ -729,11 +729,11 @@ class GPSService: "latitude": fallback_lat if fallback_lat is not None else 0.0, "longitude": fallback_lon if fallback_lon is not None else 0.0, "source": "config", - "gps_enabled_for_repeater_location": self.use_gps_for_repeater_location, - "precision_digits": self.repeater_location_precision_digits, + "advertise_gps_location": self.advertise_gps_location, + "location_precision_digits": self.location_precision_digits, } - if not self.use_gps_for_repeater_location: + if not self.advertise_gps_location: return fallback_location snapshot = snapshot or {} @@ -747,8 +747,8 @@ class GPSService: "latitude": self._apply_precision(gps_lat), "longitude": self._apply_precision(gps_lon), "source": "gps", - "gps_enabled_for_repeater_location": True, - "precision_digits": self.repeater_location_precision_digits, + "advertise_gps_location": True, + "location_precision_digits": self.location_precision_digits, } return { @@ -781,8 +781,8 @@ class GPSService: "read_timeout_seconds": self.read_timeout_seconds, "poll_interval_seconds": self.poll_interval_seconds, "stale_after_seconds": self.parser.stale_after_seconds, - "use_gps_for_repeater_location": self.use_gps_for_repeater_location, - "repeater_location_precision_digits": self.repeater_location_precision_digits, + "advertise_gps_location": self.advertise_gps_location, + "location_precision_digits": self.location_precision_digits, }, "time_sync": self._get_time_sync_status(snapshot), "repeater_location": self._resolve_repeater_location(snapshot), @@ -802,7 +802,7 @@ class GPSService: with self._location_update_lock: status = deepcopy(self._location_update_status) - if not self.enabled or not self.location_update_enabled: + if not self.enabled or not self.persist_gps_fix_enabled: status["state"] = "disabled" return status if self._location_update_callback is None: @@ -837,20 +837,20 @@ class GPSService: with self._location_update_lock: self._location_update_status.update( { - "enabled": self.location_update_enabled, + "enabled": self.persist_gps_fix_enabled, "state": state, "last_attempt": timestamp, "last_error": error, "last_latitude": latitude, "last_longitude": longitude, - "interval_seconds": self.location_update_interval_seconds, + "interval_seconds": self.persist_gps_fix_interval_seconds, } ) if success: self._location_update_status["last_success"] = timestamp def _maybe_update_repeater_location(self): - if not self.enabled or not self.location_update_enabled: + if not self.enabled or not self.persist_gps_fix_enabled: return if self._location_update_callback is None: return @@ -860,7 +860,7 @@ class GPSService: if ( self._last_location_update_monotonic is not None and now_monotonic - self._last_location_update_monotonic - < self.location_update_interval_seconds + < self.persist_gps_fix_interval_seconds ): return @@ -891,7 +891,7 @@ class GPSService: "fix": deepcopy(snapshot.get("fix") or {}), "status": deepcopy(snapshot.get("status") or {}), "time": deepcopy(snapshot.get("time") or {}), - "precision_digits": self.repeater_location_precision_digits, + "location_precision_digits": self.location_precision_digits, } try: updated = bool(self._location_update_callback(payload)) @@ -935,7 +935,7 @@ class GPSService: manual_position = deepcopy(self._extract_manual_position(self.repeater_config)) gps_fix_valid = bool(snapshot.get("status", {}).get("fix_valid")) use_manual = ( - self.use_manual_location_until_fix + self.api_fallback_to_config_location and manual_position is not None and not gps_fix_valid ) @@ -959,8 +959,8 @@ class GPSService: snapshot["position_meta"] = { "source": position_source, "source_label": position_source_label, - "policy": "manual_until_gps_fix" - if self.use_manual_location_until_fix + "policy": "fallback_to_config" + if self.api_fallback_to_config_location else "gps_only", "manual_config_available": manual_position is not None, "gps_fix_valid": gps_fix_valid,