mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-20 09:12:05 +08:00
Compare commits
134 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf84976129 | |||
| d3db789e86 | |||
| ae023d8303 | |||
| 6b102b4e63 | |||
| 71ac12f438 | |||
| 035d5fba92 | |||
| 729c508a73 | |||
| 1796598b52 | |||
| ec031ffeb6 | |||
| 9c5ee2e12a | |||
| 65386da6d4 | |||
| 00799154ef | |||
| d6731c30da | |||
| 0b54b86476 | |||
| fd675d29a9 | |||
| d58bbda789 | |||
| 50773a62ed | |||
| f5dbdc192e | |||
| f9987b2d7d | |||
| d51af1513b | |||
| 2807c949a9 | |||
| 0268d4384d | |||
| 0e60a56e56 | |||
| c5445d2a8b | |||
| 6df1edbbaf | |||
| a098a0805a | |||
| 469f1e01ef | |||
| 0fc5414f7c | |||
| d798288b2c | |||
| 0e02fc2449 | |||
| 7d15afe5bc | |||
| b6dd2d14db | |||
| 7d4e5bedaf | |||
| 1063114408 | |||
| 958b4df69f | |||
| f5953c5d8c | |||
| 72998034e6 | |||
| cefb344183 | |||
| c0da31abb6 | |||
| bd759a56cf | |||
| befc73c53e | |||
| 8dbfc267ac | |||
| d17e80ad94 | |||
| c2b7087723 | |||
| 81b37712f1 | |||
| 68270a13a3 | |||
| 18cd3633e5 | |||
| 9c6a4d4a57 | |||
| 1a4c48249b | |||
| 95d887a417 | |||
| e297b4c03f | |||
| 1132377837 | |||
| 35f03ae001 | |||
| 1c0b54a447 | |||
| 8f0cdd514e | |||
| 3681caa717 | |||
| 7446c43f69 | |||
| 5f5e3668eb | |||
| 8c07958f6f | |||
| ca1ce9bcc9 | |||
| 34a0819bc5 | |||
| c68ea82a5d | |||
| 3157054100 | |||
| 2486ef1825 | |||
| 29f15dc8ed | |||
| 31a5a3b3c0 | |||
| d8fa3cfd04 | |||
| 2a4b348497 | |||
| 3ef3aceb4b | |||
| 3d8763b3ce | |||
| b2427a5f20 | |||
| 7ddafe62cd | |||
| ff4cc96a81 | |||
| 3b1ada64be | |||
| 6a08186434 | |||
| cf2b033c79 | |||
| 9fbef36c6b | |||
| f5a38aa613 | |||
| 25f5058430 | |||
| 7b28c2f59a | |||
| 99d954de10 | |||
| b28f33481c | |||
| 589e33f665 | |||
| 39342d7b5e | |||
| fe70650f73 | |||
| e3f9fe892a | |||
| f4373fa244 | |||
| 2376802589 | |||
| c3b51d7335 | |||
| d3d8802402 | |||
| d866500c92 | |||
| 23879836d9 | |||
| 06add21971 | |||
| 66fd3d1a01 | |||
| 71f7754f51 | |||
| b5591cbd62 | |||
| 7430c450c2 | |||
| e54a39cf43 | |||
| 3afe0bcdb3 | |||
| b763f7aac1 | |||
| 450fcd4d55 | |||
| 551b4dea31 | |||
| bd269defb3 | |||
| 8998f63a28 | |||
| 399ed08926 | |||
| 90f02040fe | |||
| 8423ecedb1 | |||
| 34d1514e11 | |||
| c50d511616 | |||
| dd1479ed82 | |||
| 87ec262e39 | |||
| f82845ff42 | |||
| efcc5ccd15 | |||
| 6aac50ab56 | |||
| da0920cb60 | |||
| 091bce4a3a | |||
| 088f6aa407 | |||
| 211c8adcce | |||
| fe5366e5b2 | |||
| 1ecb0b0f66 | |||
| 51e455db79 | |||
| dc6672fa80 | |||
| 07b8e7783d | |||
| f17b0f200c | |||
| ad9bde8b1f | |||
| 8cf9f9fe23 | |||
| 713985d823 | |||
| 088f9d0b59 | |||
| 53bf5b0d41 | |||
| 8c33592628 | |||
| 3bbb33f6bd | |||
| 5bd9549bd1 | |||
| 3481702715 | |||
| c9781ee31d |
@@ -74,7 +74,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||||
cd gitlab_docs
|
cd gitlab_docs
|
||||||
git checkout main
|
git checkout main
|
||||||
git sparse-checkout set --no-cone models/
|
git sparse-checkout set --no-cone models/
|
||||||
@@ -191,7 +191,7 @@ jobs:
|
|||||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||||
run: |
|
run: |
|
||||||
echo "Cloning GitLab"
|
echo "Cloning GitLab"
|
||||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||||
cd gitlab_docs
|
cd gitlab_docs
|
||||||
echo "checkout models/${RECOMPILED_DIR}"
|
echo "checkout models/${RECOMPILED_DIR}"
|
||||||
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||||
run: |
|
run: |
|
||||||
echo "Cloning GitLab"
|
echo "Cloning GitLab"
|
||||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||||
cd gitlab_docs
|
cd gitlab_docs
|
||||||
echo "checkout models/${RECOMPILED_DIR}"
|
echo "checkout models/${RECOMPILED_DIR}"
|
||||||
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ env:
|
|||||||
PYTHONWARNINGS: error
|
PYTHONWARNINGS: error
|
||||||
BASE_IMAGE: sunnypilot-base
|
BASE_IMAGE: sunnypilot-base
|
||||||
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
|
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
|
||||||
|
MAPBOX_TOKEN_CI: ${{ secrets.MAPBOX_TOKEN_CI }}
|
||||||
|
|
||||||
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
|
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
|
||||||
BUILD: release/ci/docker_build_sp.sh base
|
BUILD: release/ci/docker_build_sp.sh base
|
||||||
|
|
||||||
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
|
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -e MAPBOX_TOKEN_CI=$MAPBOX_TOKEN_CI -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
|
||||||
|
|
||||||
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
|
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
||||||
path: ${{ github.workspace }}/selfdrive/modeld/models
|
path: ${{ github.workspace }}/selfdrive/modeld/models
|
||||||
|
- run: |
|
||||||
|
rm -f ${{ github.workspace }}/selfdrive/modeld/models/{dmonitoring_model,big_driving_policy,big_driving_vision}.onnx
|
||||||
|
|
||||||
- name: Build Model
|
- name: Build Model
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+25
-1
@@ -1,6 +1,30 @@
|
|||||||
sunnypilot Version 2025.002.000 (2025-xx-xx)
|
sunnypilot Version 2025.003.000 (20xx-xx-xx)
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
sunnypilot Version 2025.002.000 (2025-11-06)
|
||||||
|
========================
|
||||||
|
* What's Changed (sunnypilot/sunnypilot)
|
||||||
|
* models: bump model json to v8 by @Discountchubbs
|
||||||
|
* Bug: Model UI Crash Fix by @nayan8teen
|
||||||
|
* controlsd: add `CP_SP` to `get_pid_accel_limits` by @THERoenPR
|
||||||
|
* sunnylink: update uploader button logic to support novice tier and above by @devtekve
|
||||||
|
* Tesla: Coop Steering by @AmyJeanes
|
||||||
|
* ui: update discord references and add forum widget by @devtekve
|
||||||
|
* ui: Fix spacing in sunnylink panel by @devtekve
|
||||||
|
* docs: Update README installation branches and discord links by @mpurnell1 in
|
||||||
|
* stats: sunnylink integration by @devtekve
|
||||||
|
* bug: Fix initial registration for sunnylink by @devtekve
|
||||||
|
* What's Changed (sunnypilot/opendbc)
|
||||||
|
* Honda: add brake hold messages for Clarity by @mvl-boston
|
||||||
|
* interface: add `CP_SP` to `get_pid_accel_limits` method signature by @roenthomas
|
||||||
|
* Honda: use fixed accel min/max constants for Gas Interceptor by @roenthomas
|
||||||
|
* Tesla: Coop Steering by @AmyJeanes
|
||||||
|
* New Contributors (sunnypilot/sunnypilot)
|
||||||
|
* @THERoenPR made their first contribution in "controlsd: add `CP_SP` to `get_pid_accel_limits`"
|
||||||
|
* @AmyJeanes made their first contribution in "Tesla: Coop Steering"
|
||||||
|
* @mpurnell1 made their first contribution in "docs: Update README installation branches and discord links"
|
||||||
|
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2025.001.000...v2025.002.000
|
||||||
|
|
||||||
sunnypilot Version 2025.001.000 (2025-10-25)
|
sunnypilot Version 2025.001.000 (2025-10-25)
|
||||||
========================
|
========================
|
||||||
* 🛠️ Major rewrite
|
* 🛠️ Major rewrite
|
||||||
|
|||||||
+15
-1
@@ -369,6 +369,7 @@ struct CarControlSP @0xa5cd762cd951a455 {
|
|||||||
leadOne @2 :LeadData;
|
leadOne @2 :LeadData;
|
||||||
leadTwo @3 :LeadData;
|
leadTwo @3 :LeadData;
|
||||||
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
|
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
|
||||||
|
speed @5 :Float32;
|
||||||
|
|
||||||
struct Param {
|
struct Param {
|
||||||
key @0 :Text;
|
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 {
|
struct CustomReserved11 @0xc2243c65e0340384 {
|
||||||
|
|||||||
+1
-1
@@ -2632,7 +2632,7 @@ struct Event {
|
|||||||
carStateSP @114 :Custom.CarStateSP;
|
carStateSP @114 :Custom.CarStateSP;
|
||||||
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
||||||
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
||||||
customReserved10 @136 :Custom.CustomReserved10;
|
navigationd @136 :Custom.Navigationd;
|
||||||
customReserved11 @137 :Custom.CustomReserved11;
|
customReserved11 @137 :Custom.CustomReserved11;
|
||||||
customReserved12 @138 :Custom.CustomReserved12;
|
customReserved12 @138 :Custom.CustomReserved12;
|
||||||
customReserved13 @139 :Custom.CustomReserved13;
|
customReserved13 @139 :Custom.CustomReserved13;
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ _services: dict[str, tuple] = {
|
|||||||
"carStateSP": (True, 100., 10),
|
"carStateSP": (True, 100., 10),
|
||||||
"liveMapDataSP": (True, 1., 1),
|
"liveMapDataSP": (True, 1., 1),
|
||||||
"modelDataV2SP": (True, 20.),
|
"modelDataV2SP": (True, 20.),
|
||||||
|
"navigationd": (True, 3.),
|
||||||
"liveLocationKalman": (True, 20.),
|
"liveLocationKalman": (True, 20.),
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
#define DEFAULT_MODEL "TCPv3 + gWMv9 (Default)"
|
#define DEFAULT_MODEL "Firehose (Default)"
|
||||||
|
|||||||
@@ -187,6 +187,14 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
|
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
|
||||||
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
|
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
|
||||||
|
|
||||||
|
// Navigation params
|
||||||
|
{"AllowNavigation", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
{"MapboxToken", {PERSISTENT | BACKUP, STRING}},
|
||||||
|
{"MapboxSettings", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
|
{"MapboxRoute", {PERSISTENT, STRING}},
|
||||||
|
{"MapboxRecompute", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
{"NavDesiresAllowed", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
// Neural Network Lateral Control
|
// Neural Network Lateral Control
|
||||||
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: c7126f8ba6...a26f7827c5
@@ -3,6 +3,7 @@ from openpilot.common.constants import CV
|
|||||||
from openpilot.common.realtime import DT_MDL
|
from openpilot.common.realtime import DT_MDL
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode
|
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController
|
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController
|
||||||
|
from openpilot.sunnypilot.navd.navigation_desires.navigation_desires import NavigationDesires
|
||||||
|
|
||||||
LaneChangeState = log.LaneChangeState
|
LaneChangeState = log.LaneChangeState
|
||||||
LaneChangeDirection = log.LaneChangeDirection
|
LaneChangeDirection = log.LaneChangeDirection
|
||||||
@@ -51,6 +52,7 @@ class DesireHelper:
|
|||||||
self.alc = AutoLaneChangeController(self)
|
self.alc = AutoLaneChangeController(self)
|
||||||
self.lane_turn_controller = LaneTurnController(self)
|
self.lane_turn_controller = LaneTurnController(self)
|
||||||
self.lane_turn_direction = TurnDirection.none
|
self.lane_turn_direction = TurnDirection.none
|
||||||
|
self.navigation_desires = NavigationDesires()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_lane_change_direction(CS):
|
def get_lane_change_direction(CS):
|
||||||
@@ -143,3 +145,7 @@ class DesireHelper:
|
|||||||
self.desire = log.Desire.none
|
self.desire = log.Desire.none
|
||||||
|
|
||||||
self.alc.update_state()
|
self.alc.update_state()
|
||||||
|
|
||||||
|
nav_desire = self.navigation_desires.update(carstate, lateral_active)
|
||||||
|
if nav_desire != log.Desire.none and (self.desire == log.Desire.none or self.desire in (log.Desire.turnLeft, log.Desire.turnRight)):
|
||||||
|
self.desire = nav_desire
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:76e7beaa7822219b019a4352ab111d0898abf72bd4b0d9520bd8dc68174e8c31
|
oid sha256:ebb38a934d6472c061cc6010f46d9720ca132d631a47e585a893bdd41ade2419
|
||||||
size 13926460
|
size 12343535
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8f16d548ea4eb5d01518a9e90d4527cd97c31a84bcaf6f695dead8f0015fecc4
|
oid sha256:befac016a247b7ad5dc5b55d339d127774ed7bd2b848f1583f72aa4caee37781
|
||||||
size 46271942
|
size 46271991
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
|
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
|
||||||
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback',
|
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback',
|
||||||
'modelDataV2SP', 'longitudinalPlanSP'] + \
|
'modelDataV2SP', 'longitudinalPlanSP'] + \
|
||||||
self.camera_packets + self.sensor_packets + self.gps_packets,
|
self.camera_packets + self.sensor_packets + self.gps_packets,
|
||||||
ignore_alive=ignore, ignore_avg_freq=ignore,
|
ignore_alive=ignore, ignore_avg_freq=ignore,
|
||||||
ignore_valid=ignore, frequency=int(1/DT_CTRL))
|
ignore_valid=ignore, frequency=int(1/DT_CTRL))
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
|
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
|
||||||
direction = self.sm['modelV2'].meta.laneChangeDirection
|
direction = self.sm['modelV2'].meta.laneChangeDirection
|
||||||
if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \
|
if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \
|
||||||
(CS.rightBlindspot and direction == LaneChangeDirection.right):
|
(CS.rightBlindspot and direction == LaneChangeDirection.right):
|
||||||
self.events.add(EventName.laneChangeBlocked)
|
self.events.add(EventName.laneChangeBlocked)
|
||||||
else:
|
else:
|
||||||
if direction == LaneChangeDirection.left:
|
if direction == LaneChangeDirection.left:
|
||||||
@@ -301,7 +301,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
else:
|
else:
|
||||||
self.events.add(EventName.preLaneChangeRight)
|
self.events.add(EventName.preLaneChangeRight)
|
||||||
elif self.sm['modelV2'].meta.laneChangeState in (LaneChangeState.laneChangeStarting,
|
elif self.sm['modelV2'].meta.laneChangeState in (LaneChangeState.laneChangeStarting,
|
||||||
LaneChangeState.laneChangeFinishing):
|
LaneChangeState.laneChangeFinishing):
|
||||||
self.events.add(EventName.laneChange)
|
self.events.add(EventName.laneChange)
|
||||||
|
|
||||||
# Handle lane turn
|
# Handle lane turn
|
||||||
@@ -496,7 +496,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
|
|
||||||
# All pandas not in silent mode must have controlsAllowed when openpilot is enabled
|
# All pandas not in silent mode must have controlsAllowed when openpilot is enabled
|
||||||
if self.enabled and any(not ps.controlsAllowed for ps in self.sm['pandaStates']
|
if self.enabled and any(not ps.controlsAllowed for ps in self.sm['pandaStates']
|
||||||
if ps.safetyModel not in IGNORED_SAFETY_MODES):
|
if ps.safetyModel not in IGNORED_SAFETY_MODES):
|
||||||
self.mismatch_counter += 1
|
self.mismatch_counter += 1
|
||||||
|
|
||||||
return CS
|
return CS
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ qt_src = [
|
|||||||
"sunnypilot/qt/offroad/settings/max_time_offroad.cc",
|
"sunnypilot/qt/offroad/settings/max_time_offroad.cc",
|
||||||
"sunnypilot/qt/offroad/settings/brightness.cc",
|
"sunnypilot/qt/offroad/settings/brightness.cc",
|
||||||
"sunnypilot/qt/offroad/settings/models_panel.cc",
|
"sunnypilot/qt/offroad/settings/models_panel.cc",
|
||||||
|
"sunnypilot/qt/offroad/settings/navigation_panel.cc",
|
||||||
"sunnypilot/qt/offroad/settings/osm_panel.cc",
|
"sunnypilot/qt/offroad/settings/osm_panel.cc",
|
||||||
"sunnypilot/qt/offroad/settings/settings.cc",
|
"sunnypilot/qt/offroad/settings/settings.cc",
|
||||||
"sunnypilot/qt/offroad/settings/software_panel.cc",
|
"sunnypilot/qt/offroad/settings/software_panel.cc",
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021-, 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/navigation_panel.h"
|
||||||
|
|
||||||
|
NavigationPanel::NavigationPanel(QWidget* parent) : QWidget(parent) {
|
||||||
|
QVBoxLayout* main_layout = new QVBoxLayout(this);
|
||||||
|
main_layout->setContentsMargins(50, 25, 50, 25);
|
||||||
|
|
||||||
|
list = new ListWidget(this, false);
|
||||||
|
scroller = new ScrollViewSP(list, this);
|
||||||
|
main_layout->addWidget(scroller);
|
||||||
|
|
||||||
|
// Mapbox Token
|
||||||
|
mapbox_token = new ButtonControl(tr("Mapbox Token"), tr("Edit"), tr("Enter your Mapbox API token"));
|
||||||
|
QObject::connect(mapbox_token, &ButtonControl::clicked, [=]() {
|
||||||
|
QString current = QString::fromStdString(params.get("MapboxToken"));
|
||||||
|
QString token = InputDialog::getText(tr("Enter Mapbox Token"), this, "", false, -1, current);
|
||||||
|
if (!token.isEmpty()) {
|
||||||
|
params.put("MapboxToken", token.toStdString());
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
list->addItem(mapbox_token);
|
||||||
|
|
||||||
|
// Mapbox Route
|
||||||
|
mapbox_route = new ButtonControl(tr("Mapbox Route"), tr("Edit"), tr("Enter Mapbox route data"));
|
||||||
|
QObject::connect(mapbox_route, &ButtonControl::clicked, [=]() {
|
||||||
|
QString current = QString::fromStdString(params.get("MapboxRoute"));
|
||||||
|
QString route = InputDialog::getText(tr("Enter Mapbox Route"), this, "", false, -1, current);
|
||||||
|
if (!route.isEmpty()) {
|
||||||
|
params.put("MapboxRoute", route.toStdString());
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
list->addItem(mapbox_route);
|
||||||
|
|
||||||
|
// Allow Navigation
|
||||||
|
allow_navigation = new ParamControlSP("AllowNavigation", tr("Allow Navigation"), tr("Enable navigation features and start navigationd"), "", this);
|
||||||
|
QObject::connect(allow_navigation, &ParamControlSP::toggleFlipped, this, &NavigationPanel::updateNavigationVisibility);
|
||||||
|
list->addItem(allow_navigation);
|
||||||
|
|
||||||
|
// Mapbox Recompute
|
||||||
|
mapbox_recompute = new ParamControlSP("MapboxRecompute", tr("Mapbox Recompute"), tr("Enable automatic route recomputation"), "", this);
|
||||||
|
list->addItem(mapbox_recompute);
|
||||||
|
|
||||||
|
// Nav Allowed
|
||||||
|
nav_allowed = new ParamControlSP("NavDesiresAllowed", tr("Navigation Allowed"), tr("Allow navigation to automatically take turns"), "", this);
|
||||||
|
list->addItem(nav_allowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NavigationPanel::updateNavigationVisibility(bool state) {
|
||||||
|
mapbox_recompute->setVisible(state);
|
||||||
|
nav_allowed->setVisible(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NavigationPanel::showEvent(QShowEvent *event) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NavigationPanel::refresh() {
|
||||||
|
allow_navigation->refresh();
|
||||||
|
|
||||||
|
bool nav_enabled = allow_navigation->isToggled();
|
||||||
|
updateNavigationVisibility(nav_enabled);
|
||||||
|
|
||||||
|
QString token = QString::fromStdString(params.get("MapboxToken"));
|
||||||
|
mapbox_token->setValue(token.isEmpty() ? tr("Not set") : token);
|
||||||
|
|
||||||
|
QString route = QString::fromStdString(params.get("MapboxRoute"));
|
||||||
|
mapbox_route->setValue(route.isEmpty() ? tr("Not set") : route);
|
||||||
|
|
||||||
|
mapbox_recompute->refresh();
|
||||||
|
nav_allowed->refresh();
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021-, 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "selfdrive/ui/qt/offroad/settings.h"
|
||||||
|
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||||
|
#include "selfdrive/ui/qt/widgets/input.h"
|
||||||
|
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||||
|
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||||
|
#include "selfdrive/ui/sunnypilot/qt/util.h"
|
||||||
|
|
||||||
|
|
||||||
|
class NavigationPanel : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit NavigationPanel(QWidget* parent = nullptr);
|
||||||
|
void showEvent(QShowEvent *event) override;
|
||||||
|
void refresh();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void updateNavigationVisibility(bool state);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Params params;
|
||||||
|
ListWidget* list;
|
||||||
|
ScrollViewSP* scroller;
|
||||||
|
ParamControlSP* allow_navigation;
|
||||||
|
ButtonControl* mapbox_token;
|
||||||
|
ButtonControl* mapbox_route;
|
||||||
|
ParamControlSP* mapbox_recompute;
|
||||||
|
ParamControlSP* nav_allowed;
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h"
|
||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/display_panel.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/display_panel.h"
|
||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h"
|
||||||
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/navigation_panel.h"
|
||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h"
|
||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h"
|
||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h"
|
||||||
@@ -85,6 +86,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) {
|
|||||||
PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"),
|
PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"),
|
||||||
PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
|
PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
|
||||||
PanelInfo(" " + tr("Models"), new ModelsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_models.png"),
|
PanelInfo(" " + tr("Models"), new ModelsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_models.png"),
|
||||||
|
PanelInfo(" " + tr("Navigation"), new NavigationPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
|
||||||
PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"),
|
PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"),
|
||||||
PanelInfo(" " + tr("Cruise"), new LongitudinalPanel(this), "../assets/icons/speed_limit.png"),
|
PanelInfo(" " + tr("Cruise"), new LongitudinalPanel(this), "../assets/icons/speed_limit.png"),
|
||||||
PanelInfo(" " + tr("Visuals"), new VisualsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"),
|
PanelInfo(" " + tr("Visuals"), new VisualsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* See the LICENSE.md file in the root directory for more details.
|
* See the LICENSE.md file in the root directory for more details.
|
||||||
*/
|
*/
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
#include "selfdrive/ui/sunnypilot/qt/onroad/hud.h"
|
#include "selfdrive/ui/sunnypilot/qt/onroad/hud.h"
|
||||||
|
|
||||||
@@ -154,6 +155,73 @@ void HudRendererSP::updateState(const UIState &s) {
|
|||||||
|
|
||||||
allow_e2e_alerts = sm["selfdriveState"].getSelfdriveState().getAlertSize() == cereal::SelfdriveState::AlertSize::NONE &&
|
allow_e2e_alerts = sm["selfdriveState"].getSelfdriveState().getAlertSize() == cereal::SelfdriveState::AlertSize::NONE &&
|
||||||
sm.rcv_frame("driverStateV2") > s.scene.started_frame && !reversing;
|
sm.rcv_frame("driverStateV2") > s.scene.started_frame && !reversing;
|
||||||
|
|
||||||
|
// Navigationd
|
||||||
|
if (sm.updated("navigationd")) {
|
||||||
|
auto nav = sm["navigationd"].getNavigationd();
|
||||||
|
navigationValid = nav.getValid();
|
||||||
|
if (navigationValid && nav.getAllManeuvers().size() > 0) {
|
||||||
|
int currManeuverIdx = nav.getAllManeuvers().size() > 1 ? 1 : 0;
|
||||||
|
auto maneuver = nav.getAllManeuvers()[currManeuverIdx];
|
||||||
|
navigationModifier = QString::fromStdString(maneuver.getModifier());
|
||||||
|
navigationManeuverType = QString::fromStdString(maneuver.getType());
|
||||||
|
|
||||||
|
float dist = maneuver.getDistance();
|
||||||
|
if (is_metric) {
|
||||||
|
if (dist < 1000) {
|
||||||
|
if (dist < 500) {
|
||||||
|
navigationDistance = QString::number(std::round(dist / 25.0) * 25) + " m";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
navigationDistance = QString::number(std::round(dist / 50.0) * 50) + " m";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
float dist_km = dist / 1000;
|
||||||
|
if (dist_km >= 10.0) {
|
||||||
|
navigationDistance = QString::number(std::floor(dist_km)) + " km";
|
||||||
|
} else {
|
||||||
|
navigationDistance = QString::number(dist_km, 'f', 1) + " km";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
float dist_ft = dist * 3.28084f;
|
||||||
|
if (dist_ft < 1000) {
|
||||||
|
if (dist_ft <= 100){
|
||||||
|
navigationDistance = QString::number((std::round(dist_ft / 10.0) * 10)) + " ft";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
navigationDistance = QString::number((std::round(dist_ft / 50.0) * 50)) + " ft";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
float dist_mi = dist_ft / 5280;
|
||||||
|
if (dist_mi >= 10.0) {
|
||||||
|
navigationDistance = QString::number(std::floor(dist_mi)) + " mi";
|
||||||
|
} else {
|
||||||
|
navigationDistance = QString::number(dist_mi, 'f', 1) + " mi";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString instruction = QString::fromStdString(maneuver.getInstruction());
|
||||||
|
QStringList parts = instruction.split(" onto ");
|
||||||
|
if (parts.size() > 1) {
|
||||||
|
navigationStreet = parts[1].trimmed();
|
||||||
|
} else {
|
||||||
|
navigationStreet = instruction;
|
||||||
|
}
|
||||||
|
navigationStreet = navigationStreet.replace(".", "");
|
||||||
|
|
||||||
|
// Get next maneuver if available
|
||||||
|
if (nav.getAllManeuvers().size() > 2) {
|
||||||
|
auto nextManeuver = nav.getAllManeuvers()[2];
|
||||||
|
navigationNextModifier = QString::fromStdString(nextManeuver.getModifier());
|
||||||
|
navigationNextManeuverType = QString::fromStdString(nextManeuver.getType());
|
||||||
|
navigationHasNext = true;
|
||||||
|
} else {
|
||||||
|
navigationHasNext = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
||||||
@@ -287,6 +355,8 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawNavigationHUD(p, surface_rect);
|
||||||
|
|
||||||
p.restore();
|
p.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,9 +376,14 @@ bool HudRendererSP::pulseElement(int frame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HudRendererSP::drawSmartCruiseControlOnroadIcon(QPainter &p, const QRect &surface_rect, int x_offset, int y_offset, std::string name) {
|
void HudRendererSP::drawSmartCruiseControlOnroadIcon(QPainter &p, const QRect &surface_rect, int x_offset, int y_offset, std::string name) {
|
||||||
int x = surface_rect.center().x();
|
int base_x = surface_rect.center().x();
|
||||||
int y = surface_rect.height() / 4;
|
int y = surface_rect.height() / 4;
|
||||||
|
|
||||||
|
if (navigationValid) {
|
||||||
|
base_x = 618;
|
||||||
|
y = 420;
|
||||||
|
}
|
||||||
|
|
||||||
QString text = QString::fromStdString(name);
|
QString text = QString::fromStdString(name);
|
||||||
QFont font = InterFont(36, QFont::Bold);
|
QFont font = InterFont(36, QFont::Bold);
|
||||||
p.setFont(font);
|
p.setFont(font);
|
||||||
@@ -319,7 +394,7 @@ void HudRendererSP::drawSmartCruiseControlOnroadIcon(QPainter &p, const QRect &s
|
|||||||
int box_width = 160;
|
int box_width = 160;
|
||||||
int box_height = fm.height() + padding_v * 2;
|
int box_height = fm.height() + padding_v * 2;
|
||||||
|
|
||||||
QRectF bg_rect(x - (box_width / 2) + x_offset,
|
QRectF bg_rect(base_x - (box_width / 2) + x_offset,
|
||||||
y - (box_height / 2) + y_offset,
|
y - (box_height / 2) + y_offset,
|
||||||
box_width, box_height);
|
box_width, box_height);
|
||||||
|
|
||||||
@@ -684,15 +759,16 @@ void HudRendererSP::drawSpeedLimitPreActiveArrow(QPainter &p, QRect &sign_rect)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
|
void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
|
||||||
// Draw outer box + border to contain set speed
|
// ICBM counter logic
|
||||||
const QSize default_size = {172, 204};
|
if (!pcmCruiseSpeed && carControlEnabled) {
|
||||||
QSize set_speed_size = is_metric ? QSize(200, 204) : default_size;
|
if (std::nearbyint(set_speed) != std::nearbyint(speedCluster)) {
|
||||||
QRect set_speed_rect(QPoint(60 + (default_size.width() - set_speed_size.width()) / 2, 45), set_speed_size);
|
icbm_active_counter = 3 * UI_FREQ;
|
||||||
|
} else if (icbm_active_counter > 0) {
|
||||||
// Draw set speed box
|
icbm_active_counter--;
|
||||||
p.setPen(QPen(QColor(255, 255, 255, 75), 6));
|
}
|
||||||
p.setBrush(QColor(0, 0, 0, 166));
|
} else {
|
||||||
p.drawRoundedRect(set_speed_rect, 32, 32);
|
icbm_active_counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Colors based on status
|
// Colors based on status
|
||||||
QColor max_color = QColor(0xa6, 0xa6, 0xa6, 0xff);
|
QColor max_color = QColor(0xa6, 0xa6, 0xa6, 0xff);
|
||||||
@@ -713,29 +789,62 @@ void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw "MAX" or carState.cruiseState.speedCluster (when ICBM is active) text
|
|
||||||
if (!pcmCruiseSpeed && carControlEnabled) {
|
|
||||||
if (std::nearbyint(set_speed) != std::nearbyint(speedCluster)) {
|
|
||||||
icbm_active_counter = 3 * UI_FREQ;
|
|
||||||
} else if (icbm_active_counter > 0) {
|
|
||||||
icbm_active_counter--;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
icbm_active_counter = 0;
|
|
||||||
}
|
|
||||||
int max_str_size = (icbm_active_counter != 0) ? 60 : 40;
|
|
||||||
int max_str_y = (icbm_active_counter != 0) ? 15 : 27;
|
|
||||||
QString max_str = (icbm_active_counter != 0) ? QString::number(std::nearbyint(speedCluster)) : tr("MAX");
|
QString max_str = (icbm_active_counter != 0) ? QString::number(std::nearbyint(speedCluster)) : tr("MAX");
|
||||||
|
|
||||||
p.setFont(InterFont(max_str_size, QFont::DemiBold));
|
|
||||||
p.setPen(max_color);
|
|
||||||
p.drawText(set_speed_rect.adjusted(0, max_str_y, 0, 0), Qt::AlignTop | Qt::AlignHCenter, max_str);
|
|
||||||
|
|
||||||
// Draw set speed
|
|
||||||
QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(set_speed)) : "–";
|
QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(set_speed)) : "–";
|
||||||
p.setFont(InterFont(90, QFont::Bold));
|
|
||||||
p.setPen(set_speed_color);
|
if (!navigationValid) {
|
||||||
p.drawText(set_speed_rect.adjusted(0, 77, 0, 0), Qt::AlignTop | Qt::AlignHCenter, setSpeedStr);
|
// Original positions when navigation is not valid
|
||||||
|
const QSize default_size = {172, 204};
|
||||||
|
QSize set_speed_size = is_metric ? QSize(200, 204) : default_size;
|
||||||
|
QRect set_speed_rect(QPoint(60 + (default_size.width() - set_speed_size.width()) / 2, 45), set_speed_size);
|
||||||
|
|
||||||
|
// Draw set speed box
|
||||||
|
p.setPen(QPen(QColor(255, 255, 255, 75), 6));
|
||||||
|
p.setBrush(QColor(0, 0, 0, 166));
|
||||||
|
p.drawRoundedRect(set_speed_rect, 32, 32);
|
||||||
|
|
||||||
|
// Draw "MAX" or carState.cruiseState.speedCluster (when ICBM is active) text
|
||||||
|
int max_str_size = (icbm_active_counter != 0) ? 60 : 40;
|
||||||
|
int max_str_y = (icbm_active_counter != 0) ? 15 : 27;
|
||||||
|
|
||||||
|
p.setFont(InterFont(max_str_size, QFont::DemiBold));
|
||||||
|
p.setPen(max_color);
|
||||||
|
p.drawText(set_speed_rect.adjusted(0, max_str_y, 0, 0), Qt::AlignTop | Qt::AlignHCenter, max_str);
|
||||||
|
|
||||||
|
// Draw set speed
|
||||||
|
p.setFont(InterFont(90, QFont::Bold));
|
||||||
|
p.setPen(set_speed_color);
|
||||||
|
p.drawText(set_speed_rect.adjusted(0, 77, 0, 0), Qt::AlignTop | Qt::AlignHCenter, setSpeedStr);
|
||||||
|
} else {
|
||||||
|
// Modified positions when navigation is valid
|
||||||
|
const int container_width = 200;
|
||||||
|
const int container_height = 320;
|
||||||
|
const int container_x = 40;
|
||||||
|
const int container_y = 45;
|
||||||
|
|
||||||
|
QRect speed_container(container_x, container_y, container_width, container_height);
|
||||||
|
|
||||||
|
// Draw outer rounded rectangle container
|
||||||
|
p.setPen(QPen(QColor(255, 255, 255, 75), 6));
|
||||||
|
p.setBrush(QColor(0, 0, 0, 166));
|
||||||
|
p.drawRoundedRect(speed_container, 24, 24);
|
||||||
|
|
||||||
|
int divider_y = container_y + 190;
|
||||||
|
p.setPen(QPen(QColor(255, 255, 255, 50), 2));
|
||||||
|
p.drawLine(container_x + 20, divider_y, container_x + container_width - 20, divider_y);
|
||||||
|
|
||||||
|
// max label
|
||||||
|
QRect max_label_rect(container_x, container_y + 200, container_width, 35);
|
||||||
|
p.setFont(InterFont(32, QFont::Normal));
|
||||||
|
p.setPen(max_color);
|
||||||
|
p.drawText(max_label_rect, Qt::AlignCenter, max_str);
|
||||||
|
|
||||||
|
// Set speed value
|
||||||
|
QRect set_speed_rect(container_x, container_y + 240, container_width, 70);
|
||||||
|
p.setFont(InterFont(68, QFont::Bold));
|
||||||
|
p.setPen(set_speed_color);
|
||||||
|
p.drawText(set_speed_rect, Qt::AlignCenter, setSpeedStr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text) {
|
void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text) {
|
||||||
@@ -794,12 +903,40 @@ void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const Q
|
|||||||
|
|
||||||
void HudRendererSP::drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect) {
|
void HudRendererSP::drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect) {
|
||||||
QString speedStr = QString::number(std::nearbyint(speed));
|
QString speedStr = QString::number(std::nearbyint(speed));
|
||||||
|
QString unit = is_metric ? tr("km/h") : tr("mph");
|
||||||
|
|
||||||
p.setFont(InterFont(176, QFont::Bold));
|
int speed_x = surface_rect.center().x();
|
||||||
HudRenderer::drawText(p, surface_rect.center().x(), 210, speedStr);
|
int speed_y = 210;
|
||||||
|
int unit_y = 290;
|
||||||
|
QFont speed_font = InterFont(176, QFont::Bold);
|
||||||
|
QFont unit_font = InterFont(66);
|
||||||
|
|
||||||
p.setFont(InterFont(66));
|
if (navigationValid) {
|
||||||
HudRenderer::drawText(p, surface_rect.center().x(), 290, is_metric ? tr("km/h") : tr("mph"), 200);
|
speed_y = 75;
|
||||||
|
unit_y = 175;
|
||||||
|
speed_font = InterFont(100, QFont::Bold);
|
||||||
|
unit_font = InterFont(35, QFont::Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw speed
|
||||||
|
p.setFont(speed_font);
|
||||||
|
if (!navigationValid) {
|
||||||
|
HudRenderer::drawText(p, speed_x, speed_y, speedStr);
|
||||||
|
} else {
|
||||||
|
QRect current_speed_rect(40, speed_y, 200, 100);
|
||||||
|
p.setPen(Qt::white);
|
||||||
|
p.drawText(current_speed_rect, Qt::AlignCenter, speedStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw unit
|
||||||
|
p.setFont(unit_font);
|
||||||
|
if (!navigationValid) {
|
||||||
|
HudRenderer::drawText(p, speed_x, unit_y, unit, 200);
|
||||||
|
} else {
|
||||||
|
QRect unit_rect(40, unit_y, 200, 40);
|
||||||
|
p.setPen(QColor(180, 180, 180, 255));
|
||||||
|
p.drawText(unit_rect, Qt::AlignCenter, unit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HudRendererSP::drawBlinker(QPainter &p, const QRect &surface_rect) {
|
void HudRendererSP::drawBlinker(QPainter &p, const QRect &surface_rect) {
|
||||||
@@ -827,7 +964,7 @@ void HudRendererSP::drawBlinker(QPainter &p, const QRect &surface_rect) {
|
|||||||
const int circleRadius = 60;
|
const int circleRadius = 60;
|
||||||
const int arrowLength = 60;
|
const int arrowLength = 60;
|
||||||
const int x_gap = 160;
|
const int x_gap = 160;
|
||||||
const int y_offset = 272;
|
const int y_offset = navigationValid ? 352 : 300;
|
||||||
|
|
||||||
const int centerX = surface_rect.center().x();
|
const int centerX = surface_rect.center().x();
|
||||||
|
|
||||||
@@ -885,3 +1022,186 @@ void HudRendererSP::drawBlinker(QPainter &p, const QRect &surface_rect) {
|
|||||||
|
|
||||||
p.restore();
|
p.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString HudRendererSP::getNavigationIconName(const QString &type, const QString &mod) {
|
||||||
|
static QMap<QString, QString> icon_map;
|
||||||
|
if (icon_map.isEmpty()) {
|
||||||
|
icon_map["turn|uturn"] = "direction_uturn.png";
|
||||||
|
icon_map["turn|sharp right"] = "direction_turn_sharp_right.png";
|
||||||
|
icon_map["turn|right"] = "direction_turn_right.png";
|
||||||
|
icon_map["turn|slight right"] = "direction_turn_slight_right.png";
|
||||||
|
icon_map["turn|straight"] = "direction_turn_straight.png";
|
||||||
|
icon_map["turn|slight left"] = "direction_turn_slight_left.png";
|
||||||
|
icon_map["turn|left"] = "direction_turn_left.png";
|
||||||
|
icon_map["turn|sharp left"] = "direction_turn_sharp_left.png";
|
||||||
|
|
||||||
|
icon_map["arrive|right"] = "direction_arrive_right.png";
|
||||||
|
icon_map["arrive|straight"] = "direction_arrive_straight.png";
|
||||||
|
icon_map["arrive|left"] = "direction_arrive_left.png";
|
||||||
|
icon_map["arrive|"] = "direction_arrive.png";
|
||||||
|
|
||||||
|
icon_map["merge|slight right"] = "direction_merge_slight_right.png";
|
||||||
|
icon_map["merge|right"] = "direction_merge_right.png";
|
||||||
|
icon_map["merge|straight"] = "direction_merge_straight.png";
|
||||||
|
icon_map["merge|slight left"] = "direction_merge_slight_left.png";
|
||||||
|
icon_map["merge|left"] = "direction_merge_left.png";
|
||||||
|
|
||||||
|
icon_map["on ramp|sharp right"] = "direction_on_ramp_sharp_right.png";
|
||||||
|
icon_map["on ramp|right"] = "direction_on_ramp_right.png";
|
||||||
|
icon_map["on ramp|slight right"] = "direction_on_ramp_slight_right.png";
|
||||||
|
icon_map["on ramp|straight"] = "direction_on_ramp_straight.png";
|
||||||
|
icon_map["on ramp|slight left"] = "direction_on_ramp_slight_left.png";
|
||||||
|
icon_map["on ramp|left"] = "direction_on_ramp_left.png";
|
||||||
|
icon_map["on ramp|sharp left"] = "direction_on_ramp_sharp_left.png";
|
||||||
|
|
||||||
|
icon_map["off ramp|slight right"] = "direction_off_ramp_slight_right.png";
|
||||||
|
icon_map["off ramp|right"] = "direction_off_ramp_right.png";
|
||||||
|
icon_map["off ramp|slight left"] = "direction_off_ramp_slight_left.png";
|
||||||
|
icon_map["off ramp|left"] = "direction_off_ramp_left.png";
|
||||||
|
|
||||||
|
icon_map["roundabout|sharp right"] = "direction_roundabout_sharp_right.png";
|
||||||
|
icon_map["roundabout|right"] = "direction_roundabout_right.png";
|
||||||
|
icon_map["roundabout|slight right"] = "direction_roundabout_slight_right.png";
|
||||||
|
icon_map["roundabout|straight"] = "direction_roundabout_straight.png";
|
||||||
|
icon_map["roundabout|slight left"] = "direction_roundabout_slight_left.png";
|
||||||
|
icon_map["roundabout|left"] = "direction_roundabout_left.png";
|
||||||
|
icon_map["roundabout|sharp left"] = "direction_roundabout_sharp_left.png";
|
||||||
|
icon_map["roundabout|"] = "direction_roundabout.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString normalized_type = type;
|
||||||
|
if (normalized_type == "rotary") {
|
||||||
|
normalized_type = "roundabout";
|
||||||
|
} else if (normalized_type == "new name") {
|
||||||
|
normalized_type = "turn";
|
||||||
|
} else if (normalized_type == "continue") {
|
||||||
|
normalized_type = "turn";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString icon_name;
|
||||||
|
QStringList keys = {normalized_type + "|" + mod, normalized_type + "|", "turn|" + mod};
|
||||||
|
for (const QString &key : keys) {
|
||||||
|
icon_name = icon_map.value(key);
|
||||||
|
if (!icon_name.isEmpty()) break;
|
||||||
|
}
|
||||||
|
if (icon_name.isEmpty()) {
|
||||||
|
icon_name = "direction_turn_straight.png";
|
||||||
|
}
|
||||||
|
return icon_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HudRendererSP::drawNavigationHUD(QPainter &p, const QRect &surface_rect) {
|
||||||
|
if (!navigationValid) return;
|
||||||
|
p.save();
|
||||||
|
|
||||||
|
const int container_width = 1080;
|
||||||
|
const int container_height = 225;
|
||||||
|
int container_x = (surface_rect.width() - container_width) / 2;
|
||||||
|
const int container_y = 62;
|
||||||
|
const int border_radius = 42;
|
||||||
|
|
||||||
|
|
||||||
|
if (speedLimitAssistState == cereal::LongitudinalPlanSP::SpeedLimit::AssistState::PRE_ACTIVE) {
|
||||||
|
container_x += 190;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect container_rect(container_x, container_y, container_width, container_height);
|
||||||
|
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(QColor(0, 0, 0, 180));
|
||||||
|
p.drawRoundedRect(container_rect, border_radius, border_radius);
|
||||||
|
|
||||||
|
// Navigation icon
|
||||||
|
const int icon_size = 150;
|
||||||
|
const int icon_padding = 30;
|
||||||
|
const int icon_x = container_x + icon_padding;
|
||||||
|
const int icon_y = container_y;
|
||||||
|
|
||||||
|
QString icon_name = getNavigationIconName(navigationManeuverType, navigationModifier);
|
||||||
|
QPixmap nav_icon = loadPixmap("../../sunnypilot/selfdrive/assets/navigation/" + icon_name, {icon_size, icon_size});
|
||||||
|
|
||||||
|
if (!nav_icon.isNull()) {
|
||||||
|
p.drawPixmap(icon_x, icon_y, nav_icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance
|
||||||
|
p.setFont(InterFont(48, QFont::Bold));
|
||||||
|
p.setPen(Qt::white);
|
||||||
|
QRect distance_rect(icon_x - 25, icon_y + icon_size, icon_size + 50, 38);
|
||||||
|
p.drawText(distance_rect, Qt::AlignCenter, navigationDistance);
|
||||||
|
|
||||||
|
const int then_section_width = 180;
|
||||||
|
const int text_x = icon_x + icon_size + 53;
|
||||||
|
const int text_area_width = container_width - (text_x - container_x) - icon_padding - then_section_width;
|
||||||
|
|
||||||
|
// Street name
|
||||||
|
p.setFont(InterFont(75, QFont::Bold));
|
||||||
|
p.setPen(Qt::white);
|
||||||
|
QFontMetrics fm(p.font());
|
||||||
|
|
||||||
|
QString street_line1, street_line2;
|
||||||
|
QStringList words = navigationStreet.split(' ');
|
||||||
|
QString currentLine;
|
||||||
|
|
||||||
|
for (int i = 0; i < words.size(); ++i) {
|
||||||
|
QString testLine = currentLine.isEmpty() ? words[i] : currentLine + " " + words[i];
|
||||||
|
if (fm.horizontalAdvance(testLine) <= text_area_width) {
|
||||||
|
currentLine = testLine;
|
||||||
|
} else {
|
||||||
|
if (street_line1.isEmpty()) {
|
||||||
|
street_line1 = currentLine;
|
||||||
|
currentLine = words[i];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (street_line1.isEmpty()) {
|
||||||
|
street_line1 = currentLine;
|
||||||
|
} else if (!currentLine.isEmpty()) {
|
||||||
|
street_line2 = currentLine;
|
||||||
|
if (words.size() > words.indexOf(currentLine.split(' ').last()) + 1) {
|
||||||
|
street_line2 = fm.elidedText(street_line2, Qt::ElideRight, text_area_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (street_line2.isEmpty()) {
|
||||||
|
QRect street_rect(text_x, container_y + (container_height - fm.height()) / 2, text_area_width, fm.height());
|
||||||
|
p.drawText(street_rect, Qt::AlignLeft | Qt::AlignVCenter, street_line1);
|
||||||
|
} else {
|
||||||
|
QRect street_rect1(text_x, container_y + 23, text_area_width, fm.height());
|
||||||
|
p.drawText(street_rect1, Qt::AlignLeft | Qt::AlignVCenter, street_line1);
|
||||||
|
|
||||||
|
QRect street_rect2(text_x, container_y + 23 + fm.height(), text_area_width, fm.height());
|
||||||
|
p.drawText(street_rect2, Qt::AlignLeft | Qt::AlignVCenter, street_line2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next Maneuver
|
||||||
|
if (navigationHasNext) {
|
||||||
|
const int divider_x = container_x + container_width - then_section_width - 8;
|
||||||
|
p.setPen(QPen(QColor(255, 255, 255, 50), 2));
|
||||||
|
p.drawLine(divider_x, container_y + 23, divider_x, container_y + container_height - 23);
|
||||||
|
|
||||||
|
const int then_x = divider_x + 15;
|
||||||
|
const int then_icon_size = 105;
|
||||||
|
|
||||||
|
QRect then_label_rect(then_x, container_y + 30, then_section_width - 23, 38);
|
||||||
|
p.setFont(InterFont(53, QFont::Medium));
|
||||||
|
p.setPen(Qt::white);
|
||||||
|
p.drawText(then_label_rect, Qt::AlignCenter, tr("Then"));
|
||||||
|
|
||||||
|
// Next maneuver icon
|
||||||
|
const int then_icon_x = then_x + (then_section_width - 23 - then_icon_size) / 2;
|
||||||
|
const int then_icon_y = container_y + 75;
|
||||||
|
|
||||||
|
QString next_icon_name = getNavigationIconName(navigationNextManeuverType, navigationNextModifier);
|
||||||
|
QPixmap next_nav_icon = loadPixmap("../../sunnypilot/selfdrive/assets/navigation/" + next_icon_name, {then_icon_size, then_icon_size});
|
||||||
|
|
||||||
|
if (!next_nav_icon.isNull()) {
|
||||||
|
p.drawPixmap(then_icon_x, then_icon_y, next_nav_icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.restore();
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ private:
|
|||||||
void drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text = "");
|
void drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text = "");
|
||||||
void drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect);
|
void drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect);
|
||||||
void drawBlinker(QPainter &p, const QRect &surface_rect);
|
void drawBlinker(QPainter &p, const QRect &surface_rect);
|
||||||
|
void drawNavigationHUD(QPainter &p, const QRect &surface_rect);
|
||||||
|
QString getNavigationIconName(const QString &type, const QString &mod);
|
||||||
|
|
||||||
bool lead_status;
|
bool lead_status;
|
||||||
float lead_d_rel;
|
float lead_d_rel;
|
||||||
@@ -120,4 +122,13 @@ private:
|
|||||||
float speedCluster = 0;
|
float speedCluster = 0;
|
||||||
int icbm_active_counter = 0;
|
int icbm_active_counter = 0;
|
||||||
bool pcmCruiseSpeed = true;
|
bool pcmCruiseSpeed = true;
|
||||||
|
|
||||||
|
bool navigationValid;
|
||||||
|
QString navigationStreet;
|
||||||
|
QString navigationDistance;
|
||||||
|
QString navigationModifier;
|
||||||
|
QString navigationManeuverType;
|
||||||
|
QString navigationNextModifier;
|
||||||
|
QString navigationNextManeuverType;
|
||||||
|
bool navigationHasNext;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) {
|
|||||||
"wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan",
|
"wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan",
|
||||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||||
"carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters",
|
"carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters",
|
||||||
"carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP"
|
"carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP", "navigationd"
|
||||||
});
|
});
|
||||||
|
|
||||||
// update timer
|
// update timer
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#define SUNNYPILOT_VERSION "2025.002.000"
|
#define SUNNYPILOT_VERSION "2025.003.000"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
5584d697233d147e0b6402e485b7cbf8fdddb70bde4b9e3b2f6919ed5f69475f
|
70406ab4dd66d0e384734a8a56632ae4a62bc9670c2e630a0f71588c4e212cd8
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Navigation
|
||||||
|
|
||||||
|
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).
|
||||||
@@ -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
|
||||||
@@ -72,6 +72,15 @@ class Coordinate:
|
|||||||
return x * EARTH_MEAN_RADIUS
|
return x * EARTH_MEAN_RADIUS
|
||||||
|
|
||||||
|
|
||||||
|
def bearing_between_two_points(point_one: Coordinate, point_two: Coordinate) -> float:
|
||||||
|
dlon = math.radians(point_two.longitude - point_one.longitude)
|
||||||
|
bearing_radians = math.atan2(math.sin(dlon)* math.cos(point_two.latitude), math.cos(point_one.latitude) * math.sin(point_two.latitude) -
|
||||||
|
math.sin(point_one.latitude) * math.cos(point_two.latitude) * math.cos(dlon))
|
||||||
|
bearing_degrees = math.degrees(bearing_radians)
|
||||||
|
bearing_normalized = (bearing_degrees + 360) % 360
|
||||||
|
return bearing_normalized
|
||||||
|
|
||||||
|
|
||||||
def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate):
|
def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate):
|
||||||
if a.distance_to(b) < 0.01:
|
if a.distance_to(b) < 0.01:
|
||||||
return a.distance_to(p)
|
return a.distance_to(p)
|
||||||
@@ -126,6 +135,8 @@ def string_to_direction(direction: str) -> str:
|
|||||||
if d in direction:
|
if d in direction:
|
||||||
if 'slight' in direction and d in MODIFIABLE_DIRECTIONS:
|
if 'slight' in direction and d in MODIFIABLE_DIRECTIONS:
|
||||||
return 'slight' + d.capitalize()
|
return 'slight' + d.capitalize()
|
||||||
|
elif 'sharp' in direction and d in MODIFIABLE_DIRECTIONS:
|
||||||
|
return 'sharp' + d.capitalize()
|
||||||
return d
|
return d
|
||||||
return 'none'
|
return 'none'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
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 cereal.messaging as messaging
|
||||||
|
from cereal import car, log
|
||||||
|
from openpilot.common.constants import CV
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
|
||||||
|
|
||||||
|
class NavigationDesires:
|
||||||
|
def __init__(self):
|
||||||
|
self.sm = messaging.SubMaster(['navigationd'])
|
||||||
|
self.desire = log.Desire.none
|
||||||
|
self._turn_speed_limit = 20 * CV.MPH_TO_MS
|
||||||
|
self._params = Params()
|
||||||
|
self.param_counter = -1
|
||||||
|
self.nav_allowed: bool = False
|
||||||
|
|
||||||
|
def update_params(self):
|
||||||
|
self.param_counter += 1
|
||||||
|
if self.param_counter % 60 == 0: # every 3 seconds at 20hz
|
||||||
|
self.nav_allowed = self._params.get("NavDesiresAllowed", return_default=True)
|
||||||
|
|
||||||
|
def update(self, CS: car.CarState, lateral_active: bool) -> log.Desire:
|
||||||
|
self.update_params()
|
||||||
|
self.sm.update(0)
|
||||||
|
nav_msg = self.sm['navigationd']
|
||||||
|
self.desire = log.Desire.none
|
||||||
|
if self.nav_allowed and nav_msg.valid and lateral_active:
|
||||||
|
upcoming = nav_msg.upcomingTurn
|
||||||
|
if upcoming == 'slightLeft' and not CS.rightBlinker and not CS.leftBlindspot and CS.steeringPressed and CS.steeringTorque > 0:
|
||||||
|
self.desire = log.Desire.keepLeft
|
||||||
|
elif upcoming == 'slightRight' and not CS.leftBlinker and not CS.rightBlindspot and CS.steeringPressed and CS.steeringTorque < 0:
|
||||||
|
self.desire = log.Desire.keepRight
|
||||||
|
elif upcoming == 'left' and not CS.rightBlinker and not CS.leftBlindspot and CS.vEgo < self._turn_speed_limit:
|
||||||
|
self.desire = log.Desire.turnLeft
|
||||||
|
elif upcoming == 'right' and not CS.leftBlinker and not CS.rightBlindspot and CS.vEgo < self._turn_speed_limit:
|
||||||
|
self.desire = log.Desire.turnRight
|
||||||
|
return self.desire
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""
|
||||||
|
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 pytest
|
||||||
|
import types
|
||||||
|
|
||||||
|
from cereal import log
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
|
||||||
|
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||||
|
from openpilot.sunnypilot.navd.navigation_desires.navigation_desires import NavigationDesires
|
||||||
|
|
||||||
|
|
||||||
|
def make_car(vEgo=0, leftBlinker=False, rightBlinker=False, leftBlindspot=False, rightBlindspot=False, steeringPressed=False, steeringTorque=0):
|
||||||
|
return types.SimpleNamespace(
|
||||||
|
vEgo=vEgo, leftBlinker=leftBlinker, rightBlinker=rightBlinker,
|
||||||
|
leftBlindspot=leftBlindspot, rightBlindspot=rightBlindspot,
|
||||||
|
steeringPressed=steeringPressed, steeringTorque=steeringTorque
|
||||||
|
)
|
||||||
|
|
||||||
|
NAVIGATION_PARAMS: list[tuple] = [
|
||||||
|
('slightLeft', make_car(steeringPressed=True, steeringTorque=1), log.Desire.keepLeft),
|
||||||
|
('slightRight', make_car(steeringPressed=True, steeringTorque=-1), log.Desire.keepRight),
|
||||||
|
('slightLeft', make_car(vEgo=9, leftBlindspot=True), log.Desire.none),
|
||||||
|
('slightRight', make_car(vEgo=9, rightBlindspot=True), log.Desire.none),
|
||||||
|
('left', make_car(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False), log.Desire.turnLeft),
|
||||||
|
('left', make_car(vEgo=5, leftBlinker=False, rightBlinker=True), log.Desire.none),
|
||||||
|
('right', make_car(vEgo=6, rightBlinker=True, leftBlindspot=False), log.Desire.turnRight),
|
||||||
|
('right', make_car(vEgo=6, rightBlinker=True, rightBlindspot=True), log.Desire.none),
|
||||||
|
('left', make_car(vEgo=9, leftBlinker=True), log.Desire.none),
|
||||||
|
]
|
||||||
|
|
||||||
|
INTEGRATION_PARAMS: list[tuple] = [(carstate, upcoming, log.Desire.none, expected) for upcoming, carstate, expected in NAVIGATION_PARAMS] + [
|
||||||
|
(make_car(vEgo=6, leftBlinker=True, steeringPressed=True, steeringTorque=1), 'slightLeft', log.Desire.turnLeft, log.Desire.keepLeft),
|
||||||
|
(make_car(vEgo=5, rightBlinker=True, steeringPressed=True, steeringTorque=-1), 'slightRight', log.Desire.turnRight, log.Desire.keepRight),
|
||||||
|
(make_car(vEgo=9, leftBlinker=True), 'slightLeft', log.Desire.laneChangeLeft, log.Desire.laneChangeLeft),
|
||||||
|
(make_car(vEgo=9, rightBlinker=True), 'slightRight', log.Desire.laneChangeRight, log.Desire.laneChangeRight),
|
||||||
|
(make_car(vEgo=9), 'none', log.Desire.none, log.Desire.none),
|
||||||
|
]
|
||||||
|
|
||||||
|
def make_nav_msg(valid=False, upcoming='none'):
|
||||||
|
return types.SimpleNamespace(valid=valid, upcomingTurn=upcoming)
|
||||||
|
|
||||||
|
def params_setter(allowed: bool):
|
||||||
|
params = Params()
|
||||||
|
params.put("NavDesiresAllowed", allowed)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_submaster(mocker):
|
||||||
|
mock_sm = mocker.patch('cereal.messaging.SubMaster')
|
||||||
|
mock_sm_instance = mocker.Mock()
|
||||||
|
mock_sm.return_value = mock_sm_instance
|
||||||
|
mock_sm_instance.__getitem__ = mocker.Mock(return_value=make_nav_msg(valid=False))
|
||||||
|
params_setter(True)
|
||||||
|
return mock_sm_instance
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("upcoming, carstate, expected", NAVIGATION_PARAMS)
|
||||||
|
def test_navigation_desires_update(mock_submaster, mocker, upcoming, carstate, expected):
|
||||||
|
nav_desires = NavigationDesires()
|
||||||
|
nav_desires.sm.__getitem__ = mocker.Mock(return_value=make_nav_msg(valid=True, upcoming=upcoming))
|
||||||
|
nav_desires.update(carstate, True)
|
||||||
|
assert nav_desires.desire == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("msg_valid,lateral_active", [(False, True), (True, False)])
|
||||||
|
def test_invalid_or_inactive(mock_submaster, mocker, msg_valid, lateral_active):
|
||||||
|
nav_desires = NavigationDesires()
|
||||||
|
nav_desires.sm.__getitem__ = mocker.Mock(return_value=make_nav_msg(valid=msg_valid, upcoming='slightLeft'))
|
||||||
|
nav_desires.update(make_car(), lateral_active)
|
||||||
|
assert nav_desires.desire == log.Desire.none
|
||||||
|
|
||||||
|
def test_update(mock_submaster, mocker):
|
||||||
|
mock_submaster.__getitem__ = mocker.Mock(return_value=make_nav_msg(valid=True, upcoming='left'))
|
||||||
|
nav_desires = NavigationDesires()
|
||||||
|
|
||||||
|
assert nav_desires.update(make_car(leftBlinker=True, steeringPressed=True, steeringTorque=1), True) == log.Desire.turnLeft
|
||||||
|
|
||||||
|
params_setter(False)
|
||||||
|
nav_desires.param_counter = 59
|
||||||
|
nav_desires.update(make_car(leftBlinker=True), True)
|
||||||
|
assert nav_desires.desire == log.Desire.none
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("carstate, upcoming, current_desire, expected", INTEGRATION_PARAMS)
|
||||||
|
def test_desire_helper(mock_submaster, mocker, carstate, upcoming, current_desire, expected):
|
||||||
|
mock_submaster.__getitem__ = mocker.Mock(return_value=make_nav_msg(valid=True, upcoming=upcoming))
|
||||||
|
dh = DesireHelper()
|
||||||
|
dh.desire = current_desire
|
||||||
|
|
||||||
|
if current_desire in (log.Desire.laneChangeLeft, log.Desire.laneChangeRight):
|
||||||
|
dh.lane_change_state = log.LaneChangeState.laneChangeStarting
|
||||||
|
dh.lane_change_direction = log.LaneChangeDirection.left if current_desire == log.Desire.laneChangeLeft else log.LaneChangeDirection.right
|
||||||
|
|
||||||
|
dh.update(carstate, True, 1.0)
|
||||||
|
assert dh.desire == expected
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"""
|
||||||
|
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 requests
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
|
||||||
|
|
||||||
|
class MapboxIntegration:
|
||||||
|
def __init__(self):
|
||||||
|
self.params = Params()
|
||||||
|
|
||||||
|
def get_public_token(self) -> str:
|
||||||
|
token: str = self.params.get('MapboxToken', return_default=True)
|
||||||
|
return token
|
||||||
|
|
||||||
|
def set_destination(self, postvars, current_lon, current_lat, bearing=None) -> tuple[dict, bool]:
|
||||||
|
if 'latitude' in postvars and 'longitude' in postvars:
|
||||||
|
self.nav_confirmed(postvars, current_lon, current_lat, bearing)
|
||||||
|
return postvars, True
|
||||||
|
|
||||||
|
addr = postvars['place_name']
|
||||||
|
if not addr:
|
||||||
|
return postvars, False
|
||||||
|
|
||||||
|
token = self.get_public_token()
|
||||||
|
url = f'https://api.mapbox.com/geocoding/v5/mapbox.places/{quote(addr)}.json?access_token={token}&limit=1&proximity={current_lon},{current_lat}'
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
features = response.json()['features']
|
||||||
|
if features:
|
||||||
|
longitude, latitude = features[0]['geometry']['coordinates']
|
||||||
|
postvars.update({'latitude': latitude, 'longitude': longitude, 'name': addr})
|
||||||
|
self.nav_confirmed(postvars, current_lon, current_lat, bearing)
|
||||||
|
return postvars, True
|
||||||
|
except requests.RequestException:
|
||||||
|
pass # Broad exception to handle network errors like no internet without crashing navd process.
|
||||||
|
return postvars, False
|
||||||
|
|
||||||
|
def nav_confirmed(self, postvars, start_lon, start_lat, bearing=None) -> None:
|
||||||
|
if not postvars:
|
||||||
|
return
|
||||||
|
|
||||||
|
latitude = float(postvars['latitude'])
|
||||||
|
longitude = float(postvars['longitude'])
|
||||||
|
|
||||||
|
data: dict = {'navData': {'current': {'latitude': latitude, 'longitude': longitude}, 'route': {}}}
|
||||||
|
|
||||||
|
token = self.get_public_token()
|
||||||
|
route_data = self.generate_route(start_lon, start_lat, longitude, latitude, token, bearing)
|
||||||
|
if route_data:
|
||||||
|
data['navData']['route'] = route_data
|
||||||
|
self.params.put('MapboxSettings', data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_route(start_lon, start_lat, end_lon, end_lat, token, bearing=None) -> dict | None:
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'access_token': token,
|
||||||
|
'geometries': 'geojson',
|
||||||
|
'steps': 'true',
|
||||||
|
'overview': 'full',
|
||||||
|
'annotations': 'maxspeed',
|
||||||
|
'alternatives': 'false',
|
||||||
|
'banner_instructions': 'true',
|
||||||
|
}
|
||||||
|
if bearing is not None:
|
||||||
|
params['bearings'] = f'{int((bearing + 360) % 360):.0f},90;'
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(f'https://api.mapbox.com/directions/v5/mapbox/driving/{start_lon},{start_lat};{end_lon},{end_lat}', params=params, timeout=5)
|
||||||
|
data = response.json() if response.status_code == 200 else {}
|
||||||
|
except requests.RequestException:
|
||||||
|
return None
|
||||||
|
|
||||||
|
routes = data['routes'] if data else None
|
||||||
|
legs = routes[0]['legs'] if routes else None
|
||||||
|
|
||||||
|
if data.get('code') != 'Ok' or not routes or not legs:
|
||||||
|
return None
|
||||||
|
|
||||||
|
route = routes[0]
|
||||||
|
leg = legs[0]
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
{
|
||||||
|
'maneuver': step['maneuver']['type'],
|
||||||
|
'instruction': step['maneuver']['instruction'],
|
||||||
|
'distance': step['distance'],
|
||||||
|
'duration': step['duration'],
|
||||||
|
'location': {'longitude': step['maneuver']['location'][0], 'latitude': step['maneuver']['location'][1]},
|
||||||
|
'modifier': step['maneuver'].get('modifier', 'none'),
|
||||||
|
'bannerInstructions': step['bannerInstructions'],
|
||||||
|
}
|
||||||
|
for step in leg['steps']
|
||||||
|
]
|
||||||
|
|
||||||
|
maxspeed = [{'speed': item['speed'], 'unit': item['unit']} for item in leg['annotation']['maxspeed'] if 'speed' in item]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'steps': steps,
|
||||||
|
'totalDistance': route['distance'],
|
||||||
|
'totalDuration': route['duration'],
|
||||||
|
'geometry': [{'longitude': coord[0], 'latitude': coord[1]} for coord in route['geometry']['coordinates']],
|
||||||
|
'maxspeed': maxspeed,
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
"""
|
||||||
|
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 numpy import interp
|
||||||
|
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.navd.helpers import Coordinate, bearing_between_two_points, string_to_direction
|
||||||
|
|
||||||
|
|
||||||
|
class NavigationInstructions:
|
||||||
|
def __init__(self):
|
||||||
|
self.coord = Coordinate(0, 0)
|
||||||
|
self.params = Params()
|
||||||
|
|
||||||
|
self._cached_route = None
|
||||||
|
self._route_loaded = False
|
||||||
|
self._no_route = False
|
||||||
|
|
||||||
|
self.closest_idx: float = 0
|
||||||
|
self.min_distance: float = 0
|
||||||
|
|
||||||
|
def get_route_progress(self, current_lat, current_lon) -> dict | None:
|
||||||
|
route = self.get_current_route()
|
||||||
|
if not route or not route['geometry'] or not route['steps']:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.coord.latitude = current_lat
|
||||||
|
self.coord.longitude = current_lon
|
||||||
|
|
||||||
|
# Find the closest point on the route relative to self
|
||||||
|
self.closest_idx, self.min_distance = min(((idx, self.coord.distance_to(coord)) for idx, coord in enumerate(route['geometry'])), key=lambda x: x[1])
|
||||||
|
closest_cumulative = route['cumulative_distances'][self.closest_idx]
|
||||||
|
|
||||||
|
# Find the current step index, which is the HIGHEST idx where the step location cumulative less/equal closest cumulative
|
||||||
|
current_step_idx = max((idx for idx, step in enumerate(route['steps']) if step['cumulative_distance'] <= closest_cumulative), default=-1)
|
||||||
|
current_step = route['steps'][current_step_idx if current_step_idx >= 0 else 0]
|
||||||
|
|
||||||
|
# The next turn is the next step relative to our cumulative index
|
||||||
|
next_turn_idx = current_step_idx + 1
|
||||||
|
next_turn = route['steps'][next_turn_idx] if 0 <= next_turn_idx < len(route['steps']) else None
|
||||||
|
|
||||||
|
current_maxspeed = current_step['maxspeed']
|
||||||
|
|
||||||
|
distance_to_end_of_step = max(0, current_step['distance'] - (closest_cumulative - current_step['cumulative_distance']))
|
||||||
|
|
||||||
|
all_maneuvers: list = []
|
||||||
|
max_maneuvers = 3
|
||||||
|
for idx in range(current_step_idx, min(current_step_idx + max_maneuvers, len(route['steps']))):
|
||||||
|
step = route['steps'][idx]
|
||||||
|
if idx == current_step_idx:
|
||||||
|
distance = distance_to_end_of_step
|
||||||
|
else:
|
||||||
|
distance = step['cumulative_distance'] - closest_cumulative
|
||||||
|
all_maneuvers.append({'distance': distance, 'type': step['maneuver'], 'modifier': step['modifier'], 'instruction': step['instruction']})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'distance_from_route': self.min_distance,
|
||||||
|
'current_step': current_step,
|
||||||
|
'next_turn': next_turn,
|
||||||
|
'current_maxspeed': current_maxspeed,
|
||||||
|
'all_maneuvers': all_maneuvers,
|
||||||
|
'current_step_idx': current_step_idx,
|
||||||
|
'distance_to_end_of_step': distance_to_end_of_step,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_current_route(self):
|
||||||
|
if self._route_loaded and self._cached_route is not None:
|
||||||
|
return self._cached_route
|
||||||
|
if self._no_route:
|
||||||
|
return None
|
||||||
|
|
||||||
|
param_value = self.params.get('MapboxSettings')
|
||||||
|
route = param_value['navData']['route'] if param_value else None
|
||||||
|
if not route:
|
||||||
|
self._no_route = True
|
||||||
|
return None
|
||||||
|
|
||||||
|
geometry = [Coordinate(coord['latitude'], coord['longitude']) for coord in route['geometry']]
|
||||||
|
cumulative_distances = [0.0]
|
||||||
|
cumulative_distances.extend(cumulative_distances[-1] + geometry[step - 1].distance_to(geometry[step]) for step in range(1, len(geometry)))
|
||||||
|
maxspeed = [(speed['speed'], speed['unit']) for speed in route['maxspeed']]
|
||||||
|
steps = []
|
||||||
|
for step in route['steps']:
|
||||||
|
location = Coordinate(step['location']['latitude'], step['location']['longitude'])
|
||||||
|
closest_idx = min(range(len(geometry)), key=lambda i: location.distance_to(geometry[i]))
|
||||||
|
steps.append({
|
||||||
|
'bannerInstructions': step['bannerInstructions'],
|
||||||
|
'distance': step['distance'],
|
||||||
|
'duration': step['duration'],
|
||||||
|
'maneuver': step['maneuver'],
|
||||||
|
'location': location,
|
||||||
|
'cumulative_distance': cumulative_distances[closest_idx],
|
||||||
|
'maxspeed': maxspeed[min(closest_idx, len(maxspeed) - 1)] if len(maxspeed) > 0 else (0, 'kmh'),
|
||||||
|
'modifier': string_to_direction(step['modifier']),
|
||||||
|
'instruction': step['instruction'],
|
||||||
|
})
|
||||||
|
self._cached_route = {
|
||||||
|
'steps': steps,
|
||||||
|
'total_distance': route['totalDistance'],
|
||||||
|
'total_duration': route['totalDuration'],
|
||||||
|
'geometry': geometry,
|
||||||
|
'cumulative_distances': cumulative_distances,
|
||||||
|
'maxspeed': maxspeed,
|
||||||
|
}
|
||||||
|
self._route_loaded = True
|
||||||
|
return self._cached_route
|
||||||
|
|
||||||
|
def clear_route_cache(self):
|
||||||
|
self._cached_route = None
|
||||||
|
self._route_loaded = False
|
||||||
|
self._no_route = False
|
||||||
|
|
||||||
|
def route_bearing_misalign(self, route, bearing, v_ego) -> bool:
|
||||||
|
route_bearing_misalign:bool = False
|
||||||
|
|
||||||
|
if v_ego < 5.0:
|
||||||
|
route_bearing_misalign = False
|
||||||
|
elif self.closest_idx > 0 and self.closest_idx < len(route['geometry']) -1:
|
||||||
|
current_coord = route['geometry'][self.closest_idx - 1]
|
||||||
|
future_coord = route['geometry'][self.closest_idx + 1]
|
||||||
|
route_bearing = bearing_between_two_points(current_coord, future_coord)
|
||||||
|
current_bearing_normalized = (bearing + 360) % 360
|
||||||
|
bearing_difference = abs(current_bearing_normalized - route_bearing)
|
||||||
|
|
||||||
|
if min(bearing_difference, 360 - bearing_difference) > 91:
|
||||||
|
route_bearing_misalign = True # flag for recompute/cancellation
|
||||||
|
return route_bearing_misalign
|
||||||
|
|
||||||
|
def get_upcoming_turn_from_progress(self, progress, current_lat, current_lon, v_ego: float) -> str:
|
||||||
|
if progress and progress['next_turn']:
|
||||||
|
speed_breakpoints: list = [0, 5, 10, 15, 20, 25, 30, 35, 40]
|
||||||
|
distance_breakpoints: list = [20, 25, 30, 45, 60, 75, 90, 105, 120]
|
||||||
|
distance_interp = interp(v_ego, speed_breakpoints, distance_breakpoints)
|
||||||
|
|
||||||
|
self.coord.latitude = current_lat
|
||||||
|
self.coord.longitude = current_lon
|
||||||
|
distance = self.coord.distance_to(progress['next_turn']['location'])
|
||||||
|
|
||||||
|
if distance <= distance_interp:
|
||||||
|
modifier = progress['next_turn']['modifier']
|
||||||
|
return str(modifier)
|
||||||
|
return 'none'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def arrived_at_destination(progress, v_ego) -> bool:
|
||||||
|
if v_ego < 1.0:
|
||||||
|
maneuvers = progress['all_maneuvers'][0]
|
||||||
|
if maneuvers['type'] == 'arrive' or maneuvers['instruction'].startswith('Your destination'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
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 os
|
||||||
|
|
||||||
|
from openpilot.common.constants import CV
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.navd.navigation_helpers.mapbox_integration import MapboxIntegration
|
||||||
|
from openpilot.sunnypilot.navd.navigation_helpers.nav_instructions import NavigationInstructions
|
||||||
|
|
||||||
|
|
||||||
|
class TestMapbox:
|
||||||
|
@classmethod
|
||||||
|
def setup_class(cls):
|
||||||
|
cls.mapbox = MapboxIntegration()
|
||||||
|
cls.nav = NavigationInstructions()
|
||||||
|
|
||||||
|
token = os.environ.get('MAPBOX_TOKEN_CI')
|
||||||
|
if token:
|
||||||
|
cls.mapbox.params.put('MapboxToken', token)
|
||||||
|
|
||||||
|
# route setup
|
||||||
|
cls.current_lon, cls.current_lat = -119.17557, 34.23305
|
||||||
|
cls.mapbox.params.put('MapboxRoute', '740 E Ventura Blvd. Camarillo, CA')
|
||||||
|
cls.postvars = {"place_name": cls.mapbox.params.get('MapboxRoute')}
|
||||||
|
cls.postvars, cls.valid_addr = cls.mapbox.set_destination(cls.postvars, cls.current_lon, cls.current_lat)
|
||||||
|
cls.route = cls.nav.get_current_route()
|
||||||
|
cls.progress = cls.nav.get_route_progress(cls.current_lat, cls.current_lon)
|
||||||
|
|
||||||
|
def test_set_destination(self):
|
||||||
|
assert self.valid_addr
|
||||||
|
settings = self.mapbox.params.get('MapboxSettings')
|
||||||
|
assert settings is not None
|
||||||
|
dest_lat = settings['navData']['current']['latitude']
|
||||||
|
dest_lon = settings['navData']['current']['longitude']
|
||||||
|
assert dest_lat == self.postvars["latitude"] and dest_lon == self.postvars["longitude"]
|
||||||
|
|
||||||
|
def test_get_route(self):
|
||||||
|
assert self.route is not None
|
||||||
|
assert 'steps' in self.route
|
||||||
|
assert 'geometry' in self.route
|
||||||
|
assert 'maxspeed' in self.route
|
||||||
|
assert 'total_distance' in self.route
|
||||||
|
assert 'total_duration' in self.route
|
||||||
|
assert len(self.route['steps']) > 0
|
||||||
|
assert len(self.route['geometry']) > 0
|
||||||
|
assert len(self.route['maxspeed']) > 0
|
||||||
|
|
||||||
|
if self.route and 'steps' in self.route:
|
||||||
|
for step in self.route['steps']:
|
||||||
|
assert 'modifier' in step
|
||||||
|
|
||||||
|
def test_upcoming_turn_detection(self):
|
||||||
|
upcoming = self.nav.get_upcoming_turn_from_progress(self.progress, self.current_lat, self.current_lon, v_ego=40.0)
|
||||||
|
assert isinstance(upcoming, str)
|
||||||
|
assert upcoming == 'none'
|
||||||
|
|
||||||
|
if self.route['steps']:
|
||||||
|
turn_lat = self.route['steps'][1]['location'].latitude
|
||||||
|
turn_lon = self.route['steps'][1]['location'].longitude
|
||||||
|
close_lat = turn_lat - 0.000175 # slightly before the turn
|
||||||
|
if self.progress and self.progress.get('next_turn'):
|
||||||
|
expected_turn = self.progress['next_turn']['modifier']
|
||||||
|
upcoming_close = self.nav.get_upcoming_turn_from_progress(self.progress, close_lat, turn_lon, v_ego=0.0)
|
||||||
|
if expected_turn:
|
||||||
|
assert upcoming_close == expected_turn == 'right', "Should be a right turn upcoming"
|
||||||
|
|
||||||
|
def test_route_progress_tracking(self):
|
||||||
|
assert self.progress is not None
|
||||||
|
assert 'distance_from_route' in self.progress
|
||||||
|
assert 'next_turn' in self.progress
|
||||||
|
assert 'current_maxspeed' in self.progress
|
||||||
|
assert 'all_maneuvers' in self.progress
|
||||||
|
assert 'distance_to_end_of_step' in self.progress
|
||||||
|
assert self.progress['distance_from_route'] >= 0
|
||||||
|
assert isinstance(self.progress['all_maneuvers'], list)
|
||||||
|
|
||||||
|
def test_speed_limit_handling(self):
|
||||||
|
speed_limit_metric = self.progress['current_maxspeed'][0]
|
||||||
|
speed_limit_imperial = (round(speed_limit_metric * CV.KPH_TO_MPH))
|
||||||
|
assert isinstance(speed_limit_metric, int)
|
||||||
|
assert isinstance(speed_limit_imperial, int)
|
||||||
|
|
||||||
|
def test_arrival_detection(self):
|
||||||
|
is_arrived = self.nav.arrived_at_destination(self.progress, 2.0)
|
||||||
|
assert isinstance(is_arrived, bool)
|
||||||
|
assert not is_arrived
|
||||||
|
|
||||||
|
def test_bearing_misalign(self):
|
||||||
|
lat = self.route['steps'][1]['location'].latitude
|
||||||
|
lon = self.route['steps'][1]['location'].longitude
|
||||||
|
self.nav.get_route_progress(lat, lon)
|
||||||
|
route_bearing_misaligned = self.nav.route_bearing_misalign(self.route, 45, 5.0)
|
||||||
|
# based on math: closest index: 7, normalized bearing: 45 route bearing: 180.5486953778888, expected differential: 135.54869538
|
||||||
|
assert route_bearing_misaligned
|
||||||
Executable
+168
@@ -0,0 +1,168 @@
|
|||||||
|
"""
|
||||||
|
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.destination = None
|
||||||
|
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']:
|
||||||
|
parsed = parse_banner_instructions(progress['current_step']['bannerInstructions'], progress['distance_to_end_of_step'])
|
||||||
|
if parsed:
|
||||||
|
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,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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ae17f4c1896c88a6d520f1acdca99ab23d4ce1140a500b64bea3185041f8ecd9
|
||||||
|
size 1246
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:86a2eaf6070d6b93df385945bd7f4a6e9bb78b7cc42eb5b7ef9a9b6c075c80ce
|
||||||
|
size 1581
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:398f30c89e396bb4bdbac1ba6dfe292f6e3278923e14113e705f4d0591671f32
|
||||||
|
size 1528
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ae17f4c1896c88a6d520f1acdca99ab23d4ce1140a500b64bea3185041f8ecd9
|
||||||
|
size 1246
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0e23cac9bec0de20f9632d19618be9b05eedb010c6e23b0ba7a8e3cc8225f64e
|
||||||
|
size 865
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:008cf037b8713c710eacd1129973119faa2527ab1c19aee5340a16dc0659df8a
|
||||||
|
size 1063
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:579d6a27e062779d41beb9320abf5797bc8ec81de1f355a4197c3b3ee0766f28
|
||||||
|
size 1400
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a4010c8122af918dbdb50e88629adbf79f7733f022bba263e6f44f6ffae3e7a5
|
||||||
|
size 1371
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:89041889817454869d98adfb5a16fae33596c038d5484ec734fe3a5cc532c45c
|
||||||
|
size 1334
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e691dd43947c9430fcd73e6bede94aa5fd8995e9303eba53cc0ac589a7c9f106
|
||||||
|
size 1347
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:008cf037b8713c710eacd1129973119faa2527ab1c19aee5340a16dc0659df8a
|
||||||
|
size 1063
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e02ac2720f12f9373ae2d86d91fffb6203e18aba04b493bd6f41d2953a11cbc9
|
||||||
|
size 1780
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8f83214c1fa6aba33ca3e1fe29db09733a275efe19864866b9edecbb59abb171
|
||||||
|
size 1234
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:459fe6410e061542876fa7de93efaf901d15937f891e801e1b93f8270e750130
|
||||||
|
size 1613
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e3f5fb80d62c92876a5e490917f549e2a443dfc683a6787506747bbbeb75d33b
|
||||||
|
size 1538
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8f83214c1fa6aba33ca3e1fe29db09733a275efe19864866b9edecbb59abb171
|
||||||
|
size 1234
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b9c86ddd75ea1ed2c49c1bdc2e7f98ed15d12226938073688dda1b0bbdf62308
|
||||||
|
size 1439
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cc7248e62d11cf36f9a13d408a0954a51ff0eee9b8e03d8910a2eb2cb6a09ffd
|
||||||
|
size 1456
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8509bd5dbf96e44f73421dc09ca8f7c1dddf2dc21f3b4b15650472b3e06b3470
|
||||||
|
size 658
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5198a3fdb610001358501ca76ab52fe0979b9254ed85cbbb2ce1fab1cb77929a
|
||||||
|
size 2015
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:752424da0ad8430c619045455a86c640b576f8bbe6d3d8b5b6a599b1c61ef489
|
||||||
|
size 1863
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b6590df8da1f30ec61b3bdf729fda1956d340c5abab448ea0062105500cc5da5
|
||||||
|
size 1841
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b4fbd63acf8f5e7f734b332a96e9ad138ad22ee07000ad425712e71b77ce0728
|
||||||
|
size 1800
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a7a7f5bcab30ce961ff7e345c460cfbd133e45c66da1fe1f0e7bc7c826b05f30
|
||||||
|
size 1805
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:676cbe95f9a663816dd77c55dbcf2c346bfae2454be7832914b5527219aba91f
|
||||||
|
size 2473
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:008cf037b8713c710eacd1129973119faa2527ab1c19aee5340a16dc0659df8a
|
||||||
|
size 1063
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:579d6a27e062779d41beb9320abf5797bc8ec81de1f355a4197c3b3ee0766f28
|
||||||
|
size 1400
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a4010c8122af918dbdb50e88629adbf79f7733f022bba263e6f44f6ffae3e7a5
|
||||||
|
size 1371
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:89041889817454869d98adfb5a16fae33596c038d5484ec734fe3a5cc532c45c
|
||||||
|
size 1334
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e691dd43947c9430fcd73e6bede94aa5fd8995e9303eba53cc0ac589a7c9f106
|
||||||
|
size 1347
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:008cf037b8713c710eacd1129973119faa2527ab1c19aee5340a16dc0659df8a
|
||||||
|
size 1063
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e02ac2720f12f9373ae2d86d91fffb6203e18aba04b493bd6f41d2953a11cbc9
|
||||||
|
size 1780
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:08245891301f814fc0245133e39df3fb14720025cc3e4bc7131f953ead47edc8
|
||||||
|
size 1535
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8c629c7273fe2613538295aba807edfddbbae86ae77d6f9b0b60b0486e3f7ca9
|
||||||
|
size 1484
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:72c1ef39486f3dc3f7a0a97777bce16c7c369cc85bf6c82629d6780803804967
|
||||||
|
size 1765
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8b8bfd8019320127ed0fe03d4a1bddab57d5c2345b6cdeab66558ea9b53c0266
|
||||||
|
size 1699
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d10bdde82e7896de072de28da46412d5e2f6c902fd5643c6b48a8fc79ed4232f
|
||||||
|
size 113
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:579d6a27e062779d41beb9320abf5797bc8ec81de1f355a4197c3b3ee0766f28
|
||||||
|
size 1400
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a4010c8122af918dbdb50e88629adbf79f7733f022bba263e6f44f6ffae3e7a5
|
||||||
|
size 1371
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0457b0597386d0b7d3cc06d961df5fb34211ee575da7fab2f81e424d9954bb42
|
||||||
|
size 1549
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4f6e1556f0b888afb6856f8cf3e16632959190f88dd66c4d9db4832a8269cbc5
|
||||||
|
size 1552
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:89041889817454869d98adfb5a16fae33596c038d5484ec734fe3a5cc532c45c
|
||||||
|
size 1334
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e691dd43947c9430fcd73e6bede94aa5fd8995e9303eba53cc0ac589a7c9f106
|
||||||
|
size 1347
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:008cf037b8713c710eacd1129973119faa2527ab1c19aee5340a16dc0659df8a
|
||||||
|
size 1063
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a4010c8122af918dbdb50e88629adbf79f7733f022bba263e6f44f6ffae3e7a5
|
||||||
|
size 1371
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:57dbfdabe88f6abbb8993982eef72803ac504842e9a9d388e7dde1623f69f0d7
|
||||||
|
size 1551
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:579d6a27e062779d41beb9320abf5797bc8ec81de1f355a4197c3b3ee0766f28
|
||||||
|
size 1400
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a4010c8122af918dbdb50e88629adbf79f7733f022bba263e6f44f6ffae3e7a5
|
||||||
|
size 1371
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4f2016b7bac3accf541f100a2c83871618214e1806e95e4d80867d30d1dfcf03
|
||||||
|
size 1546
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:89041889817454869d98adfb5a16fae33596c038d5484ec734fe3a5cc532c45c
|
||||||
|
size 1334
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e691dd43947c9430fcd73e6bede94aa5fd8995e9303eba53cc0ac589a7c9f106
|
||||||
|
size 1347
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:008cf037b8713c710eacd1129973119faa2527ab1c19aee5340a16dc0659df8a
|
||||||
|
size 1063
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6736b6c2123cbaf90d2c8e4da79a4089ba42c0e7a220a596e45afd511166da9b
|
||||||
|
size 1524
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6a636d33568c658b05e460e05a1eac5c4e1cfced204044657a04191027c44c4c
|
||||||
|
size 1580
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a27d55d704f77bca4beaac30a2c5b5abee304986627d09f971b2a2e0367e8477
|
||||||
|
size 1478
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e711c8e2184440f4ddead06897c90758470e7475fa7e0cf8b2db567e491c8e57
|
||||||
|
size 1519
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:579d6a27e062779d41beb9320abf5797bc8ec81de1f355a4197c3b3ee0766f28
|
||||||
|
size 1400
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a4010c8122af918dbdb50e88629adbf79f7733f022bba263e6f44f6ffae3e7a5
|
||||||
|
size 1371
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0457b0597386d0b7d3cc06d961df5fb34211ee575da7fab2f81e424d9954bb42
|
||||||
|
size 1549
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:57dbfdabe88f6abbb8993982eef72803ac504842e9a9d388e7dde1623f69f0d7
|
||||||
|
size 1551
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user