Skip to content

hardwared: handle failed thermal readings without losing thermal state - #38529

Closed
JPL11 wants to merge 1 commit into
commaai:masterfrom
JPL11:thermal-readings-policy
Closed

hardwared: handle failed thermal readings without losing thermal state#38529
JPL11 wants to merge 1 commit into
commaai:masterfrom
JPL11:thermal-readings-policy

Conversation

@JPL11

@JPL11 JPL11 commented Aug 4, 2026

Copy link
Copy Markdown

Fixes #36690. Follow-up to #38424, which was closed asking the right question — what should the system do when there are no valid thermal readings. This PR implements a complete answer rather than just the crash fix.

Returning NaN alone turns out to be worse than the crash, because none of the consumers handle it:

  • python's builtin max() with NaN is order-dependent (max(nan, 5) is nan, max(5, nan) is 5), so a failed sensor is sometimes masked, sometimes not
  • a NaN reaching FirstOrderFilter.update() poisons the filter state permanently — it never recovers even after the sensor does
  • with all_comp_temp = nan both band-transition comparisons are False, so thermal_status freezes at its last value and OFFROAD_DANGER_TEMP can never trip

So:

  • ThermalZone.read() returns NaN on any read failure (the EIO in the issue, garbage values, missing zones) instead of crashing or returning a fake 0
  • hardwared aggregates over valid readings only and never feeds the filters NaN; the filters recover naturally when a sensor comes back
  • staleness policy: with zero valid readings for 10s, device_temp_engageable goes False (if we can't verify the device is cool, don't start a drive — same conservative direction as the existing OFFROAD_DANGER_TEMP logic). While already onroad, stale readings hold the last band and log an error rather than forcing a disengagement over a sensor failure. If you'd rather escalate to critical onroad (forcing offroad at the next opportunity), that's a one-line change — happy to flip it, this seemed like your call.

Platforms without thermal sensors configured (PC) keep today's behavior: empty sensor lists still read as 0.0 and are never treated as stale.

Tests cover the EIO case from the issue, garbage reads, missing zones, the NaN-order-independence of the new aggregation, and that configured-but-failing sensors propagate instead of reading as 0.

Copilot AI lite review requested due to automatic review settings August 4, 2026 16:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves thermal sensor fault-tolerance by propagating failed thermal reads as NaN, preventing NaN from poisoning hardwared’s thermal aggregation/filters, and adding an explicit “stale thermal readings” policy that blocks starting a drive when no valid readings exist for a timeout window.

Changes:

  • Update ThermalZone.read() to return NaN on read/discovery failures (EIO/garbage/missing zone) instead of crashing or returning 0.
  • Add max_valid_temp() plus NaN-safe aggregation and a 10s “stale readings” gating policy in hardwared.
  • Add unit tests for ThermalZone read failures and max_valid_temp() behavior.

Reviewed changes

Copilot reviewed 3 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
uv_sync.log Adds a captured uv sync/build log output.
scons.log Adds a captured scons build log output.
scons2.log Adds a second captured scons build log output.
openpilot/common/hardware/base.py Makes thermal zone reads return NaN on failures (rather than 0/crash).
openpilot/system/hardware/hardwared.py Aggregates temps over valid readings only, avoids feeding NaN into filters, and adds stale-reading gating.
openpilot/system/hardware/tests/test_thermal_readings.py Adds tests for the new failure semantics and NaN-safe max aggregation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +65 to +69
def max_valid_temp(temps) -> float:
# empty means the platform doesn't have these sensors; all-NaN means the
# reads are failing and the failure must propagate, not read as 0
if len(temps) == 0:
return 0.

from openpilot.common.test import OpenpilotTestCase
from openpilot.common.hardware.base import ThermalZone
from openpilot.system.hardware.hardwared import max_valid_temp
Comment on lines +20 to 30
if self.zone_number < 0:
for n in os.listdir("/sys/devices/virtual/thermal"):
if not n.startswith("thermal_zone"):
continue
with open(os.path.join("/sys/devices/virtual/thermal", n, "type")) as f:
if f.read().strip() == self.name:
self.zone_number = int(n.removeprefix("thermal_zone"))
break

with open(f"/sys/devices/virtual/thermal/thermal_zone{self.zone_number}/temp") as f:
return int(f.read()) / self.scale
Comment on lines +330 to +334
# must be at an engageable thermal band to go onroad; if we can't verify
# the device is cool (prolonged sensor failure), don't start a drive.
# while already onroad, stale readings hold the last band and log instead
# of forcing a disengagement over a sensor failure.
startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.overheated and not thermal_readings_stale
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Process replay diff report

Replays driving segments through this PR and compares the behavior to master.
Please review any changes carefully to ensure they are expected.

✅ 0 changed, 66 passed, 0 errors

@JPL11
JPL11 force-pushed the thermal-readings-policy branch from 9bacb93 to 325f332 Compare August 4, 2026 17:05
Sensor reads can fail transiently (e.g. EIO when the spmi transaction
fails), which currently crashes hardwared. Return NaN from
ThermalZone.read on any read failure, aggregate over valid readings only
(builtin max() with NaN is order-dependent, and NaN would poison the
temperature filters permanently and freeze the thermal band), and track
staleness: with no valid reading for 10s, block going onroad; while
already onroad, hold the last band and log rather than force a
disengagement over a sensor failure.
@JPL11

JPL11 commented Aug 5, 2026

Copy link
Copy Markdown
Author

CI failures are the known missing headless raylib deps on the PR runners (#38435 / #38514) — unit tests import libGLESv2 and fail before reaching any of this PR's code. Will rebase once a fix lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hardwared: gracefully handle no thermal readings

3 participants