diff --git a/common/params_keys.h b/common/params_keys.h index cd8208bbb1..2eef348720 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 30748098a3..deeec44732 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 b685116c96..3e613e1022 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 dc77aa4647..7b7412a739 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 0000000000..4cc7351b03 --- /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 0000000000..31c6a335c7 --- /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 5a94625b48..952aebd9d7 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 1dff6c6c5f..3eec13dc4c 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 6bd0a557c8..1feab03a1f 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"),