#!/usr/bin/env python3 # simple boardd wrapper that updates the panda first import os import usb1 import time import json import subprocess from typing import List, NoReturn from functools import cmp_to_key from panda import Panda, PandaDFU, PandaProtocolMismatch, FW_PATH from common.basedir import BASEDIR from common.params import Params from selfdrive.boardd.set_time import set_time from system.hardware import HARDWARE from system.swaglog import cloudlog def get_expected_signature(panda: Panda) -> bytes: try: fn = os.path.join(FW_PATH, panda.get_mcu_type().config.app_fn) return Panda.get_signature_from_firmware(fn) except Exception: cloudlog.exception("Error computing expected signature") return b"" def read_panda_logs(panda: Panda) -> None: """ Forward panda logs to the cloud """ params = Params() serial = panda.get_usb_serial() log_state = {} try: l = json.loads(params.get("PandaLogState")) for k, v in l.items(): if isinstance(k, str) and isinstance(v, int): log_state[k] = v except (TypeError, json.JSONDecodeError): cloudlog.exception("failed to parse PandaLogState") try: if serial in log_state: logs = panda.get_logs(last_id=log_state[serial]) else: logs = panda.get_logs(get_all=True) # truncate logs to 100 entries if needed MAX_LOGS = 100 if len(logs) > MAX_LOGS: cloudlog.warning(f"Panda {serial} has {len(logs)} logs, truncating to {MAX_LOGS}") logs = logs[-MAX_LOGS:] # update log state if len(logs) > 0: log_state[serial] = logs[-1]["id"] for log in logs: if log['timestamp'] is not None: log['timestamp'] = log['timestamp'].isoformat() cloudlog.event("panda_log", **log, serial=serial) params.put("PandaLogState", json.dumps(log_state)) except Exception: cloudlog.exception(f"Error getting logs for panda {serial}") def flash_panda(panda_serial: str) -> Panda: try: panda = Panda(panda_serial) except PandaProtocolMismatch: cloudlog.warning("detected protocol mismatch, reflashing panda") HARDWARE.recover_internal_panda() raise fw_signature = get_expected_signature(panda) internal_panda = panda.is_internal() panda_version = "bootstub" if panda.bootstub else panda.get_version() panda_signature = b"" if panda.bootstub else panda.get_signature() cloudlog.warning(f"Panda {panda_serial} connected, version: {panda_version}, signature {panda_signature.hex()[:16]}, expected {fw_signature.hex()[:16]}") if panda.bootstub or panda_signature != fw_signature: cloudlog.info("Panda firmware out of date, update required") panda.flash() cloudlog.info("Done flashing") if panda.bootstub: bootstub_version = panda.get_version() cloudlog.info(f"Flashed firmware not booting, flashing development bootloader. {bootstub_version=}, {internal_panda=}") if internal_panda: HARDWARE.recover_internal_panda() panda.recover(reset=(not internal_panda)) cloudlog.info("Done flashing bootstub") if panda.bootstub: cloudlog.info("Panda still not booting, exiting") raise AssertionError panda_signature = panda.get_signature() if panda_signature != fw_signature: cloudlog.info("Version mismatch after flashing, exiting") raise AssertionError return panda def panda_sort_cmp(a: Panda, b: Panda): a_type = a.get_type() b_type = b.get_type() # make sure the internal one is always first if a.is_internal() and not b.is_internal(): return -1 if not a.is_internal() and b.is_internal(): return 1 # sort by hardware type if a_type != b_type: return a_type < b_type # last resort: sort by serial number return a.get_usb_serial() < b.get_usb_serial() def main() -> NoReturn: count = 0 first_run = True params = Params() while True: try: count += 1 cloudlog.event("pandad.flash_and_connect", count=count) params.remove("PandaSignatures") # Flash all Pandas in DFU mode dfu_serials = PandaDFU.list() if len(dfu_serials) > 0: for serial in dfu_serials: cloudlog.info(f"Panda in DFU mode found, flashing recovery {serial}") PandaDFU(serial).recover() time.sleep(1) panda_serials = Panda.list() if len(panda_serials) == 0: if first_run: cloudlog.info("No pandas found, resetting internal panda") HARDWARE.reset_internal_panda() time.sleep(2) # wait to come back up continue cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}") # Flash pandas pandas: List[Panda] = [] for serial in panda_serials: pandas.append(flash_panda(serial)) # Ensure internal panda is present if expected internal_pandas = [panda for panda in pandas if panda.is_internal()] if HARDWARE.has_internal_panda() and len(internal_pandas) == 0: cloudlog.error("Internal panda is missing, resetting") HARDWARE.reset_internal_panda() time.sleep(2) # wait to come back up continue # sort pandas to have deterministic order pandas.sort(key=cmp_to_key(panda_sort_cmp)) panda_serials = list(map(lambda p: p.get_usb_serial(), pandas)) # log panda fw versions params.put("PandaSignatures", b','.join(p.get_signature() for p in pandas)) for panda in pandas: # check health for lost heartbeat health = panda.health() if health["heartbeat_lost"]: params.put_bool("PandaHeartbeatLost", True) cloudlog.event("heartbeat lost", deviceState=health, serial=panda.get_usb_serial()) read_panda_logs(panda) if first_run: if panda.is_internal(): # update time from RTC set_time(cloudlog) # reset panda to ensure we're in a good state cloudlog.info(f"Resetting panda {panda.get_usb_serial()}") if panda.is_internal(): HARDWARE.reset_internal_panda() else: panda.reset(reconnect=False) for p in pandas: p.close() # TODO: wrap all panda exceptions in a base panda exception except (usb1.USBErrorNoDevice, usb1.USBErrorPipe): # a panda was disconnected while setting everything up. let's try again cloudlog.exception("Panda USB exception while setting up") continue except PandaProtocolMismatch: cloudlog.exception("pandad.protocol_mismatch") continue except Exception: cloudlog.exception("pandad.uncaught_exception") continue first_run = False # run boardd with all connected serials as arguments os.environ['MANAGER_DAEMON'] = 'boardd' os.chdir(os.path.join(BASEDIR, "selfdrive/boardd")) subprocess.run(["./boardd", *panda_serials], check=True) if __name__ == "__main__": main()