diff --git a/selfdrive/ui/qt/widgets/wifi.cc b/selfdrive/ui/qt/widgets/wifi.cc
index d7eb8beeb..8958e48c9 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 3e68a15b7..8ef9dca91 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 be92c1426..449093886 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 000000000..e29e89ee9
--- /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 000000000..613375d12
--- /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 7b5ce4823..a014eddac 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;
}