refactor: update GPS configuration parameters and improve documentation

This commit is contained in:
Rightup
2026-04-30 20:28:50 +01:00
parent 972aefca16
commit 9d3d5e6ef0
3 changed files with 91 additions and 68 deletions
+55 -32
View File
@@ -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 (08).
# 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
+5 -5
View File
@@ -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
+31 -31
View File
@@ -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,