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" diff --git a/common/params_keys.h b/common/params_keys.h index 165f3b7b8..987cd4cc3 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -132,6 +132,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/controls/radard.py b/selfdrive/controls/radard.py index 98fce1cb2..bee424405 100755 --- a/selfdrive/controls/radard.py +++ b/selfdrive/controls/radard.py @@ -5,13 +5,17 @@ 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.car.hyundai.values import HyundaiFlags +from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP + # Default lead acceleration decay set to 50% at 1s _LEAD_ACCEL_TAU = 1.5 @@ -157,7 +161,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 +171,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 +187,20 @@ 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 or + CP.flags & (HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CAMERA_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 +256,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 +277,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() 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 f26ed0a19..3e613e102 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++; } } @@ -74,19 +75,24 @@ 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 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 +106,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/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/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); 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;"; 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 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"),