From ee87261cd5d29ff11648f32c3dda7d85b653893e Mon Sep 17 00:00:00 2001 From: whoisdomi Date: Sat, 19 Sep 2026 15:16:01 -0500 Subject: [PATCH] Car Date/Time fallback when gps/wifi not available --- system/timed.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/system/timed.py b/system/timed.py index 64fd592524..9ed5176283 100644 --- a/system/timed.py +++ b/system/timed.py @@ -17,6 +17,67 @@ try: except Exception: TimezoneFinder = None +try: + from zoneinfo import ZoneInfo +except Exception: + ZoneInfo = None + +# StarPilot variables +# Hyundai/Kia cluster wall clock, local time. Fallback when GPS has no fix. +CAR_CLOCK_ADDR = 0x4F0 +CAR_CLOCK_BUS = 1 + + +def parse_car_clock(dat: bytes): + # cluster sends all 0xFF until initialized + if len(dat) < 7: + return None + if all(b == 0xFF for b in dat) or all(b == 0 for b in dat): + return None + + hour, minute, second = dat[1], dat[2], dat[3] + month = (dat[4] >> 2) & 0x0F + year = 2000 + dat[5] + day = dat[6] + + try: + return datetime.datetime(year, month, day, hour, minute, second) + except ValueError: + return None + + +def read_car_clock(can_sock, timezone): + latest = None + for msg in messaging.drain_sock(can_sock): + for frame in msg.can: + if frame.address == CAR_CLOCK_ADDR and frame.src == CAR_CLOCK_BUS: + parsed = parse_car_clock(bytes(frame.dat)) + if parsed is not None: + latest = parsed + + if latest is None: + return None + + utc = car_clock_to_utc(latest, timezone) + if utc is None or not (min_date() <= utc <= MAX_DATE): + return None + return utc + + +def car_clock_to_utc(local_dt, timezone): + # timezone comes from the last GPS fix, so this needs one to have happened once + if ZoneInfo is None or not timezone: + return None + if isinstance(timezone, bytes): + timezone = timezone.decode("utf-8", errors="replace") + try: + tz = ZoneInfo(timezone) + except Exception: + cloudlog.exception("timed.bad_timezone") + return None + # fold=0: on the duplicated hour of a DST fall back, assume the first pass + return local_dt.replace(tzinfo=tz, fold=0).astimezone(datetime.UTC).replace(tzinfo=None) + def set_time(new_time): diff = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - new_time @@ -64,10 +125,13 @@ def main() -> NoReturn: pm = messaging.PubMaster(['clocks']) sm = messaging.SubMaster([gps_location_service]) + # StarPilot variables + can_sock = messaging.sub_sock('can', timeout=100) # StarPilot variables tf = TimezoneFinder() if TimezoneFinder is not None else None timezonefinder_logged = False + car_clock_tz_logged = False last_timezone = params.get("Timezone") if last_timezone is not None: @@ -83,11 +147,23 @@ def main() -> NoReturn: gps = sm[gps_location_service] gps_time = datetime.datetime.fromtimestamp(gps.unixTimestampMillis / 1000., datetime.UTC).replace(tzinfo=None) - if not sm.updated[gps_location_service] or (time.monotonic() - sm.logMonoTime[gps_location_service] / 1e9) > 2.0: + gps_usable = (sm.updated[gps_location_service] and + (time.monotonic() - sm.logMonoTime[gps_location_service] / 1e9) <= 2.0 and + gps.hasFix and min_date() <= gps_time <= MAX_DATE) + + # StarPilot variables + # only corrects an invalid clock, so GPS and NTP always win + if not gps_usable and not system_time_valid(): + if not last_timezone and not car_clock_tz_logged: + cloudlog.warning("timed: no saved timezone, cannot use car clock fallback") + car_clock_tz_logged = True + car_time = read_car_clock(can_sock, last_timezone) + if car_time is not None: + cloudlog.warning(f"timed: setting time from car clock: {car_time}") + set_time(car_time) continue - if not gps.hasFix: - continue - if gps_time < min_date() or gps_time > MAX_DATE: + + if not gps_usable: continue set_time(gps_time)