Skip to content

Commit f641ec0

Browse files
gijskoningspektor56
authored andcommitted
Laikad: Cache orbit and nav data (commaai#24831)
* Cache orbit and nav data * Cleanup * Cleanup * Use ProcessPoolExecutor to fetch orbits * update laika repo * Minor * Create json de/serializers Save cache only 1 minute at max * Update laika repo * Speed up json by caching json in ephemeris class * Update laika * Fix test * Use constant
1 parent a2b4f4c commit f641ec0

File tree

3 files changed

+117
-14
lines changed

3 files changed

+117
-14
lines changed

common/params.cc

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ std::unordered_map<std::string, uint32_t> keys = {
127127
{"IsTakingSnapshot", CLEAR_ON_MANAGER_START},
128128
{"IsUpdateAvailable", CLEAR_ON_MANAGER_START},
129129
{"JoystickDebugMode", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF},
130+
{"LaikadEphemeris", PERSISTENT},
130131
{"LastAthenaPingTime", CLEAR_ON_MANAGER_START},
131132
{"LastGPSPosition", PERSISTENT},
132133
{"LastManagerExitReason", CLEAR_ON_MANAGER_START},

selfdrive/locationd/laikad.py

+56-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python3
2+
import json
23
import time
34
from concurrent.futures import Future, ProcessPoolExecutor
45
from typing import List, Optional
@@ -9,9 +10,10 @@
910
from numpy.linalg import linalg
1011

1112
from cereal import log, messaging
13+
from common.params import Params, put_nonblocking
1214
from laika import AstroDog
1315
from laika.constants import SECS_IN_HR, SECS_IN_MIN
14-
from laika.ephemeris import EphemerisType, convert_ublox_ephem
16+
from laika.ephemeris import Ephemeris, EphemerisType, convert_ublox_ephem
1517
from laika.gps_time import GPSTime
1618
from laika.helpers import ConstellationId
1719
from laika.raw_gnss import GNSSMeasurement, calc_pos_fix, correct_measurements, process_measurements, read_raw_ublox
@@ -22,16 +24,40 @@
2224
from selfdrive.swaglog import cloudlog
2325

2426
MAX_TIME_GAP = 10
27+
EPHEMERIS_CACHE = 'LaikadEphemeris'
28+
CACHE_VERSION = 0.1
2529

2630

2731
class Laikad:
28-
29-
def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV)):
30-
self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types)
32+
def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV),
33+
save_ephemeris=False):
34+
self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True)
3135
self.gnss_kf = GNSSKalman(GENERATED_DIR)
3236
self.orbit_fetch_executor = ProcessPoolExecutor()
3337
self.orbit_fetch_future: Optional[Future] = None
3438
self.last_fetch_orbits_t = None
39+
self.last_cached_t = None
40+
self.save_ephemeris = save_ephemeris
41+
self.load_cache()
42+
43+
def load_cache(self):
44+
cache = Params().get(EPHEMERIS_CACHE)
45+
if not cache:
46+
return
47+
try:
48+
cache = json.loads(cache, object_hook=deserialize_hook)
49+
self.astro_dog.add_orbits(cache['orbits'])
50+
self.astro_dog.add_navs(cache['nav'])
51+
self.last_fetch_orbits_t = cache['last_fetch_orbits_t']
52+
except json.decoder.JSONDecodeError:
53+
cloudlog.exception("Error parsing cache")
54+
55+
def cache_ephemeris(self, t: GPSTime):
56+
if self.save_ephemeris and (self.last_cached_t is None or t - self.last_cached_t > SECS_IN_MIN):
57+
put_nonblocking(EPHEMERIS_CACHE, json.dumps(
58+
{'version': CACHE_VERSION, 'last_fetch_orbits_t': self.last_fetch_orbits_t, 'orbits': self.astro_dog.orbits, 'nav': self.astro_dog.nav},
59+
cls=CacheSerializer))
60+
self.last_cached_t = t
3561

