From bcb4f33c278b80d12973979c702d26155da049fe Mon Sep 17 00:00:00 2001 From: whoisdomi Date: Mon, 2 Nov 2026 13:13:27 -0600 Subject: [PATCH] c3 and c3x aggressive cooling --- openpilot/common/params_keys.h | 1 + openpilot/starpilot/galaxy/settings.py | 5 ++-- .../galaxy/tests/test_settings_controls.mjs | 5 +++- openpilot/starpilot/galaxy/web/js/settings.js | 6 +++-- .../starpilot/ui/feature_settings_owner.py | 12 +++++++++- openpilot/starpilot/ui/runtime_app.py | 19 +++++++++++++-- openpilot/system/hardware/fan_controller.py | 23 ++++++++++++++----- openpilot/system/hardware/hardwared.py | 3 ++- 8 files changed, 59 insertions(+), 15 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 9de2f9783e..f64c83f97a 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -76,6 +76,7 @@ inline static std::unordered_map keys = { {"ForceStops", {PERSISTENT, BOOL, "0"}}, {"DriveModeProfiles", {PERSISTENT, BOOL, "0"}}, {"AlwaysAllowUploads", {PERSISTENT, BOOL, "0"}}, + {"AggressiveCoolingEnabled", {PERSISTENT, BOOL, "0"}}, {"UsageStatsState", {PERSISTENT | DONT_LOG, JSON}}, {"UsageStatsStatus", {CLEAR_ON_MANAGER_START | DONT_LOG, JSON}}, {"TeslaAOLScreenTap", {PERSISTENT, BOOL, "0"}}, diff --git a/openpilot/starpilot/galaxy/settings.py b/openpilot/starpilot/galaxy/settings.py index 764b9ef741..138f5d191f 100644 --- a/openpilot/starpilot/galaxy/settings.py +++ b/openpilot/starpilot/galaxy/settings.py @@ -41,7 +41,7 @@ from openpilot.starpilot.ui.vasm_owner import ANNOTATION as VASM_ANNOTATION, RES PAGES = frozenset(("hub", "localization", "developer", "vehicle", "slc", "lane", "lane_change", "profiles", "personalities", "traffic", "aggressive", "standard", "relaxed", - "curve", "torque", "torque/bluepilot", "highway_smoothing", "aol", "wheel", "appearance", "display", "data", "power", "sounds", "pip", "sentry", "vasm", + "curve", "torque", "torque/bluepilot", "highway_smoothing", "aol", "wheel", "appearance", "display", "data", "power", "cooling", "sounds", "pip", "sentry", "vasm", "conditional", "conditional/cem", "conditional/ccm", *(f"{name}/{category}" for name in ("traffic", "aggressive", "standard", "relaxed") for category in ("acceleration", "braking", "following")))) @@ -58,7 +58,7 @@ CAR_CONTROL_SECTIONS = ( ("wheel", "Wheel Controls", ("wheel",)), ("visual", "Visual (Display & UI)", ("appearance", "pip")), ("sounds", "Sounds & Alerts", ("sounds",)), - ("device", "Device & Data", ("display", "data")), + ("device", "Device & Data", ("display", "data", "cooling")), ("localization", "Localization", ("localization",)), ("developer", "Developer", ("developer",)), ) @@ -684,6 +684,7 @@ class SettingsGateway: scope = hashlib.sha256(_session_key(token) + generation + (ctx.cp_raw or b"")).digest() result = {"page": page, "title": state.title, "subtitle": state.subtitle, "parked": state.parked, "vehicleIdentified": bool(ctx.cp is not None and getattr(ctx.cp, "carFingerprint", None)), + "coolingAvailable": HARDWARE.get_device_type() in ("tici", "tizi"), "rows": [{**_projection(row), "revision": hashlib.sha256(scope + repr(row).encode()).hexdigest()} for row in state.rows], "view": view_id} if page == "hub" or any(row.page for row in state.rows): diff --git a/openpilot/starpilot/galaxy/tests/test_settings_controls.mjs b/openpilot/starpilot/galaxy/tests/test_settings_controls.mjs index 93da10fb80..f1ee712a15 100644 --- a/openpilot/starpilot/galaxy/tests/test_settings_controls.mjs +++ b/openpilot/starpilot/galaxy/tests/test_settings_controls.mjs @@ -194,7 +194,7 @@ for (const id of ["wheel", "sounds", "localization"]) { } const deviceData = SETTINGS_SECTIONS.find((item) => item.id === "device") -assert.deepEqual(deviceData.pages, ["power", "display", "data"]) +assert.deepEqual(deviceData.pages, ["power", "display", "data", "cooling"]) assert.equal(SettingsPage.computed.atSectionRoot.call({ activeSection: deviceData, state: { data: { page: "hub" } } }), true) assert.equal(SettingsPage.computed.atSectionRoot.call({ activeSection: deviceData, @@ -203,6 +203,9 @@ assert.equal(SettingsPage.computed.atSectionRoot.call({ activeSection: deviceDat const dataLinks = SettingsPage.computed.visibleRows.call({ initialPage: "hub", activeSection: deviceData, state: { query: "", data: { page: "hub", rows: [] } } }) assert.deepEqual(dataLinks.map(({ row }) => row.page), ["power", "display", "data"]) +const coolingLinks = SettingsPage.computed.visibleRows.call({ initialPage: "hub", activeSection: deviceData, + state: { query: "", data: { page: "hub", rows: [], coolingAvailable: true } } }) +assert.deepEqual(coolingLinks.map(({ row }) => row.page), ["power", "display", "data", "cooling"]) assert.equal(SettingsPage.computed.atSectionRoot.call({initialPage:'hub',state:{data:null,parents:[]}}),true, 'section tabs stay mounted while loading a new section') diff --git a/openpilot/starpilot/galaxy/web/js/settings.js b/openpilot/starpilot/galaxy/web/js/settings.js index a1e69a4135..b7d5921535 100644 --- a/openpilot/starpilot/galaxy/web/js/settings.js +++ b/openpilot/starpilot/galaxy/web/js/settings.js @@ -22,7 +22,7 @@ export const SETTINGS_SECTIONS = Object.freeze([ { id: "wheel", label: "Wheel Controls", icon: "bi-controller", pages: ["wheel"] }, { id: "visual", label: "Visual (Display & UI)", icon: "bi-eye", pages: ["appearance", "ui_layout", "favorites", "pip"] }, { id: "sounds", label: "Sounds & Alerts", icon: "bi-volume-up", pages: ["sounds"] }, - { id: "device", label: "Device & Data", icon: "bi-hdd", pages: ["power", "display", "data"] }, + { id: "device", label: "Device & Data", icon: "bi-hdd", pages: ["power", "display", "data", "cooling"] }, { id: "localization", label: "Localization", icon: "bi-globe", pages: ["localization"] }, { id: "developer", label: "Developer", icon: "bi-code-slash", pages: ["developer"] }, ]) @@ -34,7 +34,8 @@ export const isPipCropRow = (row) => ["Visual camera crop editor", "Camera crop /^Vehicle (left|right) crop/.test(row.label || "") const SECTION_LINKS = { - device: [{ label: "Parked Power", page: "power", value: "Adjust shutdown hours and battery-voltage cutoff" }, { label: "Display", page: "display", value: "Adjust screen brightness and display timing" }, { label: "Data Uploads", page: "data", value: "Choose mobile-data uploads and usage-statistics sharing" }], + device: [{ label: "Parked Power", page: "power", value: "Adjust shutdown hours and battery-voltage cutoff" }, { label: "Display", page: "display", value: "Adjust screen brightness and display timing" }, { label: "Data Uploads", page: "data", value: "Choose mobile-data uploads and usage-statistics sharing" }, + { label: "Cooling", page: "cooling", value: "Choose the fan curve for comma 3/3X", requires: "coolingAvailable" }], visual: [{ label: "Driving Screen Widgets, Colors & Layout", page: "ui_layout" }, { label: "Quick Select", page: "favorites" }, { label: "Blind Spot Camera and Preview", page: "pip" }], } @@ -293,6 +294,7 @@ export const SettingsPage = { const section = this.activeSection const entries = inHub && SECTION_LINKS[section.id] ? SECTION_LINKS[section.id].map((row, index) => ({ row: { ...row, available: true, action: false, value: row.value || "Customize " + row.label.toLocaleLowerCase() }, index })) + .filter(({ row }) => !row.requires || this.state.data?.[row.requires]) : rows.map((row, index) => ({ row, index })).filter(({ row }) => !inHub || section.pages.includes(row.page)) if (inHub && section.id === "longitudinal") { entries.sort((a, b) => section.pages.indexOf(a.row.page) - section.pages.indexOf(b.row.page)) diff --git a/openpilot/starpilot/ui/feature_settings_owner.py b/openpilot/starpilot/ui/feature_settings_owner.py index d03c6ffb12..fa7c709188 100644 --- a/openpilot/starpilot/ui/feature_settings_owner.py +++ b/openpilot/starpilot/ui/feature_settings_owner.py @@ -17,6 +17,7 @@ from openpilot.starpilot.lateral.pause import KEYS as LATERAL_PAUSE_KEYS from openpilot.starpilot.lateral.lane_centering import STRENGTH_BASELINE, STRENGTH_MAX from opendbc.car.structs import car +from openpilot.common.hardware import HARDWARE from openpilot.common.params import UnknownKeyName from openpilot.starpilot.longitudinal.accel_profile import A_CRUISE_MAX_VALS_TRAFFIC_ALL, interpolate_accel_profile from openpilot.starpilot.longitudinal.profile_runtime import TRAFFIC_CRUISE_BRAKE_MAGNITUDE @@ -74,7 +75,10 @@ from openpilot.starpilot.curve_speed.actions import ACTION_KEYS as CURVE_ACTION_ from openpilot.starpilot.controllers.toyota_cruise import capability as toyota_cruise_capability -GLOBAL_BOOL_PREFERENCES = frozenset((LEAD_APPROACH_KEY, LEAD_TAKEOFF_KEY, NUDGE_KEY, "DisengageOnAccelerator")) +AGGRESSIVE_COOLING_KEY = "AggressiveCoolingEnabled" +AGGRESSIVE_COOLING_DESCRIPTION = ("Runs the fan sooner and harder to keep the device 10-16 °C cooler on hot days, " + + "at the cost of more fan noise.") +GLOBAL_BOOL_PREFERENCES = frozenset((LEAD_APPROACH_KEY, LEAD_TAKEOFF_KEY, NUDGE_KEY, "DisengageOnAccelerator", AGGRESSIVE_COOLING_KEY)) BOOL_DEFAULTS = { "GMPedalLongitudinal": False, "ForceStops": False, "AlwaysAllowUploads": False, "TurnAssist": True, "HighwaySmoothing": False, @@ -94,6 +98,7 @@ BOOL_DEFAULTS = { LEAD_TAKEOFF_KEY: False, NUDGE_KEY: True, "DisengageOnAccelerator": False, + AGGRESSIVE_COOLING_KEY: False, CURVE_MASTER_KEY: False, CURVE_NO_LEAD_KEY: False, "ShowCSCStatus": False, "ForceAutoTuneOff": False, "AlwaysOnLateral": True, "NostalgiaMode": False, @@ -893,6 +898,11 @@ class FeatureSettingsOwner: title = "Data Uploads" rows = [self._bool_row("AlwaysAllowUploads", "Always Allow Uploads", self.authority("preferences"), "Allow queued full logs and videos on metered networks. Uses mobile data.")] + elif page == "cooling": + title = "Cooling" + if HARDWARE.get_device_type() in ("tici", "tizi"): + rows = [self._bool_row(AGGRESSIVE_COOLING_KEY, "Aggressive Cooling", self.authority("preferences"), + AGGRESSIVE_COOLING_DESCRIPTION)] elif page == FeaturePage.HUB: rows = [FeatureRow("", "Curve Speed Controller", "Adjust how speed is reduced for curves ahead", page=FeaturePage.CURVE, available=True), FeatureRow("", "Speed Limit Controller", "Choose speed-limit sources, offsets, and confirmation behavior", page=FeaturePage.SLC, available=True), diff --git a/openpilot/starpilot/ui/runtime_app.py b/openpilot/starpilot/ui/runtime_app.py index d9f35462ea..a90dd11658 100644 --- a/openpilot/starpilot/ui/runtime_app.py +++ b/openpilot/starpilot/ui/runtime_app.py @@ -31,7 +31,7 @@ from openpilot.starpilot.drive_state.owner import DriveStateOwner, Rejected as D from openpilot.starpilot.drive_state.evidence import PhysicalSource from openpilot.starpilot.drive_state.control import DriveStateControl, LABELS from openpilot.starpilot.storage import starpilot_storage_root -from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner +from openpilot.starpilot.ui.feature_settings_owner import AGGRESSIVE_COOLING_DESCRIPTION, AGGRESSIVE_COOLING_KEY, FeatureSettingsOwner from openpilot.starpilot.ui.conditional_feature import confirmation_question as conditional_question from openpilot.starpilot.ui.appearance_owner import AppearanceOwner from openpilot.starpilot.ui.appearance_preferences import CameraViewChoice @@ -94,7 +94,7 @@ from openpilot.starpilot.ui.software_state import DownloadLabel, SoftwareAction, from openpilot.starpilot.ui.toggles_state import Personality, ToggleKey, ToggleRequest from openpilot.system.ui.lib.application import MouseEvent, MousePos, gui_app from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.list_view import button_item, text_item +from openpilot.system.ui.widgets.list_view import button_item, text_item, toggle_item from openpilot.starpilot.ui.compact_settings_page import StarCompactSettings NATIVE_SETTINGS_PANELS = (Destination.DEVICE, Destination.SOFTWARE, Destination.BLUETOOTH, Destination.TOGGLES, Destination.DEVELOPER) @@ -1845,6 +1845,8 @@ class StarMainLayout(MainLayout): toggle_items = self._large_panels[Destination.TOGGLES]._scroller._items toggle_items.insert(2, text_item("Safe Mode", "N/A")) from openpilot.common.hardware import HARDWARE + if HARDWARE.get_device_type() in ("tici", "tizi"): + self._add_aggressive_cooling(self._large_panels[Destination.DEVICE]) from openpilot.starpilot.ui.starpilot_settings_adapter_large import StarPilotSettingsAdapterLarge for destination, owner in self._large_panels.items(): if destination == Destination.DEVELOPER and HARDWARE.get_device_type() in ('mici', 'tici', 'tizi'): @@ -1861,6 +1863,19 @@ class StarMainLayout(MainLayout): self.star._native_favorite_actions = self._native_favorites self.star._publish_bookmark = self._on_bookmark_clicked + @staticmethod + def _add_aggressive_cooling(device_panel) -> None: + item = toggle_item("Aggressive Cooling", AGGRESSIVE_COOLING_DESCRIPTION, + initial_state=ui_state.params.get_bool(AGGRESSIVE_COOLING_KEY), + callback=lambda state: ui_state.params.put_bool(AGGRESSIVE_COOLING_KEY, state, block=True)) + device_panel._scroller.add_widget(item) + native_show = device_panel.show_event + + def show_event(): + item.action_item.set_state(ui_state.params.get_bool(AGGRESSIVE_COOLING_KEY)) + native_show() + device_panel.show_event = show_event + def _exit_spacepilot(self) -> None: gui_app.pop_widgets_to(self, instant=True) self._set_mode_for_state() diff --git a/openpilot/system/hardware/fan_controller.py b/openpilot/system/hardware/fan_controller.py index 869fe19c3d..9298d8831f 100755 --- a/openpilot/system/hardware/fan_controller.py +++ b/openpilot/system/hardware/fan_controller.py @@ -4,25 +4,36 @@ import numpy as np from openpilot.common.pid import PIDController from openpilot.common.hardware import HARDWARE +IS_MICI = HARDWARE.get_device_type() == "mici" + # raise fan setpoint on tici/tizi to reduce noise # after raising LMH threshold in AGNOS 18.1 to prevent CPU throttling -OFFSET = 0 if HARDWARE.get_device_type() == "mici" else 5 +STOCK_CURVE = dict(offset=0, k_p=0., ff_low=60., ff_high=100.) if IS_MICI else \ + dict(offset=5, k_p=0., ff_low=65., ff_high=105.) +# tici/tizi opt-in: runs 10-16 C cooler than stock at the cost of fan noise +AGGRESSIVE_CURVE = dict(offset=-5, k_p=1., ff_low=55., ff_high=80.) class FanController: def __init__(self, rate: int) -> None: self.last_ignition = False - self.controller = PIDController(k_p=0, k_i=4e-3, rate=rate) + self.last_aggressive = False + self.controller = PIDController(k_p=STOCK_CURVE["k_p"], k_i=4e-3, rate=rate) + + def update(self, cur_temp: float, ignition: bool, aggressive_cooling: bool = False) -> int: + aggressive = aggressive_cooling and not IS_MICI + curve = AGGRESSIVE_CURVE if aggressive else STOCK_CURVE - def update(self, cur_temp: float, ignition: bool) -> int: self.controller.pos_limit = 100 if ignition else 30 self.controller.neg_limit = 30 if ignition else 0 + self.controller._k_p = ([0], [curve["k_p"]]) - if ignition != self.last_ignition: + if ignition != self.last_ignition or aggressive != self.last_aggressive: self.controller.reset() self.last_ignition = ignition + self.last_aggressive = aggressive return int(self.controller.update( - error=(cur_temp - (75 + OFFSET)), # temperature setpoint in C - feedforward=np.interp(cur_temp, [60.0 + OFFSET, 100.0 + OFFSET], [0, 100]) + error=(cur_temp - (75 + curve["offset"])), # temperature setpoint in C + feedforward=np.interp(cur_temp, [curve["ff_low"], curve["ff_high"]], [0, 100]) )) diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index f860b44591..4686c2b805 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -375,7 +375,8 @@ def hardware_thread(end_event, hw_queue) -> None: all_comp_temp = all_temp_filter.update(max(temp_sources)) msg.deviceState.maxTempC = all_comp_temp - msg.deviceState.fanSpeedPercentDesired = fan_controller.update(all_comp_temp, onroad_conditions["ignition"]) + msg.deviceState.fanSpeedPercentDesired = fan_controller.update(all_comp_temp, onroad_conditions["ignition"], + params.get_bool("AggressiveCoolingEnabled")) is_offroad_for_5_min = (started_ts is None) and ((not started_seen) or (off_ts is None) or (time.monotonic() - off_ts > 60 * 5)) if is_offroad_for_5_min and offroad_comp_temp > OFFROAD_DANGER_TEMP: