|
|
@ -2,13 +2,13 @@ |
|
|
|
import json |
|
|
|
import json |
|
|
|
import time |
|
|
|
import time |
|
|
|
from concurrent.futures import Future, ProcessPoolExecutor |
|
|
|
from concurrent.futures import Future, ProcessPoolExecutor |
|
|
|
|
|
|
|
from enum import IntEnum |
|
|
|
from typing import List, Optional |
|
|
|
from typing import List, Optional |
|
|
|
|
|
|
|
|
|
|
|
import numpy as np |
|
|
|
import numpy as np |
|
|
|
from collections import defaultdict |
|
|
|
from collections import defaultdict |
|
|
|
|
|
|
|
|
|
|
|
import sympy |
|
|
|
import sympy |
|
|
|
from numpy.linalg import linalg |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from cereal import log, messaging |
|
|
|
from cereal import log, messaging |
|
|
|
from common.params import Params, put_nonblocking |
|
|
|
from common.params import Params, put_nonblocking |
|
|
@ -30,7 +30,7 @@ CACHE_VERSION = 0.1 |
|
|
|
|
|
|
|
|
|
|
|
class Laikad: |
|
|
|
class Laikad: |
|
|
|
def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV), |
|
|
|
def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV), |
|
|
|
save_ephemeris=False): |
|
|
|
save_ephemeris=False, last_known_position=None): |
|
|
|
self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True) |
|
|
|
self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True) |
|
|
|
self.gnss_kf = GNSSKalman(GENERATED_DIR) |
|
|
|
self.gnss_kf = GNSSKalman(GENERATED_DIR) |
|
|
|
self.orbit_fetch_executor = ProcessPoolExecutor() |
|
|
|
self.orbit_fetch_executor = ProcessPoolExecutor() |
|
|
@ -40,6 +40,7 @@ class Laikad: |
|
|
|
self.save_ephemeris = save_ephemeris |
|
|
|
self.save_ephemeris = save_ephemeris |
|
|
|
self.load_cache() |
|
|
|
self.load_cache() |
|
|
|
self.posfix_functions = {constellation: get_posfix_sympy_fun(constellation) for constellation in (ConstellationId.GPS, ConstellationId.GLONASS)} |
|
|
|
self.posfix_functions = {constellation: get_posfix_sympy_fun(constellation) for constellation in (ConstellationId.GPS, ConstellationId.GLONASS)} |
|
|
|
|
|
|
|
self.last_pos_fix = last_known_position |
|
|
|
|
|
|
|
|
|
|
|
def load_cache(self): |
|
|
|
def load_cache(self): |
|
|
|
cache = Params().get(EPHEMERIS_CACHE) |
|
|
|
cache = Params().get(EPHEMERIS_CACHE) |
|
|
@ -70,27 +71,16 @@ class Laikad: |
|
|
|
processed_measurements = process_measurements(new_meas, self.astro_dog) |
|
|
|
processed_measurements = process_measurements(new_meas, self.astro_dog) |
|
|
|
|
|
|
|
|
|
|
|
min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 4 |
|
|
|
min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 4 |
|
|
|
pos_fix = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) |
|
|
|
pos_fix, pos_fix_residual = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) |
|
|
|
|
|
|
|
if len(pos_fix) > 0: |
|
|
|
|
|
|
|
self.last_pos_fix = pos_fix[:3] |
|
|
|
|
|
|
|
est_pos = self.last_pos_fix |
|
|
|
|
|
|
|
|
|
|
|
t = ublox_mono_time * 1e-9 |
|
|
|
corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog) if est_pos is not None else [] |
|
|
|
kf_pos_std = None |
|
|
|
|
|
|
|
if all(self.kf_valid(t)): |
|
|
|
|
|
|
|
self.gnss_kf.predict(t) |
|
|
|
|
|
|
|
kf_pos_std = np.sqrt(abs(self.gnss_kf.P[GStates.ECEF_POS].diagonal())) |
|
|
|
|
|
|
|
# If localizer is valid use its position to correct measurements |
|
|
|
|
|
|
|
if kf_pos_std is not None and linalg.norm(kf_pos_std) < 100: |
|
|
|
|
|
|
|
est_pos = self.gnss_kf.x[GStates.ECEF_POS] |
|
|
|
|
|
|
|
elif len(pos_fix) > 0 and abs(np.array(pos_fix[1])).mean() < 1000: |
|
|
|
|
|
|
|
est_pos = pos_fix[0][:3] |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
est_pos = None |
|
|
|
|
|
|
|
corrected_measurements = [] |
|
|
|
|
|
|
|
if est_pos is not None: |
|
|
|
|
|
|
|
corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
t = ublox_mono_time * 1e-9 |
|
|
|
self.update_localizer(est_pos, t, corrected_measurements) |
|
|
|
self.update_localizer(est_pos, t, corrected_measurements) |
|
|
|
kf_valid = all(self.kf_valid(t)) |
|
|
|
kf_valid = all(self.kf_valid(t)) |
|
|
|
|
|
|
|
|
|
|
|
ecef_pos = self.gnss_kf.x[GStates.ECEF_POS].tolist() |
|
|
|
ecef_pos = self.gnss_kf.x[GStates.ECEF_POS].tolist() |
|
|
|
ecef_vel = self.gnss_kf.x[GStates.ECEF_VELOCITY].tolist() |
|
|
|
ecef_vel = self.gnss_kf.x[GStates.ECEF_VELOCITY].tolist() |
|
|
|
|
|
|
|
|
|
|
@ -103,6 +93,7 @@ class Laikad: |
|
|
|
dat.gnssMeasurements = { |
|
|
|
dat.gnssMeasurements = { |
|
|
|
"positionECEF": measurement_msg(value=ecef_pos, std=pos_std, valid=kf_valid), |
|
|
|
"positionECEF": measurement_msg(value=ecef_pos, std=pos_std, valid=kf_valid), |
|
|
|
"velocityECEF": measurement_msg(value=ecef_vel, std=vel_std, valid=kf_valid), |
|
|
|
"velocityECEF": measurement_msg(value=ecef_vel, std=vel_std, valid=kf_valid), |
|
|
|
|
|
|
|
"positionFixECEF": measurement_msg(value=pos_fix, std=pos_fix_residual, valid=len(pos_fix) > 0), |
|
|
|
"ubloxMonoTime": ublox_mono_time, |
|
|
|
"ubloxMonoTime": ublox_mono_time, |
|
|
|
"correctedMeasurements": meas_msgs |
|
|
|
"correctedMeasurements": meas_msgs |
|
|
|
} |
|
|
|
} |
|
|
@ -124,13 +115,7 @@ class Laikad: |
|
|
|
cloudlog.error("Time gap of over 10s detected, gnss kalman reset") |
|
|
|
cloudlog.error("Time gap of over 10s detected, gnss kalman reset") |
|
|
|
elif not valid[2]: |
|
|
|
elif not valid[2]: |
|
|
|
cloudlog.error("Gnss kalman filter state is nan") |
|
|
|
cloudlog.error("Gnss kalman filter state is nan") |
|
|
|
else: |
|
|
|
self.init_gnss_localizer(est_pos) |
|
|
|
cloudlog.error("Gnss kalman std too far") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if est_pos is None: |
|
|
|
|
|
|
|
cloudlog.info("Position fix not available when resetting kalman filter") |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
self.init_gnss_localizer(est_pos.tolist()) |
|
|
|
|
|
|
|
if len(measurements) > 0: |
|
|
|
if len(measurements) > 0: |
|
|
|
kf_add_observations(self.gnss_kf, t, measurements) |
|
|
|
kf_add_observations(self.gnss_kf, t, measurements) |
|
|
|
else: |
|
|
|
else: |
|
|
@ -141,14 +126,13 @@ class Laikad: |
|
|
|
filter_time = self.gnss_kf.filter.filter_time |
|
|
|
filter_time = self.gnss_kf.filter.filter_time |
|
|
|
return [filter_time is not None, |
|
|
|
return [filter_time is not None, |
|
|
|
filter_time is not None and abs(t - filter_time) < MAX_TIME_GAP, |
|
|
|
filter_time is not None and abs(t - filter_time) < MAX_TIME_GAP, |
|
|
|
all(np.isfinite(self.gnss_kf.x[GStates.ECEF_POS])), |
|
|
|
all(np.isfinite(self.gnss_kf.x[GStates.ECEF_POS]))] |
|
|
|
linalg.norm(self.gnss_kf.P[GStates.ECEF_POS]) < 1e5] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_gnss_localizer(self, est_pos): |
|
|
|
def init_gnss_localizer(self, est_pos): |
|
|
|
x_initial, p_initial_diag = np.copy(GNSSKalman.x_initial), np.copy(np.diagonal(GNSSKalman.P_initial)) |
|
|
|
x_initial, p_initial_diag = np.copy(GNSSKalman.x_initial), np.copy(np.diagonal(GNSSKalman.P_initial)) |
|
|
|
x_initial[GStates.ECEF_POS] = est_pos |
|
|
|
if est_pos is not None: |
|
|
|
|
|
|
|
x_initial[GStates.ECEF_POS] = est_pos |
|
|
|
p_initial_diag[GStates.ECEF_POS] = 1000 ** 2 |
|
|
|
p_initial_diag[GStates.ECEF_POS] = 1000 ** 2 |
|
|
|
|
|
|
|
|
|
|
|
self.gnss_kf.init_state(x_initial, covs_diag=p_initial_diag) |
|
|
|
self.gnss_kf.init_state(x_initial, covs_diag=p_initial_diag) |
|
|
|
|
|
|
|
|
|
|
|
def fetch_orbits(self, t: GPSTime, block): |
|
|
|
def fetch_orbits(self, t: GPSTime, block): |
|
|
@ -192,6 +176,27 @@ def create_measurement_msg(meas: GNSSMeasurement): |
|
|
|
c.pseudorangeRateStd = float(meas.observables_std['D1C']) |
|
|
|
c.pseudorangeRateStd = float(meas.observables_std['D1C']) |
|
|
|
c.satPos = meas.sat_pos_final.tolist() |
|
|
|
c.satPos = meas.sat_pos_final.tolist() |
|
|
|
c.satVel = meas.sat_vel.tolist() |
|
|
|
c.satVel = meas.sat_vel.tolist() |
|
|
|
|
|
|
|
c.satVel = meas.sat_vel.tolist() |
|
|
|
|
|
|
|
ephem = meas.sat_ephemeris |
|
|
|
|
|
|
|
assert ephem is not None |
|
|
|
|
|
|
|
if ephem.eph_type == EphemerisType.NAV: |
|
|
|
|
|
|
|
source_type = EphemerisSourceType.nav |
|
|
|
|
|
|
|
week, time_of_week = -1, -1 |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
assert ephem.file_epoch is not None |
|
|
|
|
|
|
|
week = ephem.file_epoch.week |
|
|
|
|
|
|
|
time_of_week = ephem.file_epoch.tow |
|
|
|
|
|
|
|
file_src = ephem.file_source |
|
|
|
|
|
|
|
if file_src == 'igu': # example nasa: '2214/igu22144_00.sp3.Z' |
|
|
|
|
|
|
|
source_type = EphemerisSourceType.nasaUltraRapid |
|
|
|
|
|
|
|
elif file_src == 'Sta': # example nasa: '22166/ultra/Stark_1D_22061518.sp3' |
|
|
|
|
|
|
|
source_type = EphemerisSourceType.glonassIacUltraRapid |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
raise Exception(f"Didn't expect file source {file_src}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
c.ephemerisSource.type = source_type.value |
|
|
|
|
|
|
|
c.ephemerisSource.gpsWeek = week |
|
|
|
|
|
|
|
c.ephemerisSource.gpsTimeOfWeek = time_of_week |
|
|
|
return c |
|
|
|
return c |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -242,12 +247,12 @@ def calc_pos_fix_gauss_newton(measurements, posfix_functions, x0=None, signal='C |
|
|
|
x0 = [0, 0, 0, 0, 0] |
|
|
|
x0 = [0, 0, 0, 0, 0] |
|
|
|
n = len(measurements) |
|
|
|
n = len(measurements) |
|
|
|
if n < min_measurements: |
|
|
|
if n < min_measurements: |
|
|
|
return [] |
|
|
|
return [], [] |
|
|
|
|
|
|
|
|
|
|
|
Fx_pos = pr_residual(measurements, posfix_functions, signal=signal) |
|
|
|
Fx_pos = pr_residual(measurements, posfix_functions, signal=signal) |
|
|
|
x = gauss_newton(Fx_pos, x0) |
|
|
|
x = gauss_newton(Fx_pos, x0) |
|
|
|
residual, _ = Fx_pos(x, weight=1.0) |
|
|
|
residual, _ = Fx_pos(x, weight=1.0) |
|
|
|
return x, residual |
|
|
|
return x.tolist(), residual.tolist() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pr_residual(measurements, posfix_functions, signal='C1C'): |
|
|
|
def pr_residual(measurements, posfix_functions, signal='C1C'): |
|
|
@ -314,10 +319,16 @@ def get_posfix_sympy_fun(constellation): |
|
|
|
return sympy.lambdify([x, y, z, bc, bg, pr, sat_x, sat_y, sat_z, weight], res) |
|
|
|
return sympy.lambdify([x, y, z, bc, bg, pr, sat_x, sat_y, sat_z, weight], res) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EphemerisSourceType(IntEnum): |
|
|
|
|
|
|
|
nav = 0 |
|
|
|
|
|
|
|
nasaUltraRapid = 1 |
|
|
|
|
|
|
|
glonassIacUltraRapid = 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(): |
|
|
|
def main(): |
|
|
|
sm = messaging.SubMaster(['ubloxGnss']) |
|
|
|
sm = messaging.SubMaster(['ubloxGnss']) |
|
|
|
pm = messaging.PubMaster(['gnssMeasurements']) |
|
|
|
pm = messaging.PubMaster(['gnssMeasurements']) |
|
|
|
|
|
|
|
# todo get last_known_position |
|
|
|
laikad = Laikad(save_ephemeris=True) |
|
|
|
laikad = Laikad(save_ephemeris=True) |
|
|
|
while True: |
|
|
|
while True: |
|
|
|
sm.update() |
|
|
|
sm.update() |
|
|
|