diff --git a/docs/CARS.md b/docs/CARS.md
index e10dc8ae77..dbc1dbcd74 100644
--- a/docs/CARS.md
+++ b/docs/CARS.md
@@ -4,12 +4,13 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
-# 326 Supported Cars
+# 334 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|
Hardware Needed
|Video|Setup Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Acura|ILX 2019|All|openpilot|26 mph|25 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Acura|MDX 2025|All except Type S|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Acura|RDX 2019-21|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
@@ -72,12 +73,15 @@ A supported vehicle is one that just works when you install a comma device. All
|Genesis|GV80 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai M connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[](##)|[](##)|Parts
- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
-|Honda|Accord 2023|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|Accord 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|Accord Hybrid 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|City (Brazil only) 2023|All|openpilot available[1](#footnotes)|0 mph|14 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[5](#footnotes)|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
-|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|Civic Hatchback 2019-21|All|openpilot available[1](#footnotes)|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
@@ -85,7 +89,9 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Clarity 2018-21|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector + Honda Clarity Proxy Board
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|15 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|CR-V 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|CR-V Hybrid 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|e 2020|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
@@ -96,6 +102,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|Pilot 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Azera 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
@@ -120,7 +127,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
-|Hyundai|Kona 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Hyundai|Kona 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023[6](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai R connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
@@ -298,6 +305,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
+|Toyota|Wildlander PHEV 2021|All|openpilot|19 mph|0 mph|[](##)|[](##)|Parts
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
diff --git a/opendbc_repo b/opendbc_repo
index aa0aa1b7aa..ee25c18829 160000
--- a/opendbc_repo
+++ b/opendbc_repo
@@ -1 +1 @@
-Subproject commit aa0aa1b7aacc15e5e9228a91d7f7043d8b39f9e2
+Subproject commit ee25c18829fcb229fcf6576194ddd8638b33ba55
diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript
index 810338aae8..2f3c8ddd8d 100644
--- a/selfdrive/ui/sunnypilot/SConscript
+++ b/selfdrive/ui/sunnypilot/SConscript
@@ -4,6 +4,7 @@ widgets_src = [
"sunnypilot/qt/widgets/controls.cc",
"sunnypilot/qt/widgets/drive_stats.cc",
"sunnypilot/qt/widgets/expandable_row.cc",
+ "sunnypilot/qt/widgets/external_storage.cc",
"sunnypilot/qt/widgets/prime.cc",
"sunnypilot/qt/widgets/scrollview.cc",
"sunnypilot/qt/network/networking.cc",
diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc
index 58193f9fe8..a4a6bf481c 100644
--- a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc
+++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc
@@ -5,9 +5,14 @@
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h"
+#include "selfdrive/ui/sunnypilot/qt/widgets/external_storage.h"
DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(parent) {
+ #ifndef __APPLE__
+ addItem(new ExternalStorageControl());
+ #endif
+
// Advanced Controls Toggle
showAdvancedControls = new ParamControlSP("ShowAdvancedControls", tr("Show Advanced Controls"), tr("Toggle visibility of advanced sunnypilot controls.\nThis only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state."), "");
addItem(showAdvancedControls);
diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc
index 3d4e070963..96d945ab53 100644
--- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc
+++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc
@@ -34,6 +34,27 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
vlayout->setContentsMargins(50, 20, 50, 20);
auto *list = new ListWidget(this, false);
+
+ QVBoxLayout *titleLayout = new QVBoxLayout;
+ QLabel *title = new QLabel(tr("🚀 sunnylink 🚀"));
+ title->setStyleSheet("font-size: 90px; font-weight: 500; font-family: 'Noto Color Emoji';");
+ titleLayout->addWidget(title, 0, Qt::AlignCenter);
+
+ QLabel *sunnylinkDesc = new QLabel("
"+
+ tr("For secure backup, restore, and remote configuration")+ "
");
+
+ QLabel *sponsorMsg = new QLabel(""+
+ tr("Sponsorship isn't required for basic backup/restore") + "
" +
+ tr("Click the sponsor button for more details")+ "
");
+
+ sunnylinkDesc->setStyleSheet("font-size: 40px; font-weight: 100; font-family: 'Noto';");
+ sponsorMsg->setStyleSheet("font-size: 35px; font-weight: 100; font-family: 'Noto';");
+
+ titleLayout->addWidget(sunnylinkDesc, 0, Qt::AlignCenter);
+ titleLayout->addWidget(sponsorMsg, 0, Qt::AlignCenter);
+
+ list->addItem(titleLayout);
+
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",
diff --git a/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc
new file mode 100644
index 0000000000..f951187511
--- /dev/null
+++ b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc
@@ -0,0 +1,170 @@
+/**
+ * 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/widgets/external_storage.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "common/params.h"
+#include "selfdrive/ui/qt/api.h"
+#include "selfdrive/ui/qt/widgets/input.h"
+#include "selfdrive/ui/sunnypilot/ui.h"
+
+ExternalStorageControl::ExternalStorageControl() :
+ ButtonControl(tr("External Storage"), "", tr("Extend your comma device's storage by inserting a USB drive into the aux port.")) {
+
+ QObject::connect(this, &ButtonControl::clicked, [=]() {
+ if (text() == tr("CHECK") || text() == tr("MOUNT")) {
+ mountStorage();
+ } else if (text() == tr("UNMOUNT")) {
+ unmountStorage();
+ } else if (text() == tr("FORMAT")) {
+ if (ConfirmationDialog::confirm(tr("Are you sure you want to format this drive? This will erase all data."), tr("Format"), this)) {
+ formatStorage();
+ }
+ }
+ });
+
+ QObject::connect(uiState(), &UIState::offroadTransition, this, &ExternalStorageControl::updateState);
+ updateState(!uiState()->scene.started);
+
+ refresh();
+}
+
+void ExternalStorageControl::updateState(bool offroad) {
+ setEnabled(offroad);
+}
+
+void ExternalStorageControl::debouncedRefresh() {
+ if (refreshPending) return;
+ refreshPending = true;
+
+ QTimer::singleShot(250, this, [=]() {
+ refreshPending = false;
+ refresh();
+ });
+}
+
+void ExternalStorageControl::refresh() {
+ QtConcurrent::run([=]() {
+ auto run = [](const QString &cmd) {
+ QProcess p;
+ p.start("sh", QStringList() << "-c" << cmd);
+ p.waitForFinished();
+ return p.exitCode() == 0;
+ };
+
+ bool isMounted = run("findmnt -n /mnt/external_realdata");
+ bool hasDrive = run("lsblk -f /dev/sdg");
+ bool hasFs = run("lsblk -f /dev/sdg1 | grep -q ext4");
+ bool hasLabel = run("sudo blkid /dev/sdg1 | grep -q 'LABEL=\"openpilot\"'");
+
+ QString info;
+ if (isMounted && hasLabel) {
+ QProcess df;
+ df.start("sh", QStringList() << "-c" << "df -h /mnt/external_realdata | awk 'NR==2 {print $3 \"/\" $2}'");
+ df.waitForFinished();
+ info = df.readAllStandardOutput().trimmed();
+ }
+
+ QMetaObject::invokeMethod(this, [=]() {
+ if (formatting) {
+ setValue(tr("formatting"));
+ setText(tr("FORMAT"));
+ setEnabled(false);
+ } else {
+ if (!hasDrive) {
+ setValue(tr("insert drive"));
+ setText(tr("CHECK"));
+ } else if (!hasFs || !hasLabel) {
+ setValue(tr("needs format"));
+ setText(tr("FORMAT"));
+ } else if (isMounted) {
+ setValue(info);
+ setText(tr("UNMOUNT"));
+ } else {
+ setValue("drive detected");
+ setText(tr("MOUNT"));
+ }
+ updateState(!uiState()->scene.started);
+ }
+ }, Qt::QueuedConnection);
+ });
+}
+
+void ExternalStorageControl::mountStorage() {
+ setValue(tr("mounting"));
+ setEnabled(false);
+
+ QtConcurrent::run([=]() {
+ QProcess process;
+ process.start("sh", QStringList() << "-c" <<
+ "sudo mount -o remount,rw / && "
+ "sudo mkdir -p /mnt/external_realdata && "
+ "grep -q '/dev/sdg1 /mnt/external_realdata' /etc/fstab || "
+ "echo '/dev/sdg1 /mnt/external_realdata ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab && "
+ "sudo systemctl daemon-reexec && "
+ "sudo mount /mnt/external_realdata && "
+ "sudo chown -R comma:comma /mnt/external_realdata && "
+ "sudo chmod -R 775 /mnt/external_realdata && "
+ "sudo mount -o remount,ro /");
+ process.waitForFinished();
+
+ QMetaObject::invokeMethod(this, [=]() {
+ debouncedRefresh();
+ }, Qt::QueuedConnection);
+ });
+}
+
+void ExternalStorageControl::unmountStorage() {
+ setValue(tr("unmounting"));
+ setEnabled(false);
+
+ QtConcurrent::run([=]() {
+ QProcess process;
+ process.start("sh", QStringList() << "-c" << "sudo umount /mnt/external_realdata");
+ process.waitForFinished();
+
+ QMetaObject::invokeMethod(this, [=]() {
+ debouncedRefresh();
+ }, Qt::QueuedConnection);
+ });
+}
+
+void ExternalStorageControl::formatStorage() {
+ unmountStorage();
+ formatting = true;
+ setValue(tr("formatting"));
+ setEnabled(false);
+
+ QProcess *process = new QProcess(this);
+ connect(process, static_cast(&QProcess::finished),
+ this, [=](int exitCode, QProcess::ExitStatus status) {
+ process->deleteLater();
+ formatting = false;
+ if (exitCode == 0 && status == QProcess::NormalExit) {
+ mountStorage();
+ } else {
+ setValue(tr("needs format"));
+ updateState(!uiState()->scene.started);
+ }
+ });
+ process->start("sh", QStringList() << "-c" <<
+ "sudo wipefs -a /dev/sdg && "
+ "sudo parted -s /dev/sdg mklabel gpt mkpart primary ext4 0% 100% && "
+ "sudo mkfs.ext4 -F -L openpilot /dev/sdg1"
+ );
+}
+
+void ExternalStorageControl::showEvent(QShowEvent *event) {
+ ButtonControl::showEvent(event);
+ QTimer::singleShot(100, this, &ExternalStorageControl::debouncedRefresh);
+}
diff --git a/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h
new file mode 100644
index 0000000000..d26eefd18c
--- /dev/null
+++ b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h
@@ -0,0 +1,34 @@
+/**
+ * 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 "system/hardware/hw.h"
+#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
+#define ButtonControl ButtonControlSP
+
+class ExternalStorageControl : public ButtonControl {
+ Q_OBJECT
+
+public:
+ ExternalStorageControl();
+
+protected:
+ void showEvent(QShowEvent *event) override;
+
+private:
+ Params params;
+
+ bool refreshPending = false;
+ bool formatting = false;
+ void updateState(bool offroad);
+ void refresh();
+ void debouncedRefresh();
+ void mountStorage();
+ void unmountStorage();
+ void formatStorage();
+};
diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts
index f72ea8ee83..ef7110e72e 100644
--- a/selfdrive/ui/translations/main_ar.ts
+++ b/selfdrive/ui/translations/main_ar.ts
@@ -2103,6 +2103,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts
index 193e2fd895..dde9241460 100644
--- a/selfdrive/ui/translations/main_de.ts
+++ b/selfdrive/ui/translations/main_de.ts
@@ -2085,6 +2085,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts
index 1d1723cde7..3b53e7f944 100644
--- a/selfdrive/ui/translations/main_es.ts
+++ b/selfdrive/ui/translations/main_es.ts
@@ -2087,6 +2087,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts
index 959321bb4b..45705defd9 100644
--- a/selfdrive/ui/translations/main_fr.ts
+++ b/selfdrive/ui/translations/main_fr.ts
@@ -2083,6 +2083,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts
index 28b5b00e51..3faa183954 100644
--- a/selfdrive/ui/translations/main_ja.ts
+++ b/selfdrive/ui/translations/main_ja.ts
@@ -2082,6 +2082,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts
index d69fda88b0..756cad6045 100644
--- a/selfdrive/ui/translations/main_ko.ts
+++ b/selfdrive/ui/translations/main_ko.ts
@@ -2096,6 +2096,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts
index c5171b19f7..0a886d435c 100644
--- a/selfdrive/ui/translations/main_pt-BR.ts
+++ b/selfdrive/ui/translations/main_pt-BR.ts
@@ -2087,6 +2087,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts
index 3bd2d84a92..7b89f2a694 100644
--- a/selfdrive/ui/translations/main_th.ts
+++ b/selfdrive/ui/translations/main_th.ts
@@ -2078,6 +2078,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts
index db9c15580a..0598d08364 100644
--- a/selfdrive/ui/translations/main_tr.ts
+++ b/selfdrive/ui/translations/main_tr.ts
@@ -2077,6 +2077,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts
index 2316360891..1f4db2d6d0 100644
--- a/selfdrive/ui/translations/main_zh-CHS.ts
+++ b/selfdrive/ui/translations/main_zh-CHS.ts
@@ -2082,6 +2082,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts
index 16bb393dea..1a8784846c 100644
--- a/selfdrive/ui/translations/main_zh-CHT.ts
+++ b/selfdrive/ui/translations/main_zh-CHT.ts
@@ -2082,6 +2082,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ 🚀 sunnylink 🚀
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/system/athena/athenad.py b/system/athena/athenad.py
index f97b8e55bb..42c9cf8a1c 100755
--- a/system/athena/athenad.py
+++ b/system/athena/athenad.py
@@ -381,20 +381,22 @@ def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: str = N
return {"success": 1}
-def scan_dir(path: str, prefix: str) -> list[str]:
+def scan_dir(path: str, prefix: str, base: str | None = None) -> list[str]:
+ if base is None:
+ base = path
files = []
# only walk directories that match the prefix
# (glob and friends traverse entire dir tree)
with os.scandir(path) as i:
for e in i:
- rel_path = os.path.relpath(e.path, Paths.log_root())
+ rel_path = os.path.relpath(e.path, base)
if e.is_dir(follow_symlinks=False):
# add trailing slash
rel_path = os.path.join(rel_path, '')
# if prefix is a partial dir name, current dir will start with prefix
# if prefix is a partial file name, prefix with start with dir name
if rel_path.startswith(prefix) or prefix.startswith(rel_path):
- files.extend(scan_dir(e.path, prefix))
+ files.extend(scan_dir(e.path, prefix, base))
else:
if rel_path.startswith(prefix):
files.append(rel_path)
@@ -402,7 +404,12 @@ def scan_dir(path: str, prefix: str) -> list[str]:
@dispatcher.add_method
def listDataDirectory(prefix='') -> list[str]:
- return scan_dir(Paths.log_root(), prefix)
+ internal_files = scan_dir(Paths.log_root(), prefix, Paths.log_root())
+ try:
+ external_files = scan_dir(Paths.log_root_external(), prefix, Paths.log_root_external())
+ except FileNotFoundError:
+ external_files = []
+ return sorted(set(internal_files + external_files))
@dispatcher.add_method
@@ -427,8 +434,13 @@ def uploadFilesToUrls(files_data: list[UploadFileDict]) -> UploadFilesToUrlRespo
failed.append(file.fn)
continue
- path = os.path.join(Paths.log_root(), file.fn)
- if not os.path.exists(path) and not os.path.exists(strip_zst_extension(path)):
+ path_internal = os.path.join(Paths.log_root(), file.fn)
+ path_external = os.path.join(Paths.log_root_external(), file.fn)
+ if os.path.exists(path_internal) or os.path.exists(strip_zst_extension(path_internal)):
+ path = path_internal
+ elif os.path.exists(path_external) or os.path.exists(strip_zst_extension(path_external)):
+ path = path_external
+ else:
failed.append(file.fn)
continue
diff --git a/system/hardware/hw.py b/system/hardware/hw.py
index 5e40fff136..d24857e8bd 100644
--- a/system/hardware/hw.py
+++ b/system/hardware/hw.py
@@ -20,6 +20,10 @@ class Paths:
else:
return '/data/media/0/realdata/'
+ @staticmethod
+ def log_root_external() -> str:
+ return '/mnt/external_realdata/'
+
@staticmethod
def swaglog_root() -> str:
if PC:
diff --git a/system/loggerd/config.py b/system/loggerd/config.py
index e1c47c768d..d9befb5613 100644
--- a/system/loggerd/config.py
+++ b/system/loggerd/config.py
@@ -9,21 +9,26 @@ STATS_DIR_FILE_LIMIT = 10000
STATS_SOCKET = "ipc:///tmp/stats"
STATS_FLUSH_TIME_S = 60
-def get_available_percent(default: float) -> float:
+PATH_DICT = {
+ "internal": Paths.log_root(),
+ "external": Paths.log_root_external()
+}
+
+def get_available_percent(default: float, path_type="internal") -> float:
try:
- statvfs = os.statvfs(Paths.log_root())
+ statvfs = os.statvfs(PATH_DICT[path_type])
available_percent = 100.0 * statvfs.f_bavail / statvfs.f_blocks
- except OSError:
+ except (OSError, KeyError):
available_percent = default
return available_percent
-def get_available_bytes(default: int) -> int:
+def get_available_bytes(default: int, path_type="internal") -> int:
try:
- statvfs = os.statvfs(Paths.log_root())
+ statvfs = os.statvfs(PATH_DICT[path_type])
available_bytes = statvfs.f_bavail * statvfs.f_frsize
- except OSError:
+ except (OSError, KeyError):
available_bytes = default
return available_bytes
diff --git a/system/loggerd/deleter.py b/system/loggerd/deleter.py
index eb8fd35f21..058f5c301d 100755
--- a/system/loggerd/deleter.py
+++ b/system/loggerd/deleter.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
import os
+import time
import shutil
import threading
+from pathlib import Path
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog
from openpilot.system.loggerd.config import get_available_bytes, get_available_percent
@@ -61,6 +63,41 @@ def deleter_thread(exit_event: threading.Event):
if any(name.endswith(".lock") for name in os.listdir(delete_path)):
continue
+ if Path(Paths.log_root_external()).is_mount():
+ out_of_bytes_external = get_available_bytes(default=MIN_BYTES + 1, path_type="external") < MIN_BYTES
+ out_of_percent_external = get_available_percent(default=MIN_PERCENT + 1, path_type="external") < MIN_PERCENT
+
+ if out_of_percent_external or out_of_bytes_external:
+ dirs_external = listdir_by_creation(Paths.log_root_external())
+
+ # remove the earliest external directory we can
+ for delete_dir_external in sorted(dirs_external):
+ delete_path_external = os.path.join(Paths.log_root_external(), delete_dir_external)
+ try:
+ cloudlog.warning(f"deleting {delete_path_external}")
+ shutil.rmtree(delete_path_external)
+ break
+ except OSError:
+ cloudlog.exception(f"issue deleting {delete_path_external}")
+
+ # move directory from internal to external
+ path_external = os.path.join(Paths.log_root_external(), delete_dir)
+ try:
+ cloudlog.warning(f"moving {delete_path} to {path_external}")
+ start = time.monotonic()
+ shutil.move(delete_path, path_external)
+ cloudlog.warning(f"moved {delete_path} to {path_external} in {time.monotonic() - start:.2f}s")
+ break
+ except Exception:
+ cloudlog.error(f"issue moving {delete_path} to {path_external}")
+ try:
+ cloudlog.warning(f"deleting {delete_path}")
+ shutil.rmtree(delete_path)
+ break
+ except OSError:
+ cloudlog.exception(f"issue deleting {delete_path}")
+ continue
+
try:
cloudlog.info(f"deleting {delete_path}")
shutil.rmtree(delete_path)
diff --git a/uv.lock b/uv.lock
index 49ae160993..7010cdfb83 100644
--- a/uv.lock
+++ b/uv.lock
@@ -4832,11 +4832,11 @@ wheels = [
[[package]]
name = "typing-extensions"
-version = "4.14.1"
+version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]