mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-10 13:34:21 +08:00
Compare commits
39 Commits
navigation
...
navigation
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b48214acd2 | ||
|
|
65db08f4d1 | ||
|
|
a1e305333f | ||
|
|
c14b81585e | ||
|
|
98ffbe1308 | ||
|
|
d09f74612f | ||
|
|
b69da9e5ea | ||
|
|
0ccd55a6b5 | ||
|
|
56ca486fe9 | ||
|
|
f46de2d0d5 | ||
|
|
22b010f674 | ||
|
|
365e978b42 | ||
|
|
63e5d0a476 | ||
|
|
43b4e4e271 | ||
|
|
cfb8f3ae24 | ||
|
|
0cc5e56192 | ||
|
|
7d4e5bedaf | ||
|
|
958b4df69f | ||
|
|
72998034e6 | ||
|
|
c2b7087723 | ||
|
|
9c6a4d4a57 | ||
|
|
ca1ce9bcc9 | ||
|
|
31a5a3b3c0 | ||
|
|
3ef3aceb4b | ||
|
|
3b1ada64be | ||
|
|
6a08186434 | ||
|
|
99d954de10 | ||
|
|
b28f33481c | ||
|
|
39342d7b5e | ||
|
|
450fcd4d55 | ||
|
|
551b4dea31 | ||
|
|
90f02040fe | ||
|
|
efcc5ccd15 | ||
|
|
088f6aa407 | ||
|
|
fe5366e5b2 | ||
|
|
1ecb0b0f66 | ||
|
|
51e455db79 | ||
|
|
dc6672fa80 | ||
|
|
088f9d0b59 |
@@ -369,6 +369,7 @@ struct CarControlSP @0xa5cd762cd951a455 {
|
||||
leadOne @2 :LeadData;
|
||||
leadTwo @3 :LeadData;
|
||||
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
|
||||
speed @5 :Float32;
|
||||
|
||||
struct Param {
|
||||
key @0 :Text;
|
||||
@@ -454,7 +455,20 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomReserved10 @0xcb9fd56c7057593a {
|
||||
struct Navigationd @0xcb9fd56c7057593a {
|
||||
upcomingTurn @0 :Text;
|
||||
currentSpeedLimit @1 :UInt16;
|
||||
bannerInstructions @2 :Text;
|
||||
distanceFromRoute @3 :Float32;
|
||||
allManeuvers @4 :List(Maneuver);
|
||||
valid @5 :Bool;
|
||||
|
||||
struct Maneuver {
|
||||
distance @0 :Float32;
|
||||
type @1 :Text;
|
||||
modifier @2 :Text;
|
||||
instruction @3 :Text;
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomReserved11 @0xc2243c65e0340384 {
|
||||
|
||||
@@ -2635,7 +2635,7 @@ struct Event {
|
||||
carStateSP @114 :Custom.CarStateSP;
|
||||
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
||||
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
||||
customReserved10 @136 :Custom.CustomReserved10;
|
||||
navigationd @136 :Custom.Navigationd;
|
||||
customReserved11 @137 :Custom.CustomReserved11;
|
||||
customReserved12 @138 :Custom.CustomReserved12;
|
||||
customReserved13 @139 :Custom.CustomReserved13;
|
||||
|
||||
@@ -89,6 +89,7 @@ _services: dict[str, tuple] = {
|
||||
"carStateSP": (True, 100., 10),
|
||||
"liveMapDataSP": (True, 1., 1),
|
||||
"modelDataV2SP": (True, 20.),
|
||||
"navigationd": (True, 3.),
|
||||
"liveLocationKalman": (True, 20.),
|
||||
|
||||
# debug
|
||||
|
||||
@@ -190,9 +190,11 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
|
||||
|
||||
// Navigation params
|
||||
{"AllowNavigation", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"MapboxToken", {PERSISTENT | BACKUP, STRING}},
|
||||
{"MapboxSettings", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"MapboxRoute", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"MapboxRoute", {PERSISTENT, STRING}},
|
||||
{"MapboxRecompute", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// Neural Network Lateral Control
|
||||
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
Submodule opendbc_repo updated: 62f6f9e4d4...b054629f5e
@@ -3,3 +3,4 @@
|
||||
Navigation daemon with Mapbox integration for semi-offline navigation. This module handles route planning, geocoding, and turn-by-turn instructions to support autonomous driving features.
|
||||
|
||||
- `navigation_helpers/`: Mapbox API integration and navigation instructions processing.
|
||||
- `navigationd`: Navigation service which uses mapbox integration to generate a route and keep it up to date. This service runs at three hz, using keep time to ensure the while loop only updates three times a second rather than every time sm updates, which in this case would be twenty hz (LLK).
|
||||
16
sunnypilot/navd/constants.py
Normal file
16
sunnypilot/navd/constants.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Copyright (c) 2021-, James Vecellio, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
|
||||
class NAV_CV:
|
||||
""" These distances are expected in meters format and convert to desired format """
|
||||
SHORT_DISTANCE_METERS = 200.0
|
||||
QUARTER_MILE = 402.336
|
||||
POINT_ONE_MILE = 160.9344
|
||||
METERS_TO_KILO = 1000 # divide n by this
|
||||
METERS_TO_MILE = 1609.344 # divide n by this
|
||||
METERS_TO_FEET = 3.280839895 # multiply n by this
|
||||
167
sunnypilot/navd/navigationd.py
Executable file
167
sunnypilot/navd/navigationd.py
Executable file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Copyright (c) 2021-, James Vecellio, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from math import degrees
|
||||
from numpy import interp
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from cereal import custom
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
from openpilot.sunnypilot.navd.constants import NAV_CV
|
||||
from openpilot.sunnypilot.navd.helpers import Coordinate, parse_banner_instructions
|
||||
from openpilot.sunnypilot.navd.navigation_helpers.mapbox_integration import MapboxIntegration
|
||||
from openpilot.sunnypilot.navd.navigation_helpers.nav_instructions import NavigationInstructions
|
||||
|
||||
|
||||
class Navigationd:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.mapbox = MapboxIntegration()
|
||||
self.nav_instructions = NavigationInstructions()
|
||||
|
||||
self.sm = messaging.SubMaster(['carControlSP', 'liveLocationKalman'])
|
||||
self.pm = messaging.PubMaster(['navigationd'])
|
||||
self.rk = Ratekeeper(3) # 3 Hz
|
||||
|
||||
self.route = None
|
||||
self.destination: str | None = None
|
||||
self.new_destination: str = ''
|
||||
|
||||
self.allow_navigation: bool = False
|
||||
self.recompute_allowed: bool = False
|
||||
self.allow_recompute: bool = False
|
||||
self.reroute_counter: int = 0
|
||||
self.cancel_route_counter: int = 0
|
||||
|
||||
self.frame: int = -1
|
||||
self.last_position: Coordinate | None = None
|
||||
self.last_bearing: float | None = None
|
||||
self.valid: bool = False
|
||||
|
||||
def _update_params(self):
|
||||
if self.last_position is not None:
|
||||
self.frame += 1
|
||||
if self.frame % 15 == 0:
|
||||
self.allow_navigation = self.params.get('AllowNavigation', return_default=True)
|
||||
self.new_destination = self.params.get('MapboxRoute')
|
||||
self.recompute_allowed = self.params.get('MapboxRecompute', return_default=True)
|
||||
|
||||
self.allow_recompute: bool = (self.new_destination != self.destination and self.new_destination != '') or (
|
||||
self.recompute_allowed and self.reroute_counter > 9 and self.route)
|
||||
|
||||
if self.allow_recompute:
|
||||
postvars = {'place_name': self.new_destination}
|
||||
postvars, valid_addr = self.mapbox.set_destination(postvars, self.last_position.longitude, self.last_position.latitude, self.last_bearing)
|
||||
|
||||
if valid_addr:
|
||||
self.destination = self.new_destination
|
||||
self.nav_instructions.clear_route_cache()
|
||||
self.route = self.nav_instructions.get_current_route()
|
||||
self.cancel_route_counter = 0
|
||||
self.reroute_counter = 0
|
||||
|
||||
if self.cancel_route_counter == 30:
|
||||
self.cancel_route_counter = 0
|
||||
self.params.put_nonblocking("MapboxRoute", "")
|
||||
self.nav_instructions.clear_route_cache()
|
||||
self.route = None
|
||||
|
||||
self.valid = self.route is not None
|
||||
|
||||
def _update_navigation(self) -> tuple[str, dict | None, dict]:
|
||||
banner_instructions: str = ''
|
||||
nav_data: dict = {}
|
||||
if self.allow_navigation and self.route and self.last_position is not None:
|
||||
if progress := self.nav_instructions.get_route_progress(self.last_position.latitude, self.last_position.longitude):
|
||||
v_ego = float(max(self.sm['carControlSP'].speed, 0.0))
|
||||
nav_data['upcoming_turn'] = self.nav_instructions.get_upcoming_turn_from_progress(progress, self.last_position.latitude,
|
||||
self.last_position.longitude, v_ego)
|
||||
speed_limit, _ = progress['current_maxspeed']
|
||||
nav_data['current_speed_limit'] = speed_limit
|
||||
arrived = self.nav_instructions.arrived_at_destination(progress, v_ego)
|
||||
|
||||
if progress['current_step']:
|
||||
if parsed := parse_banner_instructions(progress['current_step']['bannerInstructions'], progress['distance_to_end_of_step']):
|
||||
banner_instructions = parsed['maneuverPrimaryText']
|
||||
|
||||
nav_data['distance_from_route'] = progress['distance_from_route']
|
||||
speed_breakpoints: list = [0.0, 5.0, 10.0, 20.0, 40.0]
|
||||
distance_list: list = [100.0, 125.0, 150.0, 200.0, 250.0]
|
||||
large_distance: bool = progress['distance_from_route'] > float(interp(v_ego, speed_breakpoints, distance_list))
|
||||
|
||||
route_bearing_misalign: bool = self.nav_instructions.route_bearing_misalign(self.route, self.last_bearing, v_ego)
|
||||
|
||||
if large_distance and not arrived:
|
||||
self.cancel_route_counter = self.cancel_route_counter + 1 if progress['distance_from_route'] > NAV_CV.QUARTER_MILE else 0
|
||||
if self.recompute_allowed:
|
||||
self.reroute_counter += 1
|
||||
elif arrived:
|
||||
self.cancel_route_counter += 1
|
||||
self.recompute_allowed = False
|
||||
elif route_bearing_misalign:
|
||||
self.cancel_route_counter += 1
|
||||
if self.recompute_allowed:
|
||||
self.reroute_counter += 1
|
||||
else:
|
||||
self.cancel_route_counter = 0
|
||||
self.reroute_counter = 0
|
||||
|
||||
# Don't recompute in last segment to prevent reroute loops
|
||||
if progress['current_step_idx'] == len(self.route['steps']) - 1:
|
||||
self.recompute_allowed = False
|
||||
self.allow_navigation = False
|
||||
else:
|
||||
banner_instructions = ''
|
||||
progress = None
|
||||
nav_data = {}
|
||||
|
||||
return banner_instructions, progress, nav_data
|
||||
|
||||
def _build_navigation_message(self, banner_instructions: str, progress: dict | None, nav_data: dict, valid: bool):
|
||||
msg = messaging.new_message('navigationd')
|
||||
msg.valid = valid
|
||||
msg.navigationd.upcomingTurn = nav_data.get('upcoming_turn', 'none')
|
||||
msg.navigationd.currentSpeedLimit = nav_data.get('current_speed_limit', 0)
|
||||
msg.navigationd.bannerInstructions = banner_instructions
|
||||
msg.navigationd.distanceFromRoute = nav_data.get('distance_from_route', 0.0)
|
||||
msg.navigationd.valid = self.valid
|
||||
|
||||
all_maneuvers = (
|
||||
[custom.Navigationd.Maneuver.new_message(distance=m['distance'], type=m['type'], modifier=m['modifier'],
|
||||
instruction=m['instruction']) for m in progress['all_maneuvers']]
|
||||
if progress
|
||||
else []
|
||||
)
|
||||
msg.navigationd.allManeuvers = all_maneuvers
|
||||
return msg
|
||||
|
||||
def run(self):
|
||||
cloudlog.warning('navigationd init')
|
||||
|
||||
while True:
|
||||
self.sm.update(0)
|
||||
location = self.sm['liveLocationKalman']
|
||||
localizer_valid = location.positionGeodetic.valid if location else False
|
||||
|
||||
if localizer_valid:
|
||||
self.last_bearing = degrees(location.calibratedOrientationNED.value[2])
|
||||
self.last_position = Coordinate(location.positionGeodetic.value[0], location.positionGeodetic.value[1])
|
||||
|
||||
self._update_params()
|
||||
banner_instructions, progress, nav_data = self._update_navigation()
|
||||
|
||||
msg = self._build_navigation_message(banner_instructions, progress, nav_data, valid=localizer_valid)
|
||||
|
||||
self.pm.send('navigationd', msg)
|
||||
self.rk.keep_time()
|
||||
|
||||
|
||||
def main():
|
||||
nav = Navigationd()
|
||||
nav.run()
|
||||
0
sunnypilot/navd/tests/__init__.py
Normal file
0
sunnypilot/navd/tests/__init__.py
Normal file
67
sunnypilot/navd/tests/test_navigationd.py
Normal file
67
sunnypilot/navd/tests/test_navigationd.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Copyright (c) 2021-, James Vecellio, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import platform
|
||||
import pytest
|
||||
|
||||
import cereal.messaging as messaging
|
||||
|
||||
from openpilot.sunnypilot.navd.navigationd import Navigationd
|
||||
from openpilot.sunnypilot.navd.helpers import Coordinate
|
||||
|
||||
|
||||
class TestNavigationd:
|
||||
is_darwin = platform.system() == "Darwin"
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_method(self, mocker):
|
||||
if self.is_darwin:
|
||||
mocker.patch('cereal.messaging.SubMaster')
|
||||
mocker.patch('cereal.messaging.PubMaster')
|
||||
|
||||
def test_update_params(self):
|
||||
nav = Navigationd()
|
||||
nav.last_position = None
|
||||
nav._update_params()
|
||||
assert nav.frame == -1
|
||||
nav.last_position = Coordinate(latitude=37.0, longitude=128.0)
|
||||
nav._update_params()
|
||||
assert nav.frame == 0 # frame only updates when last position is set
|
||||
|
||||
def test_update_navigation_no_position(self):
|
||||
nav = Navigationd()
|
||||
nav.last_position = None
|
||||
banner, progress, nav_data = nav._update_navigation()
|
||||
assert banner == ''
|
||||
assert progress is None
|
||||
assert nav_data == {}
|
||||
|
||||
def test_update_navigation(self):
|
||||
nav = Navigationd()
|
||||
nav.last_position = Coordinate(latitude=37.0, longitude=128.0)
|
||||
nav.route = {'580 Winchester dr, oxnard, CA': True}
|
||||
banner, progress, nav_data = nav._update_navigation()
|
||||
assert isinstance(banner, str)
|
||||
assert not progress # no route was actually set
|
||||
assert isinstance(nav_data, dict)
|
||||
|
||||
def test_build_navigation_message(self):
|
||||
if self.is_darwin:
|
||||
nav = Navigationd()
|
||||
msg = nav._build_navigation_message('', None, {}, True)
|
||||
assert msg.navigationd.bannerInstructions == ''
|
||||
assert msg.navigationd.valid is False
|
||||
else:
|
||||
sm = messaging.SubMaster(['navigationd'])
|
||||
nav = Navigationd()
|
||||
msg = nav._build_navigation_message('', None, {}, True)
|
||||
|
||||
nav.pm.send('navigationd', msg)
|
||||
sm.update()
|
||||
received_msg = sm['navigationd']
|
||||
|
||||
assert received_msg.bannerInstructions == msg.navigationd.bannerInstructions
|
||||
assert received_msg.valid == msg.navigationd.valid
|
||||
@@ -72,6 +72,8 @@ class ControlsExt:
|
||||
|
||||
CC_SP.intelligentCruiseButtonManagement = sm['selfdriveStateSP'].intelligentCruiseButtonManagement
|
||||
|
||||
CC_SP.speed = sm['carState'].vEgo
|
||||
|
||||
return CC_SP
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -180,6 +180,9 @@ procs += [
|
||||
NativeProcess("mapd", Paths.mapd_root(), ["bash", "-c", f"{MAPD_PATH} > /dev/null 2>&1"], mapd_ready),
|
||||
PythonProcess("mapd_manager", "sunnypilot.mapd.mapd_manager", always_run),
|
||||
|
||||
# navigationd
|
||||
PythonProcess("navigationd", "sunnypilot.navd.navigationd", only_onroad),
|
||||
|
||||
# locationd
|
||||
NativeProcess("locationd_llk", "sunnypilot/selfdrive/locationd", ["./locationd"], only_onroad),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user