* init

* some fixes

* move

* more

* old navd helpers

* bring back cereal

* fix linting

* more

* add to cereal first

* sp events

* lint

* implement in long plan

* fixme-sp

* refactor state machine

* wrong state

* start refactor controller

* some type hints

* init these

* enable debug print

* ui? ui!

* print them out

* fix spinner import

* fix path

* let's use gps chips directly for now

* service missing

* publish events

* no nav for now

* need to sub

* no car state speed yet

* missed event

* Car: `CarStateSP`

* fix tests

* bring back car state speed limit

* fix

* use old controller for now

* fix

* fix source

* type hints

* none for now

* formatting

* more

* create directory if does not exist

* mypy my bt

* policy param catch exceptions

* handle all params with exceptions

* more

* single method

* define types in init

* rename

* simpler op enabled check

* more mypy stuff

* rename

* no need for brake pressed

* don't reset if gas pressed

* type hint all

* type hint all

* back to upstream

* in another pr

* no longer need data type

* qlog

* slc in another pr

* use horizontal accuracy

* set core affinity for all realtime processes

* unused

* sort

* unused

* type hint and slight cleanup

* from old implementation

* use directly

* combine pm

* slight more cleanup

* type hints

* even more type hint

* lint

* more cleanup

* even less

* license
This commit is contained in:
Jason Wen
2025-06-07 03:47:09 -04:00
committed by GitHub
parent 5be2cd9b61
commit ab6d192714
28 changed files with 1719 additions and 3 deletions

View File

