-
Notifications
You must be signed in to change notification settings - Fork 9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fan controller cleanup + testing (#23886)
* clean up fan controllers in preparation for testing * add fan controller to release * add some unit tests around the fan controller * subclass ABC
- Loading branch information
1 parent
f4c822e
commit 8c971f2
Showing
4 changed files
with
170 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import os | ||
from smbus2 import SMBus | ||
from abc import ABC, abstractmethod | ||
from common.realtime import DT_TRML | ||
from common.numpy_fast import interp | ||
from selfdrive.swaglog import cloudlog | ||
from selfdrive.controls.lib.pid import PIController | ||
|
||
class BaseFanController(ABC): | ||
@abstractmethod | ||
def update(self, max_cpu_temp: float, ignition: bool) -> int: | ||
pass | ||
|
||
|
||
class EonFanController(BaseFanController): | ||
# Temp thresholds to control fan speed - high hysteresis | ||
TEMP_THRS_H = [50., 65., 80., 10000] | ||
# Temp thresholds to control fan speed - low hysteresis | ||
TEMP_THRS_L = [42.5, 57.5, 72.5, 10000] | ||
# Fan speed options | ||
FAN_SPEEDS = [0, 16384, 32768, 65535] | ||
|
||
def __init__(self) -> None: | ||
super().__init__() | ||
cloudlog.info("Setting up EON fan handler") | ||
|
||
self.fan_speed = -1 | ||
self.setup_eon_fan() | ||
|
||
def setup_eon_fan(self) -> None: | ||
os.system("echo 2 > /sys/module/dwc3_msm/parameters/otg_switch") | ||
|
||
def set_eon_fan(self, speed: int) -> None: | ||
if self.fan_speed != speed: | ||
# FIXME: this is such an ugly hack to get the right index | ||
val = speed // 16384 | ||
|
||
bus = SMBus(7, force=True) | ||
try: | ||
i = [0x1, 0x3 | 0, 0x3 | 0x08, 0x3 | 0x10][val] | ||
bus.write_i2c_block_data(0x3d, 0, [i]) | ||
except OSError: | ||
# tusb320 | ||
if val == 0: | ||
bus.write_i2c_block_data(0x67, 0xa, [0]) | ||
else: | ||
bus.write_i2c_block_data(0x67, 0xa, [0x20]) | ||
bus.write_i2c_block_data(0x67, 0x8, [(val - 1) << 6]) | ||
bus.close() | ||
self.fan_speed = speed | ||
|
||
def update(self, max_cpu_temp: float, ignition: bool) -> int: | ||
new_speed_h = next(speed for speed, temp_h in zip(self.FAN_SPEEDS, self.TEMP_THRS_H) if temp_h > max_cpu_temp) | ||
new_speed_l = next(speed for speed, temp_l in zip(self.FAN_SPEEDS, self.TEMP_THRS_L) if temp_l > max_cpu_temp) | ||
|
||
if new_speed_h > self.fan_speed: | ||
self.set_eon_fan(new_speed_h) | ||
elif new_speed_l < self.fan_speed: | ||
self.set_eon_fan(new_speed_l) | ||
|
||
return self.fan_speed | ||
|
||
|
||
class UnoFanController(BaseFanController): | ||
def __init__(self) -> None: | ||
super().__init__() | ||
cloudlog.info("Setting up UNO fan handler") | ||
|
||
def update(self, max_cpu_temp: float, ignition: bool) -> int: | ||
new_speed = int(interp(max_cpu_temp, [40.0, 80.0], [0, 80])) | ||
|
||
if not ignition: | ||
new_speed = min(30, new_speed) | ||
|
||
return new_speed | ||
|
||
|
||
class TiciFanController(BaseFanController): | ||
def __init__(self) -> None: | ||
super().__init__() | ||
cloudlog.info("Setting up TICI fan handler") | ||
|
||
self.last_ignition = False | ||
self.controller = PIController(k_p=0, k_i=2e-3, k_f=1, neg_limit=-80, pos_limit=0, rate=(1 / DT_TRML)) | ||
|
||
def update(self, max_cpu_temp: float, ignition: bool) -> int: | ||
self.controller.neg_limit = -(80 if ignition else 30) | ||
self.controller.pos_limit = -(30 if ignition else 0) | ||
|
||
if ignition != self.last_ignition: | ||
self.controller.reset() | ||
|
||
fan_pwr_out = -int(self.controller.update( | ||
setpoint=75, | ||
measurement=max_cpu_temp, | ||
feedforward=interp(max_cpu_temp, [60.0, 100.0], [0, -80]) | ||
)) | ||
|
||
self.last_ignition = ignition | ||
return fan_pwr_out | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
#!/usr/bin/env python3 | ||
import unittest | ||
from unittest.mock import Mock, MagicMock, patch | ||
from parameterized import parameterized | ||
|
||
with patch("smbus2.SMBus", new=MagicMock()): | ||
from selfdrive.thermald.fan_controller import EonFanController, UnoFanController, TiciFanController | ||
|
||
ALL_CONTROLLERS = [(EonFanController, ), (UnoFanController,), (TiciFanController,)] | ||
GEN2_CONTROLLERS = [(UnoFanController,), (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(max_cpu_temp=100, ignition=ignition) | ||
|
||
def wind_down(self, controller, ignition=False): | ||
for _ in range(1000): | ||
controller.update(max_cpu_temp=10, ignition=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(max_cpu_temp=100, ignition=True), 70) | ||
|
||
@parameterized.expand(GEN2_CONTROLLERS) | ||
def test_offroad_limits(self, controller_class): | ||
controller = patched_controller(controller_class) | ||
self.wind_up(controller) | ||
self.assertLessEqual(controller.update(max_cpu_temp=100, ignition=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(max_cpu_temp=10, ignition=False), 0) | ||
|
||
@parameterized.expand(GEN2_CONTROLLERS) | ||
def test_limited(self, controller_class): | ||
controller = patched_controller(controller_class) | ||
self.wind_up(controller, ignition=True) | ||
self.assertGreaterEqual(controller.update(max_cpu_temp=100, ignition=True), 80) | ||
|
||
@parameterized.expand(ALL_CONTROLLERS) | ||
def test_windup_speed(self, controller_class): | ||
controller = patched_controller(controller_class) | ||
self.wind_down(controller, ignition=True) | ||
for _ in range(10): | ||
controller.update(max_cpu_temp=90, ignition=True) | ||
self.assertGreaterEqual(controller.update(max_cpu_temp=90, ignition=True), 60) | ||
|
||
if __name__ == "__main__": | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters