mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-09 21:14:44 +08:00
Compare commits
9 Commits
what-is-go
...
model-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e131b4a69 | ||
|
|
f402487f9e | ||
|
|
2c922afa12 | ||
|
|
342abcd45a | ||
|
|
6e8586e566 | ||
|
|
071147baaf | ||
|
|
18af4d6ad6 | ||
|
|
b81d5bca3c | ||
|
|
682d738ffa |
@@ -74,7 +74,7 @@ jobs:
|
||||
env:
|
||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||
run: |
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
||||
cd gitlab_docs
|
||||
git checkout main
|
||||
git sparse-checkout set --no-cone models/
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||
run: |
|
||||
echo "Cloning GitLab"
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
||||
cd gitlab_docs
|
||||
echo "checkout 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'
|
||||
run: |
|
||||
echo "Cloning GitLab"
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
||||
cd gitlab_docs
|
||||
echo "checkout models/${RECOMPILED_DIR}"
|
||||
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
||||
|
||||
3
.github/workflows/selfdrive_tests.yaml
vendored
3
.github/workflows/selfdrive_tests.yaml
vendored
@@ -21,12 +21,11 @@ env:
|
||||
PYTHONWARNINGS: error
|
||||
BASE_IMAGE: sunnypilot-base
|
||||
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 }}
|
||||
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 -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
|
||||
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
|
||||
|
||||
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
|
||||
|
||||
|
||||
@@ -156,8 +156,6 @@ jobs:
|
||||
with:
|
||||
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
||||
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
|
||||
run: |
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,30 +1,6 @@
|
||||
sunnypilot Version 2025.003.000 (20xx-xx-xx)
|
||||
sunnypilot Version 2025.002.000 (2025-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)
|
||||
========================
|
||||
* 🛠️ Major rewrite
|
||||
|
||||
@@ -454,20 +454,7 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||
}
|
||||
}
|
||||
|
||||
struct Navigationd @0xcb9fd56c7057593a {
|
||||
upcomingTurn @0 :Text;
|
||||
currentSpeedLimit @1 :UInt64;
|
||||
bannerInstructions @2 :Text;
|
||||
distanceFromRoute @3 :Float64;
|
||||
allManeuvers @4 :List(Maneuver);
|
||||
valid @5 :Bool;
|
||||
|
||||
struct Maneuver {
|
||||
distance @0 :Float64;
|
||||
type @1 :Text;
|
||||
modifier @2 :Text;
|
||||
instruction @3 :Text;
|
||||
}
|
||||
struct CustomReserved10 @0xcb9fd56c7057593a {
|
||||
}
|
||||
|
||||
struct CustomReserved11 @0xc2243c65e0340384 {
|
||||
|
||||
@@ -2632,7 +2632,7 @@ struct Event {
|
||||
carStateSP @114 :Custom.CarStateSP;
|
||||
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
||||
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
||||
navigationd @136 :Custom.Navigationd;
|
||||
customReserved10 @136 :Custom.CustomReserved10;
|
||||
customReserved11 @137 :Custom.CustomReserved11;
|
||||
customReserved12 @138 :Custom.CustomReserved12;
|
||||
customReserved13 @139 :Custom.CustomReserved13;
|
||||
|
||||
@@ -89,7 +89,6 @@ _services: dict[str, tuple] = {
|
||||
"carStateSP": (True, 100., 10),
|
||||
"liveMapDataSP": (True, 1., 1),
|
||||
"modelDataV2SP": (True, 20.),
|
||||
"navigationd": (True, 3.),
|
||||
"liveLocationKalman": (True, 20.),
|
||||
|
||||
# debug
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "Firehose (Default)"
|
||||
#define DEFAULT_MODEL "TCPv3 + gWMv9 (Default)"
|
||||
|
||||
@@ -187,14 +187,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
|
||||
{"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
|
||||
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
|
||||
Submodule opendbc_repo updated: c32e79f3c6...c7126f8ba6
@@ -3,7 +3,6 @@ from openpilot.common.constants import CV
|
||||
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.lane_turn_desire import LaneTurnController
|
||||
from openpilot.sunnypilot.navd.navigation_desires.navigation_desires import NavigationDesires
|
||||
|
||||
LaneChangeState = log.LaneChangeState
|
||||
LaneChangeDirection = log.LaneChangeDirection
|
||||
@@ -52,7 +51,6 @@ class DesireHelper:
|
||||
self.alc = AutoLaneChangeController(self)
|
||||
self.lane_turn_controller = LaneTurnController(self)
|
||||
self.lane_turn_direction = TurnDirection.none
|
||||
self.navigation_desires = NavigationDesires()
|
||||
|
||||
@staticmethod
|
||||
def get_lane_change_direction(CS):
|
||||
@@ -145,7 +143,3 @@ class DesireHelper:
|
||||
self.desire = log.Desire.none
|
||||
|
||||
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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -99,7 +99,7 @@ class SelfdriveD(CruiseHelper):
|
||||
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
|
||||
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback',
|
||||
'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_valid=ignore, frequency=int(1/DT_CTRL))
|
||||
|
||||
@@ -293,7 +293,7 @@ class SelfdriveD(CruiseHelper):
|
||||
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
|
||||
direction = self.sm['modelV2'].meta.laneChangeDirection
|
||||
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)
|
||||
else:
|
||||
if direction == LaneChangeDirection.left:
|
||||
@@ -301,7 +301,7 @@ class SelfdriveD(CruiseHelper):
|
||||
else:
|
||||
self.events.add(EventName.preLaneChangeRight)
|
||||
elif self.sm['modelV2'].meta.laneChangeState in (LaneChangeState.laneChangeStarting,
|
||||
LaneChangeState.laneChangeFinishing):
|
||||
LaneChangeState.laneChangeFinishing):
|
||||
self.events.add(EventName.laneChange)
|
||||
|
||||
# Handle lane turn
|
||||
@@ -496,7 +496,7 @@ class SelfdriveD(CruiseHelper):
|
||||
|
||||
# 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 ps.safetyModel not in IGNORED_SAFETY_MODES):
|
||||
if ps.safetyModel not in IGNORED_SAFETY_MODES):
|
||||
self.mismatch_counter += 1
|
||||
|
||||
return CS
|
||||
|
||||
@@ -30,7 +30,6 @@ qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/max_time_offroad.cc",
|
||||
"sunnypilot/qt/offroad/settings/brightness.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/settings.cc",
|
||||
"sunnypilot/qt/offroad/settings/software_panel.cc",
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* 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,7 +15,6 @@
|
||||
#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/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/sunnylink_panel.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h"
|
||||
@@ -86,7 +85,6 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) {
|
||||
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("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("Cruise"), new LongitudinalPanel(this), "../assets/icons/speed_limit.png"),
|
||||
PanelInfo(" " + tr("Visuals"), new VisualsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"),
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
#include <QPainterPath>
|
||||
#include <cmath>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/hud.h"
|
||||
|
||||
@@ -155,73 +154,6 @@ void HudRendererSP::updateState(const UIState &s) {
|
||||
|
||||
allow_e2e_alerts = sm["selfdriveState"].getSelfdriveState().getAlertSize() == cereal::SelfdriveState::AlertSize::NONE &&
|
||||
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) {
|
||||
@@ -355,8 +287,6 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
||||
}
|
||||
}
|
||||
|
||||
drawNavigationHUD(p, surface_rect);
|
||||
|
||||
p.restore();
|
||||
}
|
||||
|
||||
@@ -376,14 +306,9 @@ bool HudRendererSP::pulseElement(int frame) {
|
||||
}
|
||||
|
||||
void HudRendererSP::drawSmartCruiseControlOnroadIcon(QPainter &p, const QRect &surface_rect, int x_offset, int y_offset, std::string name) {
|
||||
int base_x = surface_rect.center().x();
|
||||
int x = surface_rect.center().x();
|
||||
int y = surface_rect.height() / 4;
|
||||
|
||||
if (navigationValid) {
|
||||
base_x = 618;
|
||||
y = 420;
|
||||
}
|
||||
|
||||
QString text = QString::fromStdString(name);
|
||||
QFont font = InterFont(36, QFont::Bold);
|
||||
p.setFont(font);
|
||||
@@ -394,7 +319,7 @@ void HudRendererSP::drawSmartCruiseControlOnroadIcon(QPainter &p, const QRect &s
|
||||
int box_width = 160;
|
||||
int box_height = fm.height() + padding_v * 2;
|
||||
|
||||
QRectF bg_rect(base_x - (box_width / 2) + x_offset,
|
||||
QRectF bg_rect(x - (box_width / 2) + x_offset,
|
||||
y - (box_height / 2) + y_offset,
|
||||
box_width, box_height);
|
||||
|
||||
@@ -759,16 +684,15 @@ void HudRendererSP::drawSpeedLimitPreActiveArrow(QPainter &p, QRect &sign_rect)
|
||||
}
|
||||
|
||||
void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
|
||||
// ICBM counter logic
|
||||
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;
|
||||
}
|
||||
// Draw outer box + border to contain set speed
|
||||
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);
|
||||
|
||||
// Colors based on status
|
||||
QColor max_color = QColor(0xa6, 0xa6, 0xa6, 0xff);
|
||||
@@ -789,62 +713,29 @@ void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
|
||||
}
|
||||
}
|
||||
|
||||
QString max_str = (icbm_active_counter != 0) ? QString::number(std::nearbyint(speedCluster)) : tr("MAX");
|
||||
QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(set_speed)) : "–";
|
||||
|
||||
if (!navigationValid) {
|
||||
// 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);
|
||||
// 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 {
|
||||
// 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);
|
||||
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");
|
||||
|
||||
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)) : "–";
|
||||
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);
|
||||
}
|
||||
|
||||
void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text) {
|
||||
@@ -903,40 +794,12 @@ void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const Q
|
||||
|
||||
void HudRendererSP::drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect) {
|
||||
QString speedStr = QString::number(std::nearbyint(speed));
|
||||
QString unit = is_metric ? tr("km/h") : tr("mph");
|
||||
|
||||
int speed_x = surface_rect.center().x();
|
||||
int speed_y = 210;
|
||||
int unit_y = 290;
|
||||
QFont speed_font = InterFont(176, QFont::Bold);
|
||||
QFont unit_font = InterFont(66);
|
||||
p.setFont(InterFont(176, QFont::Bold));
|
||||
HudRenderer::drawText(p, surface_rect.center().x(), 210, speedStr);
|
||||
|
||||
if (navigationValid) {
|
||||
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);
|
||||
}
|
||||
p.setFont(InterFont(66));
|
||||
HudRenderer::drawText(p, surface_rect.center().x(), 290, is_metric ? tr("km/h") : tr("mph"), 200);
|
||||
}
|
||||
|
||||
void HudRendererSP::drawBlinker(QPainter &p, const QRect &surface_rect) {
|
||||
@@ -964,7 +827,7 @@ void HudRendererSP::drawBlinker(QPainter &p, const QRect &surface_rect) {
|
||||
const int circleRadius = 60;
|
||||
const int arrowLength = 60;
|
||||
const int x_gap = 160;
|
||||
const int y_offset = navigationValid ? 352 : 300;
|
||||
const int y_offset = 272;
|
||||
|
||||
const int centerX = surface_rect.center().x();
|
||||
|
||||
@@ -1022,186 +885,3 @@ void HudRendererSP::drawBlinker(QPainter &p, const QRect &surface_rect) {
|
||||
|
||||
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,8 +39,6 @@ private:
|
||||
void drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text = "");
|
||||
void drawCurrentSpeedSP(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;
|
||||
float lead_d_rel;
|
||||
@@ -122,13 +120,4 @@ private:
|
||||
float speedCluster = 0;
|
||||
int icbm_active_counter = 0;
|
||||
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",
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters",
|
||||
"carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP", "navigationd"
|
||||
"carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP"
|
||||
});
|
||||
|
||||
// update timer
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define SUNNYPILOT_VERSION "2025.003.000"
|
||||
#define SUNNYPILOT_VERSION "2025.002.000"
|
||||
|
||||
@@ -1 +1 @@
|
||||
70406ab4dd66d0e384734a8a56632ae4a62bc9670c2e630a0f71588c4e212cd8
|
||||
5584d697233d147e0b6402e485b7cbf8fdddb70bde4b9e3b2f6919ed5f69475f
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -126,8 +126,6 @@ def string_to_direction(direction: str) -> str:
|
||||
if d in direction:
|
||||
if 'slight' in direction and d in MODIFIABLE_DIRECTIONS:
|
||||
return 'slight' + d.capitalize()
|
||||
elif 'sharp' in direction and d in MODIFIABLE_DIRECTIONS:
|
||||
return 'sharp' + d.capitalize()
|
||||
return d
|
||||
return 'none'
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
"""
|
||||
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.leftBlindspot or CS.vEgo < self._turn_speed_limit):
|
||||
# self.desire = log.Desire.keepLeft
|
||||
# elif upcoming == 'slightRight' and (not CS.rightBlindspot or CS.vEgo < self._turn_speed_limit):
|
||||
# self.desire = log.Desire.keepRight
|
||||
if (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
|
||||
@@ -1,96 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,113 +0,0 @@
|
||||
"""
|
||||
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,
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
"""
|
||||
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 numpy as np
|
||||
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
|
||||
from openpilot.sunnypilot.navd.helpers import Coordinate, 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
|
||||
|
||||
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
|
||||
closest_idx, 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'][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': 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 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 = np.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 get_current_speed_limit_from_progress(progress, is_metric: bool) -> int:
|
||||
if progress and progress['current_maxspeed']:
|
||||
speed, _ = progress['current_maxspeed']
|
||||
if is_metric:
|
||||
return int(speed)
|
||||
else:
|
||||
return int(round(speed * CV.KPH_TO_MPH))
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def arrived_at_destination(progress) -> bool:
|
||||
if progress['all_maneuvers'][0]['type'] == 'arrive':
|
||||
return True
|
||||
elif progress['all_maneuvers'][0]['instruction'].startswith('Your destination'):
|
||||
return True
|
||||
return False
|
||||
@@ -1,94 +0,0 @@
|
||||
"""
|
||||
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.nav.get_current_speed_limit_from_progress(self.progress, True)
|
||||
speed_limit_imperial = self.nav.get_current_speed_limit_from_progress(self.progress, False)
|
||||
assert isinstance(speed_limit_metric, int)
|
||||
assert isinstance(speed_limit_imperial, int)
|
||||
expected_metric = int(self.progress['current_maxspeed'][0])
|
||||
expected_imperial = int(round(self.progress['current_maxspeed'][0] * CV.KPH_TO_MPH))
|
||||
assert speed_limit_metric == expected_metric
|
||||
assert speed_limit_imperial == expected_imperial
|
||||
|
||||
def test_arrival_detection(self):
|
||||
is_arrived = self.nav.arrived_at_destination(self.progress)
|
||||
assert isinstance(is_arrived, bool)
|
||||
assert not is_arrived
|
||||
@@ -1,165 +0,0 @@
|
||||
"""
|
||||
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 math
|
||||
import numpy as np
|
||||
|
||||
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(['carState', '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.is_metric: bool = False
|
||||
self.valid: bool = False
|
||||
|
||||
def _update_params(self):
|
||||
if self.last_position is not None:
|
||||
self.frame += 1
|
||||
if self.frame % 9 == 0:
|
||||
self.allow_navigation = self.params.get('AllowNavigation', return_default=True)
|
||||
self.is_metric = self.params.get('IsMetric', 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)
|
||||
cloudlog.debug(f'Set new destination to: {self.new_destination}, valid: {valid_addr}')
|
||||
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, v_ego) -> tuple[str, dict | None, dict]:
|
||||
banner_instructions: str = ''
|
||||
nav_data: dict = {}
|
||||
if self.allow_navigation and self.last_position is not None:
|
||||
if progress := self.nav_instructions.get_route_progress(self.last_position.latitude, self.last_position.longitude):
|
||||
nav_data['upcoming_turn'] = self.nav_instructions.get_upcoming_turn_from_progress(progress, self.last_position.latitude,
|
||||
self.last_position.longitude, v_ego)
|
||||
nav_data['current_speed_limit'] = self.nav_instructions.get_current_speed_limit_from_progress(progress, self.is_metric)
|
||||
arrived = self.nav_instructions.arrived_at_destination(progress)
|
||||
|
||||
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(np.interp(v_ego, speed_breakpoints, distance_list))
|
||||
|
||||
if large_distance:
|
||||
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
|
||||
else:
|
||||
self.cancel_route_counter = 0
|
||||
self.reroute_counter = 0
|
||||
|
||||
# Don't recompute in last segment to prevent reroute loops
|
||||
if self.route:
|
||||
if progress['current_step_idx'] == len(self.route['steps']) - 1:
|
||||
self.reroute_counter = 0
|
||||
else:
|
||||
banner_instructions = ''
|
||||
progress = None
|
||||
nav_data = {}
|
||||
self.valid = False
|
||||
|
||||
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)
|
||||
v_ego = self.sm['carState'].vEgo
|
||||
location = self.sm['liveLocationKalman']
|
||||
localizer_valid = location.positionGeodetic.valid if location else False
|
||||
|
||||
if localizer_valid:
|
||||
self.last_bearing = math.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(v_ego)
|
||||
|
||||
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()
|
||||
@@ -1,76 +0,0 @@
|
||||
"""
|
||||
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
|
||||
|
||||
def test_cancel_route(self):
|
||||
nav = Navigationd()
|
||||
nav.last_position = Coordinate(latitude=37.0, longitude=128.0)
|
||||
nav.route = {'580 Winchester dr, oxnard, CA': True}
|
||||
nav.cancel_route_counter = 30
|
||||
nav._update_params()
|
||||
assert nav.route is None
|
||||
assert nav.destination is None
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user