This commit is contained in:
Your Name
2024-04-27 13:48:05 -05:00
parent 2fbe9dbea1
commit 931db76fc6
432 changed files with 12973 additions and 3300 deletions

View File

View File

@@ -1,6 +1,5 @@
import time
import threading
from typing import Optional
from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE
@@ -38,12 +37,14 @@ class PowerMonitoring:
self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), int(car_battery_capacity_uWh))
# FrogPilot variables
device_shutdown_setting = self.params.get_int("DeviceShutdown")
device_management = self.params.get_bool("DeviceManagement")
device_shutdown_setting = self.params.get_int("DeviceShutdown") if device_management else 33
# If the toggle is set for < 1 hour, configure by 15 minute increments
self.device_shutdown_time = (device_shutdown_setting - 3) * 3600 if device_shutdown_setting >= 4 else device_shutdown_setting * (60 * 15)
self.low_voltage_shutdown = self.params.get_float("LowVoltageShutdown") if device_management else VBATT_PAUSE_CHARGING
# Calculation tick
def calculate(self, voltage: Optional[int], ignition: bool):
def calculate(self, voltage: int | None, ignition: bool):
try:
now = time.monotonic()
@@ -113,14 +114,14 @@ class PowerMonitoring:
return int(self.car_battery_capacity_uWh)
# See if we need to shutdown
def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: Optional[float], started_seen: bool):
def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool):
if offroad_timestamp is None:
return False
now = time.monotonic()
should_shutdown = False
offroad_time = (now - offroad_timestamp)
low_voltage_shutdown = (self.car_voltage_mV < (VBATT_PAUSE_CHARGING * 1e3) and
low_voltage_shutdown = (self.car_voltage_mV < (self.low_voltage_shutdown * 1e3) and
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
should_shutdown |= offroad_time > self.device_shutdown_time
should_shutdown |= low_voltage_shutdown
@@ -128,7 +129,7 @@ class PowerMonitoring:
should_shutdown &= not ignition
should_shutdown &= (not self.params.get_bool("DisablePowerDown"))
should_shutdown &= in_car
should_shutdown &= offroad_time > DELAY_SHUTDOWN_TIME_S if self.device_shutdown_time else True # If "Instant" is selected for the timer, shutdown immediately
should_shutdown &= offroad_time > DELAY_SHUTDOWN_TIME_S
should_shutdown |= self.params.get_bool("ForcePowerDown")
should_shutdown &= started_seen or (now > MIN_ON_TIME_S)
return should_shutdown

View File

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
import unittest
from unittest.mock import Mock, patch
from parameterized import parameterized
from openpilot.selfdrive.thermald.fan_controller import TiciFanController
ALL_CONTROLLERS = [(TiciFanController,)]
def patched_controller(controller_class):
with patch("os.system", new=Mock()):
return controller_class()
class TestFanController(unittest.TestCase):
def wind_up(self, controller, ignition=True):
for _ in range(1000):
controller.update(100, ignition)
def wind_down(self, controller, ignition=False):
for _ in range(1000):
controller.update(10, ignition)
@parameterized.expand(ALL_CONTROLLERS)
def test_hot_onroad(self, controller_class):
controller = patched_controller(controller_class)
self.wind_up(controller)
self.assertGreaterEqual(controller.update(100, True), 70)
@parameterized.expand(ALL_CONTROLLERS)
def test_offroad_limits(self, controller_class):
controller = patched_controller(controller_class)
self.wind_up(controller)
self.assertLessEqual(controller.update(100, False), 30)
@parameterized.expand(ALL_CONTROLLERS)
def test_no_fan_wear(self, controller_class):
controller = patched_controller(controller_class)
self.wind_down(controller)
self.assertEqual(controller.update(10, False), 0)
@parameterized.expand(ALL_CONTROLLERS)
def test_limited(self, controller_class):
controller = patched_controller(controller_class)
self.wind_up(controller, True)
self.assertEqual(controller.update(100, True), 100)
@parameterized.expand(ALL_CONTROLLERS)
def test_windup_speed(self, controller_class):
controller = patched_controller(controller_class)
self.wind_down(controller, True)
for _ in range(10):
controller.update(90, True)
self.assertGreaterEqual(controller.update(90, True), 60)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,200 @@
#!/usr/bin/env python3
import unittest
from unittest.mock import patch
from openpilot.common.params import Params
from openpilot.selfdrive.thermald.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \
CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S
# Create fake time
ssb = 0.
def mock_time_monotonic():
global ssb
ssb += 1.
return ssb
TEST_DURATION_S = 50
GOOD_VOLTAGE = 12 * 1e3
VOLTAGE_BELOW_PAUSE_CHARGING = (VBATT_PAUSE_CHARGING - 1) * 1e3
def pm_patch(name, value, constant=False):
if constant:
return patch(f"openpilot.selfdrive.thermald.power_monitoring.{name}", value)
return patch(f"openpilot.selfdrive.thermald.power_monitoring.{name}", return_value=value)
@patch("time.monotonic", new=mock_time_monotonic)
class TestPowerMonitoring(unittest.TestCase):
def setUp(self):
self.params = Params()
# Test to see that it doesn't do anything when pandaState is None
def test_pandaState_present(self):
pm = PowerMonitoring()
for _ in range(10):
pm.calculate(None, None)
self.assertEqual(pm.get_power_used(), 0)
self.assertEqual(pm.get_car_battery_capacity(), (CAR_BATTERY_CAPACITY_uWh / 10))
# Test to see that it doesn't integrate offroad when ignition is True
def test_offroad_ignition(self):
pm = PowerMonitoring()
for _ in range(10):
pm.calculate(GOOD_VOLTAGE, True)
self.assertEqual(pm.get_power_used(), 0)
# Test to see that it integrates with discharging battery
def test_offroad_integration_discharging(self):
POWER_DRAW = 4
with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
pm = PowerMonitoring()
for _ in range(TEST_DURATION_S + 1):
pm.calculate(GOOD_VOLTAGE, False)
expected_power_usage = ((TEST_DURATION_S/3600) * POWER_DRAW * 1e6)
self.assertLess(abs(pm.get_power_used() - expected_power_usage), 10)
# Test to check positive integration of car_battery_capacity
def test_car_battery_integration_onroad(self):
POWER_DRAW = 4
with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = 0
for _ in range(TEST_DURATION_S + 1):
pm.calculate(GOOD_VOLTAGE, True)
expected_capacity = ((TEST_DURATION_S/3600) * CAR_CHARGING_RATE_W * 1e6)
self.assertLess(abs(pm.get_car_battery_capacity() - expected_capacity), 10)
# Test to check positive integration upper limit
def test_car_battery_integration_upper_limit(self):
POWER_DRAW = 4
with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh - 1000
for _ in range(TEST_DURATION_S + 1):
pm.calculate(GOOD_VOLTAGE, True)
estimated_capacity = CAR_BATTERY_CAPACITY_uWh + (CAR_CHARGING_RATE_W / 3600 * 1e6)
self.assertLess(abs(pm.get_car_battery_capacity() - estimated_capacity), 10)
# Test to check negative integration of car_battery_capacity
def test_car_battery_integration_offroad(self):
POWER_DRAW = 4
with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
for _ in range(TEST_DURATION_S + 1):
pm.calculate(GOOD_VOLTAGE, False)
expected_capacity = CAR_BATTERY_CAPACITY_uWh - ((TEST_DURATION_S/3600) * POWER_DRAW * 1e6)
self.assertLess(abs(pm.get_car_battery_capacity() - expected_capacity), 10)
# Test to check negative integration lower limit
def test_car_battery_integration_lower_limit(self):
POWER_DRAW = 4
with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = 1000
for _ in range(TEST_DURATION_S + 1):
pm.calculate(GOOD_VOLTAGE, False)
estimated_capacity = 0 - ((1/3600) * POWER_DRAW * 1e6)
self.assertLess(abs(pm.get_car_battery_capacity() - estimated_capacity), 10)
# Test to check policy of stopping charging after MAX_TIME_OFFROAD_S
def test_max_time_offroad(self):
MOCKED_MAX_OFFROAD_TIME = 3600
POWER_DRAW = 0 # To stop shutting down for other reasons
with pm_patch("MAX_TIME_OFFROAD_S", MOCKED_MAX_OFFROAD_TIME, constant=True), pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
start_time = ssb
ignition = False
while ssb <= start_time + MOCKED_MAX_OFFROAD_TIME:
pm.calculate(GOOD_VOLTAGE, ignition)
if (ssb - start_time) % 1000 == 0 and ssb < start_time + MOCKED_MAX_OFFROAD_TIME:
self.assertFalse(pm.should_shutdown(ignition, True, start_time, False))
self.assertTrue(pm.should_shutdown(ignition, True, start_time, False))
def test_car_voltage(self):
POWER_DRAW = 0 # To stop shutting down for other reasons
TEST_TIME = 350
VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S = 50
with pm_patch("VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S", VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S, constant=True), \
pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
ignition = False
start_time = ssb
for i in range(TEST_TIME):
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
if i % 10 == 0:
self.assertEqual(pm.should_shutdown(ignition, True, start_time, True),
(pm.car_voltage_mV < VBATT_PAUSE_CHARGING * 1e3 and
(ssb - start_time) > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S and
(ssb - start_time) > DELAY_SHUTDOWN_TIME_S))
self.assertTrue(pm.should_shutdown(ignition, True, start_time, True))
# Test to check policy of not stopping charging when DisablePowerDown is set
def test_disable_power_down(self):
POWER_DRAW = 0 # To stop shutting down for other reasons
TEST_TIME = 100
self.params.put_bool("DisablePowerDown", True)
with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
ignition = False
for i in range(TEST_TIME):
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
if i % 10 == 0:
self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
# Test to check policy of not stopping charging when ignition
def test_ignition(self):
POWER_DRAW = 0 # To stop shutting down for other reasons
TEST_TIME = 100
with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
ignition = True
for i in range(TEST_TIME):
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
if i % 10 == 0:
self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
# Test to check policy of not stopping charging when harness is not connected
def test_harness_connection(self):
POWER_DRAW = 0 # To stop shutting down for other reasons
TEST_TIME = 100
with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
ignition = False
for i in range(TEST_TIME):
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
if i % 10 == 0:
self.assertFalse(pm.should_shutdown(ignition, False, ssb, False))
self.assertFalse(pm.should_shutdown(ignition, False, ssb, False))
def test_delay_shutdown_time(self):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = 0
ignition = False
in_car = True
offroad_timestamp = ssb
started_seen = True
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
while ssb < offroad_timestamp + DELAY_SHUTDOWN_TIME_S:
self.assertFalse(pm.should_shutdown(ignition, in_car,
offroad_timestamp,
started_seen),
f"Should not shutdown before {DELAY_SHUTDOWN_TIME_S} seconds offroad time")
self.assertTrue(pm.should_shutdown(ignition, in_car,
offroad_timestamp,
started_seen),
f"Should shutdown after {DELAY_SHUTDOWN_TIME_S} seconds offroad time")
if __name__ == "__main__":
unittest.main()

View File

@@ -6,7 +6,6 @@ import threading
import time
from collections import OrderedDict, namedtuple
from pathlib import Path
from typing import Dict, Optional, Tuple
import psutil
@@ -50,9 +49,9 @@ THERMAL_BANDS = OrderedDict({
# Override to highest thermal band when offroad and above this temp
OFFROAD_DANGER_TEMP = 75
prev_offroad_states: Dict[str, Tuple[bool, Optional[str]]] = {}
prev_offroad_states: dict[str, tuple[bool, str | None]] = {}
tz_by_type: Optional[Dict[str, int]] = None
tz_by_type: dict[str, int] | None = None
def populate_tz_by_type():
global tz_by_type
tz_by_type = {}
@@ -87,7 +86,7 @@ def read_thermal(thermal_config):
return dat
def set_offroad_alert_if_changed(offroad_alert: str, show_alert: bool, extra_text: Optional[str]=None):
def set_offroad_alert_if_changed(offroad_alert: str, show_alert: bool, extra_text: str | None=None):
if prev_offroad_states.get(offroad_alert, None) == (show_alert, extra_text):
return
prev_offroad_states[offroad_alert] = (show_alert, extra_text)
@@ -169,16 +168,16 @@ def thermald_thread(end_event, hw_queue) -> None:
count = 0
onroad_conditions: Dict[str, bool] = {
onroad_conditions: dict[str, bool] = {
"ignition": False,
}
startup_conditions: Dict[str, bool] = {}
startup_conditions_prev: Dict[str, bool] = {}
startup_conditions: dict[str, bool] = {}
startup_conditions_prev: dict[str, bool] = {}
off_ts: Optional[float] = None
started_ts: Optional[float] = None
off_ts: float | None = None
started_ts: float | None = None
started_seen = False
startup_blocked_ts: Optional[float] = None
startup_blocked_ts: float | None = None
thermal_status = ThermalStatus.yellow
last_hw_state = HardwareState(
@@ -205,6 +204,10 @@ def thermald_thread(end_event, hw_queue) -> None:
fan_controller = None
# FrogPilot variables
device_management = params.get_bool("DeviceManagement")
offline_mode = device_management and params.get_bool("OfflineMode")
while not end_event.is_set():
sm.update(PANDA_STATES_TIMEOUT)
@@ -217,6 +220,7 @@ def thermald_thread(end_event, hw_queue) -> None:
peripheral_panda_present = peripheralState.pandaType != log.PandaState.PandaType.unknown
msg = read_thermal(thermal_config)
msg.deviceState.deviceType = HARDWARE.get_device_type()
if sm.updated['pandaStates'] and len(pandaStates) > 0:
@@ -297,12 +301,9 @@ def thermald_thread(end_event, hw_queue) -> None:
elif current_band.max_temp is not None and all_comp_temp > current_band.max_temp:
thermal_status = list(THERMAL_BANDS.keys())[band_idx + 1]
if params.get_bool("FireTheBabysitter") and params.get_bool("MuteOverheated"):
thermal_status = ThermalStatus.green
# **** starting logic ****
startup_conditions["up_to_date"] = (params.get("OfflineMode") and params.get("FireTheBabysitter")) or params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate")
startup_conditions["up_to_date"] = params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate") or offline_mode
startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall")
startup_conditions["accepted_terms"] = params.get("HasAcceptedTerms") == terms_version