Files
StarPilot/system/timed.py
T
2026-09-19 20:58:05 -05:00

186 lines
5.7 KiB
Python

#!/usr/bin/env python3
import datetime
import os
import subprocess
import time
from typing import NoReturn
import cereal.messaging as messaging
from openpilot.common.time_helpers import min_date, MAX_DATE, system_time_valid
from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params
from openpilot.common.gps import get_gps_location_service
from openpilot.system.hardware import AGNOS
try:
from timezonefinder import TimezoneFinder
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
if abs(diff) < datetime.timedelta(seconds=10):
cloudlog.debug(f"Time diff too small: {diff}")
return
cloudlog.debug(f"Setting time to {new_time}")
try:
subprocess.run(f"TZ=UTC date -s '{new_time}'", shell=True, check=True)
except subprocess.CalledProcessError:
cloudlog.exception("timed.failed_setting_time")
# StarPilot variables
def set_timezone(timezone):
valid_timezones = subprocess.check_output("timedatectl list-timezones", shell=True, encoding="utf8").strip().split("\n")
if timezone not in valid_timezones:
cloudlog.error(f"Timezone not supported {timezone}")
return
cloudlog.debug(f"Setting timezone to {timezone}")
try:
if AGNOS:
tzpath = os.path.join("/usr/share/zoneinfo/", timezone)
subprocess.check_call(f"sudo su -c 'ln -snf {tzpath} /data/etc/tmptime && mv /data/etc/tmptime /data/etc/localtime'", shell=True)
subprocess.check_call(f"sudo su -c 'echo \'{timezone}\' > /data/etc/timezone'", shell=True)
else:
subprocess.check_call(f"sudo timedatectl set-timezone {timezone}", shell=True)
except subprocess.CalledProcessError:
cloudlog.exception(f"Error setting timezone to {timezone}")
def main() -> NoReturn:
"""
timed has two responsibilities:
- getting the current time from GPS
- publishing the time in the logs
AGNOS will also use NTP to update the time.
"""
params = Params()
gps_location_service = get_gps_location_service(params)
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:
set_timezone(last_timezone)
while True:
sm.update(1000)
msg = messaging.new_message('clocks')
msg.valid = system_time_valid()
msg.clocks.wallTimeNanos = time.time_ns()
pm.send('clocks', msg)
gps = sm[gps_location_service]
gps_time = datetime.datetime.fromtimestamp(gps.unixTimestampMillis / 1000., datetime.UTC).replace(tzinfo=None)
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_usable:
continue
set_time(gps_time)
# StarPilot variables
if tf is not None:
timezone = tf.timezone_at(lng=gps.longitude, lat=gps.latitude)
if timezone is not None and timezone != last_timezone:
set_timezone(timezone)
params.put_nonblocking("Timezone", timezone)
last_timezone = timezone
elif not timezonefinder_logged:
cloudlog.warning("TimezoneFinder unavailable, skipping automatic timezone updates")
timezonefinder_logged = True
time.sleep(10)
if __name__ == "__main__":
main()