mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-02-18 14:13:53 +08:00
mapd (#989)
* 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:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
41
selfdrive/ui/sunnypilot/qt/common/json_fetcher.h
Normal file
41
selfdrive/ui/sunnypilot/qt/common/json_fetcher.h
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
143
selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h
Normal file
143
selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h
Normal 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;
|
||||
};
|
||||
283
selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.cc
Normal file
283
selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.cc
Normal 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);
|
||||
}
|
||||
}
|
||||
232
selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h
Normal file
232
selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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"),
|
||||
|
||||
0
sunnypilot/mapd/__init__.py
Normal file
0
sunnypilot/mapd/__init__.py
Normal file
22
sunnypilot/mapd/live_map_data/__init__.py
Normal file
22
sunnypilot/mapd/live_map_data/__init__.py
Normal 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)
|
||||
76
sunnypilot/mapd/live_map_data/base_map_data.py
Normal file
76
sunnypilot/mapd/live_map_data/base_map_data.py
Normal 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()
|
||||
57
sunnypilot/mapd/live_map_data/debug.py
Normal file
57
sunnypilot/mapd/live_map_data/debug.py
Normal 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()
|
||||
51
sunnypilot/mapd/live_map_data/osm_map_data.py
Normal file
51
sunnypilot/mapd/live_map_data/osm_map_data.py
Normal 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
|
||||
42
sunnypilot/mapd/live_map_data/standalone.py
Normal file
42
sunnypilot/mapd/live_map_data/standalone.py
Normal 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
152
sunnypilot/mapd/mapd_installer.py
Executable 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()
|
||||
146
sunnypilot/mapd/mapd_manager.py
Normal file
146
sunnypilot/mapd/mapd_manager.py
Normal 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
189
sunnypilot/navd/helpers.py
Normal 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
|
||||
3
sunnypilot/selfdrive/assets/offroad/icon_map.png
Normal file
3
sunnypilot/selfdrive/assets/offroad/icon_map.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57a92adcf88c7223b07697f8c2b315f4f4a34b32a866284610d5250144863c6f
|
||||
size 28235
|
||||
@@ -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"
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
2
third_party/mapd_pfeiferj/README.md
vendored
Normal 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
BIN
third_party/mapd_pfeiferj/mapd
vendored
Executable file
Binary file not shown.
Reference in New Issue
Block a user