@@ -226,7 +226,13 @@ struct BackupManagerSP @0xf98d843bfd7004a3 {
struct CarStateSP @0xb86e6369214c01c8 {
}
struct CustomReserved8 @0xf416ec09499d9d19 {
struct LiveMapDataSP @0xf416ec09499d9d19 {
speedLimitValid @0 :Bool;
speedLimit @1 :Float32;
speedLimitAheadValid @2 :Bool;
speedLimitAhead @3 :Float32;
speedLimitAheadDistance @4 :Float32;
roadName @5 :Text;
}
struct CustomReserved9 @0xa1680744031fdb2d {

View File

@@ -2610,7 +2610,7 @@ struct Event {
carControlSP @112 :Custom.CarControlSP;
backupManagerSP @113 :Custom.BackupManagerSP;
carStateSP @114 :Custom.CarStateSP;
customReserved8 @115 :Custom.CustomReserved8;
liveMapDataSP @115 :Custom.LiveMapDataSP;
customReserved9 @116 :Custom.CustomReserved9;
customReserved10 @136 :Custom.CustomReserved10;
customReserved11 @137 :Custom.CustomReserved11;

View File

@@ -84,6 +84,7 @@ _services: dict[str, tuple] = {
"carParamsSP": (True, 0.02, 1),
"carControlSP": (True, 100., 10),
"carStateSP": (True, 100., 10),
"liveMapDataSP": (True, 1., 1),
# debug
"uiDebug": (True, 0., 1),

View File

@@ -175,4 +175,24 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
// model panel params
{"LagdToggle", PERSISTENT | BACKUP},
// mapd
{"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
{"MapdVersion", PERSISTENT},
{"MapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
{"NextMapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
{"Offroad_OSMUpdateRequired", CLEAR_ON_MANAGER_START},
{"OsmDbUpdatesCheck", CLEAR_ON_MANAGER_START}, // mapd database update happens with device ON, reset on boot
{"OSMDownloadBounds", PERSISTENT},
{"OsmDownloadedDate", PERSISTENT},
{"OSMDownloadLocations", PERSISTENT},
{"OSMDownloadProgress", CLEAR_ON_MANAGER_START},
{"OsmLocal", PERSISTENT},
{"OsmLocationName", PERSISTENT},
{"OsmLocationTitle", PERSISTENT},
{"OsmLocationUrl", PERSISTENT},
{"OsmStateName", PERSISTENT},
{"OsmStateTitle", PERSISTENT},
{"OsmWayTest", PERSISTENT},
{"RoadName", CLEAR_ON_ONROAD_TRANSITION},
};

View File

@@ -48,5 +48,9 @@
"OffroadMode_Status": {
"text": "sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to \"Settings\" -> \"Device\" to exit Always Offroad mode.",
"severity": 1
},
"Offroad_OSMUpdateRequired": {
"text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1",
"severity": 0
}
}

View File

@@ -26,6 +26,7 @@ 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/osm_panel.cc",
"sunnypilot/qt/offroad/settings/settings.cc",
"sunnypilot/qt/offroad/settings/software_panel.cc",
"sunnypilot/qt/offroad/settings/sunnylink_panel.cc",
@@ -54,6 +55,10 @@ network_src = [
"sunnypilot/qt/network/sunnylink/services/user_service.cc",
]
osm_panel_qt_src = [
"sunnypilot/qt/offroad/settings/osm/models_fetcher.cc",
]
vehicle_panel_qt_src = [
"sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc",
"sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc",
@@ -76,7 +81,7 @@ brand_settings_qt_src = [
]
sp_widgets_src = widgets_src + network_src
sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src
sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src + osm_panel_qt_src
sp_qt_util = qt_util
Export('sp_widgets_src', 'sp_qt_src', "sp_qt_util")

View File

@@ -0,0 +1,41 @@
/**
* 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 <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QEventLoop>
class JsonFetcher {
public:
static QJsonObject getJsonFromURL(const QString &url) {
const auto qurl = QUrl(url);
QNetworkAccessManager manager;
const QNetworkRequest request(qurl);
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
// Send GET request
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Failed to fetch data from URL: " << reply->errorString();
return QJsonObject();
}
const QByteArray responseData = reply->readAll();
const QJsonDocument doc = QJsonDocument::fromJson(responseData);
QJsonObject json = doc.object();
reply->deleteLater();
return json;
}
};

View File

@@ -0,0 +1,63 @@
/**
* 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 <algorithm> // for std::sort
#include <deque>
#include <vector>
#include <tuple>
#include <QDir>
#include <QJsonObject>
#include "selfdrive/ui/sunnypilot/qt/common/json_fetcher.h"
static const std::tuple<QString, QString> defaultLocation = std::make_tuple("== None ==", "");
// New class LocationsFetcher that handles web requests and JSON parsing
class LocationsFetcher {
public:
inline std::vector<std::tuple<QString, QString, QString, QString> >
getLocationsFromURL(const QUrl &url, const std::tuple<QString, QString> &customLocation = defaultLocation) const {
// Initialize an empty vector to hold the locations
std::vector<std::tuple<QString, QString, QString, QString> > locations;
JsonFetcher fetcher;
QJsonObject json = fetcher.getJsonFromURL(url.toString());
for (auto it = json.begin(); it != json.end(); ++it) {
QString code = it.key();
QJsonObject obj = it.value().toObject();
QString fullName = obj["full_name"].toString();
locations.push_back(std::make_tuple(fullName, code, QString(), QString()));
}
// Sort locations by full name
std::sort(locations.begin(), locations.end(), [](const auto &lhs, const auto &rhs) {
return std::get<0>(lhs) < std::get<0>(rhs); // Compare full names
});
// Optionally, you can now add defaultName entry at the beginning
locations.insert(locations.begin(), std::tuple_cat(customLocation, std::make_tuple("", "")));
return locations;
}
inline std::vector<std::tuple<QString, QString, QString, QString> >
getLocationsFromURL(const QString &url, const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return getLocationsFromURL(QUrl(url), customLocation);
}
inline std::vector<std::tuple<QString, QString, QString, QString> >
getOsmLocations(const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return getLocationsFromURL( "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/nation_bounding_boxes.json", customLocation);
}
inline std::vector<std::tuple<QString, QString, QString, QString> >
getUsStatesLocations(const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return getLocationsFromURL( "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/us_states_bounding_boxes.json", customLocation);
}
};

View File

@@ -0,0 +1,158 @@
/**
* 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/osm/models_fetcher.h"
#include <QThread>
ModelsFetcher::ModelsFetcher(QObject *parent) : QObject(parent) {
manager = new QNetworkAccessManager(this);
}
QByteArray ModelsFetcher::verifyFileHash(const QString &filePath, const QString &expectedHash, bool &hashMatches) {
hashMatches = false; // Default to false
QByteArray fileData;
if (expectedHash.isEmpty()) {
// If no hash is provided, assume verification isn't required but return the file data
hashMatches = true;
} else {
QFile file(filePath);
if (file.open(QIODevice::ReadOnly)) {
QCryptographicHash hash(QCryptographicHash::Sha256); // Or your chosen algorithm
fileData = file.readAll(); // Read the file data once
hash.addData(fileData);
file.close();
QString currentHash = QString(hash.result().toHex());
hashMatches = (currentHash == expectedHash);
}
}
// Return the file data if hash matches or no hash was provided; empty otherwise
return hashMatches ? fileData : QByteArray();
}
void ModelsFetcher::download(const DownloadInfo &downloadInfo, const QString &filename, const QString &destinationPath) {
QString fullPath = destinationPath + "/" + filename;
QFileInfo fileInfo(fullPath);
bool hashMatches = false;
QByteArray data = verifyFileHash(fullPath, downloadInfo.sha256, hashMatches);
if (fileInfo.exists() && hashMatches) {
// Hash matches or no hash provided, and we have the file data
LOGD("File already downloaded and verified: %s", filename.toStdString().c_str());
emit downloadProgress(100);
emit downloadComplete(data, true); // Use the data returned from verifyFileHash
return; // Exit early
}
// Proceed with download if file does not exist or hash verification failed
QNetworkRequest request(downloadInfo.url);
QNetworkReply *reply = manager->get(request);
connect(reply, &QNetworkReply::downloadProgress, this, &ModelsFetcher::onDownloadProgress);
connect(reply, &QNetworkReply::finished, this, [this, reply, destinationPath, filename, downloadInfo]() {
onFinished(reply, destinationPath, filename, downloadInfo.sha256);
});
}
QString extractFileName(const QString &contentDisposition) {
const QString filenameTag = "filename=";
const int idx = contentDisposition.indexOf(filenameTag);
if (idx < 0) {
return QString();
}
QString filename = contentDisposition.mid(idx + filenameTag.length());
if (filename.startsWith("\"") && filename.endsWith("\"")) {
return filename.mid(1, filename.size() - 2);
}
return filename;
}
void ModelsFetcher::onFinished(QNetworkReply *reply, const QString &destinationPath, const QString &filename, const QString &expectedHash) {
// Handle download error
if (reply->error()) {
return; // Possibly emit a signal or log an error as per your error handling policy
}
const QByteArray data = reply->readAll();
QString finalFilename = filename;
if (finalFilename.isEmpty()) {
finalFilename = extractFileName(reply->header(QNetworkRequest::ContentDispositionHeader).toString());
}
QString finalPath = QDir(destinationPath).filePath(finalFilename);
// Save the downloaded file
QFile file(finalPath);
//ensure if the path exists and if not create it
if (!QDir().mkpath(destinationPath)) {
LOGE("Unable to create directory: %s", destinationPath.toStdString().c_str());
emit downloadFailed(filename);
return; // Stop further processing
}
//Retry the file open and write 3 times with a little delay between each retry
for (int i = 0; i < 3; i++) {
if (file.isOpen()) break;
file.open(QIODevice::WriteOnly);
if (!file.isOpen()) QThread::msleep(100);
}
// If the file is still not open, log an error and emit a failure signal
if (!file.isOpen()) {
LOGE("Unable to open file for writing: %s", finalPath.toStdString().c_str());
emit downloadFailed(filename);
return; // Stop further processing
}
file.write(data);
file.close();
bool hashMatches = false;
verifyFileHash(finalPath, expectedHash, hashMatches);
// Verify the file hash if expectedHash is provided
if (!expectedHash.isEmpty() && !hashMatches) {
LOGE("The downloaded file didn't pass the hash validation!: %s", filename.toStdString().c_str());
// Hash verification failed, handle accordingly
// This could involve deleting the file, logging an error, or emitting a failure signal
QFile::remove(finalPath); // Example action: Remove the invalid file
emit downloadFailed(filename);
return; // Stop further processing
}
emit downloadComplete(data, false); // Emit your success signal
}
void ModelsFetcher::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
const double progress = (bytesReceived * 100.0) / bytesTotal;
emit downloadProgress(progress);
}
std::vector<Model> ModelsFetcher::getModelsFromURL(const QUrl &url) {
std::vector<Model> models;
JsonFetcher fetcher;
QJsonObject json = fetcher.getJsonFromURL(url.toString());
for (auto it = json.begin(); it != json.end(); ++it) {
models.push_back(Model(it.value().toObject()));
}
return models;
}
std::vector<Model> ModelsFetcher::getModelsFromURL(const QString &url) {
return getModelsFromURL(QUrl(url));
}
std::vector<Model> ModelsFetcher::getModelsFromURL() {
return getModelsFromURL("https://docs.sunnypilot.ai/models_v5.json");
}

View File

@@ -0,0 +1,143 @@
/**
* 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 <algorithm> // for std::sort
#include <cassert>
#include <deque>
#include <vector>
#include <QDir>
#include <QJsonObject>
#include <QTimer>
#include "common/swaglog.h"
#include "common/util.h"
#include "selfdrive/ui/sunnypilot/ui.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/sunnypilot/qt/common/json_fetcher.h"
#ifdef SUNNYPILOT
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
#else
#include "selfdrive/ui/qt/widgets/controls.h"
#endif
#include "system/hardware/hw.h"
static const QString MODELS_PATH = Hardware::PC() ? QDir::homePath() + "/.comma/media/0/models/" : "/data/media/0/models/";
struct DownloadInfo {
QString url;
QString sha256;
};
// New class ModelsFetcher with a new function that handles web requests and JSON parsing for the new JSON structure
class Model {
public:
explicit Model(const QJsonObject &json) {
displayName = json["display_name"].toString();
fullName = json["full_name"].toString();
fileName = json["file_name"].toString();
// Parse downloadUri as an object
QJsonObject downloadUriObj = json["download_uri"].toObject();
downloadUri.url = downloadUriObj["url"].toString();
downloadUri.sha256 = downloadUriObj["sha256"].toString();
fullNameNav = json["full_name_nav"].toString();
fileNameNav = json["file_name_nav"].toString();
// Parse downloadUriNav as an object
QJsonObject downloadUriNavObj = json["download_uri_nav"].toObject();
downloadUriNav.url = downloadUriNavObj["url"].toString();
downloadUriNav.sha256 = downloadUriNavObj["sha256"].toString();
fullNameMetadata = json["full_name_metadata"].toString();
fileNameMetadata = json["file_name_metadata"].toString();
// Parse downloadUriMetadata as an object
QJsonObject downloadUriMetadataObj = json["download_uri_metadata"].toObject();
downloadUriMetadata.url = downloadUriMetadataObj["url"].toString();
downloadUriMetadata.sha256 = downloadUriMetadataObj["sha256"].toString();
index = json["index"].toString();
environment = json["environment"].toString();
generation = json["generation"].toString();
}
// Method to convert model back to QJsonObject, if needed
QJsonObject toJson() const {
QJsonObject json;
json["display_name"] = displayName;
json["full_name"] = fullName;
json["file_name"] = fileName;
QJsonObject uriObj;
uriObj["url"] = downloadUri.url;
uriObj["sha256"] = downloadUri.sha256;
json["download_uri"] = uriObj;
QJsonObject uriNavObj;
uriNavObj["url"] = downloadUriNav.url;
uriNavObj["sha256"] = downloadUriNav.sha256;
json["download_uri_nav"] = uriNavObj;
QJsonObject uriMetadataObj;
uriMetadataObj["url"] = downloadUriMetadata.url;
uriMetadataObj["sha256"] = downloadUriMetadata.sha256;
json["download_uri_metadata"] = uriMetadataObj;
json["full_name_nav"] = fullNameNav;
json["file_name_nav"] = fileNameNav;
json["full_name_metadata"] = fullNameMetadata;
json["file_name_metadata"] = fileNameMetadata;
json["index"] = index;
json["environment"] = environment;
json["generation"] = generation;
return json;
}
QString displayName;
QString fullName;
QString fileName;
DownloadInfo downloadUri;
DownloadInfo downloadUriNav;
DownloadInfo downloadUriMetadata;
QString fullNameNav;
QString fileNameNav;
QString fullNameMetadata;
QString fileNameMetadata;
QString index;
QString environment;
QString generation;
};
class ModelsFetcher : public QObject {
Q_OBJECT
public:
explicit ModelsFetcher(QObject *parent = nullptr);
void download(const DownloadInfo &url, const QString &filename = "", const QString &destinationPath = MODELS_PATH);
static std::vector<Model> getModelsFromURL(const QUrl &url);
static std::vector<Model> getModelsFromURL(const QString &url);
static std::vector<Model> getModelsFromURL();
signals:
void downloadProgress(double percentage);
void downloadComplete(const QByteArray &data, bool fromCache = false);
void downloadFailed(const QString &filename);
private:
// static bool verifyFileHash(const QString& filePath, const QString& expectedHash);
static QByteArray verifyFileHash(const QString &filePath, const QString &expectedHash, bool &hashMatches);
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void onFinished(QNetworkReply *reply, const QString &destinationPath, const QString &filename,
const QString &expectedHash);
QNetworkAccessManager *manager;
};

View File

@@ -0,0 +1,283 @@
/**
* 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/osm_panel.h"
#include <tuple>
#include <vector>
#include <string>
#include "common/swaglog.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
OsmPanel::OsmPanel(QWidget *parent) : QFrame(parent) {
main_layout = new QStackedLayout(this);
const auto list = new ListWidgetSP(this, false);
list->addItem(mapdVersion = new LabelControlSP(tr("Mapd Version"), "Loading..."));
list->addItem(setupOsmDeleteMapsButton(parent));
list->addItem(offlineMapsETA = new LabelControlSP(tr("Offline Maps ETA"), ""));
list->addItem(offlineMapsElapsed = new LabelControlSP(tr("Time Elapsed"), ""));
list->addItem(setupOsmUpdateButton(parent));
list->addItem(setupOsmDownloadButton(parent));
list->addItem(setupUsStatesButton(parent));
connect(uiStateSP(), &UIStateSP::offroadTransition, [=](bool offroad) {
updateLabels();
});
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, QOverload<>::of(&OsmPanel::updateLabels));
timer->start(FAST_REFRESH_INTERVAL); // Time specified in milliseconds.
updateLabels();
osmScreen = new QWidget(this);
auto *vlayout = new QVBoxLayout(osmScreen);
vlayout->setContentsMargins(50, 20, 50, 20);
vlayout->addWidget(new ScrollViewSP(list, this), 1);
main_layout->addWidget(osmScreen);
}
ButtonControlSP *OsmPanel::setupOsmDeleteMapsButton(QWidget *parent) {
osmDeleteMapsBtn = new ButtonControlSP(tr("Downloaded Maps"), tr("DELETE")); // Updated on updateLabels()
connect(osmDeleteMapsBtn, &ButtonControlSP::clicked, [=]() {
if (showConfirmationDialog(parent, tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all the maps?"), tr("Yes, delete all the maps."))) {
QtConcurrent::run([=]() {
QDir dir(MAP_PATH);
osmDeleteMapsBtn->setEnabled(false);
osmDeleteMapsBtn->setText("");
dir.removeRecursively();
updateMapSize();
osmDeleteMapsBtn->setEnabled(true);
osmDeleteMapsBtn->setText(tr("DELETE"));
});
updateLabels();
}
});
return osmDeleteMapsBtn;
}
ButtonControlSP *OsmPanel::setupOsmUpdateButton(QWidget *parent) {
osmUpdateBtn = new ButtonControlSP(tr("Database Update"), tr("CHECK")); // Updated on updateLabels()
connect(osmUpdateBtn, &ButtonControlSP::clicked, [=]() {
if (osm_download_in_progress && !download_failed_state) {
updateLabels();
} else if (showConfirmationDialog(parent)) {
osm_download_in_progress = true;
params.putBool("OsmDbUpdatesCheck", true);
updateLabels();
}
});
return osmUpdateBtn;
}
ButtonControlSP *OsmPanel::setupOsmDownloadButton(QWidget *parent) {
osmDownloadBtn = new ButtonControlSP(tr("Country"), tr("SELECT"));
connect(osmDownloadBtn, &ButtonControlSP::clicked, [=]() {
osmDownloadBtn->setEnabled(false);
osmDownloadBtn->setValue(tr("Fetching Country list..."));
const std::vector<std::tuple<QString, QString, QString, QString> > locations = getOsmLocations();
osmDownloadBtn->setEnabled(true);
osmDownloadBtn->setValue("");
const QString initTitle = QString::fromStdString(params.get("OsmLocationTitle"));
const QString currentTitle = ((initTitle == "== None ==") || (initTitle.length() == 0)) ? "== None ==" : initTitle;
QStringList locationTitles;
for (auto &loc: locations) {
locationTitles.push_back(std::get<0>(loc));
}
const QString selection = MultiOptionDialog::getSelection(tr("Country"), locationTitles, currentTitle, this);
if (!selection.isEmpty()) {
params.put("OsmLocal", "1");
params.put("OsmLocationTitle", selection.toStdString());
for (auto &loc: locations) {
if (std::get<0>(loc) == selection) {
params.put("OsmLocationName", std::get<1>(loc).toStdString());
break;
}
}
if (params.get("OsmLocationName") == "US") {
usStatesBtn->click();
return;
} else if (selection != "== None ==") {
if (showConfirmationDialog(parent)) {
osm_download_in_progress = true;
params.putBool("OsmDbUpdatesCheck", true);
updateLabels();
}
}
}
updateLabels();
});
return osmDownloadBtn;
}
ButtonControlSP *OsmPanel::setupUsStatesButton(QWidget *parent) {
usStatesBtn = new ButtonControlSP(tr("State"), tr("SELECT"));
connect(usStatesBtn, &ButtonControlSP::clicked, [=]() {
const std::tuple<QString, QString> allStatesOption = std::make_tuple("All States (~4.8 GB)", "All");
usStatesBtn->setEnabled(false);
usStatesBtn->setValue(tr("Fetching State list..."));
const std::vector<std::tuple<QString, QString, QString, QString> > locations =
getUsStatesLocations(allStatesOption);
usStatesBtn->setEnabled(true);
usStatesBtn->setValue("");
const QString initTitle = QString::fromStdString(params.get("OsmStateTitle"));
const QString currentTitle = ((initTitle == std::get<0>(allStatesOption)) || (initTitle.length() == 0)) ? tr("All") : initTitle;
QStringList locationTitles;
for (auto &loc: locations) {
locationTitles.push_back(std::get<0>(loc));
}
const QString selection = MultiOptionDialog::getSelection(tr("State"), locationTitles, currentTitle, this);
if (!selection.isEmpty()) {
params.put("OsmStateTitle", selection.toStdString());
for (auto &loc: locations) {
if (std::get<0>(loc) == selection) {
params.put("OsmStateName", std::get<1>(loc).toStdString());
break;
}
}
usStatesBtn->setValue(selection);
if (showConfirmationDialog(parent)) {
osm_download_in_progress = true;
params.putBool("OsmDbUpdatesCheck", true);
updateLabels();
}
}
updateLabels();
});
usStatesBtn->setVisible(false); // initially hidden
return usStatesBtn;
}
void OsmPanel::showEvent(QShowEvent *event) {
updateLabels(); // For snappier feeling
if (!timer->isActive()) {
timer->start(FAST_REFRESH_INTERVAL);
}
}
void OsmPanel::hideEvent(QHideEvent *event) {
if (timer->isActive()) {
timer->stop();
}
}
void OsmPanel::updateLabels() {
if (!isVisible()) {
return;
}
mapd_version = params.get("MapdVersion");
mapdVersion->setText(mapd_version.c_str());
updateMapSize();
osm_download_locations = mem_params.get("OSMDownloadLocations");
osm_download_in_progress = !osm_download_locations.empty();
timer->setInterval(osm_download_in_progress ? FAST_REFRESH_INTERVAL : SLOW_REFRESH_INTERVAL);
LOGT("Timer Interval %d", timer->interval());
const std::string osmLastDownloadTimeStr = params.get("OsmDownloadedDate");
if (!lastDownloadedTimePoint.has_value() && !osmLastDownloadTimeStr.empty()) {
const double osmLastDownloadTime = std::stod(osmLastDownloadTimeStr);
lastDownloadedTimePoint = std::chrono::system_clock::from_time_t(static_cast<std::time_t>(osmLastDownloadTime));
}
osmDownloadBtn->setEnabled(!osm_download_in_progress);
usStatesBtn->setEnabled(!osm_download_in_progress);
updateDownloadProgress();
const QString locationName = QString::fromStdString(params.get("OsmLocationName"));
const bool isUs = !locationName.isEmpty() && locationName == "US";
usStatesBtn->setVisible(isUs);
if (!locationName.isEmpty()) {
if (!isUs) {
params.remove("OsmStateName");
params.remove("OsmStateTitle");
}
osmUpdateBtn->setVisible(true);
} else {
params.remove("OsmLocal");
params.remove("OsmLocationName");
params.remove("OsmLocationTitle");
params.remove("OsmStateName");
params.remove("OsmStateTitle");
osmUpdateBtn->setVisible(false);
usStatesBtn->setVisible(false);
}
osmDownloadBtn->setValue(QString::fromStdString(params.get("OsmLocationTitle")));
usStatesBtn->setValue(QString::fromStdString(params.get("OsmStateTitle")));
update();
}
void OsmPanel::updateDownloadProgress() {
const auto pending_update_check = params.getBool("OsmDbUpdatesCheck");
const QJsonObject osmDownloadProgress = QJsonDocument::fromJson(params.get("OSMDownloadProgress").c_str()).object();
if (osm_download_in_progress && lastDownloadedTimePoint.has_value()) {
offlineMapsETA->setVisible(true);
offlineMapsElapsed->setVisible(true);
offlineMapsETA->setText(calculateETA(osmDownloadProgress, lastDownloadedTimePoint.value()));
offlineMapsElapsed->setText(calculateElapsedTime(osmDownloadProgress, lastDownloadedTimePoint.value()));
} else {
offlineMapsETA->setVisible(false);
offlineMapsElapsed->setVisible(false);
}
const int total_files = extractIntFromJson(osmDownloadProgress, "total_files");
const int downloaded_files = extractIntFromJson(osmDownloadProgress, "downloaded_files");
download_failed_state = total_files && osm_download_in_progress && !lastDownloadedTimePoint.has_value() && downloaded_files < total_files;
QString updateButtonText = processUpdateStatus(pending_update_check, total_files, downloaded_files, osmDownloadProgress, download_failed_state);
osmUpdateBtn->setValue(updateButtonText);
osmUpdateBtn->setText(osm_download_in_progress && !download_failed_state ? tr("REFRESH") : tr("UPDATE"));
osmDeleteMapsBtn->setValue(formatSize(mapsDirSize));
}
int OsmPanel::extractIntFromJson(const QJsonObject &json, const QString &key) {
return (json.contains(key)) ? json[key].toInt() : 0;
}
QString OsmPanel::processUpdateStatus(bool pending_update, int total_files, int downloaded_files, const QJsonObject &json, bool failed_state) {
if (pending_update && !osm_download_in_progress && !total_files) {
lastDownloadedTimePoint.reset();
return tr("Download starting...");
} else if (failed_state) {
return tr("Error: Invalid download. Retry.");
} else if (osm_download_in_progress && total_files > downloaded_files) {
return formatDownloadStatus(json);
} else if (osm_download_in_progress && downloaded_files >= total_files) {
osm_download_in_progress = false;
lastDownloadedTimePoint.reset();
return tr("Download complete!");
}
if (lastDownloadedTimePoint.has_value()) {
QDateTime dateTime = QDateTime::fromTime_t(std::chrono::system_clock::to_time_t(lastDownloadedTimePoint.value())); //fromMSecsSinceEpoch(duration);
dateTime = dateTime.toLocalTime();
return QString("%1").arg(dateTime.toString("yyyy-MM-dd HH:mm:ss"));
}
return "";
}
void OsmPanel::updateMapSize() {
if (mapSizeFuture.has_value() && mapSizeFuture.value().isFinished()) {
mapsDirSize = mapSizeFuture.value().result();
}
if (!mapSizeFuture.has_value() || !mapSizeFuture.value().isRunning()) {
mapSizeFuture = QtConcurrent::run(getDirSize, MAP_PATH);
}
}

View File

@@ -0,0 +1,232 @@
/**
* 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 <deque>
#include <chrono>
#include <map>
#include <optional>
#include <string>
#include <tuple>
#include <vector>
#include <QDir>
#include <QFileInfo>
#include <QtConcurrent/QtConcurrent>
#include "selfdrive/ui/qt/network/wifi_manager.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/sunnypilot/ui.h"
#include "system/hardware/hw.h"
constexpr int FAST_REFRESH_INTERVAL = 1000; // ms
constexpr int SLOW_REFRESH_INTERVAL = 5000; // ms
static const QString MAP_PATH = Hardware::PC() ? QDir::homePath() + "/.comma/media/0/osm/offline/" : "/data/media/0/osm/offline/";
class OsmPanel : public QFrame {
Q_OBJECT
public:
explicit OsmPanel(QWidget *parent = nullptr);
private:
QStackedLayout *main_layout = nullptr;
QWidget *osmScreen = nullptr;
Params params;
Params mem_params{Hardware::PC() ? "" : "/dev/shm/params"};
std::map<std::string, ParamControlSP *> toggles;
std::optional<QFuture<quint64> > mapSizeFuture;
const SubMaster &sm = *uiStateSP()->sm;
bool is_onroad = false;
std::string mapd_version;
bool isWifi() const { return sm["deviceState"].getDeviceState().getNetworkType() == cereal::DeviceState::NetworkType::WIFI; }
bool isMetered() const { return sm["deviceState"].getDeviceState().getNetworkMetered(); }
bool osm_download_in_progress = false;
bool download_failed_state = false;
quint64 mapsDirSize = 0;
QLabel *osmUpdateLbl;
ButtonControlSP *osmDownloadBtn;
ButtonControlSP *osmUpdateBtn;
ButtonControlSP *usStatesBtn;
ButtonControlSP *osmDeleteMapsBtn;
ButtonControlSP *setupOsmDeleteMapsButton(QWidget *parent);;
ButtonControlSP *setupOsmUpdateButton(QWidget *parent);
ButtonControlSP *setupOsmDownloadButton(QWidget *parent);
ButtonControlSP *setupUsStatesButton(QWidget *parent);
QTimer *timer;
std::string osm_download_locations;
// void updateButtonControlSP(ButtonControlSP *btnControl, QWidget *parent, const QString &initTitle, const QString &allStatesOption);
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
void updateLabels();
void updateDownloadProgress();
static int extractIntFromJson(const QJsonObject &json, const QString &key);
QString processUpdateStatus(bool pending_update_check, int total_files, int downloaded_files, const QJsonObject &json, bool failed_state);
ConfirmationDialog *confirmationDialog;
LabelControlSP *mapdVersion;
LabelControlSP *offlineMapsStatus;
LabelControlSP *offlineMapsETA;
LabelControlSP *offlineMapsElapsed;
std::optional<std::chrono::system_clock::time_point> lastDownloadedTimePoint;
LocationsFetcher locationsFetcher;
void updateMapSize();
bool showConfirmationDialog(QWidget *parent,
const QString &message = QString(),
const QString &confirmButtonText = QString()) const {
const auto _is_metered = isMetered();
const QString warning_message = _is_metered ? tr("\n\nWarning: You are on a metered connection!") : QString();
QString final_message = message.isEmpty() ? tr("This will start the download process and it might take a while to complete.") : message;
final_message += warning_message; // Append the warning message if the connection is metered
const QString final_buttonText = confirmButtonText.isEmpty() ? (_is_metered ? tr("Continue on Metered") : tr("Start Download")) : confirmButtonText;
return ConfirmationDialog::confirm(final_message, final_buttonText, parent);
}
// Refactored methods
std::vector<std::tuple<QString, QString, QString, QString> > getOsmLocations(const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return locationsFetcher.getOsmLocations(customLocation);
}
std::vector<std::tuple<QString, QString, QString, QString> > getUsStatesLocations(const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return locationsFetcher.getUsStatesLocations(customLocation);
}
static QString formatTime(const long timeInSeconds) {
const long minutes = timeInSeconds / 60;
const long seconds = timeInSeconds % 60;
QString formattedTime;
if (minutes > 0) {
formattedTime = QString::number(minutes) + tr("m ");
}
formattedTime += QString::number(seconds) + tr("s");
return formattedTime;
}
static QString calculateElapsedTime(const QJsonObject &jsonData, const std::chrono::system_clock::time_point &startTime) {
using namespace std::chrono;
if (!jsonData.contains("total_files") || !jsonData.contains("downloaded_files"))
return tr("Calculating...");
const int totalFiles = jsonData["total_files"].toInt();
const int downloadedFiles = jsonData["downloaded_files"].toInt();
if (downloadedFiles >= totalFiles || totalFiles <= 0) return tr("Downloaded");
const long elapsed = duration_cast<seconds>(system_clock::now() - startTime).count();
if (elapsed == 0 || downloadedFiles == 0) return tr("Calculating...");
return formatTime(elapsed);
}
static QString calculateETA(const QJsonObject &jsonData, const std::chrono::system_clock::time_point &startTime) {
using namespace std::chrono;
static steady_clock::time_point lastUpdateTime = steady_clock::now();
static std::deque<double> rateHistory;
constexpr int minDataPoints = 3;
constexpr int historySize = 10;
static QString lastETA = tr("Calculating ETA...");
if (duration_cast<seconds>(steady_clock::now() - lastUpdateTime).count() < 1) {
return lastETA;
}
if (!jsonData.contains("total_files") || !jsonData.contains("downloaded_files"))
return lastETA;
const int totalFiles = jsonData["total_files"].toInt();
const int downloadedFiles = jsonData["downloaded_files"].toInt();
if (totalFiles <= 0 || downloadedFiles >= totalFiles) {
return totalFiles <= 0 ? tr("Ready") : tr("Downloaded");
}
const long elapsed = duration_cast<seconds>(system_clock::now() - startTime).count();
if (elapsed == 0 || downloadedFiles == 0) return lastETA;
const double rate = downloadedFiles / static_cast<double>(elapsed);
if (rateHistory.size() >= historySize) rateHistory.pop_front();
rateHistory.push_back(rate);
if (rateHistory.size() < minDataPoints) return lastETA;
double weightedSum = 0;
for (int i = 0, weight = 1; i < rateHistory.size(); ++i, ++weight) {
weightedSum += rateHistory[i] * weight;
}
const double avgRate = 2 * weightedSum / (rateHistory.size() * (rateHistory.size() + 1));
const long remainingTime = static_cast<long>((totalFiles - downloadedFiles) / avgRate);
if (remainingTime <= 0) return lastETA;
lastETA = tr("Time remaining: ") + formatTime(remainingTime);
lastUpdateTime = steady_clock::now();
return lastETA;
}
static QString formatDownloadStatus(const QJsonObject &json) {
if (!json.contains("total_files") || !json.contains("downloaded_files"))
return "";
const int total_files = json["total_files"].toInt();
const int downloaded_files = json["downloaded_files"].toInt();
if (total_files <= 0) return tr("Ready");
if (downloaded_files >= total_files) return tr("Downloaded");
const int percentage = static_cast<int>(100.0 * downloaded_files / total_files);
return QString::asprintf("%d/%d (%d%%)", downloaded_files, total_files, percentage);
}
QString formatSize(quint64 size) const {
if (size == 0 && (!mapSizeFuture.has_value() || mapSizeFuture.value().isRunning())) {
return tr("Calculating...");
}
constexpr qint64 kb = 1024;
constexpr qint64 mb = 1024 * kb;
constexpr qint64 gb = 1024 * mb;
if (size < gb) {
const double sizeMB = size / static_cast<double>(mb);
return QString::number(sizeMB, 'f', 2) + " MB";
} else {
const double sizeGB = size / static_cast<double>(gb);
return QString::number(sizeGB, 'f', 2) + " GB";
}
}
static quint64 getDirSize(QString dirPath) {
quint64 size = 0;
const QString actualDirPath = dirPath.startsWith("~") ? dirPath.replace(0, 1, QDir::homePath()) : dirPath;
QDirIterator it(actualDirPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks, QDirIterator::Subdirectories);
while (it.hasNext()) {
it.next();
if (it.fileInfo().isFile()) {
size += it.fileInfo().size();
}
}
return size;
}
};

View File

@@ -18,6 +18,7 @@
#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/longitudinal_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h"
@@ -85,6 +86,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) {
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"),
PanelInfo(" " + tr("OSM"), new OsmPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"),
PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"),
PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"),

View File

View File

@@ -0,0 +1,22 @@
"""
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.
"""
from openpilot.common.swaglog import cloudlog
LOOK_AHEAD_HORIZON_TIME = 15. # s. Time horizon for look ahead of turn speed sections to provide on liveMapDataSP msg.
_DEBUG = False
_CLOUDLOG_DEBUG = False
ROAD_NAME_TIMEOUT = 30 # secs
R = 6373000.0 # approximate radius of earth in mts
QUERY_RADIUS = 3000 # mts. Radius to use on OSM data queries.
QUERY_RADIUS_OFFLINE = 2250 # mts. Radius to use on offline OSM data queries.
def get_debug(msg, log_to_cloud=True):
if _CLOUDLOG_DEBUG and log_to_cloud:
cloudlog.debug(msg)
if _DEBUG:
print(msg)

View File

@@ -0,0 +1,76 @@
"""
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.
"""
import time
from abc import abstractmethod, ABC
from cereal import messaging
from openpilot.common.gps import get_gps_location_service
from openpilot.common.params import Params
from openpilot.sunnypilot.navd.helpers import Coordinate, coordinate_from_param
class BaseMapData(ABC):
def __init__(self):
self.params = Params()
self.gps_location_service = get_gps_location_service(self.params)
self.sm = messaging.SubMaster(['livePose', 'carControl'] + [self.gps_location_service])
self.pm = messaging.PubMaster(['liveMapDataSP'])
self.last_position = coordinate_from_param("LastGPSPosition", self.params)
self.last_altitude = None
@abstractmethod
def update_location(self) -> None:
pass
@abstractmethod
def get_current_speed_limit(self) -> float:
pass
@abstractmethod
def get_next_speed_limit_and_distance(self) -> tuple[float, float]:
pass
@abstractmethod
def get_current_road_name(self) -> str:
pass
def get_current_location(self) -> None:
gps = self.sm[self.gps_location_service]
# ignore the message if the fix is invalid
gps_ok = self.sm.updated[self.gps_location_service] or (time.monotonic() - self.sm.logMonoTime[self.gps_location_service] / 1e9) > 2.0
if not gps_ok and self.sm['livePose'].inputsOK:
return None
# livePose has these data, but aren't on cereal
self.last_position = Coordinate(gps.latitude, gps.longitude)
self.last_altitude = gps.altitude
def publish(self) -> None:
speed_limit = self.get_current_speed_limit()
next_speed_limit, next_speed_limit_distance = self.get_next_speed_limit_and_distance()
mapd_sp_send = messaging.new_message('liveMapDataSP')
mapd_sp_send.valid = self.sm.all_checks(service_list=[self.gps_location_service, 'livePose'])
live_map_data = mapd_sp_send.liveMapDataSP
live_map_data.speedLimitValid = bool(speed_limit > 0)
live_map_data.speedLimit = speed_limit
live_map_data.speedLimitAheadValid = bool(next_speed_limit > 0)
live_map_data.speedLimitAhead = next_speed_limit
live_map_data.speedLimitAheadDistance = next_speed_limit_distance
live_map_data.roadName = self.get_current_road_name()
self.pm.send('liveMapDataSP', mapd_sp_send)
def tick(self) -> None:
self.sm.update()
self.get_current_location()
self.update_location()
self.publish()

View File

@@ -0,0 +1,57 @@
"""
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.
"""
# DISCLAIMER: This code is intended principally for development and debugging purposes.
# Although it provides a standalone entry point to the program, users should refer
# to the actual implementations for consumption. Usage outside of development scenarios
# is not advised and could lead to unpredictable results.
import threading
import traceback
from cereal import messaging
from openpilot.common.gps import get_gps_location_service
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver
from openpilot.sunnypilot.mapd.live_map_data import get_debug
def excepthook(args):
get_debug(f'MapD: Threading exception:\n{args}')
traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback)
def live_map_data_sp_thread():
config_realtime_process([0, 1, 2, 3], 5)
params = Params()
gps_location_service = get_gps_location_service(params)
while True:
live_map_data_sp_thread_debug(gps_location_service)
def live_map_data_sp_thread_debug(gps_location_service):
_sub_master = messaging.SubMaster(['carState', 'livePose', 'liveMapDataSP', 'longitudinalPlanSP', gps_location_service])
_sub_master.update()
v_ego = _sub_master['carState'].vEgo
long_spl = _sub_master['longitudinalPlanSP'].speedLimit
_policy = Policy.car_state_priority
_resolver = SpeedLimitResolver(_policy)
_speed_limit, _distance, _source = _resolver.resolve(v_ego, long_spl, _sub_master)
print(_speed_limit, _distance, _source, " <-> ", long_spl)
def main():
threading.excepthook = excepthook
live_map_data_sp_thread()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,51 @@
"""
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.
"""
import json
import platform
from openpilot.common.params import Params
from openpilot.sunnypilot.mapd.live_map_data.base_map_data import BaseMapData
from openpilot.sunnypilot.navd.helpers import Coordinate
class OsmMapData(BaseMapData):
def __init__(self):
super().__init__()
self.params = Params()
self.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else self.params
def update_location(self) -> None:
if self.last_position is None or self.last_altitude is None:
return
params = {
"latitude": self.last_position.latitude,
"longitude": self.last_position.longitude,
"altitude": self.last_altitude,
}
self.mem_params.put("LastGPSPosition", json.dumps(params))
def get_current_speed_limit(self) -> float:
return float(self.mem_params.get("MapSpeedLimit", encoding='utf8') or 0.0)
def get_current_road_name(self) -> str:
return self.mem_params.get("RoadName", encoding='utf8') or ""
def get_next_speed_limit_and_distance(self) -> tuple[float, float]:
next_speed_limit_section_str = self.mem_params.get("NextMapSpeedLimit", encoding='utf8')
next_speed_limit_section = json.loads(next_speed_limit_section_str) if next_speed_limit_section_str else {}
next_speed_limit = next_speed_limit_section.get('speedlimit', 0.0)
next_speed_limit_latitude = next_speed_limit_section.get('latitude')
next_speed_limit_longitude = next_speed_limit_section.get('longitude')
next_speed_limit_distance = 0.0
if next_speed_limit_latitude and next_speed_limit_longitude:
next_speed_limit_coordinates = Coordinate(next_speed_limit_latitude, next_speed_limit_longitude)
next_speed_limit_distance = (self.last_position or Coordinate(0, 0)).distance_to(next_speed_limit_coordinates)
return next_speed_limit, next_speed_limit_distance

View File

@@ -0,0 +1,42 @@
"""
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.
"""
# DISCLAIMER: This code is intended principally for development and debugging purposes.
# Although it provides a standalone entry point to the program, users should refer
# to the actual implementations for consumption. Usage outside of development scenarios
# is not advised and could lead to unpredictable results.
import threading
import traceback
from openpilot.common.realtime import Ratekeeper, config_realtime_process
from openpilot.sunnypilot.mapd.live_map_data import get_debug
from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData
def excepthook(args):
get_debug(f'MapD: Threading exception:\n{args}')
traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback)
def live_map_data_sp_thread():
config_realtime_process([0, 1, 2, 3], 5)
live_map_sp = OsmMapData()
rk = Ratekeeper(1, print_delay_threshold=None)
while True:
live_map_sp.tick()
rk.keep_time()
def main():
threading.excepthook = excepthook
live_map_data_sp_thread()
if __name__ == "__main__":
main()

152
sunnypilot/mapd/mapd_installer.py Executable file
View File

@@ -0,0 +1,152 @@
"""
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.
"""
#!/usr/bin/env python3
import logging
import os
import stat
import time
import traceback
import requests
from pathlib import Path
from urllib.request import urlopen
from cereal import messaging
from openpilot.common.params import Params
from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR
from openpilot.system.hardware.hw import Paths
from openpilot.system.ui.spinner import Spinner
from openpilot.system.version import is_prebuilt
import openpilot.system.sentry as sentry
VERSION = 'v1.9.0'
URL = f"https://github.com/pfeiferj/openpilot-mapd/releases/download/{VERSION}/mapd"
class MapdInstallManager:
def __init__(self, spinner_ref: Spinner):
self._spinner = spinner_ref
def download(self) -> None:
self.ensure_directories_exist()
self._download_file()
self.update_installed_version(VERSION)
def check_and_download(self) -> None:
if self.download_needed():
self.download()
@staticmethod
def download_needed() -> bool:
return not os.path.exists(MAPD_PATH) or MapdInstallManager.get_installed_version() != VERSION
@staticmethod
def ensure_directories_exist() -> None:
if not os.path.exists(Paths.mapd_root()):
os.makedirs(Paths.mapd_root())
if not os.path.exists(MAPD_BIN_DIR):
os.makedirs(MAPD_BIN_DIR)
@staticmethod
def _safe_write_and_set_executable(file_path: Path, content: bytes) -> None:
with open(file_path, 'wb') as output:
output.write(content)
output.flush()
os.fsync(output.fileno())
current_permissions = stat.S_IMODE(os.lstat(file_path).st_mode)
os.chmod(file_path, current_permissions | stat.S_IEXEC)
def _download_file(self, num_retries=5) -> None:
temp_file = Path(MAPD_PATH + ".tmp")
download_timeout = 60
for cnt in range(num_retries):
try:
response = requests.get(URL, stream=True, timeout=download_timeout)
response.raise_for_status()
self._safe_write_and_set_executable(temp_file, response.content)
# No exceptions encountered. Safe to replace original file.
temp_file.replace(MAPD_PATH)
return
except requests.exceptions.ReadTimeout:
self._spinner.update(f"ReadTimeout caught. Timeout is [{download_timeout}]. Retrying download... [{cnt}]")
time.sleep(0.5)
except requests.exceptions.RequestException as e:
self._spinner.update(f"RequestException caught: {e}. Retrying download... [{cnt}]")
time.sleep(0.5)
# Delete temp file if the process was not successful.
if temp_file.exists():
temp_file.unlink()
logging.error("Failed to download file after all retries")
@staticmethod
def update_installed_version(version: str) -> None:
Params().put("MapdVersion", version)
@staticmethod
def get_installed_version() -> str:
return Params().get("MapdVersion", encoding="utf-8") or ""
def wait_for_internet_connection(self, return_on_failure: bool = False) -> bool:
max_retries = 10
for retries in range(max_retries + 1):
self._spinner.update(f"Waiting for internet connection... [{retries}/{max_retries}]")
time.sleep(2)
try:
_ = urlopen('https://sentry.io', timeout=10)
return True
except Exception as e:
print(f'Wait for internet failed: {e}')
if return_on_failure and retries == max_retries:
return False
return False
def non_prebuilt_install(self) -> None:
sm = messaging.SubMaster(['deviceState'])
metered = sm['deviceState'].networkMetered
if metered:
self._spinner.update("Can't proceed with mapd install since network is metered!")
time.sleep(5)
return
try:
self.ensure_directories_exist()
if not self.download_needed():
self._spinner.update("Mapd is good!")
time.sleep(0.1)
return
if self.wait_for_internet_connection(return_on_failure=True):
self._spinner.update(f"Downloading pfeiferj's mapd [{install_manager.get_installed_version()}] => [{VERSION}].")
time.sleep(0.1)
self.check_and_download()
self._spinner.close()
except Exception:
for i in range(6):
self._spinner.update("Failed to download OSM maps won't work until properly downloaded!" +
"Try again manually rebooting. " +
f"Boot will continue in {5 - i}s...")
time.sleep(1)
sentry.init(sentry.SentryProject.SELFDRIVE)
traceback.print_exc()
sentry.capture_exception()
if __name__ == "__main__":
spinner = Spinner()
install_manager = MapdInstallManager(spinner)
install_manager.ensure_directories_exist()
if is_prebuilt():
debug_msg = f"[DEBUG] This is prebuilt, no mapd install required. VERSION: [{VERSION}], Param [{install_manager.get_installed_version()}]"
spinner.update(debug_msg)
install_manager.update_installed_version(VERSION)
else:
spinner.update(f"Checking if mapd is installed and valid. Prebuilt [{is_prebuilt()}]")
install_manager.non_prebuilt_install()

View File

@@ -0,0 +1,146 @@
"""
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.
"""
import json
import time
import platform
import os
import glob
import shutil
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper, config_realtime_process
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData
from openpilot.system.hardware.hw import Paths
# PFEIFER - MAPD {{
params = Params()
mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else params
# }} PFEIFER - MAPD
MAPD_BIN_DIR = os.path.join(BASEDIR, 'third_party/mapd_pfeiferj')
MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd')
def get_files_for_cleanup() -> list[str]:
paths = [
f"{Paths.mapd_root()}/db",
f"{Paths.mapd_root()}/v*"
]
files_to_remove = []
for path in paths:
if os.path.exists(path):
files = glob.glob(path + '/**', recursive=True)
files_to_remove.extend(files)
# check for version and mapd files
if not os.path.isfile(MAPD_PATH):
files_to_remove.append(MAPD_PATH)
return files_to_remove
def cleanup_old_osm_data(files_to_remove: list[str]) -> None:
for file in files_to_remove:
# Remove trailing slash if path is file
if file.endswith('/') and os.path.isfile(file[:-1]):
file = file[:-1]
# Try to remove as file or symbolic link first
if os.path.islink(file) or os.path.isfile(file):
os.remove(file)
elif os.path.isdir(file): # If it's a directory
shutil.rmtree(file, ignore_errors=False)
def request_refresh_osm_location_data(nations: list[str], states: list[str] = None) -> None:
params.put("OsmDownloadedDate", str(time.time()))
params.put_bool("OsmDbUpdatesCheck", False)
osm_download_locations = json.dumps({
"nations": nations,
"states": states or []
})
print(f"Downloading maps for {osm_download_locations}")
mem_params.put("OSMDownloadLocations", osm_download_locations)
def filter_nations_and_states(nations: list[str], states: list[str] = None) -> tuple[list[str], list[str]]:
"""Filters and prepares nation and state data for OSM map download.
If the nation is 'US' and a specific state is provided, the nation 'US' is removed from the list.
If the nation is 'US' and the state is 'All', the 'All' is removed from the list.
The idea behind these filters is that if a specific state in the US is provided,
there's no need to download map data for the entire US. Conversely,
if the state is unspecified (i.e., 'All'), we intend to download map data for the whole US,
and 'All' isn't a valid state name, so it's removed.
Parameters:
nations (list): A list of nations for which the map data is to be downloaded.
states (list, optional): A list of states for which the map data is to be downloaded. Defaults to None.
Returns:
tuple: Two lists. The first list is filtered nations and the second list is filtered states.
"""
if "US" in nations and states and not any(x.lower() == "all" for x in states):
# If a specific state in the US is provided, remove 'US' from nations
nations.remove("US")
elif "US" in nations and states and any(x.lower() == "all" for x in states):
# If 'All' is provided as a state (case invariant), remove those instances from states
states = [x for x in states if x.lower() != "all"]
elif "US" not in nations and states and any(x.lower() == "all" for x in states):
states.remove("All")
return nations, states or []
def update_osm_db() -> None:
# last_downloaded_date = float(params.get('OsmDownloadedDate', encoding='utf-8') or 0.0)
# if params.get_bool("OsmDbUpdatesCheck") or time.time() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60
if params.get_bool("OsmDbUpdatesCheck"):
cleanup_old_osm_data(get_files_for_cleanup())
country = params.get('OsmLocationName', encoding='utf-8')
state = params.get('OsmStateName', encoding='utf-8') or "All"
filtered_nations, filtered_states = filter_nations_and_states([country], [state])
request_refresh_osm_location_data(filtered_nations, filtered_states)
if not mem_params.get("OSMDownloadBounds"):
mem_params.put("OSMDownloadBounds", "")
if not mem_params.get("LastGPSPosition"):
mem_params.put("LastGPSPosition", "{}")
def main_thread():
config_realtime_process([0, 1, 2, 3], 5)
rk = Ratekeeper(1, print_delay_threshold=None)
live_map_sp = OsmMapData()
# Create folder needed for OSM
try:
os.mkdir(Paths.mapd_root())
except FileExistsError:
pass
except PermissionError:
cloudlog.exception(f"mapd: failed to make {Paths.mapd_root()}")
while True:
show_alert = get_files_for_cleanup() and params.get_bool("OsmLocal")
set_offroad_alert("Offroad_OSMUpdateRequired", show_alert, "This alert will be cleared when new maps are downloaded.")
update_osm_db()
live_map_sp.tick()
rk.keep_time()
def main():
main_thread()
if __name__ == "__main__":
main()

189
sunnypilot/navd/helpers.py Normal file
View File

@@ -0,0 +1,189 @@
from __future__ import annotations
import json
import math
import numpy as np
from typing import Any, cast
from openpilot.common.conversions import Conversions
from openpilot.common.params import Params
DIRECTIONS = ('left', 'right', 'straight')
MODIFIABLE_DIRECTIONS = ('left', 'right')
EARTH_MEAN_RADIUS = 6371007.2
SPEED_CONVERSIONS = {
'km/h': Conversions.KPH_TO_MS,
'mph': Conversions.MPH_TO_MS,
}
class Coordinate:
def __init__(self, latitude: float, longitude: float) -> None:
self.latitude = latitude
self.longitude = longitude
self.annotations: dict[str, float] = {}
@classmethod
def from_mapbox_tuple(cls, t: tuple[float, float]) -> Coordinate:
return cls(t[1], t[0])
def as_dict(self) -> dict[str, float]:
return {'latitude': self.latitude, 'longitude': self.longitude}
def __str__(self) -> str:
return f'Coordinate({self.latitude}, {self.longitude})'
def __repr__(self) -> str:
return self.__str__()
def __eq__(self, other) -> bool:
if not isinstance(other, Coordinate):
return False
return (self.latitude == other.latitude) and (self.longitude == other.longitude)
def __sub__(self, other: Coordinate) -> Coordinate:
return Coordinate(self.latitude - other.latitude, self.longitude - other.longitude)
def __add__(self, other: Coordinate) -> Coordinate:
return Coordinate(self.latitude + other.latitude, self.longitude + other.longitude)
def __mul__(self, c: float) -> Coordinate:
return Coordinate(self.latitude * c, self.longitude * c)
def dot(self, other: Coordinate) -> float:
return self.latitude * other.latitude + self.longitude * other.longitude
def distance_to(self, other: Coordinate) -> float:
# Haversine formula
dlat = math.radians(other.latitude - self.latitude)
dlon = math.radians(other.longitude - self.longitude)
haversine_dlat = math.sin(dlat / 2.0)
haversine_dlat *= haversine_dlat
haversine_dlon = math.sin(dlon / 2.0)
haversine_dlon *= haversine_dlon
y = haversine_dlat \
+ math.cos(math.radians(self.latitude)) \
* math.cos(math.radians(other.latitude)) \
* haversine_dlon
x = 2 * math.asin(math.sqrt(y))
return x * EARTH_MEAN_RADIUS
def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate):
if a.distance_to(b) < 0.01:
return a.distance_to(p)
ap = p - a
ab = b - a
t = np.clip(ap.dot(ab) / ab.dot(ab), 0.0, 1.0)
projection = a + ab * t
return projection.distance_to(p)
def distance_along_geometry(geometry: list[Coordinate], pos: Coordinate) -> float:
if len(geometry) <= 2:
return geometry[0].distance_to(pos)
# 1. Find segment that is closest to current position
# 2. Total distance is sum of distance to start of closest segment
# + all previous segments
total_distance = 0.0
total_distance_closest = 0.0
closest_distance = 1e9
for i in range(len(geometry) - 1):
d = minimum_distance(geometry[i], geometry[i + 1], pos)
if d < closest_distance:
closest_distance = d
total_distance_closest = total_distance + geometry[i].distance_to(pos)
total_distance += geometry[i].distance_to(geometry[i + 1])
return total_distance_closest
def coordinate_from_param(param: str, params: Params = None) -> Coordinate | None:
if params is None:
params = Params()
json_str = params.get(param)
if json_str is None:
return None
pos = json.loads(json_str)
if 'latitude' not in pos or 'longitude' not in pos:
return None
return Coordinate(pos['latitude'], pos['longitude'])
def string_to_direction(direction: str) -> str:
for d in DIRECTIONS:
if d in direction:
if 'slight' in direction and d in MODIFIABLE_DIRECTIONS:
return 'slight' + d.capitalize()
return d
return 'none'
def maxspeed_to_ms(maxspeed: dict[str, str | float]) -> float:
unit = cast(str, maxspeed['unit'])
speed = cast(float, maxspeed['speed'])
return SPEED_CONVERSIONS[unit] * speed
def field_valid(dat: dict, field: str) -> bool:
return field in dat and dat[field] is not None
def parse_banner_instructions(banners: Any, distance_to_maneuver: float = 0.0) -> dict[str, Any] | None:
if not len(banners):
return None
instruction = {}
# A segment can contain multiple banners, find one that we need to show now
current_banner = banners[0]
for banner in banners:
if distance_to_maneuver < banner['distanceAlongGeometry']:
current_banner = banner
# Only show banner when close enough to maneuver
instruction['showFull'] = distance_to_maneuver < current_banner['distanceAlongGeometry']
# Primary
p = current_banner['primary']
if field_valid(p, 'text'):
instruction['maneuverPrimaryText'] = p['text']
if field_valid(p, 'type'):
instruction['maneuverType'] = p['type']
if field_valid(p, 'modifier'):
instruction['maneuverModifier'] = p['modifier']
# Secondary
if field_valid(current_banner, 'secondary'):
instruction['maneuverSecondaryText'] = current_banner['secondary']['text']
# Lane lines
if field_valid(current_banner, 'sub'):
lanes = []
for component in current_banner['sub']['components']:
if component['type'] != 'lane':
continue
lane = {
'active': component['active'],
'directions': [string_to_direction(d) for d in component['directions']],
}
if field_valid(component, 'active_direction'):
lane['activeDirection'] = string_to_direction(component['active_direction'])
lanes.append(lane)
instruction['lanes'] = lanes
return instruction

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57a92adcf88c7223b07697f8c2b315f4f4a34b32a866284610d5250144863c6f
size 28235

View File

@@ -77,3 +77,11 @@ class Paths:
return str(Path(Paths.comma_home()) / "community" / "crashes")
else:
return "/data/community/crashes"
@staticmethod
def mapd_root() -> str:
if PC:
return str(Path(Paths.comma_home()) / "media" / "0" / "osm")
else:
return "/data/media/0/osm"

View File

@@ -18,6 +18,8 @@ from openpilot.common.swaglog import cloudlog, add_file_handler
from openpilot.system.version import get_build_metadata, terms_version, training_version
from openpilot.system.hardware.hw import Paths
from openpilot.sunnypilot.mapd.mapd_installer import VERSION
def manager_init() -> None:
save_bootlog()
@@ -54,6 +56,7 @@ def manager_init() -> None:
("MadsMainCruiseAllowed", "1"),
("MadsSteeringMode", "0"),
("MadsUnifiedEngagementMode", "1"),
("MapdVersion", f"{VERSION}"),
("MaxTimeOffroad", "1800"),
("Brightness", "0"),
("ModelManager_LastSyncTime", "0"),

View File

@@ -6,6 +6,9 @@ from cereal import car, custom
from openpilot.common.params import Params
from openpilot.system.hardware import PC, TICI
from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess
from openpilot.system.hardware.hw import Paths
from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH
from sunnypilot.models.helpers import get_active_model_runner
from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader
@@ -157,6 +160,10 @@ procs += [
# Backup
PythonProcess("backup_manager", "sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)),
# mapd
NativeProcess("mapd", Paths.mapd_root(), [MAPD_PATH], always_run),
PythonProcess("mapd_manager", "sunnypilot.mapd.mapd_manager", always_run),
]
if os.path.exists("./github_runner.sh"):

2
third_party/mapd_pfeiferj/README.md vendored Normal file
View File

@@ -0,0 +1,2 @@
# MAPD implementation by pfeiferj
https://github.com/pfeiferj/openpilot-mapd/releases/

BIN
third_party/mapd_pfeiferj/mapd vendored Executable file

Binary file not shown.