eg25-control: cache the location assistance data
This commit is contained in:
parent
a0c2ed38e6
commit
92451d1e28
|
@ -61,6 +61,8 @@ POWER_ENDPOINT = "/sys/class/modem-power/modem-power/device/powered"
|
||||||
# also at xtrapath5 and xtrapath6 subdomains.
|
# also at xtrapath5 and xtrapath6 subdomains.
|
||||||
# the AGPS data here is an almanac good for 7 days.
|
# the AGPS data here is an almanac good for 7 days.
|
||||||
AGPS_DATA_URI_BASE = "https://xtrapath4.izatcloud.net"
|
AGPS_DATA_URI_BASE = "https://xtrapath4.izatcloud.net"
|
||||||
|
ON_DISK_TIME_FMT = '%Y/%m/%d,%H:%M:%S'
|
||||||
|
AGPS_CACHE_REFRESH_AFTER = datetime.timedelta(days=1)
|
||||||
|
|
||||||
class AgpsDataVariant:
|
class AgpsDataVariant:
|
||||||
# GNSS-AP-Note 1.4:
|
# GNSS-AP-Note 1.4:
|
||||||
|
@ -104,12 +106,25 @@ class Executor:
|
||||||
def __init__(self, dry_run: bool = False):
|
def __init__(self, dry_run: bool = False):
|
||||||
self.dry_run = dry_run
|
self.dry_run = dry_run
|
||||||
|
|
||||||
|
def read_file(self, path: str, default: bytes) -> bytes:
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
return f.read()
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.debug(f"failed to read {path}: {e}")
|
||||||
|
return default
|
||||||
|
|
||||||
@destructive
|
@destructive
|
||||||
def write_file(self, path: str, data: bytes) -> None:
|
def write_file(self, path: str, data: bytes) -> None:
|
||||||
logger.debug(f"echo {data!r} > {path}")
|
logger.debug(f"echo {data!r} > {path}")
|
||||||
with open(path, 'wb') as f:
|
with open(path, 'wb') as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
||||||
|
@destructive
|
||||||
|
def mkdir(self, path: str) -> None:
|
||||||
|
logger.debug(f"mkdir {path}")
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
|
||||||
@destructive(return_=b'')
|
@destructive(return_=b'')
|
||||||
def exec(self, cmd: list[str], check: bool = True) -> bytes:
|
def exec(self, cmd: list[str], check: bool = True) -> bytes:
|
||||||
logger.debug(" ".join(cmd))
|
logger.debug(" ".join(cmd))
|
||||||
|
@ -270,7 +285,7 @@ class Sequencer:
|
||||||
def _at_autogps(self, enable: AutoGps) -> str:
|
def _at_autogps(self, enable: AutoGps) -> str:
|
||||||
return self._at_structured_cmd("QGPSCFG", "autogps", enable)
|
return self._at_structured_cmd("QGPSCFG", "autogps", enable)
|
||||||
|
|
||||||
def _get_assistance_data(self, variant: AgpsDataVariant) -> str | None:
|
def _download_assistance_data(self, variant: AgpsDataVariant) -> str | None:
|
||||||
try:
|
try:
|
||||||
self.executor.exec(["curl", f"{self.AGPS_DATA_URI_BASE}/{variant}", "-o", variant])
|
self.executor.exec(["curl", f"{self.AGPS_DATA_URI_BASE}/{variant}", "-o", variant])
|
||||||
return variant
|
return variant
|
||||||
|
@ -278,6 +293,54 @@ class Sequencer:
|
||||||
logger.warning(f"AGPS data download failed: {e}")
|
logger.warning(f"AGPS data download failed: {e}")
|
||||||
return None # TODO: could be smarter: return cached AGPS data?
|
return None # TODO: could be smarter: return cached AGPS data?
|
||||||
|
|
||||||
|
def _cache_assistance_data(self, fresh_path: str) -> None:
|
||||||
|
'''call after successful upload to indicate that the data recently retrieved should be cached'''
|
||||||
|
if fresh_path.startswith("cache/"):
|
||||||
|
return # if the caller used cached data, avoid updating its timestamp
|
||||||
|
|
||||||
|
# TODO: we should use fs time for this
|
||||||
|
now = datetime.datetime.now().strftime(ON_DISK_TIME_FMT)
|
||||||
|
try:
|
||||||
|
self.executor.mkdir("cache")
|
||||||
|
self.executor.exec(["mv", fresh_path, "cache"])
|
||||||
|
self.executor.write_file(f"cache/{fresh_path}.ts", now.encode())
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.warning(f"failed to cache AGPS data: {e}")
|
||||||
|
|
||||||
|
def _get_cached_assistance_data(self, variant: AgpsDataVariant) -> tuple[bool, str | None]:
|
||||||
|
'''
|
||||||
|
returns:
|
||||||
|
- whether cached data is fresh (i.e. won't benefit from an update)
|
||||||
|
- a path to the cached AGPS data, or None if no valid cached entry
|
||||||
|
'''
|
||||||
|
last_cache_datestr = self.executor.read_file(f"cache/{variant}.ts", default=b'').decode('utf-8')
|
||||||
|
if not last_cache_datestr:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
last_cache_date = datetime.datetime.strptime(last_cache_datestr, ON_DISK_TIME_FMT)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"failed to decode cache timestamp for {variant}: {e}")
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
is_fresh = datetime.datetime.now() - last_cache_date < AGPS_CACHE_REFRESH_AFTER
|
||||||
|
return is_fresh, f"cache/{variant}"
|
||||||
|
|
||||||
|
def _get_any_assistance_data(self, variant: AgpsDataVariant) -> str | None:
|
||||||
|
'''
|
||||||
|
checks if we have fresh cached data:
|
||||||
|
- if so, returns that.
|
||||||
|
- if not, *tries* to update it.
|
||||||
|
- if update succeeds, returns this new data
|
||||||
|
- if update fails, returns stale cached data (because it might still have *some* use/lifetime)
|
||||||
|
'''
|
||||||
|
is_fresh, cached_path = self._get_cached_assistance_data(variant)
|
||||||
|
if is_fresh: return cached_path
|
||||||
|
|
||||||
|
better_data = self._download_assistance_data(variant)
|
||||||
|
return better_data or cached_path
|
||||||
|
|
||||||
|
|
||||||
@log_scope("powering modem...", "modem powered")
|
@log_scope("powering modem...", "modem powered")
|
||||||
def power_on(self) -> None:
|
def power_on(self) -> None:
|
||||||
self.executor.write_file(self.power_endpoint, b'1')
|
self.executor.write_file(self.power_endpoint, b'1')
|
||||||
|
@ -285,6 +348,11 @@ class Sequencer:
|
||||||
logger.info("modem hasn't appeared: sleeping for 1s")
|
logger.info("modem hasn't appeared: sleeping for 1s")
|
||||||
time.sleep(1) # wait for modem to appear
|
time.sleep(1) # wait for modem to appear
|
||||||
|
|
||||||
|
# set modem to use UTC time instead of local time.
|
||||||
|
# modemmanager sends CTZU=3 during init and that causes `AT+CCLK?` to return a timestamp that's off by 600+ days
|
||||||
|
# see: <https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/issues/360>
|
||||||
|
self._at_structured_cmd("CTZU", value="1")
|
||||||
|
|
||||||
@log_scope("halting modem...", "modem halted")
|
@log_scope("halting modem...", "modem halted")
|
||||||
def power_off(self) -> None:
|
def power_off(self) -> None:
|
||||||
self.executor.write_file(self.power_endpoint, b'0')
|
self.executor.write_file(self.power_endpoint, b'0')
|
||||||
|
@ -351,11 +419,6 @@ class Sequencer:
|
||||||
|
|
||||||
@log_scope("configuring gps...", "gps configured")
|
@log_scope("configuring gps...", "gps configured")
|
||||||
def enable_gps(self) -> None:
|
def enable_gps(self) -> None:
|
||||||
# set modem to use UTC time instead of local time.
|
|
||||||
# modemmanager sends CTZU=3 during init and that causes `AT+CCLK?` to return a timestamp that's off by 600+ days
|
|
||||||
# see: <https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/issues/360>
|
|
||||||
self._at_structured_cmd("CTZU", value="1")
|
|
||||||
|
|
||||||
# disable GNSS, because it's only configurable while offline
|
# disable GNSS, because it's only configurable while offline
|
||||||
self._at_structured_cmd("QGPSEND", check=False)
|
self._at_structured_cmd("QGPSEND", check=False)
|
||||||
# self._at_structured_cmd("QGPS", value="0")
|
# self._at_structured_cmd("QGPS", value="0")
|
||||||
|
@ -365,9 +428,11 @@ class Sequencer:
|
||||||
|
|
||||||
# now = datetime.datetime.now().strftime('%Y/%m/%d,%H:%M:%S') # UTC
|
# now = datetime.datetime.now().strftime('%Y/%m/%d,%H:%M:%S') # UTC
|
||||||
# self._at_structured_cmd("QGPSXTRATIME", value=f"0,\"{now}\"")
|
# self._at_structured_cmd("QGPSXTRATIME", value=f"0,\"{now}\"")
|
||||||
locdata = self._get_assistance_data(AgpsDataVariant.gps_glonass_beidou)
|
locdata = self._get_any_assistance_data(AgpsDataVariant.gps_glonass_beidou)
|
||||||
if locdata:
|
if locdata:
|
||||||
self._mmcli([f"--location-inject-assistance-data={locdata}"])
|
ret = self._try_mmcli([f"--location-inject-assistance-data={locdata}"])
|
||||||
|
if ret is not None:
|
||||||
|
self._cache_assistance_data(locdata)
|
||||||
|
|
||||||
self._at_gnssconfig(GNSSConfig.gps_glonass_beidou_galileo)
|
self._at_gnssconfig(GNSSConfig.gps_glonass_beidou_galileo)
|
||||||
self._at_odpcontrol(ODPControl.disable)
|
self._at_odpcontrol(ODPControl.disable)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user