From cac85271150a2e421233073c2269092868fb13d6 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 16 Mar 2025 21:22:14 +0100 Subject: [PATCH 1/2] sunnylink: pairing and sponsorship (#523) * Refactor sunnylink panel code for clarity and initialization fixes. Replaced explicit pointer types with `auto` for cleaner code and added proper initialization for the `offroad` boolean member. Simplified toggle logic by consolidating description updates for enabling/disabling sunnylink. These changes improve code readability and maintainability. * Add Sunnylink sponsor and GitHub pairing functionality This update introduces a feature to manage sponsorship-based roles and GitHub account pairing for Sunnylink. It includes new sponsor popups, sponsor-specific widgets, QR code logic, and backend API integrations. Additionally, new models and services support sponsor tier management and user-role synchronization. * Translation files * Param keys * Add setup functions for SunnyLink sponsor and pair buttons Introduce `setup_settings_sunnylink_sponsor_button` and `setup_settings_sunnylink_pair_button` to handle specific SunnyLink UI interactions. These functions streamline button clicks for sponsor and pairing actions within SunnyLink settings. * Add new SunnyLink test cases for sponsor and pair buttons Added `settings_sunnylink_sponsor_button` and `settings_sunnylink_pair_button` to the UI test case dictionary. This extends the SunnyLink test coverage to include sponsor and pairing functionalities. * No need to import sunnylink from here, and it causes just circular dependency * Enhance SunnylinkPanel functionality in off-road settings This commit enhances the functionality of the SunnylinkPanel in the off-road settings of the SunnyPilot user interface. A paramWatcher is added to the SunnylinkPanel to observe "SunnylinkEnabled" parameter changes. Update functionalities are enhanced to handle showing and hiding of components based on various circumstances, such as whether the system is 'on-road' or 'off-road', and whether Sunnylink is enabled or not. The stopSunnylink and startSunnylink functions were also added to start or stop processes accordingly when Sunnylink is enabled or disabled. Additionally, the ui.h file is updated to efficiently handle Sunnylink roles and device users. * Refactor SunnylinkPanel initialization and handling. Reorganized SunnylinkPanel to improve structure and clarity by separating sunnylink client initialization and list widget setup. Enabled automatic sunnylink startup when the feature is enabled. Added minor formatting fixes for label display consistency. * Add missing include for in ui.h Including ensures compatibility with standard C++ features and prevents potential compilation errors. This addition aligns with best practices for maintaining robust and clean code. * Updated setup_settings_sunnylink_sponsor_button and setup_settings_sunnylink_pair_button function signatures Added an optional 'scroll' parameter to the setup_settings_sunnylink_sponsor_button and setup_settings_sunnylink_pair_button functions in the test_ui module. The modifications were made to allow for more flexible function usage by potentially enabling scroll operations during the execution of these UI setup steps. * Enable Sunnylink initialization on panel show event Begin Sunnylink connection automatically when the panel is displayed, ensuring the feature is active if enabled. Additionally, update the sponsor button text formatting for more concise styling. * Translations * Added checks for new UI files in PRs The git workflow script `ui_preview.yaml` has been modified. The script now checks if the master branch contains a file corresponding to a UI file present in the PR. If a UI file in the PR does not have a match on the master branch, it is marked as new. These enhancements improve the comparison of UI changes between the master and PR branches, particularly with the identification of new UI files. * cleanup * duh --------- Co-authored-by: Jason Wen --- common/api/__init__.py | 13 +- common/params_keys.h | 2 + selfdrive/ui/qt/api.cc | 25 +-- selfdrive/ui/qt/api.h | 13 +- selfdrive/ui/sunnypilot/SConscript | 12 +- selfdrive/ui/sunnypilot/qt/api.cc | 63 ++++++++ selfdrive/ui/sunnypilot/qt/api.h | 36 +++++ .../qt/network/sunnylink/models/role_model.h | 56 +++++++ .../sunnylink/models/sponsor_role_model.h | 83 ++++++++++ .../qt/network/sunnylink/models/user_model.h | 37 +++++ .../sunnylink/services/base_device_service.cc | 57 +++++++ .../sunnylink/services/base_device_service.h | 31 ++++ .../sunnylink/services/role_service.cc | 36 +++++ .../network/sunnylink/services/role_service.h | 32 ++++ .../sunnylink/services/user_service.cc | 42 ++++++ .../network/sunnylink/services/user_service.h | 32 ++++ .../qt/network/sunnylink/sunnylink_client.cc | 14 ++ .../qt/network/sunnylink/sunnylink_client.h | 22 +++ .../settings/sunnylink/sponsor_widget.cc | 142 ++++++++++++++++++ .../settings/sunnylink/sponsor_widget.h | 48 ++++++ .../qt/offroad/settings/sunnylink_panel.cc | 139 ++++++++++++++--- .../qt/offroad/settings/sunnylink_panel.h | 20 ++- .../ui/sunnypilot/qt/request_repeater.cc | 41 +++++ selfdrive/ui/sunnypilot/qt/request_repeater.h | 33 ++++ selfdrive/ui/sunnypilot/ui.cc | 10 ++ selfdrive/ui/sunnypilot/ui.h | 47 ++++++ selfdrive/ui/tests/test_ui/run.py | 12 ++ selfdrive/ui/translations/main_ar.ts | 85 ++++++++++- selfdrive/ui/translations/main_de.ts | 83 ++++++++++ selfdrive/ui/translations/main_es.ts | 87 +++++++++++ selfdrive/ui/translations/main_fr.ts | 83 ++++++++++ selfdrive/ui/translations/main_ja.ts | 85 ++++++++++- selfdrive/ui/translations/main_ko.ts | 85 ++++++++++- selfdrive/ui/translations/main_pt-BR.ts | 85 ++++++++++- selfdrive/ui/translations/main_th.ts | 83 ++++++++++ selfdrive/ui/translations/main_tr.ts | 83 ++++++++++ selfdrive/ui/translations/main_zh-CHS.ts | 85 ++++++++++- selfdrive/ui/translations/main_zh-CHT.ts | 85 ++++++++++- 38 files changed, 1973 insertions(+), 54 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/api.cc create mode 100644 selfdrive/ui/sunnypilot/qt/api.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.cc create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.cc create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.cc create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.cc create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h create mode 100644 selfdrive/ui/sunnypilot/qt/request_repeater.cc create mode 100644 selfdrive/ui/sunnypilot/qt/request_repeater.h diff --git a/common/api/__init__.py b/common/api/__init__.py index 5542760802..3e238ccc7a 100644 --- a/common/api/__init__.py +++ b/common/api/__init__.py @@ -1,13 +1,9 @@ from openpilot.common.api.comma_connect import CommaConnectApi -from sunnypilot.sunnylink.api import SunnylinkApi class Api: - def __init__(self, dongle_id, use_sunnylink=False): - if use_sunnylink: - self.service = SunnylinkApi(dongle_id) - else: - self.service = CommaConnectApi(dongle_id) + def __init__(self, dongle_id): + self.service = CommaConnectApi(dongle_id) def request(self, method, endpoint, **params): return self.service.request(method, endpoint, **params) @@ -22,6 +18,5 @@ class Api: return self.service.get_token(expiry_hours) -def api_get(endpoint, method='GET', timeout=None, access_token=None, use_sunnylink=False, **params): - return SunnylinkApi(None).api_get(endpoint, method, timeout, access_token, **params) if use_sunnylink \ - else CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params) +def api_get(endpoint, method='GET', timeout=None, access_token=None, **params): + return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params) diff --git a/common/params_keys.h b/common/params_keys.h index 307f7ada46..6bcf897c3e 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -144,6 +144,8 @@ inline static std::unordered_map keys = { // sunnylink params {"EnableSunnylinkUploader", PERSISTENT | BACKUP}, {"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START}, + {"SunnylinkCache_Roles", PERSISTENT}, + {"SunnylinkCache_Users", PERSISTENT}, {"SunnylinkDongleId", PERSISTENT}, {"SunnylinkdPid", PERSISTENT}, {"SunnylinkEnabled", PERSISTENT}, diff --git a/selfdrive/ui/qt/api.cc b/selfdrive/ui/qt/api.cc index 6889b40e51..85a3144bd2 100644 --- a/selfdrive/ui/qt/api.cc +++ b/selfdrive/ui/qt/api.cc @@ -79,31 +79,36 @@ bool HttpRequest::timeout() const { return reply && reply->error() == QNetworkReply::OperationCanceledError; } -void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Method method) { - if (active()) { - qDebug() << "HttpRequest is active"; - return; - } +QNetworkRequest HttpRequest::prepareRequest(const QString &requestURL) { + QNetworkRequest request; QString token; if (create_jwt) { - token = CommaApi::create_jwt(); + token = GetJwtToken(); } else { QString token_json = QString::fromStdString(util::read_file(util::getenv("HOME") + "/.comma/auth.json")); QJsonDocument json_d = QJsonDocument::fromJson(token_json.toUtf8()); token = json_d["access_token"].toString(); } - QNetworkRequest request; request.setUrl(QUrl(requestURL)); - request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + request.setRawHeader("User-Agent", GetUserAgent().toUtf8()); if (!token.isEmpty()) { request.setRawHeader(QByteArray("Authorization"), ("JWT " + token).toUtf8()); } + return request; +} - if (method == HttpRequest::Method::GET) { +void HttpRequest::sendRequest(const QString &requestURL, const Method method) { + if (active()) { + qDebug() << "HttpRequest is active"; + return; + } + + QNetworkRequest request = prepareRequest(requestURL); + if (method == Method::GET) { reply = nam()->get(request); - } else if (method == HttpRequest::Method::DELETE) { + } else if (method == Method::DELETE) { reply = nam()->deleteResource(request); } diff --git a/selfdrive/ui/qt/api.h b/selfdrive/ui/qt/api.h index ad64d7e722..80b85f7ca7 100644 --- a/selfdrive/ui/qt/api.h +++ b/selfdrive/ui/qt/api.h @@ -5,6 +5,7 @@ #include #include +#include "util.h" #include "common/util.h" namespace CommaApi { @@ -23,10 +24,11 @@ class HttpRequest : public QObject { Q_OBJECT public: - enum class Method {GET, DELETE}; + enum class Method {GET, DELETE, POST, PUT}; explicit HttpRequest(QObject* parent, bool create_jwt = true, int timeout = 20000); - void sendRequest(const QString &requestURL, const Method method = Method::GET); + virtual void sendRequest(const QString &requestURL, Method method); + void sendRequest(const QString &requestURL) { sendRequest(requestURL, Method::GET);} bool active() const; bool timeout() const; @@ -35,13 +37,14 @@ signals: protected: QNetworkReply *reply = nullptr; - -private: static QNetworkAccessManager *nam(); QTimer *networkTimer = nullptr; bool create_jwt; + virtual QNetworkRequest prepareRequest(const QString& requestURL); + [[nodiscard]] virtual QString GetJwtToken() const { return CommaApi::create_jwt(); } + [[nodiscard]] virtual QString GetUserAgent() const { return getUserAgent(); } -private slots: +protected slots: void requestTimeout(); void requestFinished(); }; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 2975450124..4ebfc10940 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -1,4 +1,5 @@ widgets_src = [ + "sunnypilot/qt/request_repeater.cc", "sunnypilot/qt/widgets/toggle.cc", "sunnypilot/qt/widgets/controls.cc", "sunnypilot/qt/widgets/drive_stats.cc", @@ -13,6 +14,7 @@ qt_util = [ qt_src = [ "sunnypilot/ui.cc", + "sunnypilot/qt/api.cc", "sunnypilot/qt/sidebar.cc", "sunnypilot/qt/window.cc", "sunnypilot/qt/home.cc", @@ -21,6 +23,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", + "sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc", "sunnypilot/qt/offroad/settings/sunnypilot_panel.cc", "sunnypilot/qt/offroad/settings/trips_panel.cc", "sunnypilot/qt/offroad/settings/vehicle_panel.cc", @@ -35,11 +38,18 @@ sunnypilot_panel_qt_src = [ "sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc", ] +network_src = [ + "sunnypilot/qt/network/sunnylink/sunnylink_client.cc", + "sunnypilot/qt/network/sunnylink/services/base_device_service.cc", + "sunnypilot/qt/network/sunnylink/services/role_service.cc", + "sunnypilot/qt/network/sunnylink/services/user_service.cc", +] + vehicle_panel_qt_src = [ "sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc", ] -sp_widgets_src = widgets_src +sp_widgets_src = widgets_src + network_src sp_qt_src = qt_src + sunnypilot_panel_qt_src + vehicle_panel_qt_src sp_qt_util = qt_util diff --git a/selfdrive/ui/sunnypilot/qt/api.cc b/selfdrive/ui/sunnypilot/qt/api.cc new file mode 100644 index 0000000000..d75d8d39f0 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/api.cc @@ -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. + */ + +#include "selfdrive/ui/sunnypilot/qt/api.h" + +#include +#include + +#include "util.h" +#include "selfdrive/ui/qt/util.h" + +namespace SunnylinkApi { + QString create_jwt(const QJsonObject &payloads, int expiry, bool sunnylink) { + QJsonObject header = {{"alg", "RS256"}}; + + auto t = QDateTime::currentSecsSinceEpoch(); + auto dongle_id = sunnylink ? getSunnylinkDongleId() : getDongleId(); + QJsonObject payload = {{"identity", dongle_id.value_or("")}, {"nbf", t}, {"iat", t}, {"exp", t + expiry}}; + for (auto it = payloads.begin(); it != payloads.end(); ++it) { + payload.insert(it.key(), it.value()); + } + + auto b64_opts = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals; + QString jwt = QJsonDocument(header).toJson(QJsonDocument::Compact).toBase64(b64_opts) + '.' + + QJsonDocument(payload).toJson(QJsonDocument::Compact).toBase64(b64_opts); + + auto hash = QCryptographicHash::hash(jwt.toUtf8(), QCryptographicHash::Sha256); + return jwt + "." + CommaApi::rsa_sign(hash).toBase64(b64_opts); + } +} // namespace SunnylinkApi + +void HttpRequestSP::sendRequest(const QString& requestURL, Method method, const QByteArray& payload) { + if (active()) { + return; + } + QNetworkRequest request = prepareRequest(requestURL); + + if(!payload.isEmpty() && (method == Method::POST || method == Method::PUT)) { + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + } + + switch (method) { + case Method::GET: + reply = nam()->get(request); + break; + case Method::DELETE: + reply = nam()->deleteResource(request); + break; + case Method::POST: + reply = nam()->post(request, payload); + break; + case Method::PUT: + reply = nam()->put(request, payload); + break; + } + + networkTimer->start(); + connect(reply, &QNetworkReply::finished, this, &HttpRequestSP::requestFinished); +} diff --git a/selfdrive/ui/sunnypilot/qt/api.h b/selfdrive/ui/sunnypilot/qt/api.h new file mode 100644 index 0000000000..592b2e668f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/api.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/api.h" +#include "selfdrive/ui/sunnypilot/qt/util.h" +#include "common/util.h" + +namespace SunnylinkApi { + QByteArray rsa_encrypt(const QByteArray& data); + QByteArray rsa_decrypt(const QByteArray& data); + QString create_jwt(const QJsonObject& payloads = {}, int expiry = 3600, bool sunnylink = false); +} + +class HttpRequestSP : public HttpRequest { + Q_OBJECT + +public: + explicit HttpRequestSP(QObject* parent, bool create_jwt = true, int timeout = 20000, bool sunnylink = false) : + HttpRequest(parent, create_jwt, timeout), sunnylink(sunnylink) {} + + using HttpRequest::sendRequest; + void sendRequest(const QString& requestURL, Method method, const QByteArray& payload); + +private: + bool sunnylink; + +protected: + [[nodiscard]] QString GetJwtToken() const override { return SunnylinkApi::create_jwt({}, 3600, sunnylink); } + [[nodiscard]] QString GetUserAgent() const override { return getUserAgent(sunnylink); } +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h new file mode 100644 index 0000000000..04b4dccf14 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h @@ -0,0 +1,56 @@ +/** + * 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 + +enum class RoleType { + ReadOnly, + Sponsor, + Admin +}; + +// haha, a role model xD +class RoleModel { +protected: + QJsonObject m_raw_json_object; + +public: + RoleType roleType; + + explicit RoleModel(const RoleType &roleType) : roleType(roleType) { m_raw_json_object = toJson(); } + explicit RoleModel(const QJsonObject &json) : RoleModel(stringToRoleType(json["role_type"].toString())) { m_raw_json_object = json; } + + [[nodiscard]] QJsonObject toJson() const { + QJsonObject json; + json["role_type"] = roleTypeToString(roleType); + return json; + } + + static RoleType stringToRoleType(const QString &roleTypeString) { + if (roleTypeString == "ReadOnly") return RoleType::ReadOnly; + if (roleTypeString == "Sponsor") return RoleType::Sponsor; + + return RoleType::Admin; // Default to Admin + } + + static QString roleTypeToString(const RoleType &roleType) { + switch (roleType) { + case RoleType::ReadOnly: + return "ReadOnly"; + case RoleType::Sponsor: + return "Sponsor"; + default: // RoleType::Admin + return "Admin"; + } + } + + template ::value>::type> T as() const { + return T(m_raw_json_object); + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h new file mode 100644 index 0000000000..7e1772b591 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h @@ -0,0 +1,83 @@ +/** + * 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 + +enum class SponsorTier { + Free, + Novice, + Supporter, + Contributor, + Benefactor, + Guardian, +}; + +// haha, a role model xD +class SponsorRoleModel final : RoleModel { +public: + SponsorTier roleTier; + + explicit SponsorRoleModel(const RoleType &roleType, const SponsorTier &roleTier) : RoleModel(roleType), roleTier(roleTier) {} + explicit SponsorRoleModel(const QJsonObject &json) : RoleModel(json), roleTier(stringToSponsorTier(json["role_tier"].toString())) {} + + [[nodiscard]] QJsonObject toJson() const { + QJsonObject json = RoleModel::toJson(); + json["role_tier"] = sponsorTierToString(roleTier); + return json; + } + + static SponsorTier stringToSponsorTier(const QString &sponsorTierString) { + const auto sponsorTierStringLower = sponsorTierString.toLower(); + if (sponsorTierStringLower == "guardian") return SponsorTier::Guardian; + if (sponsorTierStringLower == "novice") return SponsorTier::Novice; + if (sponsorTierStringLower == "supporter") return SponsorTier::Supporter; + if (sponsorTierStringLower == "contributor") return SponsorTier::Contributor; + if (sponsorTierStringLower == "benefactor") return SponsorTier::Benefactor; + + // Default to Free + return SponsorTier::Free; + } + + static QString sponsorTierToString(const SponsorTier &sponsorTier) { + switch (sponsorTier) { + case SponsorTier::Guardian: + return "Guardian"; + case SponsorTier::Novice: + return "Novice"; + case SponsorTier::Supporter: + return "Supporter"; + case SponsorTier::Contributor: + return "Contributor"; + case SponsorTier::Benefactor: + return "Benefactor"; + + default: // SponsorTier::Free + return "Free"; + } + } + [[nodiscard]] auto getSponsorTierString() const { return sponsorTierToString(roleTier); } + + static QString sponsorTierToColor(const SponsorTier &sponsorTier) { + switch (sponsorTier) { + case SponsorTier::Guardian: + return "gold"; + case SponsorTier::Benefactor: + return "mediumseagreen"; + case SponsorTier::Contributor: + return "steelblue"; + case SponsorTier::Supporter: + return "mediumpurple"; + case SponsorTier::Novice: + return "white"; + default: // SponsorTier::Free + return "silver"; + } + } + [[nodiscard]] auto getSponsorTierColor() const { return sponsorTierToColor(roleTier); } +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h new file mode 100644 index 0000000000..4d255ac8e3 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h @@ -0,0 +1,37 @@ +/** + * 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 + +class UserModel { +public: + QString device_id; + QString user_id; + qint64 created_at; + qint64 updated_at; + QString token_hash; + + explicit UserModel(const QJsonObject &json) { + device_id = json["device_id"].toString(); + user_id = json["user_id"].toString(); + created_at = json["created_at"].toInt(); + updated_at = json["updated_at"].toInt(); + token_hash = json["token_hash"].toString(); + } + + [[nodiscard]] QJsonObject toJson() const { + QJsonObject json; + json["device_id"] = device_id; + json["user_id"] = user_id; + json["created_at"] = created_at; + json["updated_at"] = updated_at; + json["token_hash"] = token_hash; + return json; + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.cc b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.cc new file mode 100644 index 0000000000..3919a1821f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.cc @@ -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. + */ + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink//services/base_device_service.h" + +#include "selfdrive/ui/sunnypilot/qt/request_repeater.h" + +#include "common/swaglog.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" + +BaseDeviceService::BaseDeviceService(QObject* parent) : QObject(parent) { + param_watcher = new ParamWatcher(this); + connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + paramsRefresh(); + }); + param_watcher->addParam("SunnylinkEnabled"); +} + +void BaseDeviceService::paramsRefresh() { +} + +void BaseDeviceService::loadDeviceData(const QString &url, bool poll) { + if (!is_sunnylink_enabled()) { + LOGW("Sunnylink is not enabled, refusing to load data."); + return; + } + + auto sl_dongle_id = getSunnylinkDongleId(); + if (!sl_dongle_id.has_value()) + return; + + QString fullUrl = SUNNYLINK_BASE_URL + "/device/" + *sl_dongle_id + url; + if (poll && !isCurrentyPolling()) { + LOGD("Polling %s", qPrintable(fullUrl)); + LOGD("Cache key: SunnylinkCache_%s", qPrintable(QString(getCacheKey()))); + repeater = new RequestRepeaterSP(this, fullUrl, "SunnylinkCache_" + getCacheKey(), 60, false, true); + connect(repeater, &RequestRepeaterSP::requestDone, this, &BaseDeviceService::handleResponse); + } else if (isCurrentyPolling()) { + repeater->ForceUpdate(); + } else { + LOGD("Sending one-time %s", qPrintable(fullUrl)); + initial_request = new HttpRequestSP(this, true, 10000, true); + connect(initial_request, &HttpRequestSP::requestDone, this, &BaseDeviceService::handleResponse); + } +} + +void BaseDeviceService::stopPolling() { + if (repeater != nullptr) { + repeater->deleteLater(); + repeater = nullptr; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h new file mode 100644 index 0000000000..8ef99d1013 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/request_repeater.h" +#include "selfdrive/ui/qt/util.h" + +class BaseDeviceService : public QObject { + Q_OBJECT + +protected: + void paramsRefresh(); + void loadDeviceData(const QString &url, bool poll = false); + virtual void handleResponse(const QString &response, bool success) = 0; + + static bool is_sunnylink_enabled() { return Params().getBool("SunnylinkEnabled");} + ParamWatcher* param_watcher; + HttpRequestSP* initial_request = nullptr; + RequestRepeaterSP* repeater = nullptr; + +public: + explicit BaseDeviceService(QObject* parent = nullptr); + virtual QString getCacheKey() const = 0; + bool isCurrentyPolling() {return repeater != nullptr;} + void stopPolling(); +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.cc b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.cc new file mode 100644 index 0000000000..bffe32839f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.cc @@ -0,0 +1,36 @@ +/** + * 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/network/sunnylink/services/role_service.h" + +#include +#include + +RoleService::RoleService(QObject* parent) : BaseDeviceService(parent) {} + +void RoleService::load() { + loadDeviceData(url); +} + +void RoleService::startPolling() { + loadDeviceData(url, true); +} + +void RoleService::handleResponse(const QString &response, bool success) { + if (!success) return; + + QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); + QJsonArray jsonArray = doc.array(); + + std::vector roles; + for (const auto &value : jsonArray) { + roles.emplace_back(value.toObject()); + } + + emit rolesReady(roles); + uiStateSP()->setSunnylinkRoles(roles); +} diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h new file mode 100644 index 0000000000..abd0e1ab83 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h @@ -0,0 +1,32 @@ +/** + * 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 + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h" + +class RoleService : public BaseDeviceService { + Q_OBJECT + +public: + explicit RoleService(QObject* parent = nullptr); + void load(); + void startPolling(); + [[nodiscard]] QString getCacheKey() const final { return "Roles"; } + +signals: + void rolesReady(const std::vector &roles); + +protected: + void handleResponse(const QString&response, bool success) override; + +private: + QString url = "/roles"; +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.cc b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.cc new file mode 100644 index 0000000000..cd16360c26 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.cc @@ -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. + */ + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h" + +#include +#include + +#include "selfdrive/ui/sunnypilot/ui.h" + +UserService::UserService(QObject* parent) : BaseDeviceService(parent) { + url = "/users"; +} + +void UserService::load() { + loadDeviceData(url); +} + +void UserService::startPolling() { + loadDeviceData(url, true); +} + +void UserService::handleResponse(const QString &response, bool success) { + if (!success) { + return; + } + + QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); + QJsonArray jsonArray = doc.array(); + + std::vector users; + for (const auto &value : jsonArray) { + users.emplace_back(value.toObject()); + } + + emit usersReady(users); + uiStateSP()->setSunnylinkDeviceUsers(users); +} diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h new file mode 100644 index 0000000000..90eb354751 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h @@ -0,0 +1,32 @@ +/** + * 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 + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h" + +class UserService : public BaseDeviceService { + Q_OBJECT + +public: + explicit UserService(QObject* parent = nullptr); + void load(); + void startPolling(); + [[nodiscard]] QString getCacheKey() const final { return "Users"; }; + +signals: + void usersReady(const std::vector&users); + +protected: + void handleResponse(const QString&response, bool success) override; + +private: + QString url = "/users"; +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.cc b/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.cc new file mode 100644 index 0000000000..4428eb55c4 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.cc @@ -0,0 +1,14 @@ +/** + * 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/network/sunnylink/sunnylink_client.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h" + +SunnylinkClient::SunnylinkClient(QObject* parent) : QObject(parent) { + role_service = new RoleService(parent); + user_service = new UserService(parent); +} diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h new file mode 100644 index 0000000000..0961449926 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h @@ -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. + */ + +#pragma once + +#include + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h" + +class SunnylinkClient : public QObject { + Q_OBJECT + +public: + explicit SunnylinkClient(QObject* parent); + RoleService* role_service; + UserService* user_service; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc new file mode 100644 index 0000000000..7b5ce48238 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc @@ -0,0 +1,142 @@ +/** + * 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/sunnylink/sponsor_widget.h" + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/api.h" +#include "selfdrive/ui/sunnypilot/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h" + +// Sponsor Upsell +using qrcodegen::QrCode; + +SunnylinkSponsorQRWidget::SunnylinkSponsorQRWidget(bool sponsor_pair, QWidget* parent) : QWidget(parent), sponsor_pair(sponsor_pair) { + timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, &SunnylinkSponsorQRWidget::refresh); +} + +void SunnylinkSponsorQRWidget::showEvent(QShowEvent *event) { + refresh(); + timer->start(5 * 60 * 1000); + device()->setOffroadBrightness(100); +} + +void SunnylinkSponsorQRWidget::hideEvent(QHideEvent *event) { + timer->stop(); + device()->setOffroadBrightness(BACKLIGHT_OFFROAD); +} + +void SunnylinkSponsorQRWidget::refresh() { + QString qrString; + + if (sponsor_pair) { + QString token = SunnylinkApi::create_jwt({}, 3600, true); + auto sl_dongle_id = getSunnylinkDongleId(); + QByteArray payload = QString("1|" + sl_dongle_id.value_or("") + "|" + token).toUtf8().toBase64(); + qrString = SUNNYLINK_BASE_URL + "/sso?state=" + payload; + } else { + qrString = "https://github.com/sponsors/sunnyhaibin"; + } + + this->updateQrCode(qrString); + update(); +} + +void SunnylinkSponsorQRWidget::updateQrCode(const QString &text) { + QrCode qr = QrCode::encodeText(text.toUtf8().data(), QrCode::Ecc::LOW); + qint32 sz = qr.getSize(); + QImage im(sz, sz, QImage::Format_RGB32); + + QRgb black = qRgb(0, 0, 0); + QRgb white = qRgb(255, 255, 255); + for (int y = 0; y < sz; y++) { + for (int x = 0; x < sz; x++) { + im.setPixel(x, y, qr.getModule(x, y) ? black : white); + } + } + + // Integer division to prevent anti-aliasing + int final_sz = ((width() / sz) - 1) * sz; + img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly); +} + +void SunnylinkSponsorQRWidget::paintEvent(QPaintEvent *e) { + QPainter p(this); + p.fillRect(rect(), Qt::white); + + QSize s = (size() - img.size()) / 2; + p.drawPixmap(s.width(), s.height(), img); +} + +QStringList SunnylinkSponsorPopup::getInstructions(bool sponsor_pair) { + QStringList instructions; + if (sponsor_pair) { + instructions << tr("Scan the QR code to login to your GitHub account") + << tr("Follow the prompts to complete the pairing process") + << tr("Re-enter the \"sunnylink\" panel to verify sponsorship status") + << tr("If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot"); + } else { + instructions << tr("Scan the QR code to visit sunnyhaibin's GitHub Sponsors page") + << tr("Choose your sponsorship tier and confirm your support") + << tr("Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status"); + } + return instructions; +} + +SunnylinkSponsorPopup::SunnylinkSponsorPopup(bool sponsor_pair, QWidget *parent) : DialogBase(parent), sponsor_pair(sponsor_pair) { + auto *hlayout = new QHBoxLayout(this); + auto sunnylink_client = new SunnylinkClient(this); + hlayout->setContentsMargins(0, 0, 0, 0); + hlayout->setSpacing(0); + + setStyleSheet("SunnylinkSponsorPopup { background-color: #E0E0E0; }"); + + // text + auto vlayout = new QVBoxLayout(); + vlayout->setContentsMargins(85, 70, 50, 70); + vlayout->setSpacing(50); + hlayout->addLayout(vlayout, 1); + { + auto close = new QPushButton(QIcon(":/icons/close.svg"), "", this); + close->setIconSize(QSize(80, 80)); + close->setStyleSheet("border: none;"); + vlayout->addWidget(close, 0, Qt::AlignLeft); + connect(close, &QPushButton::clicked, this, [=] { + sunnylink_client->role_service->load(); + sunnylink_client->user_service->load(); + QDialog::reject(); + }); + + //vlayout->addSpacing(30); + + const QString titleText = sponsor_pair ? tr("Pair your GitHub account") : tr("Early Access: Become a sunnypilot Sponsor"); + const auto title = new QLabel(titleText, this); + title->setStyleSheet("font-size: 75px; color: black;"); + title->setWordWrap(true); + vlayout->addWidget(title); + + QStringList instructions = getInstructions(sponsor_pair); + QString instructionsHtml = "
    "; + for (const auto & instruction : instructions) { + instructionsHtml += QString("
  1. %1
  2. ").arg(instruction); + } + instructionsHtml += "
"; + const auto instructionsLabel = new QLabel(instructionsHtml, this); + + + instructionsLabel->setStyleSheet("font-size: 47px; font-weight: bold; color: black;"); + instructionsLabel->setWordWrap(true); + vlayout->addWidget(instructionsLabel); + + vlayout->addStretch(); + } + + // QR code + auto *qr = new SunnylinkSponsorQRWidget(sponsor_pair, this); + hlayout->addWidget(qr, 1); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h new file mode 100644 index 0000000000..9f15a6084f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h @@ -0,0 +1,48 @@ +/** + * 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 +#include + +#include "common/util.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +const QString SUNNYLINK_BASE_URL = util::getenv("SUNNYLINK_API_HOST", "https://stg.api.sunnypilot.ai").c_str(); + +class SunnylinkSponsorQRWidget : public QWidget { + Q_OBJECT + +public: + explicit SunnylinkSponsorQRWidget(bool sponsor_pair = false, QWidget* parent = 0); + void paintEvent(QPaintEvent*) override; + +private: + QPixmap img; + QTimer *timer; + void updateQrCode(const QString &text); + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + + bool sponsor_pair = false; + +private slots: + void refresh(); +}; + +// sponsor popup widget +class SunnylinkSponsorPopup : public DialogBase { + Q_OBJECT + +public: + explicit SunnylinkSponsorPopup(bool sponsor_pair = false, QWidget* parent = 0); + +private: + static QStringList getInstructions(bool sponsor_pair); + bool sponsor_pair = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc index 12ae2209f2..706e89ddf0 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc @@ -12,12 +12,26 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { main_layout = new QStackedLayout(this); - ListWidget *list = new ListWidget(this, false); + sunnylink_client = new SunnylinkClient(this); + param_watcher = new ParamWatcher(this); + param_watcher->addParam("SunnylinkEnabled"); + connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + paramsRefresh(param_name, param_value); + }); + + is_sunnylink_enabled = Params().getBool("SunnylinkEnabled"); + connect(uiStateSP(), &UIStateSP::sunnylinkRolesChanged, this, &SunnylinkPanel::updatePanel); + connect(uiStateSP(), &UIStateSP::sunnylinkDeviceUsersChanged, this, &SunnylinkPanel::updatePanel); + connect(uiStateSP(), &UIStateSP::offroadTransition, [=](bool offroad) { + is_onroad = !offroad; + updatePanel(); + }); sunnylinkScreen = new QWidget(this); - QVBoxLayout *vlayout = new QVBoxLayout(sunnylinkScreen); + auto vlayout = new QVBoxLayout(sunnylinkScreen); vlayout->setContentsMargins(50, 20, 50, 20); + auto *list = new ListWidget(this, false); QString sunnylinkEnabledBtnDesc = tr("This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that."); sunnylinkEnabledBtn = new ParamControl( "SunnylinkEnabled", @@ -26,23 +40,47 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { ""); list->addItem(sunnylinkEnabledBtn); + status_popup = new SunnylinkSponsorPopup(false, this); + sponsorBtn = new ButtonControlSP( + tr("Sponsor Status"), tr("SPONSOR"), + tr("Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.")); + list->addItem(sponsorBtn); + connect(sponsorBtn, &ButtonControlSP::clicked, [=]() { + status_popup->exec(); + }); + list->addItem(horizontal_line()); + + pair_popup = new SunnylinkSponsorPopup(true, this); + pairSponsorBtn = new ButtonControlSP( + tr("Pair GitHub Account"), tr("PAIR"), + tr("Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.") + "🌟"); + list->addItem(pairSponsorBtn); + connect(pairSponsorBtn, &ButtonControlSP::clicked, [=]() { + if (getSunnylinkDongleId().value_or(tr("N/A")) == "N/A") { + ConfirmationDialog::alert(tr("sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again."), this); + } else { + pair_popup->exec(); + } + }); + list->addItem(horizontal_line()); + connect(sunnylinkEnabledBtn, &ParamControl::showDescriptionEvent, [=]() { // resets the description to the default one for the Easter egg sunnylinkEnabledBtn->setDescription(sunnylinkEnabledBtnDesc); }); connect(sunnylinkEnabledBtn, &ParamControl::toggleFlipped, [=](bool enabled) { + QString description; if (enabled) { - auto proud_description = ""+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ ""; - sunnylinkEnabledBtn->showDescription(); - sunnylinkEnabledBtn->setDescription(proud_description); + description = ""+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ ""; } else { - auto shame_description = ""+ tr("👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉.")+ ""; - sunnylinkEnabledBtn->showDescription(); - sunnylinkEnabledBtn->setDescription(shame_description); - } + description = ""+ tr("👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉.")+ ""; - updatePanel(offroad); + } + sunnylinkEnabledBtn->showDescription(); + sunnylinkEnabledBtn->setDescription(description); + + updatePanel(); }); QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnylinkPanel::updatePanel); @@ -51,22 +89,81 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { vlayout->addWidget(sunnylinkScroller); main_layout->addWidget(sunnylinkScreen); + + if (is_sunnylink_enabled) { + startSunnylink(); + } +} + +void SunnylinkPanel::paramsRefresh(const QString ¶m_name, const QString ¶m_value) { + // We do it on paramsRefresh because the toggleEvent happens before the value is updated + if (param_name == "SunnylinkEnabled" && param_value == "1") { + startSunnylink(); + } else if (param_name == "SunnylinkEnabled" && param_value == "0") { + stopSunnylink(); + } + + updatePanel(); +} + +void SunnylinkPanel::startSunnylink() const { + if (!sunnylink_client->role_service->isCurrentyPolling()) { + sunnylink_client->role_service->startPolling(); + } else { + sunnylink_client->role_service->load(); + } + + if (!sunnylink_client->user_service->isCurrentyPolling()) { + sunnylink_client->user_service->startPolling(); + } else { + sunnylink_client->user_service->load(); + } +} + +void SunnylinkPanel::stopSunnylink() const { + sunnylink_client->role_service->stopPolling(); + sunnylink_client->user_service->stopPolling(); } void SunnylinkPanel::showEvent(QShowEvent *event) { - updatePanel(offroad); + updatePanel(); + if (is_sunnylink_enabled) { + startSunnylink(); + } } -void SunnylinkPanel::updatePanel(bool _offroad) { - QString sunnylink_device_id = tr("Device ID") + " " + getSunnylinkDongleId().value_or(tr("N/A")); - - sunnylinkEnabledBtn->setEnabled(_offroad); - - if (sunnylinkEnabledBtn->isToggled()) { - sunnylinkEnabledBtn->setValue(sunnylink_device_id); - } else { - sunnylinkEnabledBtn->setValue(""); +void SunnylinkPanel::updatePanel() { + if (!isVisible()) { + return; } - offroad = _offroad; + const auto sunnylinkDongleId = getSunnylinkDongleId().value_or(tr("N/A")); + sunnylinkEnabledBtn->setEnabled(!is_onroad); + + is_sunnylink_enabled = Params().getBool("SunnylinkEnabled"); + bool is_sub = uiStateSP()->isSunnylinkSponsor() && is_sunnylink_enabled; + auto max_current_sponsor_rule = uiStateSP()->sunnylinkSponsorRole(); + auto role_name = max_current_sponsor_rule.getSponsorTierString(); + std::optional role_color = max_current_sponsor_rule.getSponsorTierColor(); + bool is_paired = uiStateSP()->isSunnylinkPaired(); + auto paired_users = uiStateSP()->sunnylinkDeviceUsers(); + + sunnylinkEnabledBtn->setEnabled(!is_onroad); + sunnylinkEnabledBtn->setValue(tr("Device ID") + " " + sunnylinkDongleId); + + sponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled); + sponsorBtn->setText(is_sub ? tr("THANKS ♥")/* + " ♥️"*/ : tr("SPONSOR")); + sponsorBtn->setValue(is_sub ? tr(role_name.toStdString().c_str()) : tr("Not Sponsor"), role_color); + + pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled); + pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired")); + + + if (!is_sunnylink_enabled) { + sunnylinkEnabledBtn->setValue(""); + sponsorBtn->setValue(""); + pairSponsorBtn->setValue(""); + } + + update(); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h index 824a081af3..d231757e36 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h @@ -7,7 +7,10 @@ #pragma once +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h" + #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" class SunnylinkPanel : public QFrame { @@ -16,16 +19,29 @@ class SunnylinkPanel : public QFrame { public: explicit SunnylinkPanel(QWidget *parent = nullptr); void showEvent(QShowEvent *event) override; + void paramsRefresh(const QString¶m_name, const QString¶m_value); public slots: - void updatePanel(bool _offroad); + void updatePanel(); private: Params params; QStackedLayout *main_layout = nullptr; QWidget *sunnylinkScreen = nullptr; ScrollViewSP *sunnylinkScroller = nullptr; - bool offroad; + SunnylinkSponsorPopup *status_popup; + SunnylinkSponsorPopup *pair_popup; + ButtonControlSP* sponsorBtn; + ButtonControlSP* pairSponsorBtn; + SunnylinkClient* sunnylink_client; ParamControl *sunnylinkEnabledBtn; + bool is_onroad = false; + bool is_backup = false; + bool is_restore = false; + bool is_sunnylink_enabled = false; + ParamWatcher *param_watcher; + QString sunnylinkBtnDescription; + void stopSunnylink() const; + void startSunnylink() const; }; diff --git a/selfdrive/ui/sunnypilot/qt/request_repeater.cc b/selfdrive/ui/sunnypilot/qt/request_repeater.cc new file mode 100644 index 0000000000..e137d56172 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/request_repeater.cc @@ -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. + */ + +#include "selfdrive/ui/sunnypilot/qt/request_repeater.h" + +RequestRepeaterSP::RequestRepeaterSP(QObject *parent, const QString &requestURL, const QString &cacheKey, + int period, bool whileOnroad, bool sunnylink) : HttpRequestSP(parent, true, 20000, sunnylink) { + request_url = requestURL; + while_onroad = whileOnroad; + timer = new QTimer(this); + timer->setTimerType(Qt::VeryCoarseTimer); + connect(timer, &QTimer::timeout, [=]() { this->timerTick(); }); + timer->start(period * 1000); + + if (!cacheKey.isEmpty()) { + prevResp = QString::fromStdString(params.get(cacheKey.toStdString())); + if (!prevResp.isEmpty()) { + QTimer::singleShot(500, [=]() { emit requestDone(prevResp, true, QNetworkReply::NoError); }); + } + connect(this, &HttpRequest::requestDone, [=](const QString &resp, bool success) { + if (success && resp != prevResp) { + params.put(cacheKey.toStdString(), resp.toStdString()); + prevResp = resp; + } + }); + } + + // Don't wait for the timer to fire to send the first request + ForceUpdate(); +} + +void RequestRepeaterSP::timerTick() { + if ((!uiState()->scene.started || while_onroad) && device()->isAwake() && !active()) { + LOGD("Sending request for %s", qPrintable(request_url)); + sendRequest(request_url); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/request_repeater.h b/selfdrive/ui/sunnypilot/qt/request_repeater.h new file mode 100644 index 0000000000..0cdb370f9b --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/request_repeater.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/request_repeater.h" + +#include "common/swaglog.h" +#include "common/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/api.h" + +class RequestRepeaterSP : public HttpRequestSP { + +private: + Params params; + QTimer *timer; + QString prevResp; + QString request_url; + bool while_onroad; + void timerTick(); + +public: + RequestRepeaterSP(QObject *parent, const QString &requestURL, const QString &cacheKey = "", int period = 0, bool whileOnroad=false, bool sunnylink = false); + void ForceUpdate() { + LOGD("Forcing update for %s", qPrintable(request_url)); + timerTick(); + } +}; diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index 37da83949b..e27771b9bb 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -48,6 +48,16 @@ UIStateSP *uiStateSP() { return &ui_state; } +void UIStateSP::setSunnylinkRoles(const std::vector& roles) { + sunnylinkRoles = roles; + emit sunnylinkRolesChanged(roles); +} + +void UIStateSP::setSunnylinkDeviceUsers(const std::vector& users) { + sunnylinkUsers = users; + emit sunnylinkDeviceUsersChanged(users); +} + DeviceSP *deviceSP() { static DeviceSP _device; return &_device; diff --git a/selfdrive/ui/sunnypilot/ui.h b/selfdrive/ui/sunnypilot/ui.h index 65c9237969..08e1d37b0f 100644 --- a/selfdrive/ui/sunnypilot/ui.h +++ b/selfdrive/ui/sunnypilot/ui.h @@ -7,6 +7,11 @@ #pragma once +#include + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h" #include "selfdrive/ui/ui.h" class UIStateSP : public UIState { @@ -15,12 +20,54 @@ class UIStateSP : public UIState { public: UIStateSP(QObject *parent = 0); void updateStatus() override; + void setSunnylinkRoles(const std::vector &roles); + void setSunnylinkDeviceUsers(const std::vector &users); + + inline std::vector sunnylinkDeviceRoles() const { return sunnylinkRoles; } + inline bool isSunnylinkAdmin() const { + return std::any_of(sunnylinkRoles.begin(), sunnylinkRoles.end(), [](const RoleModel &role) { + return role.roleType == RoleType::Admin; + }); + } + inline bool isSunnylinkSponsor() const { + return std::any_of(sunnylinkRoles.begin(), sunnylinkRoles.end(), [](const RoleModel &role) { + return role.roleType == RoleType::Sponsor && role.as().roleTier != SponsorTier::Free; + }); + } + inline SponsorRoleModel sunnylinkSponsorRole() const { + std::optional sponsorRoleWithHighestTier = std::nullopt; + for (const auto &role : sunnylinkRoles) { + if(role.roleType != RoleType::Sponsor) + continue; + + if (auto sponsorRole = role.as(); !sponsorRoleWithHighestTier.has_value() || sponsorRoleWithHighestTier->roleTier < sponsorRole.roleTier) { + sponsorRoleWithHighestTier = sponsorRole; + } + } + return sponsorRoleWithHighestTier.value_or(SponsorRoleModel(RoleType::Sponsor, SponsorTier::Free)); + } + inline SponsorTier sunnylinkSponsorTier() const { + return sunnylinkSponsorRole().roleTier; + } + inline std::vector sunnylinkDeviceUsers() const { return sunnylinkUsers; } + inline bool isSunnylinkPaired() const { + return std::any_of(sunnylinkUsers.begin(), sunnylinkUsers.end(), [](const UserModel &user) { + return user.user_id.toLower() != "unregisteredsponsor" && user.user_id.toLower() != "temporarysponsor"; + }); + } signals: + void sunnylinkRoleChanged(bool subscriber); + void sunnylinkRolesChanged(std::vector roles); + void sunnylinkDeviceUsersChanged(std::vector users); void uiUpdate(const UIStateSP &s); private slots: void update() override; + +private: + std::vector sunnylinkRoles = {}; + std::vector sunnylinkUsers = {}; }; UIStateSP *uiStateSP(); diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index f371c7bbae..f3fd6aa915 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -207,6 +207,16 @@ def setup_settings_sunnylink(click, pm: PubMaster, scroll=None): click(278, 522) time.sleep(UI_DELAY) +def setup_settings_sunnylink_sponsor_button(click, pm: PubMaster, scroll=None): + setup_settings_sunnylink(click, pm) + click(1967, 225) + time.sleep(UI_DELAY) + +def setup_settings_sunnylink_pair_button(click, pm: PubMaster, scroll=None): + setup_settings_sunnylink(click, pm) + click(1967, 432) + time.sleep(UI_DELAY) + def setup_settings_sunnypilot(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(278, 852) @@ -268,6 +278,8 @@ CASES = { CASES.update({ "settings_sunnylink": setup_settings_sunnylink, + "settings_sunnylink_sponsor_button": setup_settings_sunnylink_sponsor_button, + "settings_sunnylink_pair_button": setup_settings_sunnylink_pair_button, "settings_sunnypilot": setup_settings_sunnypilot, "settings_sunnypilot_mads": setup_settings_sunnypilot_mads, "settings_trips": setup_settings_trips, diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 40c61f8cd4..4cb648dc3d 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -981,7 +981,7 @@ This may take up to a minute. Firehose - + خرطوم الحريق @@ -1450,6 +1450,89 @@ This may take up to a minute. N/A غير متاح + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + إقران + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 773a354d9e..5a994c4ad6 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -1428,6 +1428,89 @@ This may take up to a minute. N/A Nicht verfügbar + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 5e3b854f8c..77b1ecb6e4 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -1428,6 +1428,93 @@ Esto puede tardar un minuto. N/A N/A + + Sponsor Status + Estado de patrocinio + + + SPONSOR + PATROCINADOR + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + Conviértete en patrocinador de sunnypilot para obtener acceso anticipado a las funciones de sunnylink cuando estén disponibles. + + + Pair GitHub Account + Emparejar cuenta de GitHub + + + PAIR + EMPAREJAR + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + Empareja tu cuenta de GitHub para otorgar a tu dispositivo beneficios de patrocinador, incluido acceso a la API en sunnylink. + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + ID del dongle de sunnylink no encontrado. Esto puede deberse a una conexión débil o a un problema de registro en sunnylink. Por favor, reinicia y vuelve a intentarlo. + + + THANKS + GRACIAS + + + Not Sponsor + No patrocinador + + + Paired + Emparejado + + + Not Paired + No emparejado + + + THANKS ♥ + GRACIAS ♥ + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + Escanea el código QR para iniciar sesión en tu cuenta de GitHub + + + Follow the prompts to complete the pairing process + Sigue las indicaciones para completar el proceso de emparejamiento + + + Re-enter the "sunnylink" panel to verify sponsorship status + Vuelve a ingresar al panel de "sunnylink" para verificar el estado de patrocinio + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + Si el estado de patrocinio no se actualizó, por favor contacta a un moderador en Discord en https://discord.gg/sunnypilot + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + Escanea el código QR para visitar la página de GitHub Sponsors de sunnyhaibin + + + Choose your sponsorship tier and confirm your support + Elige tu nivel de patrocinio y confirma tu apoyo + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + Únete a nuestra comunidad en Discord en https://discord.gg/sunnypilot y contacta a un moderador para confirmar tu estado de patrocinio + + + Pair your GitHub account + Empareja tu cuenta de GitHub + + + Early Access: Become a sunnypilot Sponsor + Acceso temprano: conviértete en patrocinador de sunnypilot + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 88484e6220..94ebdb0a15 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -1428,6 +1428,89 @@ Cela peut prendre jusqu'à une minute. N/A N/A + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + ASSOCIER + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 0cda3f4c73..1c6ab9a8b4 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -954,7 +954,7 @@ This may take up to a minute. Firehose - + データ学習 @@ -1423,6 +1423,89 @@ This may take up to a minute. N/A 該当なし + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + OK + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 7e190c12b0..3286cc081f 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -956,7 +956,7 @@ This may take up to a minute. Firehose - + 파이어호스 @@ -1425,6 +1425,89 @@ This may take up to a minute. N/A N/A + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + 동기화 + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 742da57160..7a18c35989 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -961,7 +961,7 @@ Isso pode levar até um minuto. Firehose - + Firehose @@ -1430,6 +1430,89 @@ Isso pode levar até um minuto. N/A N/A + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + PAREAR + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 4a66da1156..abb3088dfe 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -1423,6 +1423,89 @@ This may take up to a minute. N/A ไม่มี + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + จับคู่ + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 6a344607fd..4e0e93c708 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -1421,6 +1421,89 @@ This may take up to a minute. N/A N/A + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index dd98792569..caa32b4f2b 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -954,7 +954,7 @@ This may take up to a minute. Firehose - + 训练上传 @@ -1423,6 +1423,89 @@ This may take up to a minute. N/A N/A + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + 配对 + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 25c5afbdfc..da6e52702a 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -954,7 +954,7 @@ This may take up to a minute. Firehose - + 訓練上傳 @@ -1423,6 +1423,89 @@ This may take up to a minute. N/A 無法使用 + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + 配對 + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel From 42871f7a9d20363244c944a6b17baf4e09eb3991 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 16 Mar 2025 19:53:53 -0400 Subject: [PATCH 2/2] pandad: dedicated aligned buffer for deserialization (#671) * pandad: dedicated aligned buffer for deserialization * ide pls --- selfdrive/pandad/panda_safety.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/selfdrive/pandad/panda_safety.cc b/selfdrive/pandad/panda_safety.cc index ef9b459e5b..48589c4146 100644 --- a/selfdrive/pandad/panda_safety.cc +++ b/selfdrive/pandad/panda_safety.cc @@ -64,10 +64,12 @@ std::vector PandaSafety::fetchCarParams() { // TODO-SP: Use structs instead of vector void PandaSafety::setSafetyMode(const std::vector ¶ms_string) { AlignedBuffer aligned_buf; + AlignedBuffer aligned_buf_sp; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(params_string[0].data(), params_string[0].size())); cereal::CarParams::Reader car_params = cmsg.getRoot(); - capnp::FlatArrayMessageReader cmsg_sp(aligned_buf.align(params_string[1].data(), params_string[1].size())); + capnp::FlatArrayMessageReader cmsg_sp(aligned_buf_sp.align(params_string[1].data(), params_string[1].size())); cereal::CarParamsSP::Reader car_params_sp = cmsg_sp.getRoot(); auto safety_configs = car_params.getSafetyConfigs();