3662
def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False):
3763
if ublox_msg.which == 'measurementReport':
@@ -83,7 +109,8 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False):
83109
return dat
84110
elif ublox_msg.which == 'ephemeris':
85111
ephem = convert_ublox_ephem(ublox_msg.ephemeris)
86-
self.astro_dog.add_navs([ephem])
112+
self.astro_dog.add_navs({ephem.prn: [ephem]})
113+
self.cache_ephemeris(t=ephem.epoch)
87114
# elif ublox_msg.which == 'ionoData':
88115
# todo add this. Needed to better correct messages offline. First fix ublox_msg.cc to sent them.
89116

@@ -101,7 +128,7 @@ def update_localizer(self, pos_fix, t: float, measurements: List[GNSSMeasurement
101128
cloudlog.error("Gnss kalman std too far")
102129

103130
if len(pos_fix) == 0:
104-
cloudlog.warning("Position fix not available when resetting kalman filter")
131+
cloudlog.info("Position fix not available when resetting kalman filter")
105132
return
106133
post_est = pos_fix[0][:3].tolist()
107134
self.init_gnss_localizer(post_est)
@@ -134,10 +161,11 @@ def fetch_orbits(self, t: GPSTime, block):
134161
self.orbit_fetch_future.result()
135162
if self.orbit_fetch_future.done():
136163
ret = self.orbit_fetch_future.result()
164+
self.last_fetch_orbits_t = t
137165
if ret:
138166
self.astro_dog.orbits, self.astro_dog.orbit_fetched_times = ret
167+
self.cache_ephemeris(t=t)
139168
self.orbit_fetch_future = None
140-
self.last_fetch_orbits_t = t
141169

142170

143171
def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types):
@@ -193,11 +221,31 @@ def get_bearing_from_gnss(ecef_pos, ecef_vel, vel_std):
193221
return float(np.rad2deg(bearing)), float(bearing_std)
194222

195223

224+
class CacheSerializer(json.JSONEncoder):
225+
226+
def default(self, o):
227+
if isinstance(o, Ephemeris):
228+
return o.to_json()
229+
if isinstance(o, GPSTime):
230+
return o.__dict__
231+
if isinstance(o, np.ndarray):
232+
return o.tolist()
233+
return json.JSONEncoder.default(self, o)
234+
235+
236+
def deserialize_hook(dct):
237+
if 'ephemeris' in dct:
238+
return Ephemeris.from_json(dct)
239+
if 'week' in dct:
240+
return GPSTime(dct['week'], dct['tow'])
241+
return dct
242+
243+
196244
def main():
197245
sm = messaging.SubMaster(['ubloxGnss'])
198246
pm = messaging.PubMaster(['gnssMeasurements'])
199247

200-
laikad = Laikad()
248+
laikad = Laikad(save_ephemeris=True)
201249
while True:
202250
sm.update()
203251

selfdrive/locationd/test/test_laikad.py

+60-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
#!/usr/bin/env python3
2+
import time
23
import unittest
34
from datetime import datetime
45
from unittest import mock
5-
from unittest.mock import Mock
6+
from unittest.mock import Mock, patch
67

8+
from common.params import Params
79
from laika.ephemeris import EphemerisType
810
from laika.gps_time import GPSTime
9-
from laika.helpers import ConstellationId
11+
from laika.helpers import ConstellationId, TimeRangeHolder
1012
from laika.raw_gnss import GNSSMeasurement, read_raw_ublox
11-
from selfdrive.locationd.laikad import Laikad, create_measurement_msg
13+
from selfdrive.locationd.laikad import EPHEMERIS_CACHE, Laikad, create_measurement_msg
1214
from selfdrive.test.openpilotci import get_url
1315
from tools.lib.logreader import LogReader
1416

@@ -20,12 +22,14 @@ def get_log(segs=range(0)):
2022
return [m for m in logs if m.which() == 'ubloxGnss']
2123

2224

23-
def verify_messages(lr, laikad):
25+
def verify_messages(lr, laikad, return_one_success=False):
2426
good_msgs = []
2527
for m in lr:
2628
msg = laikad.process_ublox_msg(m.ubloxGnss, m.logMonoTime, block=True)
2729
if msg is not None and len(msg.gnssMeasurements.correctedMeasurements) > 0:
2830
good_msgs.append(msg)
31+
if return_one_success:
32+
return msg
2933
return good_msgs
3034

3135

@@ -35,6 +39,9 @@ class TestLaikad(unittest.TestCase):
3539
def setUpClass(cls):
3640
cls.logs = get_log(range(1))
3741

42+
def setUp(self):
43+
Params().delete(EPHEMERIS_CACHE)
44+
3845
def test_create_msg_without_errors(self):
3946
gpstime = GPSTime.from_datetime(datetime.now())
4047
meas = GNSSMeasurement(ConstellationId.GPS, 1, gpstime.week, gpstime.tow, {'C1C': 0., 'D1C': 0.}, {'C1C': 0., 'D1C': 0.})
@@ -81,8 +88,7 @@ def test_laika_get_orbits(self):
8188
first_gps_time = self.get_first_gps_time()
8289
# Pretend process has loaded the orbits on startup by using the time of the first gps message.
8390
laikad.fetch_orbits(first_gps_time, block=True)
84-
self.assertEqual(29, len(laikad.astro_dog.orbits.values()))
85-
self.assertGreater(min([len(v) for v in laikad.astro_dog.orbits.values()]), 0)
91+
self.dict_has_values(laikad.astro_dog.orbits)
8692

8793
@unittest.skip("Use to debug live data")
8894
def test_laika_get_orbits_now(self):
@@ -109,6 +115,54 @@ def test_get_orbits_in_process(self):
109115
self.assertGreater(len(laikad.astro_dog.orbit_fetched_times._ranges), 0)
110116
self.assertEqual(None, laikad.orbit_fetch_future)
111117

118+
def test_cache(self):
119+
laikad = Laikad(auto_update=True, save_ephemeris=True)
120+
first_gps_time = self.get_first_gps_time()
121+
122+
def wait_for_cache():
123+
max_time = 2
124+
while Params().get(EPHEMERIS_CACHE) is None:
125+
time.sleep(0.1)
126+
max_time -= 0.1
127+
if max_time == 0:
128+
self.fail("Cache has not been written after 2 seconds")
129+
# Test cache with no ephemeris
130+
laikad.cache_ephemeris(t=GPSTime(0, 0))
131+
wait_for_cache()
132+
Params().delete(EPHEMERIS_CACHE)
133+
134+
laikad.astro_dog.get_navs(first_gps_time)
135+
laikad.fetch_orbits(first_gps_time, block=True)
136+
137+
# Wait for cache to save
138+
wait_for_cache()
139+
140+
# Check both nav and orbits separate
141+
laikad = Laikad(auto_update=False, valid_ephem_types=EphemerisType.NAV)
142+
# Verify orbits and nav are loaded from cache
143+
self.dict_has_values(laikad.astro_dog.orbits)
144+
self.dict_has_values(laikad.astro_dog.nav)
145+
# Verify cache is working for only nav by running a segment
146+
msg = verify_messages(self.logs, laikad, return_one_success=True)
147+
self.assertIsNotNone(msg)
148+
149+
with patch('selfdrive.locationd.laikad.get_orbit_data', return_value=None) as mock_method:
150+
# Verify no orbit downloads even if orbit fetch times is reset since the cache has recently been saved and we don't want to download high frequently
151+
laikad.astro_dog.orbit_fetched_times = TimeRangeHolder()
152+
laikad.fetch_orbits(first_gps_time, block=False)
153+
mock_method.assert_not_called()
154+
155+
# Verify cache is working for only orbits by running a segment
156+
laikad = Laikad(auto_update=False, valid_ephem_types=EphemerisType.ULTRA_RAPID_ORBIT)
157+
msg = verify_messages(self.logs, laikad, return_one_success=True)
158+
self.assertIsNotNone(msg)
159+
# Verify orbit data is not downloaded
160+
mock_method.assert_not_called()
161+
162+
def dict_has_values(self, dct):
163+
self.assertGreater(len(dct), 0)
164+
self.assertGreater(min([len(v) for v in dct.values()]), 0)
165+
112166

113167
if __name__ == "__main__":
114168
unittest.main()

0 commit comments

Comments
 (0)