navd.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. #!/usr/bin/env python3
  2. import math
  3. import os
  4. import threading
  5. import requests
  6. import numpy as np
  7. import cereal.messaging as messaging
  8. from cereal import log
  9. from common.api import Api
  10. from common.params import Params
  11. from common.realtime import Ratekeeper
  12. from common.transformations.coordinates import ecef2geodetic
  13. from selfdrive.navd.helpers import (Coordinate, coordinate_from_param,
  14. distance_along_geometry, maxspeed_to_ms,
  15. minimum_distance,
  16. parse_banner_instructions)
  17. from system.swaglog import cloudlog
  18. REROUTE_DISTANCE = 25
  19. MANEUVER_TRANSITION_THRESHOLD = 10
  20. VALID_POS_STD = 50.0
  21. class RouteEngine:
  22. def __init__(self, sm, pm):
  23. self.sm = sm
  24. self.pm = pm
  25. self.params = Params()
  26. # Get last gps position from params
  27. self.last_position = coordinate_from_param("LastGPSPosition", self.params)
  28. self.last_bearing = None
  29. self.gps_ok = False
  30. self.localizer_valid = False
  31. self.nav_destination = None
  32. self.step_idx = None
  33. self.route = None
  34. self.route_geometry = None
  35. self.recompute_backoff = 0
  36. self.recompute_countdown = 0
  37. self.ui_pid = None
  38. if "MAPBOX_TOKEN" in os.environ:
  39. self.mapbox_token = os.environ["MAPBOX_TOKEN"]
  40. self.mapbox_host = "https://api.mapbox.com"
  41. else:
  42. try:
  43. self.mapbox_token = Api(self.params.get("DongleId", encoding='utf8')).get_token(expiry_hours=4 * 7 * 24)
  44. except FileNotFoundError:
  45. cloudlog.exception("Failed to generate mapbox token due to missing private key. Ensure device is registered.")
  46. self.mapbox_token = ""
  47. self.mapbox_host = "https://maps.comma.ai"
  48. def update(self):
  49. self.sm.update(0)
  50. if self.sm.updated["managerState"]:
  51. ui_pid = [p.pid for p in self.sm["managerState"].processes if p.name == "ui" and p.running]
  52. if ui_pid:
  53. if self.ui_pid and self.ui_pid != ui_pid[0]:
  54. cloudlog.warning("UI restarting, sending route")
  55. threading.Timer(5.0, self.send_route).start()
  56. self.ui_pid = ui_pid[0]
  57. self.update_location()
  58. self.recompute_route()
  59. self.send_instruction()
  60. def update_location(self):
  61. location = self.sm['liveLocationKalman']
  62. laikad = self.sm['gnssMeasurements']
  63. locationd_valid = (location.status == log.LiveLocationKalman.Status.valid) and location.positionGeodetic.valid
  64. laikad_valid = laikad.positionECEF.valid and np.linalg.norm(laikad.positionECEF.std) < VALID_POS_STD
  65. self.localizer_valid = locationd_valid or laikad_valid
  66. self.gps_ok = location.gpsOK or laikad_valid
  67. if locationd_valid:
  68. self.last_bearing = math.degrees(location.calibratedOrientationNED.value[2])
  69. self.last_position = Coordinate(location.positionGeodetic.value[0], location.positionGeodetic.value[1])
  70. elif laikad_valid:
  71. geodetic = ecef2geodetic(laikad.positionECEF.value)
  72. self.last_position = Coordinate(geodetic[0], geodetic[1])
  73. self.last_bearing = None
  74. def recompute_route(self):
  75. if self.last_position is None:
  76. return
  77. new_destination = coordinate_from_param("NavDestination", self.params)
  78. if new_destination is None:
  79. self.clear_route()
  80. return
  81. should_recompute = self.should_recompute()
  82. if new_destination != self.nav_destination:
  83. cloudlog.warning(f"Got new destination from NavDestination param {new_destination}")
  84. should_recompute = True
  85. # Don't recompute when GPS drifts in tunnels
  86. if not self.gps_ok and self.step_idx is not None:
  87. return
  88. if self.recompute_countdown == 0 and should_recompute:
  89. self.recompute_countdown = 2**self.recompute_backoff
  90. self.recompute_backoff = min(6, self.recompute_backoff + 1)
  91. self.calculate_route(new_destination)
  92. else:
  93. self.recompute_countdown = max(0, self.recompute_countdown - 1)
  94. def calculate_route(self, destination):
  95. cloudlog.warning(f"Calculating route {self.last_position} -> {destination}")
  96. self.nav_destination = destination
  97. lang = self.params.get('LanguageSetting', encoding='utf8')
  98. if lang is not None:
  99. lang = lang.replace('main_', '')
  100. params = {
  101. 'access_token': self.mapbox_token,
  102. 'annotations': 'maxspeed',
  103. 'geometries': 'geojson',
  104. 'overview': 'full',
  105. 'steps': 'true',
  106. 'banner_instructions': 'true',
  107. 'alternatives': 'false',
  108. 'language': lang,
  109. }
  110. if self.last_bearing is not None:
  111. params['bearings'] = f"{(self.last_bearing + 360) % 360:.0f},90;"
  112. url = self.mapbox_host + f'/directions/v5/mapbox/driving-traffic/{self.last_position.longitude},{self.last_position.latitude};{destination.longitude},{destination.latitude}'
  113. try:
  114. resp = requests.get(url, params=params, timeout=10)
  115. resp.raise_for_status()
  116. r = resp.json()
  117. if len(r['routes']):
  118. self.route = r['routes'][0]['legs'][0]['steps']
  119. self.route_geometry = []
  120. maxspeed_idx = 0
  121. maxspeeds = r['routes'][0]['legs'][0]['annotation']['maxspeed']
  122. # Convert coordinates
  123. for step in self.route:
  124. coords = []
  125. for c in step['geometry']['coordinates']:
  126. coord = Coordinate.from_mapbox_tuple(c)
  127. # Last step does not have maxspeed
  128. if (maxspeed_idx < len(maxspeeds)):
  129. maxspeed = maxspeeds[maxspeed_idx]
  130. if ('unknown' not in maxspeed) and ('none' not in maxspeed):
  131. coord.annotations['maxspeed'] = maxspeed_to_ms(maxspeed)
  132. coords.append(coord)
  133. maxspeed_idx += 1
  134. self.route_geometry.append(coords)
  135. maxspeed_idx -= 1 # Every segment ends with the same coordinate as the start of the next
  136. self.step_idx = 0
  137. else:
  138. cloudlog.warning("Got empty route response")
  139. self.clear_route()
  140. except requests.exceptions.RequestException:
  141. cloudlog.exception("failed to get route")
  142. self.clear_route()
  143. self.send_route()
  144. def send_instruction(self):
  145. msg = messaging.new_message('navInstruction')
  146. if self.step_idx is None:
  147. msg.valid = False
  148. self.pm.send('navInstruction', msg)
  149. return
  150. step = self.route[self.step_idx]
  151. geometry = self.route_geometry[self.step_idx]
  152. along_geometry = distance_along_geometry(geometry, self.last_position)
  153. distance_to_maneuver_along_geometry = step['distance'] - along_geometry
  154. # Current instruction
  155. msg.navInstruction.maneuverDistance = distance_to_maneuver_along_geometry
  156. parse_banner_instructions(msg.navInstruction, step['bannerInstructions'], distance_to_maneuver_along_geometry)
  157. # Compute total remaining time and distance
  158. remaining = 1.0 - along_geometry / max(step['distance'], 1)
  159. total_distance = step['distance'] * remaining
  160. total_time = step['duration'] * remaining
  161. total_time_typical = step['duration_typical'] * remaining
  162. # Add up totals for future steps
  163. for i in range(self.step_idx + 1, len(self.route)):
  164. total_distance += self.route[i]['distance']
  165. total_time += self.route[i]['duration']
  166. total_time_typical += self.route[i]['duration_typical']
  167. msg.navInstruction.distanceRemaining = total_distance
  168. msg.navInstruction.timeRemaining = total_time
  169. msg.navInstruction.timeRemainingTypical = total_time_typical
  170. # Speed limit
  171. closest_idx, closest = min(enumerate(geometry), key=lambda p: p[1].distance_to(self.last_position))
  172. if closest_idx > 0:
  173. # If we are not past the closest point, show previous
  174. if along_geometry < distance_along_geometry(geometry, geometry[closest_idx]):
  175. closest = geometry[closest_idx - 1]
  176. if ('maxspeed' in closest.annotations) and self.localizer_valid:
  177. msg.navInstruction.speedLimit = closest.annotations['maxspeed']
  178. # Speed limit sign type
  179. if 'speedLimitSign' in step:
  180. if step['speedLimitSign'] == 'mutcd':
  181. msg.navInstruction.speedLimitSign = log.NavInstruction.SpeedLimitSign.mutcd
  182. elif step['speedLimitSign'] == 'vienna':
  183. msg.navInstruction.speedLimitSign = log.NavInstruction.SpeedLimitSign.vienna
  184. self.pm.send('navInstruction', msg)
  185. # Transition to next route segment
  186. if distance_to_maneuver_along_geometry < -MANEUVER_TRANSITION_THRESHOLD:
  187. if self.step_idx + 1 < len(self.route):
  188. self.step_idx += 1
  189. self.recompute_backoff = 0
  190. self.recompute_countdown = 0
  191. else:
  192. cloudlog.warning("Destination reached")
  193. Params().remove("NavDestination")
  194. # Clear route if driving away from destination
  195. dist = self.nav_destination.distance_to(self.last_position)
  196. if dist > REROUTE_DISTANCE:
  197. self.clear_route()
  198. def send_route(self):
  199. coords = []
  200. if self.route is not None:
  201. for path in self.route_geometry:
  202. coords += [c.as_dict() for c in path]
  203. msg = messaging.new_message('navRoute')
  204. msg.navRoute.coordinates = coords
  205. self.pm.send('navRoute', msg)
  206. def clear_route(self):
  207. self.route = None
  208. self.route_geometry = None
  209. self.step_idx = None
  210. self.nav_destination = None
  211. def should_recompute(self):
  212. if self.step_idx is None or self.route is None:
  213. return True
  214. # Don't recompute in last segment, assume destination is reached
  215. if self.step_idx == len(self.route) - 1:
  216. return False
  217. # Compute closest distance to all line segments in the current path
  218. min_d = REROUTE_DISTANCE + 1
  219. path = self.route_geometry[self.step_idx]
  220. for i in range(len(path) - 1):
  221. a = path[i]
  222. b = path[i + 1]
  223. if a.distance_to(b) < 1.0:
  224. continue
  225. min_d = min(min_d, minimum_distance(a, b, self.last_position))
  226. return min_d > REROUTE_DISTANCE
  227. # TODO: Check for going wrong way in segment
  228. def main(sm=None, pm=None):
  229. if sm is None:
  230. sm = messaging.SubMaster(['liveLocationKalman', 'gnssMeasurements', 'managerState'])
  231. if pm is None:
  232. pm = messaging.PubMaster(['navInstruction', 'navRoute'])
  233. rk = Ratekeeper(1.0)
  234. route_engine = RouteEngine(sm, pm)
  235. while True:
  236. route_engine.update()
  237. rk.keep_time()
  238. if __name__ == "__main__":
  239. main()