openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

396 lines
16 KiB

from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from typing import DefaultDict, Dict, Iterable, List, Optional, Union
from .constants import SECS_IN_DAY, SECS_IN_HR
from .helpers import ConstellationId, get_constellation, get_closest, get_el_az, TimeRangeHolder
from .ephemeris import Ephemeris, EphemerisType, GLONASSEphemeris, GPSEphemeris, PolyEphemeris, parse_sp3_orbits, parse_rinex_nav_msg_gps, \
parse_rinex_nav_msg_glonass
from .downloader import download_orbits_gps, download_orbits_russia_src, download_nav, download_ionex, download_dcb, download_prediction_orbits_russia_src
from .downloader import download_cors_station
from .trop import saast
from .iono import IonexMap, parse_ionex, get_slant_delay
from .dcb import DCB, parse_dcbs
from .gps_time import GPSTime
from .dgps import get_closest_station_names, parse_dgps
from . import constants
MAX_DGPS_DISTANCE = 100_000 # in meters, because we're not barbarians
class AstroDog:
'''
auto_update: flag indicating whether laika should fetch files from web automatically
cache_dir: directory where data files are downloaded to and cached
dgps: flag indicating whether laika should use dgps (CORS)
data to calculate pseudorange corrections
valid_const: list of constellation identifiers laika will try process
valid_ephem_types: set of ephemeris types that are allowed to use and download.
Default is set to use all orbit ephemeris types
clear_old_ephemeris: flag indicating if ephemeris for an individual satellite should be overwritten when new ephemeris is added.
'''
def __init__(self, auto_update=True,
cache_dir='/tmp/gnss/',
dgps=False,
valid_const=(ConstellationId.GPS, ConstellationId.GLONASS),
valid_ephem_types=EphemerisType.all_orbits(),
clear_old_ephemeris=False):
for const in valid_const:
if not isinstance(const, ConstellationId):
raise TypeError(f"valid_const must be a list of ConstellationId, got {const}")
self.auto_update = auto_update
self.cache_dir = cache_dir
self.clear_old_ephemeris = clear_old_ephemeris
self.dgps = dgps
if not isinstance(valid_ephem_types, Iterable):
valid_ephem_types = [valid_ephem_types]
self.pull_orbit = len(set(EphemerisType.all_orbits()) & set(valid_ephem_types)) > 0
self.pull_nav = EphemerisType.NAV in valid_ephem_types
self.use_qcom_poly = EphemerisType.QCOM_POLY in valid_ephem_types
self.valid_const = valid_const
self.valid_ephem_types = valid_ephem_types
self.orbit_fetched_times = TimeRangeHolder()
self.navs_fetched_times = TimeRangeHolder()
self.dcbs_fetched_times = TimeRangeHolder()
self.dgps_delays = []
self.ionex_maps: List[IonexMap] = []
self.orbits: DefaultDict[str, List[PolyEphemeris]] = defaultdict(list)
self.qcom_polys: DefaultDict[str, List[PolyEphemeris]] = defaultdict(list)
self.navs: DefaultDict[str, List[Union[GPSEphemeris, GLONASSEphemeris]]] = defaultdict(list)
self.dcbs: DefaultDict[str, List[DCB]] = defaultdict(list)
self.cached_ionex: Optional[IonexMap] = None
self.cached_dgps = None
self.cached_orbit: DefaultDict[str, Optional[PolyEphemeris]] = defaultdict(lambda: None)
self.cached_qcom_polys: DefaultDict[str, Optional[PolyEphemeris]] = defaultdict(lambda: None)
self.cached_nav: DefaultDict[str, Union[GPSEphemeris, GLONASSEphemeris, None]] = defaultdict(lambda: None)
self.cached_dcb: DefaultDict[str, Optional[DCB]] = defaultdict(lambda: None)
def get_ionex(self, time) -> Optional[IonexMap]:
ionex: Optional[IonexMap] = self._get_latest_valid_data(self.ionex_maps, self.cached_ionex, self.get_ionex_data, time)
if ionex is None:
if self.auto_update:
raise RuntimeError("Pulled ionex, but still can't get valid for time " + str(time))
else:
self.cached_ionex = ionex
return ionex
def get_nav(self, prn, time):
skip_download = time in self.navs_fetched_times
nav = self._get_latest_valid_data(self.navs[prn], self.cached_nav[prn], self.get_nav_data, time, skip_download)
if nav is not None:
self.cached_nav[prn] = nav
return nav
@staticmethod
def _select_valid_temporal_items(item_dict, time, cache):
'''Returns only valid temporal item for specific time from currently fetched
data.'''
result = {}
for prn, temporal_objects in item_dict.items():
cached = cache[prn]
if cached is not None and cached.valid(time):
obj = cached
else:
obj = get_closest(time, temporal_objects)
if obj is None or not obj.valid(time):
continue
cache[prn] = obj
result[prn] = obj
return result
def get_all_ephem_prns(self):
return set(self.orbits.keys()).union(set(self.navs.keys())).union(set(self.qcom_polys.keys()))
def get_navs(self, time):
if time not in self.navs_fetched_times:
self.get_nav_data(time)
return AstroDog._select_valid_temporal_items(self.navs, time, self.cached_nav)
def get_orbit(self, prn: str, time: GPSTime):
skip_download = time in self.orbit_fetched_times
orbit = self._get_latest_valid_data(self.orbits[prn], self.cached_orbit[prn], self.get_orbit_data, time, skip_download)
if orbit is not None:
self.cached_orbit[prn] = orbit
return orbit
def get_qcom_poly(self, prn: str, time: GPSTime):
poly = self._get_latest_valid_data(self.qcom_polys[prn], self.cached_qcom_polys[prn], None, time, True)
if poly is not None:
self.cached_qcom_polys[prn] = poly
return poly
def get_orbits(self, time):
if time not in self.orbit_fetched_times:
self.get_orbit_data(time)
return AstroDog._select_valid_temporal_items(self.orbits, time, self.cached_orbit)
def get_dcb(self, prn, time):
skip_download = time in self.dcbs_fetched_times
dcb = self._get_latest_valid_data(self.dcbs[prn], self.cached_dcb[prn], self.get_dcb_data, time, skip_download)
if dcb is not None:
self.cached_dcb[prn] = dcb
return dcb
def get_dgps_corrections(self, time, recv_pos):
latest_data = self._get_latest_valid_data(self.dgps_delays, self.cached_dgps, self.get_dgps_data, time, recv_pos=recv_pos)
if latest_data is None:
if self.auto_update:
raise RuntimeError("Pulled dgps, but still can't get valid for time " + str(time))
else:
self.cached_dgps = latest_data
return latest_data
def add_qcom_polys(self, new_ephems: Dict[str, List[Ephemeris]]):
self._add_ephems(new_ephems, self.qcom_polys)
def add_orbits(self, new_ephems: Dict[str, List[Ephemeris]]):
self._add_ephems(new_ephems, self.orbits)
def add_navs(self, new_ephems: Dict[str, List[Ephemeris]]):
self._add_ephems(new_ephems, self.navs)
def _add_ephems(self, new_ephems: Dict[str, List[Ephemeris]], ephems_dict):
for k, v in new_ephems.items():
if len(v) > 0:
if self.clear_old_ephemeris:
ephems_dict[k] = v
else:
ephems_dict[k].extend(v)
def add_ephem_fetched_time(self, ephems, fetched_times):
min_epochs = []
max_epochs = []
for v in ephems.values():
if len(v) > 0:
min_ephem, max_ephem = self.get_epoch_range(v)
min_epochs.append(min_ephem)
max_epochs.append(max_ephem)
if len(min_epochs) > 0:
min_epoch = min(min_epochs)
max_epoch = max(max_epochs)
fetched_times.add(min_epoch, max_epoch)
def get_nav_data(self, time):
def download_and_parse(constellation, parse_rinex_nav_func):
file_path = download_nav(time, cache_dir=self.cache_dir, constellation=constellation)
return parse_rinex_nav_func(file_path) if file_path else {}
fetched_ephems = {}
if ConstellationId.GPS in self.valid_const:
fetched_ephems = download_and_parse(ConstellationId.GPS, parse_rinex_nav_msg_gps)
if ConstellationId.GLONASS in self.valid_const:
for k, v in download_and_parse(ConstellationId.GLONASS, parse_rinex_nav_msg_glonass).items():
fetched_ephems.setdefault(k, []).extend(v)
self.add_navs(fetched_ephems)
if sum([len(v) for v in fetched_ephems.values()]) == 0:
begin_day = GPSTime(time.week, SECS_IN_DAY * (time.tow // SECS_IN_DAY))
end_day = GPSTime(time.week, SECS_IN_DAY * (1 + (time.tow // SECS_IN_DAY)))
self.navs_fetched_times.add(begin_day, end_day)
def download_parse_orbit(self, gps_time: GPSTime, skip_before_epoch=None) -> Dict[str, List[PolyEphemeris]]:
# Download multiple days to be able to polyfit at the start-end of the day
time_steps = [gps_time - SECS_IN_DAY, gps_time, gps_time + SECS_IN_DAY]
with ThreadPoolExecutor() as executor:
futures_other = [executor.submit(download_orbits_russia_src, t, self.cache_dir, self.valid_ephem_types) for t in time_steps]
futures_gps = None
if ConstellationId.GPS in self.valid_const:
futures_gps = [executor.submit(download_orbits_gps, t, self.cache_dir, self.valid_ephem_types) for t in time_steps]
ephems_other = parse_sp3_orbits([f.result() for f in futures_other if f.result()], self.valid_const, skip_before_epoch)
ephems_us = parse_sp3_orbits([f.result() for f in futures_gps if f.result()], self.valid_const, skip_before_epoch) if futures_gps else {}
return {k: ephems_other.get(k, []) + ephems_us.get(k, []) for k in set(list(ephems_other.keys()) + list(ephems_us.keys()))}
def download_parse_prediction_orbit(self, gps_time: GPSTime):
assert EphemerisType.ULTRA_RAPID_ORBIT in self.valid_ephem_types
skip_until_epoch = gps_time - 2 * SECS_IN_HR
result = download_prediction_orbits_russia_src(gps_time, self.cache_dir)
if result is not None:
result = [result]
elif ConstellationId.GPS in self.valid_const:
# Slower fallback. Russia src prediction orbits are published from 2022
result = [download_orbits_gps(t, self.cache_dir, self.valid_ephem_types) for t in [gps_time - SECS_IN_DAY, gps_time]]
if result is None:
return {}
return parse_sp3_orbits(result, self.valid_const, skip_until_epoch=skip_until_epoch)
def get_orbit_data(self, time: GPSTime, only_predictions=False):
if only_predictions:
ephems_sp3 = self.download_parse_prediction_orbit(time)
else:
ephems_sp3 = self.download_parse_orbit(time)
if sum([len(v) for v in ephems_sp3.values()]) < 5:
raise RuntimeError(f'No orbit data found. For Time {time.as_datetime()} constellations {self.valid_const} valid ephem types {self.valid_ephem_types}')
self.add_ephem_fetched_time(ephems_sp3, self.orbit_fetched_times)
self.add_orbits(ephems_sp3)
def get_dcb_data(self, time):
file_path_dcb = download_dcb(time, cache_dir=self.cache_dir)
dcbs = parse_dcbs(file_path_dcb, self.valid_const)
for dcb in dcbs:
self.dcbs[dcb.prn].append(dcb)
if len(dcbs) != 0:
min_epoch, max_epoch = self.get_epoch_range(dcbs)
self.dcbs_fetched_times.add(min_epoch, max_epoch)
def get_epoch_range(self, new_ephems):
min_ephem = min(new_ephems, key=lambda e: e.epoch)
max_ephem = max(new_ephems, key=lambda e: e.epoch)
min_epoch = min_ephem.epoch - min_ephem.max_time_diff
max_epoch = max_ephem.epoch + max_ephem.max_time_diff
return min_epoch, max_epoch
def get_ionex_data(self, time):
file_path_ionex = download_ionex(time, cache_dir=self.cache_dir)
ionex_maps = parse_ionex(file_path_ionex)
for im in ionex_maps:
self.ionex_maps.append(im)
def get_dgps_data(self, time, recv_pos):
station_names = get_closest_station_names(recv_pos, k=8, max_distance=MAX_DGPS_DISTANCE, cache_dir=self.cache_dir)
for station_name in station_names:
file_path_station = download_cors_station(time, station_name, cache_dir=self.cache_dir)
if file_path_station:
dgps = parse_dgps(station_name, file_path_station,
self, max_distance=MAX_DGPS_DISTANCE,
required_constellations=self.valid_const)
if dgps is not None:
self.dgps_delays.append(dgps)
break
def get_tgd_from_nav(self, prn, time):
if get_constellation(prn) not in self.valid_const:
return None
eph = self.get_nav(prn, time)
if eph:
return eph.get_tgd()
return None
def get_eph(self, prn, time):
if get_constellation(prn) not in self.valid_const:
return None
eph = None
if self.pull_orbit:
eph = self.get_orbit(prn, time)
if not eph and self.pull_nav:
eph = self.get_nav(prn, time)
if not eph and self.use_qcom_poly:
eph = self.get_qcom_poly(prn, time)
return eph
def get_sat_info(self, prn, time):
eph = self.get_eph(prn, time)
if eph:
return eph.get_sat_info(time)
return None
def get_all_sat_info(self, time):
ephs = {}
if self.pull_orbit:
ephs = self.get_orbits(time)
if len(ephs) == 0 and self.pull_nav:
ephs = self.get_navs(time)
return {prn: eph.get_sat_info(time) for prn, eph in ephs.items()}
def get_glonass_channel(self, prn, time):
nav = self.get_nav(prn, time)
if nav:
return nav.channel
return None
def get_frequency(self, prn, time, signal='C1C'):
if get_constellation(prn) == ConstellationId.GPS:
switch = {'1': constants.GPS_L1,
'2': constants.GPS_L2,
'5': constants.GPS_L5,
'6': constants.GALILEO_E6,
'7': constants.GALILEO_E5B,
'8': constants.GALILEO_E5AB}
freq = switch.get(signal[1])
if freq:
return freq
raise NotImplementedError("Dont know this GPS frequency: ", signal, prn)
elif get_constellation(prn) == ConstellationId.GLONASS:
n = self.get_glonass_channel(prn, time)
if n is None:
return None
switch = {'1': constants.GLONASS_L1 + n * constants.GLONASS_L1_DELTA,
'2': constants.GLONASS_L2 + n * constants.GLONASS_L2_DELTA,
'5': constants.GLONASS_L5 + n * constants.GLONASS_L5_DELTA,
'6': constants.GALILEO_E6,
'7': constants.GALILEO_E5B,
'8': constants.GALILEO_E5AB}
freq = switch.get(signal[1])
if freq:
return freq
raise NotImplementedError("Dont know this GLONASS frequency: ", signal, prn)
def get_delay(self, prn, time, rcv_pos, no_dgps=False, signal='C1C', freq=None):
sat_info = self.get_sat_info(prn, time)
if sat_info is None:
return None
sat_pos = sat_info[0]
el, az = get_el_az(rcv_pos, sat_pos)
if el < 0.2:
return None
if self.dgps and not no_dgps:
return self._get_delay_dgps(prn, rcv_pos, time)
ionex = self.get_ionex(time)
if not freq and ionex is not None:
freq = self.get_frequency(prn, time, signal)
dcb = self.get_dcb(prn, time)
# When using internet we expect all data or return None
if self.auto_update and (ionex is None or dcb is None or freq is None):
return None
if ionex is not None:
iono_delay = ionex.get_delay(rcv_pos, az, el, sat_pos, time, freq)
else:
# 5m vertical delay is a good default
iono_delay = get_slant_delay(rcv_pos, az, el, sat_pos, time, freq, vertical_delay=5.0)
trop_delay = saast(rcv_pos, el)
code_bias = dcb.get_delay(signal) if dcb is not None else 0.
return iono_delay + trop_delay + code_bias
def _get_delay_dgps(self, prn, rcv_pos, time):
dgps_corrections = self.get_dgps_corrections(time, rcv_pos)
if dgps_corrections is None:
return None
return dgps_corrections.get_delay(prn, time)
def _get_latest_valid_data(self, data, latest_data, download_data_func, time, skip_download=False, recv_pos=None):
def is_valid(latest_data):
if recv_pos is None:
return latest_data is not None and latest_data.valid(time)
else:
return latest_data is not None and latest_data.valid(time, recv_pos)
if is_valid(latest_data):
return latest_data
latest_data = get_closest(time, data, recv_pos=recv_pos)
if is_valid(latest_data):
return latest_data
if skip_download or not self.auto_update:
return None
if recv_pos is not None:
download_data_func(time, recv_pos)
else:
download_data_func(time)
latest_data = get_closest(time, data, recv_pos=recv_pos)
if is_valid(latest_data):
return latest_data
return None