diff --git a/CLAUDE.md b/CLAUDE.md index 0dabec2..cf98ec2 100644 --- a/CLAUDE.md +++ b/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: diff --git a/selfdrive/clearpilot/dashcamd b/selfdrive/clearpilot/dashcamd index 9a756bc..be3cc3a 100755 Binary files a/selfdrive/clearpilot/dashcamd and b/selfdrive/clearpilot/dashcamd differ diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index d86108a..fbcad91 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -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'] diff --git a/selfdrive/frogpilot/controls/frogpilot_planner.py b/selfdrive/frogpilot/controls/frogpilot_planner.py index 78bf726..a04c4bf 100755 --- a/selfdrive/frogpilot/controls/frogpilot_planner.py +++ b/selfdrive/frogpilot/controls/frogpilot_planner.py @@ -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) diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index 6e154bf..3347861 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -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__": diff --git a/selfdrive/locationd/paramsd.py b/selfdrive/locationd/paramsd.py index 0cfae89..7b57e71 100755 --- a/selfdrive/locationd/paramsd.py +++ b/selfdrive/locationd/paramsd.py @@ -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 = { diff --git a/selfdrive/locationd/torqued.py b/selfdrive/locationd/torqued.py index 06f9044..98d0b29 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -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: diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index 579e790..12addef 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -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, diff --git a/selfdrive/thermald/power_monitoring.py b/selfdrive/thermald/power_monitoring.py index eb83e6f..3163ac2 100755 --- a/selfdrive/thermald/power_monitoring.py +++ b/selfdrive/thermald/power_monitoring.py @@ -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 diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 1f1910b..8c786f2 100755 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -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(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); diff --git a/system/clearpilot/gpsd.py b/system/clearpilot/gpsd.py index cf4f402..e74a9a1 100644 --- a/system/clearpilot/gpsd.py +++ b/system/clearpilot/gpsd.py @@ -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: