From 350b0740a718b648dd07c28a6edd1602458509a9 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 16 Jun 2026 17:58:00 -0700 Subject: [PATCH 1/4] usb logging --- cereal/log.capnp | 9 +++++ cereal/services.py | 1 + system/hardware/usbd.py | 58 ++++++++++++++++++++++++++++++++ system/manager/process_config.py | 1 + 4 files changed, 69 insertions(+) create mode 100644 system/hardware/usbd.py diff --git a/cereal/log.capnp b/cereal/log.capnp index 6cf4781228aaa4..f8ad1bfa997baf 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -684,6 +684,14 @@ struct PeripheralState { } } +struct UsbState { + connected @0 :Bool; + speedMbps @1 :UInt16; + pmActive @2 :Bool; + disconnectCount @3 :UInt32; + overCurrentCount @4 :UInt32; +} + struct RadarState @0x9a185389d6fdd05f { mdMonoTime @6 :UInt64; carStateMonoTime @11 :UInt64; @@ -2472,6 +2480,7 @@ struct Event { temperatureSensor @97 :SensorEventData; pandaStates @81 :List(PandaState); peripheralState @80 :PeripheralState; + usbState @152 :UsbState; radarState @13 :RadarState; liveTracks @131 :Car.RadarData; sendcan @17 :List(CanData); diff --git a/cereal/services.py b/cereal/services.py index c2d38d852db133..36a196c9d59784 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -32,6 +32,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] "selfdriveState": (True, 100., 10), "pandaStates": (True, 10., 1), "peripheralState": (True, 2., 1), + "usbState": (True, 10., 1), "radarState": (True, 20., 5), "roadEncodeIdx": (False, 20., 1), "liveTracks": (True, 20.), diff --git a/system/hardware/usbd.py b/system/hardware/usbd.py new file mode 100644 index 00000000000000..88ab8230484a95 --- /dev/null +++ b/system/hardware/usbd.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import os + +from cereal import messaging +from openpilot.common.realtime import Ratekeeper +from openpilot.common.swaglog import cloudlog + +RATE = 10 # Hz +DEVICE = "/sys/bus/usb/devices/4-1" # aux USB +PORT = "/sys/bus/usb/devices/usb4/4-0:1.0/usb4-port1" + + +def read(attr: str) -> str | None: + try: + with open(os.path.join(DEVICE, attr)) as f: + return f.read().strip() + except OSError: + return None + + +def over_current_count() -> int: + try: + with open(os.path.join(PORT, "over_current_count")) as f: + return int(f.read()) + except (OSError, ValueError): + return 0 + + +def main(): + pm = messaging.PubMaster(['usbState']) + rk = Ratekeeper(RATE) + disconnect_count = 0 + was_connected = False + + while True: + speed = read("speed") + connected = speed is not None + if was_connected and not connected: + disconnect_count += 1 + cloudlog.event("usb_disconnected", count=disconnect_count) + elif connected and not was_connected: + cloudlog.event("usb_connected", idVendor=read("idVendor"), idProduct=read("idProduct"), speed=speed) + was_connected = connected + + msg = messaging.new_message('usbState', valid=True) + state = msg.usbState + state.connected = connected + state.speedMbps = int(speed) if (speed and speed.isdigit()) else 0 + state.pmActive = read("power/runtime_status") == "active" + state.disconnectCount = disconnect_count + state.overCurrentCount = over_current_count() + + pm.send('usbState', msg) + rk.keep_time() + + +if __name__ == "__main__": + main() diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 7ed4a2dacb2c3c..4dff442c1619ee 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -106,6 +106,7 @@ def and_(*fns): PythonProcess("lateral_maneuversd", "tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver), PythonProcess("radard", "selfdrive.controls.radard", only_onroad), PythonProcess("hardwared", "system.hardware.hardwared", always_run), + PythonProcess("usbd", "system.hardware.usbd", always_run), PythonProcess("modem", "system.hardware.tici.modem", always_run, enabled=TICI), PythonProcess("tombstoned", "system.tombstoned", always_run, enabled=not PC), PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC), From 3f0a4908094e620e8f5cb0b5f015cc930e166fb7 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 16 Jun 2026 19:02:03 -0700 Subject: [PATCH 2/4] fix test --- selfdrive/test/test_onroad.py | 1 + system/hardware/usbd.py | 0 2 files changed, 1 insertion(+) mode change 100644 => 100755 system/hardware/usbd.py diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 8161dca130c527..175c726007a7ff 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -48,6 +48,7 @@ "selfdrive.modeld.modeld": 22.0, "selfdrive.modeld.dmonitoringmodeld": 18.0, "system.hardware.hardwared": 4.0, + "system.hardware.usbd": 1.0, "selfdrive.locationd.calibrationd": 2.0, "selfdrive.locationd.torqued": 5.0, "selfdrive.locationd.locationd": 25.0, diff --git a/system/hardware/usbd.py b/system/hardware/usbd.py old mode 100644 new mode 100755 From 80e84a9b06dd6b00d38030fa2e8aea4c08ee735a Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 17 Jun 2026 05:35:31 -0700 Subject: [PATCH 3/4] discover by PID --- system/hardware/usbd.py | 43 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/system/hardware/usbd.py b/system/hardware/usbd.py index 88ab8230484a95..f56bf050afb77e 100755 --- a/system/hardware/usbd.py +++ b/system/hardware/usbd.py @@ -1,27 +1,38 @@ #!/usr/bin/env python3 -import os +from pathlib import Path from cereal import messaging from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.modeld.helpers import USBGPU_VID, USBGPU_PID RATE = 10 # Hz -DEVICE = "/sys/bus/usb/devices/4-1" # aux USB -PORT = "/sys/bus/usb/devices/usb4/4-0:1.0/usb4-port1" -def read(attr: str) -> str | None: +def find_device() -> Path | None: + # discover the eGPU by VID/PID + for d in Path("/sys/bus/usb/devices").glob("*"): + try: + if int((d / "idVendor").read_text(), 16) == USBGPU_VID and \ + int((d / "idProduct").read_text(), 16) == USBGPU_PID: + return d + except Exception: + pass + return None + + +def read(device: Path, attr: str) -> str | None: try: - with open(os.path.join(DEVICE, attr)) as f: - return f.read().strip() + return (device / attr).read_text().strip() except OSError: return None -def over_current_count() -> int: +def over_current_count(device: Path) -> int: + # upstream root-hub port, e.g. device "4-1" -> usb4/4-0:1.0/usb4-port1 + bus, _, port = device.name.partition("-") try: - with open(os.path.join(PORT, "over_current_count")) as f: - return int(f.read()) + return int((Path(f"/sys/bus/usb/devices/usb{bus}/{bus}-0:1.0/usb{bus}-port{port}") / "over_current_count").read_text()) except (OSError, ValueError): return 0 @@ -33,22 +44,24 @@ def main(): was_connected = False while True: - speed = read("speed") - connected = speed is not None + device = find_device() + connected = device is not None if was_connected and not connected: disconnect_count += 1 cloudlog.event("usb_disconnected", count=disconnect_count) elif connected and not was_connected: - cloudlog.event("usb_connected", idVendor=read("idVendor"), idProduct=read("idProduct"), speed=speed) + cloudlog.event("usb_connected", speed=read(device, "speed")) was_connected = connected msg = messaging.new_message('usbState', valid=True) state = msg.usbState state.connected = connected - state.speedMbps = int(speed) if (speed and speed.isdigit()) else 0 - state.pmActive = read("power/runtime_status") == "active" + if device is not None: + speed = read(device, "speed") + state.speedMbps = int(speed) if (speed and speed.isdigit()) else 0 + state.pmActive = read(device, "power/runtime_status") == "active" + state.overCurrentCount = over_current_count(device) state.disconnectCount = disconnect_count - state.overCurrentCount = over_current_count() pm.send('usbState', msg) rk.keep_time() From d29a5d9db3ee674edb75a0303f957ce6a49f7132 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 17 Jun 2026 11:39:29 -0700 Subject: [PATCH 4/4] hardwared, add counters --- cereal/log.capnp | 27 +++++++--- cereal/services.py | 1 - selfdrive/test/test_onroad.py | 1 - system/hardware/hardwared.py | 7 +++ system/hardware/usb.py | 90 ++++++++++++++++++++++++++++++++ system/hardware/usbd.py | 71 ------------------------- system/manager/process_config.py | 1 - 7 files changed, 118 insertions(+), 80 deletions(-) create mode 100644 system/hardware/usb.py delete mode 100755 system/hardware/usbd.py diff --git a/cereal/log.capnp b/cereal/log.capnp index f8ad1bfa997baf..37ee2476f47d8b 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -414,6 +414,10 @@ struct CanData { struct DeviceState @0xa4d8b5af2aa492eb { deviceType @45 :InitData.DeviceType; + # usb + chestnutPresent @51 :Bool; + usbState @52 :UsbState; + networkType @22 :NetworkType; networkInfo @31 :NetworkInfo; networkStrength @24 :NetworkStrength; @@ -685,11 +689,23 @@ struct PeripheralState { } struct UsbState { - connected @0 :Bool; - speedMbps @1 :UInt16; - pmActive @2 :Bool; - disconnectCount @3 :UInt32; - overCurrentCount @4 :UInt32; + vbusMv @0 :UInt32; + devices @1 :List(Device); + + struct Device { + busnum @0 :UInt8; + devnum @1 :UInt8; + vendorId @2 :UInt16; + productId @3 :UInt16; + speedMbps @4 :UInt16; + product @5 :Text; + pmActive @6 :Bool; + runtimeSuspendedMs @7 :UInt64; + + # error counters + overCurrentCount @8 :UInt32; + linkErrorCount @9 :UInt32; + } } struct RadarState @0x9a185389d6fdd05f { @@ -2480,7 +2496,6 @@ struct Event { temperatureSensor @97 :SensorEventData; pandaStates @81 :List(PandaState); peripheralState @80 :PeripheralState; - usbState @152 :UsbState; radarState @13 :RadarState; liveTracks @131 :Car.RadarData; sendcan @17 :List(CanData); diff --git a/cereal/services.py b/cereal/services.py index 36a196c9d59784..c2d38d852db133 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -32,7 +32,6 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] "selfdriveState": (True, 100., 10), "pandaStates": (True, 10., 1), "peripheralState": (True, 2., 1), - "usbState": (True, 10., 1), "radarState": (True, 20., 5), "roadEncodeIdx": (False, 20., 1), "liveTracks": (True, 20.), diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 175c726007a7ff..8161dca130c527 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -48,7 +48,6 @@ "selfdrive.modeld.modeld": 22.0, "selfdrive.modeld.dmonitoringmodeld": 18.0, "system.hardware.hardwared": 4.0, - "system.hardware.usbd": 1.0, "selfdrive.locationd.calibrationd": 2.0, "selfdrive.locationd.torqued": 5.0, "selfdrive.locationd.locationd": 25.0, diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index 5db73403e11f98..4c7a9e86a6942a 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -18,6 +18,7 @@ from openpilot.common.realtime import DT_HW from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.system.hardware import HARDWARE, TICI, AGNOS, PC +from openpilot.system.hardware.usb import UsbLogger from openpilot.system.loggerd.config import get_available_percent from openpilot.system.statsd import statlog from openpilot.common.swaglog import cloudlog @@ -153,6 +154,7 @@ def hardware_thread(end_event, hw_queue) -> None: pm = messaging.PubMaster(['deviceState']) sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates"], poll="pandaStates") + usb_logger = UsbLogger() count = 0 onroad_conditions: dict[str, bool] = { @@ -256,6 +258,11 @@ def hardware_thread(end_event, hw_queue) -> None: msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness() + try: + usb_logger.update(msg.deviceState) + except Exception: + cloudlog.exception("usb_logger update failed") + # this subset is only used for offroad temp_sources = [ msg.deviceState.memoryTempC, diff --git a/system/hardware/usb.py b/system/hardware/usb.py new file mode 100644 index 00000000000000..5bd60caa31de3c --- /dev/null +++ b/system/hardware/usb.py @@ -0,0 +1,90 @@ +from pathlib import Path + +from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.modeld.helpers import USBGPU_VID, USBGPU_PID + + +def read(path: Path) -> str | None: + try: + return path.read_text().strip() + except OSError: + return None + + +def read_int(path: Path, base: int = 10) -> int: + s = read(path) + try: + return int(s, base) if s is not None else 0 + except ValueError: + return 0 + + +def usb_devices() -> list[Path]: + # enumerated USB devices + devices = (d for d in Path("/sys/bus/usb/devices").glob("*") if (d / "idVendor").exists()) + return sorted(devices, key=lambda p: p.name) + + +def root_hub_port(device: Path) -> Path: + bus, _, port = device.name.partition("-") + return Path(f"/sys/bus/usb/devices/usb{bus}/{bus}-0:1.0/usb{bus}-port{port}") + + +def controller(device: Path) -> Path | None: + # get SS port registers + for parent in device.resolve().parents: + if parent.name.endswith(".ssusb"): + return parent + return None + + +class UsbLogger: + def __init__(self): + self.prev: set[tuple[int, int]] = set() + + def update(self, device_state) -> None: + devices = usb_devices() + + # low level state + state = device_state.usbState + state.vbusMv = read_int(Path("/sys/class/power_supply/usb/voltage_now")) // 1000 + entries = state.init('devices', len(devices)) + + present: dict[tuple[int, int], Path] = {} + chestnut_present = False + for entry, device in zip(entries, devices, strict=True): + vendor_id = read_int(device / "idVendor", 16) + product_id = read_int(device / "idProduct", 16) + busnum = read_int(device / "busnum") + devnum = read_int(device / "devnum") + present[(busnum, devnum)] = device + + entry.busnum = busnum + entry.devnum = devnum + entry.vendorId = vendor_id + entry.productId = product_id + entry.speedMbps = read_int(device / "speed") + entry.product = read(device / "product") or "" + entry.pmActive = read(device / "power/runtime_status") == "active" + entry.runtimeSuspendedMs = read_int(device / "power/runtime_suspended_time") + entry.overCurrentCount = read_int(root_hub_port(device) / "over_current_count") + + ctrl = controller(device) + if ctrl is not None: + entry.linkErrorCount = read_int(ctrl / "portli", 0) & 0xFFFF # decode PORTLI[15:0] + + if (vendor_id, product_id) == (USBGPU_VID, USBGPU_PID): + chestnut_present = True + + # parse peripherals + device_state.chestnutPresent = chestnut_present + + # connect/disconnect events + for key in present.keys() - self.prev: + device = present[key] + cloudlog.event("usb_connected", busnum=key[0], devnum=key[1], + vid=read(device / "idVendor"), pid=read(device / "idProduct"), + speed=read(device / "speed"), product=read(device / "product")) + for key in self.prev - present.keys(): + cloudlog.event("usb_disconnected", busnum=key[0], devnum=key[1]) + self.prev = set(present) diff --git a/system/hardware/usbd.py b/system/hardware/usbd.py deleted file mode 100755 index f56bf050afb77e..00000000000000 --- a/system/hardware/usbd.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -from pathlib import Path - -from cereal import messaging -from openpilot.common.realtime import Ratekeeper -from openpilot.common.swaglog import cloudlog -from openpilot.selfdrive.modeld.helpers import USBGPU_VID, USBGPU_PID - -RATE = 10 # Hz - - -def find_device() -> Path | None: - # discover the eGPU by VID/PID - for d in Path("/sys/bus/usb/devices").glob("*"): - try: - if int((d / "idVendor").read_text(), 16) == USBGPU_VID and \ - int((d / "idProduct").read_text(), 16) == USBGPU_PID: - return d - except Exception: - pass - return None - - -def read(device: Path, attr: str) -> str | None: - try: - return (device / attr).read_text().strip() - except OSError: - return None - - -def over_current_count(device: Path) -> int: - # upstream root-hub port, e.g. device "4-1" -> usb4/4-0:1.0/usb4-port1 - bus, _, port = device.name.partition("-") - try: - return int((Path(f"/sys/bus/usb/devices/usb{bus}/{bus}-0:1.0/usb{bus}-port{port}") / "over_current_count").read_text()) - except (OSError, ValueError): - return 0 - - -def main(): - pm = messaging.PubMaster(['usbState']) - rk = Ratekeeper(RATE) - disconnect_count = 0 - was_connected = False - - while True: - device = find_device() - connected = device is not None - if was_connected and not connected: - disconnect_count += 1 - cloudlog.event("usb_disconnected", count=disconnect_count) - elif connected and not was_connected: - cloudlog.event("usb_connected", speed=read(device, "speed")) - was_connected = connected - - msg = messaging.new_message('usbState', valid=True) - state = msg.usbState - state.connected = connected - if device is not None: - speed = read(device, "speed") - state.speedMbps = int(speed) if (speed and speed.isdigit()) else 0 - state.pmActive = read(device, "power/runtime_status") == "active" - state.overCurrentCount = over_current_count(device) - state.disconnectCount = disconnect_count - - pm.send('usbState', msg) - rk.keep_time() - - -if __name__ == "__main__": - main() diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 4dff442c1619ee..7ed4a2dacb2c3c 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -106,7 +106,6 @@ def and_(*fns): PythonProcess("lateral_maneuversd", "tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver), PythonProcess("radard", "selfdrive.controls.radard", only_onroad), PythonProcess("hardwared", "system.hardware.hardwared", always_run), - PythonProcess("usbd", "system.hardware.usbd", always_run), PythonProcess("modem", "system.hardware.tici.modem", always_run, enabled=TICI), PythonProcess("tombstoned", "system.tombstoned", always_run, enabled=not PC), PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC),