From f833819143926bf987070632588a0dc271e84c55 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 31 Oct 2025 17:54:39 +0100 Subject: [PATCH 1/4] ci: update trigger for prebuilt (#1439) Updated workflow `if` conditions to use `vars.PREBUILT_PR_LABEL`. --- .github/workflows/sunnypilot-master-dev-prep.yaml | 4 ++-- .../ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sunnypilot-master-dev-prep.yaml b/.github/workflows/sunnypilot-master-dev-prep.yaml index 968540955f..e93e778aa6 100644 --- a/.github/workflows/sunnypilot-master-dev-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-prep.yaml @@ -42,7 +42,7 @@ jobs: if: ( (github.event_name == 'workflow_dispatch') || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev')))) + || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == vars.PREBUILT_PR_LABEL || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, vars.PREBUILT_PR_LABEL)))) ) steps: - uses: actions/checkout@v4 @@ -54,7 +54,7 @@ jobs: uses: ./.github/workflows/wait-for-action # Path to where you place the action if: ( (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev')))) + || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == vars.PREBUILT_PR_LABEL || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, vars.PREBUILT_PR_LABEL)))) ) with: workflow: selfdrive_tests.yaml # The workflow file to monitor diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc index 4bbf894572..86b5b93e27 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc @@ -290,7 +290,7 @@ void SunnylinkPanel::updatePanel() { pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled); pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired")); - sunnylinkUploaderEnabledBtn->setEnabled(max_current_sponsor_rule.roleTier == SponsorTier::Guardian && is_sunnylink_enabled); + sunnylinkUploaderEnabledBtn->setEnabled(max_current_sponsor_rule.roleTier >= SponsorTier::Novice && is_sunnylink_enabled); if (!is_sunnylink_enabled) { sunnylinkEnabledBtn->setValue(""); From f60c2b6a835201ac06fa109ae98b14c36afabe18 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 1 Nov 2025 12:14:57 +0100 Subject: [PATCH 2/4] sunnylink: update uploader button logic to support novice tier and above (#1438) * sunnylink: update uploader button logic to support novice tier and above - Adjusted the enable condition to include SponsorTier::Novice and above. * sunnylink: improve uploader button visibility and accessibility logic - Made uploader button conditionally visible based on user tier and settings. - Clarified button label to specify testing purposes only. --- .../ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc index 86b5b93e27..924d1d3c09 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc @@ -90,7 +90,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { QString sunnylinkUploaderDesc = tr("Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)"); sunnylinkUploaderEnabledBtn = new ParamControlSP( "EnableSunnylinkUploader", - tr("[Don't use] Enable sunnylink uploader"), + tr("Enable sunnylink uploader (just for testing infrastructure)"), sunnylinkUploaderDesc, "", nullptr, true); list->addItem(sunnylinkUploaderEnabledBtn); @@ -290,7 +290,10 @@ void SunnylinkPanel::updatePanel() { pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled); pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired")); - sunnylinkUploaderEnabledBtn->setEnabled(max_current_sponsor_rule.roleTier >= SponsorTier::Novice && is_sunnylink_enabled); + bool can_do_uploads = max_current_sponsor_rule.roleTier >= SponsorTier::Novice && is_sunnylink_enabled; + sunnylinkUploaderEnabledBtn->setVisible(can_do_uploads); + sunnylinkUploaderEnabledBtn->setEnabled(can_do_uploads); + if (!is_sunnylink_enabled) { sunnylinkEnabledBtn->setValue(""); From 682d738ffac3432549643a9889e1424192f88291 Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Sun, 2 Nov 2025 02:47:30 +0000 Subject: [PATCH 3/4] Tesla: Coop Steering (#1283) * Tesla: Coop Steering * bump * bump * sync with opendbc/master * resolve comment * add oscillation warning and add confirmation * styling desc * beta --------- Co-authored-by: Jason Wen --- common/params_keys.h | 1 + opendbc_repo | 2 +- .../settings/vehicle/tesla_settings.cc | 34 +++++++++++++++++++ .../offroad/settings/vehicle/tesla_settings.h | 2 +- sunnypilot/selfdrive/car/interfaces.py | 5 +++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index d8485be157..df53261eb2 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -208,6 +208,7 @@ inline static std::unordered_map keys = { {"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}}, {"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}}, {"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}}, {"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}}, {"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}}, diff --git a/opendbc_repo b/opendbc_repo index e0e1626820..c7126f8ba6 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit e0e1626820d6a18a984ae69eebc012180699d41c +Subproject commit c7126f8ba620eb06fe345013081e97503331bbe1 diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc index 50ab023021..2fffdb8ad4 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc @@ -8,7 +8,41 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h" TeslaSettings::TeslaSettings(QWidget *parent) : BrandSettingsInterface(parent) { + constexpr int coopSteeringMinKmh = 23; // minimum speed for cooperative steering (enforced by Tesla firmware) + constexpr int oemSteeringMinKmh = 48; // minimum speed for OEM lane departure avoidance (enforced by Tesla firmware) + bool is_metric = params.getBool("IsMetric"); + QString unit = is_metric ? "km/h" : "mph"; + int display_value_coop; + int display_value_oem; + if (is_metric) { + display_value_coop = coopSteeringMinKmh; + display_value_oem = oemSteeringMinKmh; + } else { + display_value_coop = static_cast(std::round(coopSteeringMinKmh * KM_TO_MILE)); + display_value_oem = static_cast(std::round(oemSteeringMinKmh * KM_TO_MILE)); + } + const QString coop_desc = QString("%1

" + "%2
" + "%3
") + .arg(tr("Warning: May experience steering oscillations below %5 %6 during turns, recommend disabling this feature if you experience these.")) + .arg(tr("Allows the driver to provide limited steering input while openpilot is engaged.")) + .arg(tr("Only works above %4 %6.")) + .arg(display_value_coop) + .arg(display_value_oem) + .arg(unit); + + coopSteeringToggle = new ParamControlSP( + "TeslaCoopSteering", + tr("Cooperative Steering (Beta)"), + coop_desc, + "", + this + ); + list->addItem(coopSteeringToggle); + coopSteeringToggle->showDescription(); + coopSteeringToggle->setConfirmation(true, false); } void TeslaSettings::updateSettings() { + coopSteeringToggle->setEnabled(offroad); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h index 37f2936cdf..a1294513ee 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h @@ -22,5 +22,5 @@ public: void updateSettings() override; private: - bool offroad = false; + ParamControlSP *coopSteeringToggle = nullptr; }; diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index 7d63d3c5ba..59cfeabd6e 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -115,4 +115,9 @@ def initialize_params(params) -> list[dict[str, Any]]: "SubaruStopAndGoManualParkingBrake", ]) + # tesla + keys.extend([ + "TeslaCoopSteering", + ]) + return [{k: params.get(k, return_default=True)} for k in keys] From b81d5bca3c4418fad601f103f070f309077c16ed Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 2 Nov 2025 06:50:41 +0100 Subject: [PATCH 4/4] ui: update discord references and add forum widget (#1440) * sunnylink: introduce community popup with QR code embedding - Added `SunnylinkCommunityPopup` widget to promote the sunnypilot Community Forum. - Integrated a QR code generator and display for quick access. - Updated `WiFiPromptWidget` to include a "Learn More" button triggering the community popup. * sunnylink: adjust community popup styling for better layout - Reduced font size of description text slightly for consistency. - Decreased QR code dimensions to improve visual balance. * Making more space out of thin air * sunnylink: update community references to use forum links - Replaced Discord links with Community Forum URLs for better alignment. - Improved clarity in sponsorship instructions. --- selfdrive/ui/qt/widgets/wifi.cc | 11 +- selfdrive/ui/qt/widgets/wifi.h | 5 + selfdrive/ui/sunnypilot/SConscript | 1 + .../settings/sunnylink/community_widget.cc | 139 ++++++++++++++++++ .../settings/sunnylink/community_widget.h | 40 +++++ .../settings/sunnylink/sponsor_widget.cc | 4 +- 6 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h diff --git a/selfdrive/ui/qt/widgets/wifi.cc b/selfdrive/ui/qt/widgets/wifi.cc index d7eb8beeb3..8958e48c93 100644 --- a/selfdrive/ui/qt/widgets/wifi.cc +++ b/selfdrive/ui/qt/widgets/wifi.cc @@ -11,17 +11,18 @@ WiFiPromptWidget::WiFiPromptWidget(QWidget *parent) : QFrame(parent) { main_layout->setContentsMargins(56, 40, 56, 40); main_layout->setSpacing(42); - QLabel *title = new QLabel(tr("🔥 Firehose Mode 🔥")); - title->setStyleSheet("font-size: 64px; font-weight: 500;"); + community_popup = new SunnylinkCommunityPopup(this); + QLabel *title = new QLabel(tr("sunnypilot Community")); + title->setStyleSheet("font-size: 56px; font-weight: 500;"); main_layout->addWidget(title); - QLabel *desc = new QLabel(tr("Maximize your training data uploads to improve openpilot's driving models.")); + QLabel *desc = new QLabel(tr("Need help or have ideas?
Join our community now!")); desc->setStyleSheet("font-size: 40px; font-weight: 400;"); desc->setWordWrap(true); main_layout->addWidget(desc); - QPushButton *settings_btn = new QPushButton(tr("Open")); - connect(settings_btn, &QPushButton::clicked, [=]() { emit openSettings(1, "FirehosePanel"); }); + QPushButton *settings_btn = new QPushButton(tr("Learn More")); + connect(settings_btn, &QPushButton::clicked, [=]() { community_popup->exec(); }); settings_btn->setStyleSheet(R"( QPushButton { font-size: 48px; diff --git a/selfdrive/ui/qt/widgets/wifi.h b/selfdrive/ui/qt/widgets/wifi.h index 3e68a15b7b..8ef9dca91a 100644 --- a/selfdrive/ui/qt/widgets/wifi.h +++ b/selfdrive/ui/qt/widgets/wifi.h @@ -3,12 +3,17 @@ #include #include +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h" + class WiFiPromptWidget : public QFrame { Q_OBJECT public: explicit WiFiPromptWidget(QWidget* parent = 0); +private: + SunnylinkCommunityPopup *community_popup; + signals: void openSettings(int index = 0, const QString ¶m = ""); }; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index be92c14261..449093886a 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -35,6 +35,7 @@ qt_src = [ "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/sunnylink/community_widget.cc", "sunnypilot/qt/offroad/settings/trips_panel.cc", "sunnypilot/qt/offroad/settings/vehicle_panel.cc", "sunnypilot/qt/offroad/settings/visuals_panel.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.cc new file mode 100644 index 0000000000..e29e89ee98 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.cc @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2025-, sunnypilot 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/community_widget.h" + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/util.h" + +using qrcodegen::QrCode; + +// --- SunnylinkCommunityQRWidget --- + +SunnylinkCommunityQRWidget::SunnylinkCommunityQRWidget(QWidget* parent) + : QWidget(parent) {} + +void SunnylinkCommunityQRWidget::showEvent(QShowEvent *event) { + updateQrCode(SUNNYLINK_COMMUNITY_URL); + update(); +} + +void SunnylinkCommunityQRWidget::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); + } + } + + int final_sz = ((width() / sz) - 1) * sz; + img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly); +} + +void SunnylinkCommunityQRWidget::paintEvent(QPaintEvent *e) { + QPainter p(this); + p.fillRect(rect(), Qt::white); + + if (!img.isNull()) { + QSize s = (size() - img.size()) / 2; + p.drawPixmap(s.width(), s.height(), img); + } +} + +// --- SunnylinkCommunityPopup --- + +QStringList SunnylinkCommunityPopup::getInstructions() { + QStringList instructions; + instructions << tr("Scan the QR code and join us!"); + return instructions; +} + +SunnylinkCommunityPopup::SunnylinkCommunityPopup(QWidget* parent) + : DialogBase(parent) { + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + // Solarized Light base3 background + setStyleSheet("SunnylinkCommunityPopup { background-color: #FDF6E3; }"); + + // Header spanning full width + auto headerWidget = new QWidget(this); + auto headerLayout = new QHBoxLayout(headerWidget); + headerLayout->setContentsMargins(85, 50, 85, 30); + headerLayout->setSpacing(30); + + auto close = new QPushButton(QIcon(":/icons/close.svg"), "", this); + close->setIconSize(QSize(80, 80)); + close->setStyleSheet("border: none;"); + connect(close, &QPushButton::clicked, this, &QDialog::reject); + headerLayout->addWidget(close, 0, Qt::AlignLeft | Qt::AlignVCenter); + + const auto title = new QLabel(tr("Join the sunnypilot Community Forum"), this); + // Solarized base02 for text + title->setStyleSheet("font-size: 65px; color: #073642;"); + title->setWordWrap(false); + title->setAlignment(Qt::AlignCenter); + headerLayout->addWidget(title, 1); + + // Spacer to balance the close button on the right + auto spacer = new QWidget(this); + spacer->setFixedSize(80, 80); + headerLayout->addWidget(spacer, 0); + + mainLayout->addWidget(headerWidget); + + // Two-column content layout + auto contentLayout = new QHBoxLayout(); + contentLayout->setContentsMargins(0, 0, 0, 0); + contentLayout->setSpacing(0); + mainLayout->addLayout(contentLayout, 66); + + // Left side: description + auto leftLayout = new QVBoxLayout(); + leftLayout->setContentsMargins(85, 40, 50, 70); + leftLayout->setSpacing(35); + contentLayout->addLayout(leftLayout, 40); + + // Hype / intro paragraph + const auto desc = new QLabel(tr( + "We're excited to announce our sunnypilot Community Forum

" + "Over the years, Discord just hasn't scaled well for our growing community.
" + "It's noisy, unsearchable, and great discussions disappear too easily.
" + "Our new community forum aims to fix that by making it easier to find answers, share ideas, track feedback, report bugs, help newcomers and more!

" + "Here's what's waiting for you:
" + "• Fully indexable and discoverable through search engines 🔎
" + "• AI-powered🤖 topic and chat summaries, spam detection, and more
" + "• A trust-level system✅ that rewards meaningful contributions
" + "• Designed to work on your own time.🧘

" + "Scan the QR code on the right and join the discussion!" + ), this); + // Solarized base01 for body text + desc->setStyleSheet("font-size: 40px; color: #586E75;"); + desc->setWordWrap(true); + leftLayout->addWidget(desc); + + leftLayout->addStretch(); + + // Right side: QR code and instructions + auto rightLayout = new QVBoxLayout(); + rightLayout->setContentsMargins(50, 40, 85, 70); + rightLayout->setSpacing(40); + contentLayout->addLayout(rightLayout, 1); + + // QR code (smaller, fixed size) + auto *qr = new SunnylinkCommunityQRWidget(this); + qr->setFixedSize(500, 500); + rightLayout->addStretch(); + rightLayout->addWidget(qr, 0, Qt::AlignCenter); + rightLayout->addStretch(); +} \ No newline at end of file diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h new file mode 100644 index 0000000000..613375d12c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h @@ -0,0 +1,40 @@ +/** +* Copyright (c) 2025-, sunnypilot 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_COMMUNITY_URL = "https://community.sunnypilot.ai/sp-qr"; + +class SunnylinkCommunityQRWidget : public QWidget { + Q_OBJECT + +public: + explicit SunnylinkCommunityQRWidget(QWidget* parent = nullptr); + void paintEvent(QPaintEvent*) override; + +private: + QPixmap img; + void updateQrCode(const QString &text); + void showEvent(QShowEvent *event) override; +}; + +// Popup widget +class SunnylinkCommunityPopup : public DialogBase { + Q_OBJECT + +public: + explicit SunnylinkCommunityPopup(QWidget* parent = nullptr); + +private: + static QStringList getInstructions(); +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc index 7b5ce48238..a014eddacb 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc @@ -79,11 +79,11 @@ QStringList SunnylinkSponsorPopup::getInstructions(bool 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"); + << tr("If sponsorship status was not updated, please contact a moderator on our forum at https://community.sunnypilot.ai"); } 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"); + << tr("Join our Community Forum at https://community.sunnypilot.ai and reach out to a moderator if you have issues"); } return instructions; }