From 0a2fd7bd61186cab6c0937a821b1e2ee48a1fbdb Mon Sep 17 00:00:00 2001 From: Nayan Date: Thu, 10 Apr 2025 15:20:41 -0400 Subject: [PATCH 1/7] UI: Update `AbstractControlSP_SELECTOR` and `OptionControlSP` (#800) * controls * Adjust label width dynamically based on layout type. Updated the label's fixed width to be conditional on the layout type, improving adaptability for different inline layouts. Additionally, corrected indentation in the width calculation loop for consistency. * Refactor OptionControlSP to improve parameter value handling and encapsulate logic in dedicated methods * Refactor getParamValue to return an integer and ensure value is updated correctly in button click handler * Trying to unify a bit the logic. still WIP * Reducing a bit the change footprint * Refactor spacing item handling to prevent duplicate insertion and improve layout management --------- Co-authored-by: DevTekVE --- .../ui/sunnypilot/qt/widgets/controls.cc | 11 +--- selfdrive/ui/sunnypilot/qt/widgets/controls.h | 63 +++++++++++++++---- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc index d02efc0a8..09ab5417e 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc @@ -132,10 +132,7 @@ AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, con if (isVisible && spacingItem) { main_layout->removeItem(spacingItem); - delete spacingItem; - spacingItem = nullptr; - } else if (!isVisible && spacingItem == nullptr) { - spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + } else if (!isVisible && spacingItem != nullptr && main_layout->indexOf(spacingItem) == -1) { main_layout->insertItem(main_layout->indexOf(description), spacingItem); } } @@ -145,8 +142,7 @@ AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, con } main_layout->addLayout(hlayout); - if (!desc.isEmpty() && spacingItem == nullptr) { - spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + if (!desc.isEmpty() && spacingItem != nullptr && main_layout->indexOf(spacingItem) == -1) { main_layout->insertItem(main_layout->count(), spacingItem); } @@ -166,8 +162,7 @@ void AbstractControlSP_SELECTOR::hideEvent(QHideEvent *e) { description->hide(); } - if (spacingItem == nullptr) { - spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + if (spacingItem != nullptr && main_layout->indexOf(spacingItem) == -1) { main_layout->insertItem(main_layout->indexOf(description), spacingItem); } } diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index f46031088..bb3cec88f 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -96,11 +96,10 @@ class AbstractControlSP_SELECTOR : public AbstractControlSP { Q_OBJECT protected: + QSpacerItem *spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); void hideEvent(QHideEvent *e) override; -private: - QSpacerItem *spacingItem = nullptr; }; // widget to display a value @@ -417,14 +416,29 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { Q_OBJECT private: + bool isInlineLayout; + QHBoxLayout *optionSelectorLayout = isInlineLayout ? new QHBoxLayout() : hlayout; + struct MinMaxValue { int min_value; int max_value; }; + + int getParamValue() { + const auto param_value = QString::fromStdString(params.get(key)); + const auto result = valueMap != nullptr ? valueMap->key(param_value) : param_value; + return result.toInt(); + } + + // Although the method is not static, and thus has access to the value property, I prefer to be explicit about the value. + void setParamValue(const int new_value) { + const auto value_str = valueMap != nullptr ? valueMap->value(QString::number(new_value)) : QString::number(new_value); + params.put(key, value_str.toStdString()); + } public: OptionControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const MinMaxValue &range, const int per_value_change = 1) : _title(title), AbstractControlSP_SELECTOR(title, desc, icon) { + const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, const QMap *valMap = nullptr) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr), _title(title), valueMap(valMap), isInlineLayout(inline_layout) { const QString style = R"( QPushButton { border-radius: 20px; @@ -444,14 +458,27 @@ public: } )"; + if (inline_layout) { + optionSelectorLayout->setMargin(0); + optionSelectorLayout->setSpacing(0); + if (!title.isEmpty()) { + main_layout->removeWidget(title_label); + hlayout->addWidget(title_label, 1); + } + if (spacingItem != nullptr && main_layout->indexOf(spacingItem) != -1) { + main_layout->removeItem(spacingItem); + spacingItem = nullptr; + } + } + label.setStyleSheet(label_enabled_style); - label.setFixedWidth(300); + label.setFixedWidth(inline_layout ? 350 : 300); label.setAlignment(Qt::AlignCenter); const std::vector button_texts{"-", "+"}; key = param.toStdString(); - value = atoi(params.get(key).c_str()); + value = getParamValue(); button_group = new QButtonGroup(this); button_group->setExclusive(true); @@ -459,19 +486,18 @@ public: QPushButton *button = new QPushButton(button_texts[i], this); button->setStyleSheet(style + ((i == 0) ? "QPushButton { text-align: left; }" : "QPushButton { text-align: right; }")); - hlayout->addWidget(button, 0, ((i == 0) ? Qt::AlignLeft : Qt::AlignRight) | Qt::AlignVCenter); + optionSelectorLayout->addWidget(button, 0, ((i == 0) ? Qt::AlignLeft : Qt::AlignRight) | Qt::AlignVCenter); if (i == 0) { - hlayout->addWidget(&label, 0, Qt::AlignCenter); + optionSelectorLayout->addWidget(&label, 0, Qt::AlignCenter); } button_group->addButton(button, i); QObject::connect(button, &QPushButton::clicked, [=]() { int change_value = (i == 0) ? -per_value_change : per_value_change; - key = param.toStdString(); - value = atoi(params.get(key).c_str()); + value = getParamValue(); // in case it changed externally, we need to get the latest value. value += change_value; value = std::clamp(value, range.min_value, range.max_value); - params.put(key, QString::number(value).toStdString()); + setParamValue(value); button_group->button(0)->setEnabled(!(value <= range.min_value)); button_group->button(1)->setEnabled(!(value >= range.max_value)); @@ -484,7 +510,13 @@ public: }); } - hlayout->setAlignment(Qt::AlignLeft); + optionSelectorLayout->setAlignment(Qt::AlignLeft); + if (isInlineLayout) { + QFrame *container = new QFrame; + container->setLayout(optionSelectorLayout); + container->setStyleSheet("background-color: #393939; border-radius: 20px;"); + hlayout->addWidget(container); + } } void setUpdateOtherToggles(bool _update) { @@ -506,6 +538,10 @@ public: protected: void paintEvent(QPaintEvent *event) override { + if (isInlineLayout) { + return; + } + QPainter p(this); p.setRenderHint(QPainter::Antialiasing); @@ -513,8 +549,8 @@ protected: int w = 0; int h = 150; - for (int i = 0; i < hlayout->count(); ++i) { - QWidget *widget = qobject_cast(hlayout->itemAt(i)->widget()); + for (int i = 0; i < optionSelectorLayout->count(); ++i) { + QWidget *widget = qobject_cast(optionSelectorLayout->itemAt(i)->widget()); if (widget) { w += widget->width(); } @@ -544,6 +580,7 @@ private: std::map option_label = {}; bool request_update = false; QString _title = ""; + const QMap *valueMap; const QString label_enabled_style = "font-size: 50px; font-weight: 450; color: #FFFFFF;"; const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;"; From 6a00ac9cd0d16a96e376eb0fc45d31dd79d66e7c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 11 Apr 2025 14:28:59 -0400 Subject: [PATCH 2/7] Radard: vision-based yRel for Hyundai single-lead systems (#805) Introduced processing for custom yRel values using HyundaiFlagsSP when the enhanced SCC flag is enabled. Updated `radard` to handle `CarParamsSP` and make necessary adjustments for Hyundai vehicles with specific SCC configurations. --- selfdrive/controls/radard.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/selfdrive/controls/radard.py b/selfdrive/controls/radard.py index 98fce1cb2..a5dd3400e 100755 --- a/selfdrive/controls/radard.py +++ b/selfdrive/controls/radard.py @@ -5,13 +5,16 @@ from collections import deque from typing import Any import capnp -from cereal import messaging, log, car +from cereal import messaging, log, car, custom from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL, Priority, config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.common.simple_kalman import KF1D +from opendbc.car import structs +from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP + # Default lead acceleration decay set to 50% at 1s _LEAD_ACCEL_TAU = 1.5 @@ -157,7 +160,7 @@ def get_RadarState_from_vision(lead_msg: capnp._DynamicStructReader, v_ego: floa def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capnp._DynamicStructReader, - model_v_ego: float, low_speed_override: bool = True) -> dict[str, Any]: + model_v_ego: float, CP: structs.CarParams, CP_SP: structs.CarParamsSP, low_speed_override: bool = True) -> dict[str, Any]: # Determine leads, this is where the essential logic happens if len(tracks) > 0 and ready and lead_msg.prob > .5: track = match_vision_to_track(v_ego, lead_msg, tracks) @@ -167,6 +170,7 @@ def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capn lead_dict = {'status': False} if track is not None: lead_dict = track.get_RadarState(lead_msg.prob) + lead_dict = get_custom_yrel(CP, CP_SP, lead_dict, lead_msg) elif (track is None) and ready and (lead_msg.prob > .5): lead_dict = get_RadarState_from_vision(lead_msg, v_ego, model_v_ego) @@ -182,8 +186,19 @@ def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capn return lead_dict +def get_custom_yrel(CP: structs.CarParams, CP_SP: structs.CarParamsSP, lead_dict: dict[str, Any], + lead_msg: capnp._DynamicStructReader) -> dict[str, Any]: + if CP.brand == "hyundai" and CP_SP.flags & HyundaiFlagsSP.ENHANCED_SCC: + lead_dict['yRel'] = float(-lead_msg.y[0]) + + return lead_dict + + class RadarD: - def __init__(self, delay: float = 0.0): + def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParams, delay: float = 0.0): + self.CP = CP + self.CP_SP = CP_SP + self.current_time = 0.0 self.tracks: dict[int, Track] = {} @@ -239,8 +254,8 @@ class RadarD: model_v_ego = self.v_ego leads_v3 = sm['modelV2'].leadsV3 if len(leads_v3) > 1: - self.radar_state.leadOne = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[0], model_v_ego, low_speed_override=True) - self.radar_state.leadTwo = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[1], model_v_ego, low_speed_override=False) + self.radar_state.leadOne = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[0], model_v_ego, self.CP, self.CP_SP, low_speed_override=True) + self.radar_state.leadTwo = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[1], model_v_ego, self.CP, self.CP_SP, low_speed_override=False) def publish(self, pm: messaging.PubMaster): assert self.radar_state is not None @@ -260,11 +275,15 @@ def main() -> None: CP = messaging.log_from_bytes(Params().get("CarParams", block=True), car.CarParams) cloudlog.info("radard got CarParams") + cloudlog.info("radard is waiting for CarParamsSP") + CP_SP = messaging.log_from_bytes(Params().get("CarParamsSP", block=True), custom.CarParamsSP) + cloudlog.info("radard got CarParamsSP") + # *** setup messaging sm = messaging.SubMaster(['modelV2', 'carState', 'liveTracks'], poll='modelV2') pm = messaging.PubMaster(['radarState']) - RD = RadarD(CP.radarDelay) + RD = RadarD(CP, CP_SP, CP.radarDelay) while 1: sm.update() From fb87ba681ad79995f63015ad4ede0c1778ae3e9b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 11 Apr 2025 16:06:45 -0400 Subject: [PATCH 3/7] MADS: transition to soft-disable for specific gear states (#791) * MADS: transition to soft-disable for specific gear states This commit enhances the condition checking functionality of the Modular Assistive Driving System (MADS) to implement a 'soft-disable' feature during the vehicle's active motion when a non-forward drive gear is engaged. It includes utilizing structs to reference various car state attributes and modifying a function definition to improve clarity. This adjustment boosts the system's reaction to gear shifts, increasing the safety and efficiency of the driving assist system. * structs --- sunnypilot/mads/mads.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 93f4ad37e..2d7d41b4b 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -5,16 +5,18 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import car, log, custom +from cereal import log, custom +from opendbc.car import structs from opendbc.car.hyundai.values import HyundaiFlags from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState -ButtonType = car.CarState.ButtonEvent.Type +ButtonType = structs.CarState.ButtonEvent.Type EventName = log.OnroadEvent.EventName EventNameSP = custom.OnroadEventSP.EventName -SafetyModel = car.CarParams.SafetyModel +GearShifter = structs.CarState.GearShifter +SafetyModel = structs.CarParams.SafetyModel SET_SPEED_BUTTONS = (ButtonType.accelCruise, ButtonType.resumeCruise, ButtonType.decelCruise, ButtonType.setCruise) IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput) @@ -48,7 +50,7 @@ class ModularAssistiveDrivingSystem: self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") - def update_events(self, CS: car.CarState): + def update_events(self, CS: structs.CarState): def update_unified_engagement_mode(): uem_blocked = self.enabled or (self.selfdrive.enabled and self.selfdrive.enabled_prev) if (self.unified_engagement_mode and uem_blocked) or not self.unified_engagement_mode: @@ -70,7 +72,7 @@ class ModularAssistiveDrivingSystem: if self.events.has(EventName.seatbeltNotLatched): replace_event(EventName.seatbeltNotLatched, EventNameSP.silentSeatbeltNotLatched) transition_paused_state() - if self.events.has(EventName.wrongGear): + if self.events.has(EventName.wrongGear) and (CS.standstill or CS.gearShifter == GearShifter.reverse): replace_event(EventName.wrongGear, EventNameSP.silentWrongGear) transition_paused_state() if self.events.has(EventName.reverseGear): @@ -133,7 +135,7 @@ class ModularAssistiveDrivingSystem: else: self.events.remove(EventName.wrongCarMode) - def update(self, CS: car.CarState): + def update(self, CS: structs.CarState): if not self.enabled_toggle: return From 92707e891237abf8a6852cb96205073fea553f65 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 11 Apr 2025 16:21:49 -0400 Subject: [PATCH 4/7] UI: Device & Sunnylink Panels - Standardize push button size & alignment (#806) * layout adjustments * sunnylink_panel --------- Co-authored-by: Jason Wen --- .../qt/offroad/settings/device_panel.cc | 21 ++++++++++--------- .../qt/offroad/settings/sunnylink_panel.cc | 9 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index f26ed0a19..b685116c9 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -31,15 +31,16 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { continue; } - auto *btn = new PushButtonSP(text, 720, this, param); + auto *btn = new PushButtonSP(text, 750, this, param); btn->setObjectName(id); - - device_grid_layout->addWidget(btn, row, col); buttons[id] = btn; - col++; - if (col > 1) { - col = 0; + if (col==0) { + device_grid_layout->addWidget(btn, row, col, Qt::AlignLeft); + col++; + } else { + device_grid_layout->addWidget(btn, row, col, Qt::AlignRight); + col=0; row++; } } @@ -79,14 +80,14 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { // offroad mode and power buttons QHBoxLayout *power_layout = new QHBoxLayout(); - power_layout->setSpacing(5); + power_layout->setSpacing(25); - PushButtonSP *rebootBtn = new PushButtonSP(tr("Reboot"), 720, this); + PushButtonSP *rebootBtn = new PushButtonSP(tr("Reboot"), 750, this); rebootBtn->setStyleSheet(rebootButtonStyle); power_layout->addWidget(rebootBtn); QObject::connect(rebootBtn, &PushButtonSP::clicked, this, &DevicePanelSP::reboot); - PushButtonSP *poweroffBtn = new PushButtonSP(tr("Power Off"), 720, this); + PushButtonSP *poweroffBtn = new PushButtonSP(tr("Power Off"), 750, this); poweroffBtn->setStyleSheet(powerOffButtonStyle); power_layout->addWidget(poweroffBtn); QObject::connect(poweroffBtn, &PushButtonSP::clicked, this, &DevicePanelSP::poweroff); @@ -100,7 +101,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { QObject::connect(offroadBtn, &PushButtonSP::clicked, this, &DevicePanelSP::setOffroadMode); QVBoxLayout *power_group_layout = new QVBoxLayout(); - power_group_layout->setSpacing(30); + power_group_layout->setSpacing(25); power_group_layout->addWidget(offroadBtn, 0, Qt::AlignHCenter); power_group_layout->addLayout(power_layout); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc index c9eccb2c4..1358cbc95 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc @@ -85,7 +85,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { }); // Backup Settings - backupSettings = new PushButtonSP(tr("Backup Settings"), 730, this); + backupSettings = new PushButtonSP(tr("Backup Settings"), 750, this); backupSettings->setObjectName("backup_btn"); connect(backupSettings, &QPushButton::clicked, [=]() { backupSettings->setEnabled(false); @@ -96,7 +96,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { }); // Restore Settings - restoreSettings = new PushButtonSP(tr("Restore Settings"), 730, this); + restoreSettings = new PushButtonSP(tr("Restore Settings"), 750, this); restoreSettings->setObjectName("restore_btn"); connect(restoreSettings, &QPushButton::clicked, [=]() { restoreSettings->setEnabled(false); @@ -108,10 +108,9 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { // Settings Restore and Settings Backup in the same horizontal space auto settings_layout = new QHBoxLayout; settings_layout->setContentsMargins(0, 0, 0, 30); - settings_layout->addWidget(backupSettings); + settings_layout->addWidget(backupSettings, 0, Qt::AlignLeft); settings_layout->addSpacing(10); - settings_layout->addWidget(restoreSettings); - settings_layout->setAlignment(Qt::AlignLeft); + settings_layout->addWidget(restoreSettings, 0, Qt::AlignRight); list->addItem(settings_layout); QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnylinkPanel::updatePanel); From a598d385f2db0f31c58505cbe9c46912b805598e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 12 Apr 2025 05:47:00 -0400 Subject: [PATCH 5/7] ci: Remove redundant Panda build step from prebuilt workflow (#810) The Panda build step was unnecessary as it is not utilized in this workflow. This change simplifies the workflow and reduces redundant actions, improving efficiency. --- .github/workflows/sunnypilot-build-prebuilt.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 6bd72ef50..c7c72919c 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -122,10 +122,6 @@ jobs: fi PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable - - name: Build Panda - run: | - scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} ${{ github.workspace }}/panda - - name: Build Main Project run: | export PYTHONPATH="$BUILD_DIR" From f1d703e6e4dc3c7514c51ef1c215d8c4887a8f5e Mon Sep 17 00:00:00 2001 From: Nayan Date: Sat, 12 Apr 2025 10:33:17 -0400 Subject: [PATCH 6/7] Device: Customizable Max Time Offroad (#796) * Max Time Offroad * Refactor & Fix param * Error Handling * rename SP variable * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.cc Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/max_time_offroad.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/max_time_offroad.cc Co-authored-by: DevTekVE * UI layout changes for better alignment selector is not big enough -> make it bigger ;) OptionControlSP now includes a QMap argument to allow actual values to be set in param directly * Rebase & resolve reviews * change default to be closer to OP default * me dumb * MaxTimeOffroad: Add support for 30h limit and improve label formatting * power_monitoring: Refactor MaxTimeOffroad parameter handling for clarity * test: Add unit tests for MaxTimeOffroad parameter handling * power_monitoring: Update MaxTimeOffroad handling to use seconds and improve shutdown logic * power_monitoring: Improve exception handling and remove redundant shutdown check for MaxTimeOffroad --------- Co-authored-by: DevTekVE Co-authored-by: Jason Wen --- common/params_keys.h | 1 + selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/device_panel.cc | 5 ++ .../qt/offroad/settings/device_panel.h | 2 + .../qt/offroad/settings/max_time_offroad.cc | 53 +++++++++++++++++++ .../qt/offroad/settings/max_time_offroad.h | 25 +++++++++ system/hardware/power_monitoring.py | 20 ++++++- .../hardware/tests/test_power_monitoring.py | 37 ++++++++++++- system/manager/manager.py | 1 + 9 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h diff --git a/common/params_keys.h b/common/params_keys.h index cd8208bbb..2eef34872 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -127,6 +127,7 @@ inline static std::unordered_map keys = { {"CarParamsSPPersistent", PERSISTENT}, {"CarPlatformBundle", PERSISTENT}, {"EnableGithubRunner", PERSISTENT | BACKUP}, + {"MaxTimeOffroad", PERSISTENT | BACKUP}, {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, {"OffroadMode", CLEAR_ON_MANAGER_START}, {"OffroadMode_Status", CLEAR_ON_MANAGER_START}, diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 30748098a..deeec4473 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -21,6 +21,7 @@ qt_src = [ "sunnypilot/qt/offroad/offroad_home.cc", "sunnypilot/qt/offroad/settings/device_panel.cc", "sunnypilot/qt/offroad/settings/lateral_panel.cc", + "sunnypilot/qt/offroad/settings/max_time_offroad.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index b685116c9..3e613e102 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -75,6 +75,11 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { connect(buttons["resetParams"], &PushButtonSP::clicked, this, &DevicePanelSP::resetSettings); + // Max Time Offroad + maxTimeOffroad = new MaxTimeOffroad(); + connect(maxTimeOffroad, &OptionControlSP::updateLabels, maxTimeOffroad, &MaxTimeOffroad::refresh); + addItem(maxTimeOffroad); + addItem(device_grid_layout); // offroad mode and power buttons diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h index dc77aa464..7b7412a73 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h @@ -7,6 +7,7 @@ #pragma once +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" @@ -23,6 +24,7 @@ public: private: std::map buttons; PushButtonSP *offroadBtn; + MaxTimeOffroad *maxTimeOffroad; const QString alwaysOffroadStyle = R"( PushButtonSP { diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.cc new file mode 100644 index 000000000..4cc7351b0 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.cc @@ -0,0 +1,53 @@ +/** + * 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/max_time_offroad.h" + +// Map of Max Offroad Time Options (Minutes) +const QMap MaxTimeOffroad::offroad_time_options = { + {"0", "0"}, // Always On + {"1", "5"}, + {"2", "10"}, + {"3", "15"}, + {"4", "30"}, + {"5", "60"}, + {"6", "120"}, + {"7", "180"}, + {"8", "300"}, + {"9", "600"}, + {"10", "1440"}, + {"11", "1800"} +}; + +MaxTimeOffroad::MaxTimeOffroad() : OptionControlSP( + "MaxTimeOffroad", + tr("Max Time Offroad"), + tr("Device will automatically shutdown after set time once the engine is turned off.
(30h is the default)"), + "../assets/offroad/icon_blank.png", + {0, 11}, 1, true, &offroad_time_options) { + + refresh(); +} + +void MaxTimeOffroad::refresh() { + const int maxOffroadInMinutes = QString::fromStdString(params.get("MaxTimeOffroad")).toInt(); + const bool useHours = maxOffroadInMinutes >= 60; + + QString label; + if (maxOffroadInMinutes == 0) { + label = tr("Always On"); + } else { + const int value = useHours ? maxOffroadInMinutes / 60 : maxOffroadInMinutes; + label = QString("%1%2").arg(value).arg(useHours ? tr("h") : tr("m")); + } + + if (maxOffroadInMinutes == 1800) { + label += tr(" (default)"); + } + + setLabel(label); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h new file mode 100644 index 000000000..31c6a335c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h @@ -0,0 +1,25 @@ +/** + * 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/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class MaxTimeOffroad : public OptionControlSP { + Q_OBJECT + +public: + static const QMap offroad_time_options; + + MaxTimeOffroad(); + void refresh(); + +private: + Params params; +}; diff --git a/system/hardware/power_monitoring.py b/system/hardware/power_monitoring.py index 5a94625b4..952aebd9d 100644 --- a/system/hardware/power_monitoring.py +++ b/system/hardware/power_monitoring.py @@ -106,7 +106,23 @@ class PowerMonitoring: def get_car_battery_capacity(self) -> int: return int(self.car_battery_capacity_uWh) - # See if we need to shutdown + # Max Time Offroad + def max_time_offroad_exceeded(self, offroad_time): + """ + Check if the max time offroad has been exceeded. If the value is 0, it means no limit. + :param offroad_time: Time spent offroad in seconds + :return: True if the max time offroad has been exceeded, False otherwise + """ + try: + param = self.params.get("MaxTimeOffroad", encoding="utf8") + sp_max_time_val_s = int(param) * 60 if param is not None and int(param) >= 0 else MAX_TIME_OFFROAD_S + except Exception: + sp_max_time_val_s = MAX_TIME_OFFROAD_S + + return sp_max_time_val_s > 0 and offroad_time >= sp_max_time_val_s + + +# See if we need to shutdown def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool): if offroad_timestamp is None: return False @@ -116,7 +132,7 @@ class PowerMonitoring: offroad_time = (now - offroad_timestamp) low_voltage_shutdown = (self.car_voltage_mV < (VBATT_PAUSE_CHARGING * 1e3) and offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S) - should_shutdown |= offroad_time > MAX_TIME_OFFROAD_S + should_shutdown |= self.max_time_offroad_exceeded(offroad_time) should_shutdown |= low_voltage_shutdown should_shutdown |= (self.car_battery_capacity_uWh <= 0) should_shutdown &= not ignition diff --git a/system/hardware/tests/test_power_monitoring.py b/system/hardware/tests/test_power_monitoring.py index 1dff6c6c5..3eec13dc4 100644 --- a/system/hardware/tests/test_power_monitoring.py +++ b/system/hardware/tests/test_power_monitoring.py @@ -2,7 +2,7 @@ import pytest from openpilot.common.params import Params from openpilot.system.hardware.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \ - CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S + CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S, MAX_TIME_OFFROAD_S # Create fake time ssb = 0. @@ -197,3 +197,38 @@ class TestPowerMonitoring: offroad_timestamp, started_seen), \ f"Should shutdown after {DELAY_SHUTDOWN_TIME_S} seconds offroad time" + + @pytest.mark.parametrize( + "max_time_offroad, offroad_time_min, expected_result", + [ + # No max time set – fallback to default (30 hours) + (None, 0, False), + (None, MAX_TIME_OFFROAD_S + 1, True), # exceeds 30h (1800+ mins) + + # Valid max time values (in minutes) + ("60", 59, False), # under limit + ("60", 120, True), # over limit + ("10", 8, False), # under limit + ("10", 11, True), # over limit + + # Edge case: max time is zero → no limit enforced + ("0", 0, False), + ("0", 400, False), + + # Invalid max time formats or negative values → fallback to 30 hours + ("invalid", 100, False), # should fallback to 30h + ("-1", MAX_TIME_OFFROAD_S + 1, True), # should fallback to 30h, and exceed it + ] + ) + def test_max_time_offroad_exceeded(self, max_time_offroad, offroad_time_min, expected_result): + # Set the parameter if provided + if max_time_offroad is not None: + self.params.put("MaxTimeOffroad", max_time_offroad) + + # Convert offroad time from minutes to seconds + offroad_time_s = offroad_time_min * 60 + + pm = PowerMonitoring() + result = pm.max_time_offroad_exceeded(offroad_time_s) + + assert result == expected_result diff --git a/system/manager/manager.py b/system/manager/manager.py index 6bd0a557c..1feab03a1 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -50,6 +50,7 @@ def manager_init() -> None: ("MadsMainCruiseAllowed", "1"), ("MadsPauseLateralOnBrake", "0"), ("MadsUnifiedEngagementMode", "1"), + ("MaxTimeOffroad", "1800"), ("ModelManager_LastSyncTime", "0"), ("ModelManager_ModelsCache", ""), ("NeuralNetworkLateralControl", "0"), From 43eefed5142bdf523dd2402085744f1086d3100f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 12 Apr 2025 12:22:42 -0400 Subject: [PATCH 7/7] Hyundai longitudinal: Parse lead info for camera-based SCC platforms (#809) * Hyundai longitudinal: Parse lead info for camera-based SCC platforms * fix * update * bump * update tests * lol why is this here * bump --------- Co-authored-by: DevTekVE Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> --- opendbc_repo | 2 +- selfdrive/controls/radard.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 807ce9ff4..7942a631b 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 807ce9ff4329c4b42c3a331204bd9a7c130cbde4 +Subproject commit 7942a631bd4ff84a6a20ad36a16a36861eaf2543 diff --git a/selfdrive/controls/radard.py b/selfdrive/controls/radard.py index a5dd3400e..bee424405 100755 --- a/selfdrive/controls/radard.py +++ b/selfdrive/controls/radard.py @@ -13,6 +13,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.common.simple_kalman import KF1D from opendbc.car import structs +from opendbc.car.hyundai.values import HyundaiFlags from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP @@ -188,7 +189,8 @@ def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capn def get_custom_yrel(CP: structs.CarParams, CP_SP: structs.CarParamsSP, lead_dict: dict[str, Any], lead_msg: capnp._DynamicStructReader) -> dict[str, Any]: - if CP.brand == "hyundai" and CP_SP.flags & HyundaiFlagsSP.ENHANCED_SCC: + if CP.brand == "hyundai" and (CP_SP.flags & HyundaiFlagsSP.ENHANCED_SCC or + CP.flags & (HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CAMERA_SCC)): lead_dict['yRel'] = float(-lead_msg.y[0]) return lead_dict