IQ.Pilot Release Commit @ d2ce8a8

This commit is contained in:
IQ.Lvbs CI [bot]
2026-08-28 08:35:52 -05:00
parent 9206164707
commit ee1dca77c7
210 changed files with 19726 additions and 455 deletions

View File

@@ -0,0 +1,47 @@
# eGPU dock bring-up runbook
Our flasher is a port of comma's known-working one, byte-identical firmware
bundle, but it has never touched real hardware. Order matters: everything
read-only first, evidence at every step.
Run everything as root from the repo root on the device, dock on the USB-C
port, car ignition off.
## 1. Read-only probe (safe, run first, share the output)
sudo python3 iqpilot/system/hardware/egpu_dock/dock_probe.py
Expected on a dock previously flashed by stock openpilot: product matches the
bundled `custom ed4e39b7-CLEAN`, USB3 speed, PCIe link L0, stable config read.
Any other result: stop and send the output before proceeding.
## 2. Flash-path validation (writes, but writes the same bytes)
A stock-flashed dock already runs our exact bundled firmware, so the
no-op path proves version detection:
sudo python3 iqpilot/system/hardware/egpu_dock/flash.py
Expected: "firmware matches" and no write. Then exercise the full write path
by reflashing the identical image:
sudo python3 iqpilot/system/hardware/egpu_dock/flash.py --force
This backs up the per-unit config page to /data/egpu_dock_config/ first and
verifies every sector; identical bytes make it the lowest-risk possible
full-path test. Re-run step 1 after; product string and config sha must be
unchanged.
## 3. Runtime
Set `IQEgpuEnabled`, go onroad (bench is fine), and confirm iqegpumodeld
downloads/compiles and the selector reports UsbGpu* status. The runtime gate
requires the exact bundled firmware product string, so a dock that failed
step 2 will be treated as absent by design.
## If anything goes wrong
The dock falling back to the ROM bootloader (product "USB 3.2 PCIe
TinyEnclosure" or AS2462*) is recoverable: flash.py handles ROM recovery, and
the config backup from step 2 is on disk. Do not improvise register writes;
capture output and stop.

View File

@@ -0,0 +1,46 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
import hashlib
import sys
from iqpilot.system.hardware.egpu_dock.flash import (
Flash, RomFallback, bundled_version, find_dock, in_rom_bootloader, link_up, stable_read,
)
def main() -> int:
path, vid_pid, product = find_dock()
if path is None:
print("no eGPU dock enumerated")
return 1
print(f"dock at {path}")
print(f" vid:pid {vid_pid[0]}:{vid_pid[1]}")
print(f" product {product!r}")
print(f" bundled {bundled_version()!r}")
print(f" match {product == bundled_version()}")
with open(path + "/speed") as fs:
speed = int(fs.read())
print(f" usb speed {speed} Mbps ({'USB3' if speed >= 5000 else 'USB2 - register reads capped at 64B'})")
if in_rom_bootloader(vid_pid, product):
print(" state ROM bootloader (config page lost or firmware invalid)")
return 2
print(f" pcie link {'L0 (trained)' if link_up() else 'not trained'}")
flash = Flash()
try:
flash.connect()
config = stable_read(flash, 0, 0x100, 3)
print(f" config sha256={hashlib.sha256(config).hexdigest()[:16]} "
f"(stable over 3 reads, {sum(1 for b in config if b != 0xFF)} non-blank bytes)")
except (RomFallback, OSError, RuntimeError, TimeoutError) as e:
print(f" config read failed: {type(e).__name__}: {e}")
return 3
finally:
flash.close()
print("all read-only checks passed")
return 0
if __name__ == "__main__":
sys.exit(main())

Binary file not shown.

View File

@@ -0,0 +1,617 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
import argparse
import ctypes
import errno
import fcntl
import glob
import hashlib
import os
import re
import signal
import struct
import sys
import time
import zlib
from pathlib import Path
VID_PIDS = (("add1", "0001"), ("3801", "0001"))
ROM_VID_PIDS = (("174c", "2464"), ("174c", "2463"))
ROM_PRODUCT = "USB 3.2 PCIe TinyEnclosure"
FIRMWARE_PATH = Path(__file__).with_name("firmware_wrapped.bin")
CONFIG_DIR = "/data/egpu_dock_config"
LEGACY_CONFIG_DIR = "/data/chestnut_config"
PM_PATHS = ("/sys/bus/platform/devices/a800000.ssusb", "/sys/bus/platform/devices/a600000.ssusb",
"/sys/bus/usb/devices/usb4")
VBUS_PATH = "/sys/kernel/debug/regulator/smb2-vbus/enable"
IMAGE_OFFSET = 0x100
SECTOR, PAGE = 4096, 128
MAX_REGISTER_READ_SIZE = 255
MAX_CODE_SIZE = 0x10000
FLASH_BUDGET = 600.0
USBDEVFS_CONTROL = 0xC0185500
USBDEVFS_BULK = 0xC0185502
USBDEVFS_SETINTERFACE = 0x80085504
USBDEVFS_SETCONFIGURATION = 0x80045505
USBDEVFS_CLAIMINTERFACE = 0x8004550F
USBDEVFS_RESET = 0x5514
USBDEVFS_CLEAR_HALT = 0x80045515
_deadline = float("inf")
def check_budget():
if time.monotonic() > _deadline:
raise TimeoutError(f"flash did not converge within {FLASH_BUDGET:g}s")
class Ctrl(ctypes.Structure):
_fields_ = [("request_type", ctypes.c_uint8), ("request", ctypes.c_uint8),
("value", ctypes.c_uint16), ("index", ctypes.c_uint16),
("length", ctypes.c_uint16), ("timeout", ctypes.c_uint32),
("data", ctypes.c_void_p)]
class Bulk(ctypes.Structure):
_fields_ = [("ep", ctypes.c_uint), ("len", ctypes.c_uint),
("timeout", ctypes.c_uint), ("data", ctypes.c_void_p)]
class RomFallback(Exception):
pass
def find_dock():
found = []
for d in glob.glob("/sys/bus/usb/devices/*"):
try:
with open(d + "/idVendor") as fv, open(d + "/idProduct") as fp:
vid_pid = (fv.read().strip(), fp.read().strip())
if vid_pid in VID_PIDS + ROM_VID_PIDS:
with open(d + "/product") as fpr:
found.append((d, vid_pid, fpr.read().strip()))
except OSError:
pass
if len(found) > 1:
raise RuntimeError(f"expected one eGPU dock, found {len(found)}")
return found[0] if found else (None, None, None)
def in_rom_bootloader(vid_pid, product):
return vid_pid in ROM_VID_PIDS or product == ROM_PRODUCT or (product or "").startswith("AS2462")
def disable_runtime_pm(path):
control = os.path.join(path, "power/control")
if not os.path.exists(control):
return
with open(control, "w") as f:
f.write("on\n")
with open(control) as fh:
applied = fh.read().strip()
if applied != "on":
raise RuntimeError(f"could not disable USB runtime PM: {control}")
delay = os.path.join(path, "power/autosuspend_delay_ms")
if os.path.exists(delay):
with open(delay, "w") as f:
f.write("-1\n")
def unbind_drivers(path):
for interface in glob.glob(path + ":*"):
driver = interface + "/driver"
if os.path.islink(driver):
with open(os.path.realpath(driver) + "/unbind", "w") as f:
f.write(os.path.basename(interface))
def open_device(path):
with open(path + "/busnum") as fb, open(path + "/devnum") as fd_:
bus, dev = int(fb.read()), int(fd_.read())
return os.open(f"/dev/bus/usb/{bus:03d}/{dev:03d}", os.O_RDWR)
def link_up() -> bool:
try:
path, _, _ = find_dock()
if path is None:
return False
fd = open_device(path)
except (OSError, RuntimeError):
return False
try:
fcntl.ioctl(fd, USBDEVFS_CONTROL, Ctrl(0x40, 0xF3, 1, 0, 0, 2000, None))
buf = (ctypes.c_ubyte * 1)()
fcntl.ioctl(fd, USBDEVFS_CONTROL, Ctrl(0xC0, 0xE4, 0xB450, 0, 1, 1000, ctypes.cast(buf, ctypes.c_void_p)))
return buf[0] == 0x78
except OSError:
return False
finally:
os.close(fd)
def claim_interface(path, setup=False):
disable_runtime_pm(path)
unbind_drivers(path)
fd = open_device(path)
try:
if setup:
fcntl.ioctl(fd, USBDEVFS_SETCONFIGURATION, struct.pack("I", 1))
fcntl.ioctl(fd, USBDEVFS_CLAIMINTERFACE, struct.pack("I", 0))
if setup:
fcntl.ioctl(fd, USBDEVFS_SETINTERFACE, struct.pack("II", 0, 0))
except OSError as e:
os.close(fd)
if e.errno == errno.EBUSY:
raise RuntimeError("eGPU dock is in use, stop the model/GPU processes before flashing") from e
raise
return fd
class Flash:
def __init__(self):
self.fd = -1
self.max_register_read_size = MAX_REGISTER_READ_SIZE
def close(self):
if self.fd >= 0:
os.close(self.fd)
self.fd = -1
def connect(self, timeout=5.0):
self.close()
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
path, vid_pid, product = find_dock()
if in_rom_bootloader(vid_pid, product):
raise RomFallback("eGPU dock fell back to the ROM bootloader")
if path is not None:
try:
with open(path + "/speed") as fs:
speed = int(fs.read())
except (OSError, ValueError):
speed = 0
self.max_register_read_size = 64 if speed < 5000 else MAX_REGISTER_READ_SIZE
self.fd = claim_interface(path)
return
time.sleep(0.1)
raise RuntimeError(f"eGPU dock did not enumerate within {timeout:g}s")
def reg_write(self, addr, value):
fcntl.ioctl(self.fd, USBDEVFS_CONTROL,
Ctrl(0x40, 0xE5, addr & 0xFFFF, value & 0xFFFF, 0, 2000, None))
def reg_read(self, addr, length=1):
buf = (ctypes.c_ubyte * length)()
fcntl.ioctl(self.fd, USBDEVFS_CONTROL,
Ctrl(0xC0, 0xE4, addr & 0xFFFF, 0, length, 2000, ctypes.cast(buf, ctypes.c_void_p)))
return bytes(buf)
def write_buffer(self, data):
for i, value in enumerate(data):
self.reg_write(0x7000 + i, value)
def wait_controller(self, timeout=2.0):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if not self.reg_read(0xC8A9)[0] & 1:
return
raise TimeoutError("flash controller timeout")
def transaction(self, command, addr=0, length=0, addr_len=0x07, mode=0):
for reg, value in ((0xC8AD, mode), (0xC8AE, 0), (0xC8AF, 0), (0xC8AA, command), (0xC8AC, addr_len),
(0xC8A1, addr), (0xC8A2, addr >> 8), (0xC8AB, addr >> 16), (0xC8A3, length >> 8), (0xC8A4, length)):
self.reg_write(reg, value & 0xFF)
self.reg_write(0xC8A9, 1)
self.wait_controller()
for _ in range(4):
self.reg_write(0xC8AD, 0)
def write_enable(self):
for reg, value in ((0xC8AD, 0), (0xC8AA, 0x06), (0xC8AC, 0x04), (0xC8A3, 0), (0xC8A4, 0), (0xC8A9, 1)):
self.reg_write(reg, value)
self.wait_controller()
def status(self):
self.transaction(0x05, length=1, addr_len=0x04)
return self.reg_read(0x7000)[0]
def wait_write_done(self, timeout=10.0):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if not self.status() & 1:
return
time.sleep(0.005)
raise TimeoutError("SPI flash WIP timeout")
def init(self):
self.reg_write(0xCC33, 0x04)
self.reg_write(0xCA81, self.reg_read(0xCA81)[0] | 1)
self.reg_write(0xC805, 0x02)
self.reg_write(0xC8A6, 0x04)
for _ in range(5):
self.write_enable()
self.write_buffer(bytes(4))
self.transaction(0x01, length=1, addr_len=0x04, mode=1)
time.sleep(0.01)
if not self.status() & 0x1C:
return
raise RuntimeError("could not clear SPI block protection")
def read(self, addr, length):
out = bytearray()
while len(out) < length:
n = min(4096, length - len(out))
self.transaction(0x03, addr + len(out), max(4096, n))
for off in range(0, n, self.max_register_read_size):
out += self.reg_read(0x7000 + off, min(self.max_register_read_size, n - off))
return bytes(out)
def erase_sector(self, addr):
self.write_enable()
self.transaction(0x20, addr)
self.wait_write_done()
def program(self, addr, data):
self.write_buffer(data + bytes((-len(data)) % 4))
self.write_enable()
self.transaction(0x02, addr, len(data), mode=1)
self.wait_write_done()
def validate_image(data):
if len(data) < 10:
raise ValueError("wrapped firmware is too short")
body_len = int.from_bytes(data[:4], "little")
if body_len > MAX_CODE_SIZE:
raise ValueError(f"wrapped firmware body exceeds {MAX_CODE_SIZE} bytes")
if len(data) != body_len + 10 or data[4 + body_len] != 0xA5:
raise ValueError("invalid wrapped firmware length or magic")
body = data[4:4 + body_len]
if data[5 + body_len] != sum(body) & 0xFF:
raise ValueError("invalid wrapped firmware checksum")
if data[6 + body_len:] != zlib.crc32(body).to_bytes(4, "little"):
raise ValueError("invalid wrapped firmware CRC")
def image_product(image):
match = re.search(rb"custom [0-9a-f]{8}-CLEAN", image)
if match is None:
raise ValueError("no product string in wrapped firmware")
return match.group().decode()
def reconnect(flash):
attempt = 0
while True:
attempt += 1
check_budget()
try:
flash.connect()
flash.init()
return
except (OSError, TimeoutError, RuntimeError) as e:
print(f"waiting for eGPU dock (attempt {attempt}): {e}", flush=True)
time.sleep(1)
def with_retries(flash, label, operation):
attempt = 0
while True:
attempt += 1
try:
return operation()
except (OSError, TimeoutError, RuntimeError) as e:
check_budget()
print(f"{label} attempt {attempt}: {e}", flush=True)
reconnect(flash)
def stable_read(flash, addr, length, count=2):
def read():
reads = [flash.read(addr, length) for _ in range(count)]
if any(x != reads[0] for x in reads[1:]):
raise RuntimeError(f"unstable flash read at 0x{addr:05x}")
return reads[0]
return with_retries(flash, f"read 0x{addr:05x}", read)
def program_sector(flash, addr, target):
def program():
flash.erase_sector(addr)
if flash.read(addr, SECTOR) != bytes([0xFF]) * SECTOR:
raise RuntimeError("sector erase verification failed")
for off in range(0, SECTOR, PAGE):
chunk = target[off:off + PAGE]
if chunk != bytes([0xFF]) * len(chunk):
flash.program(addr + off, chunk)
if flash.read(addr + off, len(chunk)) != chunk:
raise RuntimeError(f"page verify failed at 0x{addr + off:05x}")
if flash.read(addr, SECTOR) != target:
raise RuntimeError("sector verification failed")
with_retries(flash, f"sector 0x{addr:05x}", program)
def config_path():
return os.path.join(CONFIG_DIR, f"{os.uname().nodename}.bin")
def legacy_config_path():
return os.path.join(LEGACY_CONFIG_DIR, f"{os.uname().nodename}.bin")
def saved_config(path, data):
os.makedirs(os.path.dirname(path), exist_ok=True)
try:
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError as e:
with open(path, "rb") as fh:
backup = fh.read()
if len(backup) != 0x100:
raise RuntimeError(f"invalid config backup: {path}") from e
if backup != data:
print(f"restoring config from {path}", flush=True)
return backup
with os.fdopen(fd, "wb") as f:
f.write(data)
f.flush()
os.fsync(f.fileno())
return data
def rom_write(image, config):
path, _, _ = find_dock()
if path is None:
raise RuntimeError("eGPU dock disappeared before recovery")
unbind_drivers(path)
fd = open_device(path)
try:
fcntl.ioctl(fd, USBDEVFS_RESET)
finally:
os.close(fd)
time.sleep(3)
path, _, _ = find_dock()
if path is None:
raise RuntimeError("eGPU dock did not re-enumerate after reset")
fd = claim_interface(path, setup=True)
for ep in (0x02, 0x81):
fcntl.ioctl(fd, USBDEVFS_CLEAR_HALT, struct.pack("I", ep))
tag = 0
def bulk(ep, payload, timeout):
buf = ctypes.create_string_buffer(bytes(payload), len(payload))
fcntl.ioctl(fd, USBDEVFS_BULK, Bulk(ep, len(payload), timeout, ctypes.cast(buf, ctypes.c_void_p)))
return buf.raw
def cmd(cdb, data=b"", timeout=30000):
nonlocal tag
tag += 1
bulk(0x02, struct.pack("<IIIBBB16s", 0x43425355, tag, len(data), 0, 0, len(cdb), cdb), timeout)
if data:
bulk(0x02, data, timeout)
try:
csw = bulk(0x81, bytes(13), timeout)
except OSError as e:
if e.errno != errno.EPIPE:
raise
fcntl.ioctl(fd, USBDEVFS_CLEAR_HALT, struct.pack("I", 0x81))
csw = bulk(0x81, bytes(13), timeout)
if csw[:4] != b"USBS" or csw[12] != 0:
raise RuntimeError(f"ROM flash command {cdb[0]:02x} {cdb[1]:02x} failed")
print("recovering from the ROM bootloader", flush=True)
try:
cmd(struct.pack(">BBB12x", 0xE1, 0x50, 0), config[:0x80])
cmd(struct.pack(">BBB12x", 0xE1, 0x50, 1), config[0x80:])
cmd(struct.pack(">BBI", 0xE3, 0x50, min(len(image), 0xFF00)), image[:0xFF00])
if len(image) > 0xFF00:
cmd(struct.pack(">BBI", 0xE3, 0xD0, len(image) - 0xFF00), image[0xFF00:])
cmd(struct.pack(">BB13x", 0xE8, 0x51))
finally:
os.close(fd)
print("recovery flash done", flush=True)
def vbus_write(value):
try:
with open(VBUS_PATH, "w") as f:
f.write(value + "\n")
except OSError:
pass
def vbus_cycle():
if os.path.exists(VBUS_PATH):
vbus_write("0")
time.sleep(2)
vbus_write("1")
time.sleep(5)
def activate(expected_product):
if not os.path.exists(VBUS_PATH):
print("no VBUS control, firmware activates on the next dock power cycle", flush=True)
return
print("power-cycling the eGPU dock VBUS", flush=True)
vbus_write("0")
disconnected = False
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
path, _, _ = find_dock()
if path is None:
disconnected = True
break
time.sleep(0.2)
time.sleep(1)
vbus_write("1")
if not disconnected:
print("dock stayed powered, firmware activates on its next power cycle", flush=True)
return
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
_, _, product = find_dock()
if product is not None:
if product == expected_product:
print(f"activated {expected_product}", flush=True)
else:
print(f"dock re-enumerated with {product!r}, firmware activates on its next power cycle", flush=True)
return
time.sleep(0.2)
print("dock did not re-enumerate, firmware activates on its next power cycle", flush=True)
def defer_signal(signum, _frame):
os.write(1, f"signal {signum} deferred until the dock is powered back up\n".encode())
def flash_dock(expected_version=None, force=False):
global _deadline
image = FIRMWARE_PATH.read_bytes()
validate_image(image)
expected_product = image_product(image)
if expected_version is not None and expected_product != f"custom {expected_version}-CLEAN":
raise RuntimeError(f"bundled firmware is {expected_product!r}, expected version {expected_version}")
path, vid_pid, product = find_dock()
if path is None:
print("no eGPU dock connected", flush=True)
return
if product == expected_product and not force:
print(f"eGPU dock firmware is up to date ({expected_product})", flush=True)
return
_deadline = time.monotonic() + FLASH_BUDGET
for pm_path in PM_PATHS:
disable_runtime_pm(pm_path)
previous = {sig: signal.signal(sig, defer_signal) for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP)}
try:
if in_rom_bootloader(vid_pid, product):
if not recover_from_rom(image, expected_product):
return
force, product = True, None
write_image(image, expected_product, product, force)
finally:
for sig, handler in previous.items():
signal.signal(sig, handler)
def recover_from_rom(image, expected_product):
backup = config_path()
if not os.path.isfile(backup) and os.path.isfile(legacy_config_path()):
backup = legacy_config_path()
if not os.path.isfile(backup):
raise RuntimeError(f"cannot recover from the ROM bootloader without a config backup at {config_path()}")
with open(backup, "rb") as fh:
config = fh.read()
if len(config) != 0x100:
raise RuntimeError(f"invalid config backup: {backup}")
committed = False
while True:
check_budget()
path, vid_pid, product = find_dock()
if path is None:
if committed:
print("dock is offline, recovered firmware boots on its next power cycle", flush=True)
return False
vbus_cycle()
continue
if not in_rom_bootloader(vid_pid, product):
return True
if committed:
print("dock stayed powered, recovered firmware boots on its next power cycle", flush=True)
return False
try:
rom_write(image, config)
committed = True
except (OSError, TimeoutError, RuntimeError) as e:
print(f"ROM recovery failed, retrying: {e}", flush=True)
vbus_cycle()
continue
activate(expected_product)
def write_image(image, expected_product, product, force):
if force:
print(f"forced reflash of {expected_product}", flush=True)
else:
print(f"eGPU dock firmware mismatch: {product!r}; expected {expected_product!r}", flush=True)
flash = Flash()
try:
reconnect(flash)
config = stable_read(flash, 0, 0x100, 3)
config = saved_config(config_path(), config)
image_end = IMAGE_OFFSET + len(image)
first_sector = IMAGE_OFFSET & ~(SECTOR - 1)
span = (image_end + SECTOR - 1) & ~(SECTOR - 1)
current = stable_read(flash, first_sector, span - first_sector)
target = bytearray(current)
target[:len(config)] = config
target[IMAGE_OFFSET - first_sector:image_end - first_sector] = image
target = bytes(target)
print(f"target {len(image)} bytes at 0x{IMAGE_OFFSET:05x}, sha256={hashlib.sha256(image).hexdigest()}", flush=True)
if not _still_offroad():
raise RuntimeError("device went onroad before any sector was written; aborting flash")
for addr in range(first_sector, span, SECTOR):
off = addr - first_sector
wanted = target[off:off + SECTOR]
if current[off:off + SECTOR] == wanted:
print(f"sector 0x{addr:05x}: unchanged", flush=True)
else:
print(f"sector 0x{addr:05x}: programming", flush=True)
program_sector(flash, addr, wanted)
verified = stable_read(flash, first_sector, span - first_sector, 3)
if verified != target:
raise RuntimeError("final full-image verification failed")
print(f"verified sha256={hashlib.sha256(verified).hexdigest()}", flush=True)
finally:
flash.close()
activate(expected_product)
def bundled_version() -> str:
return image_product(FIRMWARE_PATH.read_bytes())
def _still_offroad() -> bool:
try:
from iqpilot.common.params import Params
return bool(Params().get_bool("IsOffroad"))
except Exception:
return True
def dock_needs_flash(usb_devices: list[dict]) -> bool:
try:
expected = bundled_version()
except (OSError, ValueError):
return False
ids = tuple(tuple(int(x, 16) for x in p) for p in VID_PIDS + ROM_VID_PIDS)
return any((d.get("vendorId"), d.get("productId")) in ids and d.get("product") != expected
for d in usb_devices)
def main():
parser = argparse.ArgumentParser(description="check and flash the bundled eGPU dock firmware")
parser.add_argument("version", nargs="?", help="expected firmware version hash")
parser.add_argument("--force", action="store_true", help="reflash even when the version matches")
args = parser.parse_args()
if os.geteuid() != 0:
raise RuntimeError("flash.py must run as root")
flash_dock(expected_version=args.version, force=args.force)
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"FAIL: {type(e).__name__}: {e}", file=sys.stderr)
sys.exit(1)

