diag: per-publisher valid=False logging; 30min shutdown; daylight fix; UI tweaks
CLAUDE.md: added a "Logging" rule — never use cloudlog (upstream cloud
pipeline, effectively a black hole for us), always use
print(..., file=sys.stderr, flush=True). Manager redirects each process's
stderr to /data/log2/current/{proc}.log. Prefix our lines with "CLP ".
Diagnostic logging — when a publisher sets its own msg.valid=False, log
which specific subscriber tripped the check. Only fires on transition
(True→False) so we don't spam. Covers the services whose cascades we've
been chasing:
- frogpilot_planner (frogpilotPlan)
- longitudinal_planner (longitudinalPlan)
- paramsd (liveParameters)
- calibrationd (liveCalibration)
- torqued (liveTorqueParameters)
- dmonitoringd (driverMonitoringState)
gpsd.is_daylight: fixed a day-boundary bug where the function would flip
to "night" at UTC midnight regardless of actual local sunset. At 85W
sunset is ~00:20 UTC next day, so between local 8pm and actual sunset
the function used *tomorrow's* sunrise/sunset and said night. Now checks
yesterday/today/tomorrow windows with UTC-day offsets.
ui/onroad.cc: nightrider tire-path outline is now light blue (#99CCFF)
at 3px (was white/status-tinted at 6px); lane lines 5% thinner (float
pen width).
thermald/power_monitoring: auto-shutdown timer 10min → 30min.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
14
CLAUDE.md
14
CLAUDE.md
@@ -37,6 +37,20 @@ This is self-driving software. All changes must be deliberate and well-understoo
|
||||
- **Test everything thoroughly** — Brian must always be in the loop
|
||||
- **Do not refactor, clean up, or "improve" code beyond the specific request**
|
||||
|
||||
### Logging
|
||||
|
||||
**NEVER use `cloudlog`.** It's comma.ai's cloud telemetry pipeline, not ours — writes go to a publisher that's effectively a black hole for us (and the only thing it could do if ever reachable is bother the upstream FrogPilot developer). Our changes must always use **file logging** instead.
|
||||
|
||||
Use `print(..., file=sys.stderr, flush=True)`. `manager.py` redirects each managed process's stderr to `/data/log2/current/{process}.log`, so these lines land in the per-process log we already grep. Prefix custom log lines with `CLP ` so they're easy to filter out from upstream noise.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import sys
|
||||
print(f"CLP frogpilotPlan valid=False: carState_freq_ok={sm.freq_ok['carState']}", file=sys.stderr, flush=True)
|
||||
```
|
||||
|
||||
Do not use `cloudlog.warning`, `cloudlog.info`, `cloudlog.error`, `cloudlog.event`, or `cloudlog.exception` in any CLEARPILOT-added code. Existing upstream/FrogPilot `cloudlog` calls can stay untouched.
|
||||
|
||||
### File Ownership
|
||||
|
||||
We operate as `root` on this device, but openpilot runs as the `comma` user (uid=1000, gid=1000). After any code changes that touch multiple files or before testing:
|
||||
|
||||
Binary file not shown.
@@ -145,6 +145,7 @@ class LongitudinalPlanner:
|
||||
|
||||
# CLEARPILOT: track model FPS for dynamic dt adjustment
|
||||
self.model_fps = 20
|
||||
self._dbg_prev_valid = True # CLEARPILOT: diagnostic logging on valid transitions
|
||||
|
||||
self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS
|
||||
|
||||
@@ -277,7 +278,18 @@ class LongitudinalPlanner:
|
||||
def publish(self, sm, pm):
|
||||
plan_send = messaging.new_message('longitudinalPlan')
|
||||
|
||||
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState'])
|
||||
valid = sm.all_checks(service_list=['carState', 'controlsState'])
|
||||
# CLEARPILOT: log on transition into invalid — stderr goes to our plannerd.log
|
||||
if valid != self._dbg_prev_valid and not valid:
|
||||
import sys
|
||||
print(
|
||||
"CLP longitudinalPlan valid=False: "
|
||||
f"carState(a={sm.alive['carState']},v={sm.valid['carState']},f={sm.freq_ok['carState']}) "
|
||||
f"controlsState(a={sm.alive['controlsState']},v={sm.valid['controlsState']},f={sm.freq_ok['controlsState']})",
|
||||
file=sys.stderr, flush=True
|
||||
)
|
||||
self._dbg_prev_valid = valid
|
||||
plan_send.valid = valid
|
||||
|
||||
longitudinalPlan = plan_send.longitudinalPlan
|
||||
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
|
||||
|
||||
@@ -58,6 +58,9 @@ class FrogPilotPlanner:
|
||||
self.params = Params()
|
||||
self.params_memory = Params("/dev/shm/params")
|
||||
|
||||
# CLEARPILOT: track valid transitions so we only log when it flips, not every cycle
|
||||
self._dbg_prev_valid = True
|
||||
|
||||
self.cem = ConditionalExperimentalMode()
|
||||
self.lead_one = Lead()
|
||||
self.mtsc = MapTurnSpeedController()
|
||||
@@ -242,7 +245,18 @@ class FrogPilotPlanner:
|
||||
|
||||
def publish(self, sm, pm):
|
||||
frogpilot_plan_send = messaging.new_message('frogpilotPlan')
|
||||
frogpilot_plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState'])
|
||||
valid = sm.all_checks(service_list=['carState', 'controlsState'])
|
||||
# CLEARPILOT: log on transition into invalid — stderr goes to our plannerd.log
|
||||
if valid != self._dbg_prev_valid and not valid:
|
||||
import sys
|
||||
print(
|
||||
"CLP frogpilotPlan valid=False: "
|
||||
f"carState(a={sm.alive['carState']},v={sm.valid['carState']},f={sm.freq_ok['carState']}) "
|
||||
f"controlsState(a={sm.alive['controlsState']},v={sm.valid['controlsState']},f={sm.freq_ok['controlsState']})",
|
||||
file=sys.stderr, flush=True
|
||||
)
|
||||
self._dbg_prev_valid = valid
|
||||
frogpilot_plan_send.valid = valid
|
||||
frogpilotPlan = frogpilot_plan_send.frogpilotPlan
|
||||
|
||||
frogpilotPlan.accelerationJerk = A_CHANGE_COST * (float(self.jerk) if self.lead_one.status else 1)
|
||||
|
||||
@@ -263,6 +263,7 @@ def main() -> NoReturn:
|
||||
sm = messaging.SubMaster(['cameraOdometry', 'carState', 'carParams'], poll='cameraOdometry')
|
||||
|
||||
calibrator = Calibrator(param_put=True)
|
||||
dbg_prev_valid = True # CLEARPILOT: track valid transitions
|
||||
|
||||
while 1:
|
||||
timeout = 0 if sm.frame == -1 else 100
|
||||
@@ -284,7 +285,15 @@ def main() -> NoReturn:
|
||||
|
||||
# 4Hz driven by cameraOdometry
|
||||
if sm.frame % 5 == 0:
|
||||
calibrator.send_data(pm, sm.all_checks())
|
||||
cal_valid = sm.all_checks()
|
||||
# CLEARPILOT: log per-sub detail on transition to invalid — goes to calibrationd.log
|
||||
if cal_valid != dbg_prev_valid and not cal_valid:
|
||||
import sys
|
||||
bad = [s for s in sm.alive if not (sm.alive[s] and sm.valid[s] and sm.freq_ok.get(s, True))]
|
||||
details = [f"{s}(a={sm.alive[s]},v={sm.valid[s]},f={sm.freq_ok[s]})" for s in bad]
|
||||
print(f"CLP liveCalibration valid=False: {' '.join(details)}", file=sys.stderr, flush=True)
|
||||
dbg_prev_valid = cal_valid
|
||||
calibrator.send_data(pm, cal_valid)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -187,6 +187,7 @@ def main():
|
||||
avg_offset_valid = True
|
||||
total_offset_valid = True
|
||||
roll_valid = True
|
||||
dbg_prev_valid = True # CLEARPILOT: track valid transitions
|
||||
|
||||
while True:
|
||||
sm.update()
|
||||
@@ -256,7 +257,15 @@ def main():
|
||||
liveParameters.filterState.std = P.tolist()
|
||||
liveParameters.filterState.valid = True
|
||||
|
||||
msg.valid = sm.all_checks()
|
||||
lp_valid = sm.all_checks()
|
||||
# CLEARPILOT: on transition to invalid, log per-subscriber state to paramsd.log
|
||||
if lp_valid != dbg_prev_valid and not lp_valid:
|
||||
import sys
|
||||
bad = [s for s in sm.alive if not (sm.alive[s] and sm.valid[s] and sm.freq_ok.get(s, True))]
|
||||
details = [f"{s}(a={sm.alive[s]},v={sm.valid[s]},f={sm.freq_ok[s]})" for s in bad]
|
||||
print(f"CLP liveParameters valid=False: {' '.join(details)}", file=sys.stderr, flush=True)
|
||||
dbg_prev_valid = lp_valid
|
||||
msg.valid = lp_valid
|
||||
|
||||
if sm.frame % 1200 == 0: # once a minute
|
||||
params = {
|
||||
|
||||
@@ -226,6 +226,8 @@ def main(demo=False):
|
||||
with car.CarParams.from_bytes(params.get("CarParams", block=True)) as CP:
|
||||
estimator = TorqueEstimator(CP)
|
||||
|
||||
dbg_prev_valid = True # CLEARPILOT: track valid transitions
|
||||
|
||||
while True:
|
||||
sm.update()
|
||||
if sm.all_checks():
|
||||
@@ -236,7 +238,15 @@ def main(demo=False):
|
||||
|
||||
# 4Hz driven by liveLocationKalman
|
||||
if sm.frame % 5 == 0:
|
||||
pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks()))
|
||||
tq_valid = sm.all_checks()
|
||||
# CLEARPILOT: log per-sub detail on transition to invalid — goes to torqued.log
|
||||
if tq_valid != dbg_prev_valid and not tq_valid:
|
||||
import sys
|
||||
bad = [s for s in sm.alive if not (sm.alive[s] and sm.valid[s] and sm.freq_ok.get(s, True))]
|
||||
details = [f"{s}(a={sm.alive[s]},v={sm.valid[s]},f={sm.freq_ok[s]})" for s in bad]
|
||||
print(f"CLP liveTorqueParameters valid=False: {' '.join(details)}", file=sys.stderr, flush=True)
|
||||
dbg_prev_valid = tq_valid
|
||||
pm.send('liveTorqueParameters', estimator.get_msg(valid=tq_valid))
|
||||
|
||||
# Cache points every 60 seconds while onroad
|
||||
if sm.frame % 240 == 0:
|
||||
|
||||
@@ -21,6 +21,7 @@ def dmonitoringd_thread():
|
||||
|
||||
v_cruise_last = 0
|
||||
driver_engaged = False
|
||||
dbg_prev_valid = True # CLEARPILOT: track valid transitions
|
||||
|
||||
# 10Hz <- dmonitoringmodeld
|
||||
while True:
|
||||
@@ -54,8 +55,16 @@ def dmonitoringd_thread():
|
||||
# Update events from driver state
|
||||
driver_status.update_events(events, driver_engaged, sm['controlsState'].enabled, sm['carState'].standstill)
|
||||
|
||||
# CLEARPILOT: log on transition to invalid so we can see which sub caused the cascade
|
||||
dm_valid = sm.all_checks()
|
||||
if dm_valid != dbg_prev_valid and not dm_valid:
|
||||
import sys
|
||||
bad = [s for s in sm.alive if not (sm.alive[s] and sm.valid[s] and sm.freq_ok.get(s, True))]
|
||||
details = [f"{s}(a={sm.alive[s]},v={sm.valid[s]},f={sm.freq_ok[s]})" for s in bad]
|
||||
print(f"CLP driverMonitoringState valid=False: {' '.join(details)}", file=sys.stderr, flush=True)
|
||||
dbg_prev_valid = dm_valid
|
||||
# build driverMonitoringState packet
|
||||
dat = messaging.new_message('driverMonitoringState', valid=sm.all_checks())
|
||||
dat = messaging.new_message('driverMonitoringState', valid=dm_valid)
|
||||
dat.driverMonitoringState = {
|
||||
"events": events.to_msg(),
|
||||
"faceDetected": driver_status.face_detected,
|
||||
|
||||
@@ -36,8 +36,8 @@ class PowerMonitoring:
|
||||
# Reset capacity if it's low
|
||||
self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), int(car_battery_capacity_uWh))
|
||||
|
||||
# CLEARPILOT: hardcoded 10 minute shutdown timer (not user-configurable)
|
||||
self.device_shutdown_time = 600
|
||||
# CLEARPILOT: hardcoded 30 minute shutdown timer (not user-configurable)
|
||||
self.device_shutdown_time = 1800
|
||||
self.low_voltage_shutdown = VBATT_PAUSE_CHARGING
|
||||
|
||||
# Calculation tick
|
||||
|
||||
@@ -858,12 +858,14 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
|
||||
|
||||
// CLEARPILOT: nightrider lines are 1px wider (3 instead of 2)
|
||||
int outlineWidth = outlineOnly ? 3 : 2;
|
||||
// CLEARPILOT: lane lines 5% thinner than the generic outline (QPen accepts float width)
|
||||
float laneLineWidth = outlineOnly ? (float)outlineWidth * 0.95f : (float)outlineWidth;
|
||||
|
||||
// lanelines
|
||||
for (int i = 0; i < std::size(scene.lane_line_vertices); ++i) {
|
||||
QColor lineColor = QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp<float>(scene.lane_line_probs[i], 0.0, 0.7));
|
||||
if (outlineOnly) {
|
||||
painter.setPen(QPen(lineColor, outlineWidth));
|
||||
painter.setPen(QPen(lineColor, laneLineWidth));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
} else {
|
||||
painter.setPen(Qt::NoPen);
|
||||
@@ -952,9 +954,11 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
|
||||
}
|
||||
|
||||
if (outlineOnly) {
|
||||
// CLEARPILOT: center path (tire track) is 2x wider than other lines in nightrider
|
||||
painter.setPen(QPen(QColor(center_lane_color.red(), center_lane_color.green(),
|
||||
center_lane_color.blue(), 180), outlineWidth * 2));
|
||||
// CLEARPILOT: in nightrider, the tire path outline is light blue at 3px.
|
||||
// Uses a fixed light-blue instead of center_lane_color (which is status-tinted) so
|
||||
// the path reads as a neutral guide, not as engagement/status feedback.
|
||||
QColor lightBlue(153, 204, 255, 220); // #99CCFF light blue, mostly opaque
|
||||
painter.setPen(QPen(lightBlue, 3));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
} else {
|
||||
painter.setPen(Qt::NoPen);
|
||||
|
||||
@@ -20,17 +20,16 @@ from openpilot.common.time import system_time_valid
|
||||
from openpilot.system.hardware.tici.pins import GPIO
|
||||
|
||||
|
||||
def is_daylight(lat: float, lon: float, utc_dt: datetime.datetime) -> bool:
|
||||
"""Return True if it's between sunrise and sunset at the given location.
|
||||
Uses NOAA simplified solar position equations. Pure math, no external libs."""
|
||||
def _sunrise_sunset_min(lat: float, lon: float, utc_dt: datetime.datetime):
|
||||
"""Compute (sunrise_min, sunset_min) in UTC minutes since midnight of utc_dt's day.
|
||||
Values can be negative or >1440 for western/eastern longitudes. Returns
|
||||
(None, None) for polar night, ('always', 'always') for midnight sun."""
|
||||
n = utc_dt.timetuple().tm_yday
|
||||
gamma = 2 * math.pi / 365 * (n - 1 + (utc_dt.hour - 12) / 24)
|
||||
# Equation of time (minutes)
|
||||
eqtime = 229.18 * (0.000075 + 0.001868 * math.cos(gamma)
|
||||
- 0.032077 * math.sin(gamma)
|
||||
- 0.014615 * math.cos(2 * gamma)
|
||||
- 0.040849 * math.sin(2 * gamma))
|
||||
# Solar declination (radians)
|
||||
decl = (0.006918 - 0.399912 * math.cos(gamma)
|
||||
+ 0.070257 * math.sin(gamma)
|
||||
- 0.006758 * math.cos(2 * gamma)
|
||||
@@ -42,14 +41,41 @@ def is_daylight(lat: float, lon: float, utc_dt: datetime.datetime) -> bool:
|
||||
cos_ha = (math.cos(zenith) / (math.cos(lat_rad) * math.cos(decl))
|
||||
- math.tan(lat_rad) * math.tan(decl))
|
||||
if cos_ha < -1:
|
||||
return True # midnight sun
|
||||
return ('always', 'always') # midnight sun
|
||||
if cos_ha > 1:
|
||||
return False # polar night
|
||||
return (None, None) # polar night
|
||||
ha = math.degrees(math.acos(cos_ha))
|
||||
sunrise_min = 720 - 4 * (lon + ha) - eqtime
|
||||
sunset_min = 720 - 4 * (lon - ha) - eqtime
|
||||
return (sunrise_min, sunset_min)
|
||||
|
||||
|
||||
def is_daylight(lat: float, lon: float, utc_dt: datetime.datetime) -> bool:
|
||||
"""Return True if the sun is currently above the horizon at (lat, lon).
|
||||
|
||||
Handles west-of-Greenwich correctly: at UTC midnight it may still be
|
||||
evening local time, and the relevant sunset is the PREVIOUS UTC day's
|
||||
value (which is >1440 min if we re-ref to that day, i.e. it's past
|
||||
midnight UTC). Symmetric case for east-of-Greenwich at the other end.
|
||||
|
||||
Strategy: compute sunrise/sunset for yesterday, today, and tomorrow (each
|
||||
relative to its own UTC midnight), shift them all onto today's clock
|
||||
(yesterday = -1440, tomorrow = +1440), and check if now_min falls inside
|
||||
any of the three [sunrise, sunset] intervals.
|
||||
"""
|
||||
now_min = utc_dt.hour * 60 + utc_dt.minute + utc_dt.second / 60
|
||||
return sunrise_min <= now_min <= sunset_min
|
||||
for day_offset in (-1, 0, 1):
|
||||
d = utc_dt + datetime.timedelta(days=day_offset)
|
||||
sr, ss = _sunrise_sunset_min(lat, lon, d)
|
||||
if sr == 'always':
|
||||
return True
|
||||
if sr is None:
|
||||
continue # polar night this day
|
||||
sr += day_offset * 1440
|
||||
ss += day_offset * 1440
|
||||
if sr <= now_min <= ss:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def at_cmd(cmd: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user