View File

@@ -0,0 +1,89 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
import time
from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT, is_egpu_usb_device
EGPU_POWERED_VOLTAGE = 5000
GPU_TEMP_LIMIT = 110.
MEMORY_TEMP_LIMIT = 108.
TEMP_HYSTERESIS = 5.
FAN_START_GPU_TEMP = 60.
FAN_STOP_GPU_TEMP = 50.
FAN_START_MEMORY_TEMP = 70.
FAN_STOP_MEMORY_TEMP = 60.
FAN_STALLED_RPM = 250
class EgpuDockStatus:
def __init__(self):
self.offroad = True
self.pcie_failed = False
self.power_lost = False
self.power_restored = False
self.link_failures = 0
self.model_loading_seen = False
self.model_attempted = False
self.overheated = False
self.fans_obstructed = False
self.usb_seen = False
self.usb_failed = False
def update(self, offroad, usb_state, firmware_failed, model_loading, model_active, compiled, state, set_alert):
detected = [d for d in usb_state if is_egpu_usb_device(d["vendorId"], d["productId"], include_bootloader=True)]
devices = [d for d in detected if is_egpu_usb_device(d["vendorId"], d["productId"])]
firmware_ok = len(devices) == 1 and devices[0]["product"] == EGPU_DOCK_FW_PRODUCT
if self.offroad and not offroad:
self.pcie_failed = False
self.power_lost = False
self.power_restored = False
self.link_failures = 0
self.model_loading_seen = False
self.model_attempted = False
self.usb_seen = firmware_ok
self.usb_failed = False
self.model_loading_seen |= model_loading
self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None
if not offroad and self.usb_seen and not firmware_ok:
self.usb_failed = True
if not offroad and self.model_attempted and state is not None:
power_lost = state.supplyFault or state.supplyVoltage < EGPU_POWERED_VOLTAGE
self.link_failures = self.link_failures + 1 if state.pcieLtssm != 0x78 else 0
self.pcie_failed |= self.link_failures >= 2 or power_lost
self.power_lost |= power_lost
if self.pcie_failed and self.power_lost and state is not None:
self.power_restored |= not state.supplyFault and state.supplyVoltage >= EGPU_POWERED_VOLTAGE
if self.usb_failed:
self.pcie_failed = False
self.power_lost = False
self.power_restored = False
if state is not None:
gpu_limit = GPU_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.)
memory_limit = MEMORY_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.)
self.overheated = state.tempC >= gpu_limit or state.memoryTempC >= memory_limit
fan_hot = (state.tempC >= (FAN_STOP_GPU_TEMP if self.fans_obstructed else FAN_START_GPU_TEMP) or
state.memoryTempC >= (FAN_STOP_MEMORY_TEMP if self.fans_obstructed else FAN_START_MEMORY_TEMP))
self.fans_obstructed = fan_hot and state.fanSpeedRpm < FAN_STALLED_RPM
slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000
set_alert("Offroad_EgpuNotDetected", self.usb_failed)
set_alert("Offroad_EgpuFansObstructed", self.fans_obstructed)
set_alert("Offroad_EgpuOverheated", self.overheated)
set_alert("Offroad_EgpuUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None)
if self.power_lost:
pcie_action = "12V power was interrupted, possibly by engine start-stop. "
pcie_action += ("Cycle ignition to reload the model." if self.power_restored else
"Check 12V, then cycle ignition to reload the model.")
else:
pcie_action = "Check 12V connection."
set_alert("Offroad_EgpuPcieUnavailable", self.pcie_failed, pcie_action)
set_alert("Offroad_EgpuUncompiled", offroad and firmware_ok and not compiled)
set_alert("Offroad_EgpuUpdateFailed", offroad and firmware_failed)
self.offroad = offroad