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

View File

@@ -3,24 +3,13 @@ import numpy as np
class FanController:
def __init__(self) -> None:
self.last_ignition = False
def update(self, cur_temp: float, ignition: bool, max_cool: bool = False) -> int:
if max_cool:
self.last_ignition = ignition
return 100
if cur_temp < 70.0:
fan_pwr_out = 0
elif cur_temp > 85.0:
fan_pwr_out = 100
else:
# 70°C → 0%, 85°C → 80%, target 75°C
fan_pwr_out = int(np.interp(cur_temp, [70.0, 85.0], [0, 80]))
fan_pwr_out = int(np.interp(cur_temp, [70.0, 85.0, 90.0], [0, 80, 100]))
if not ignition:
fan_pwr_out = min(fan_pwr_out, 30)
self.last_ignition = ignition
return fan_pwr_out

View File

@@ -1,6 +1,8 @@
#!/usr/bin/env python3
import fcntl
import os
import subprocess
import sys
import queue
import struct
import threading
@@ -16,10 +18,14 @@ from iqpilot.cereal.services import SERVICE_LIST
from iqpilot.common.iq_perf import PerfSample, PerfTraceEmitter
from iqpilot.common.utils import strip_deprecated_keys
from iqpilot.common.filter_simple import FirstOrderFilter
from iqpilot.common.basedir import BASEDIR
from iqpilot.common.params import Params
from iqpilot.common.realtime import DT_HW
from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from iqpilot.system.hardware import HARDWARE, TICI, AGNOS
from iqpilot.system.hardware.egpu_dock.flash import dock_needs_flash
from iqpilot.system.hardware.egpu_dock.status import EgpuDockStatus
from iqpilot.system.hardware.usb import get_link_error_count, get_usb_state, set_usb_state, usb3_lane
from iqpilot.system.loggerd.config import get_available_percent
from iqpilot.common.swaglog import cloudlog
from iqpilot.system.hardware.power_monitoring import PowerMonitoring, VBATT_LOW_POWER_EXIT
@@ -41,7 +47,8 @@ CAN_STARTUP_RECOVERY_MAX_ATTEMPTS = 2
ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp'])
HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'network_strength', 'network_stats',
'network_metered', 'modem_temps'])
'network_metered', 'modem_temps', 'usb_state', 'usb_link_errors',
'usb3_lane'])
# List of thermal bands. We will stay within this region as long as we are within the bounds.
# When exiting the bounds, we'll jump to the lower or higher band. Bands are ordered in the dict.
@@ -161,6 +168,56 @@ class _CarParamsCache:
class EgpuDockFlasher:
"""Flash the dock's firmware offroad when it does not match what we ship.
Same policy as stock: the model runtime ignores a dock until its product
string matches, so a mismatched dock is unusable until this runs. Bounded
attempts, offroad only, one flash in flight at a time.
"""
MAX_ATTEMPTS = 3
RETRY_INTERVAL = 20.
def __init__(self):
self.thread: threading.Thread | None = None
self.attempts = 0
self.last_attempt = 0.
self.flashed = False
self.mismatch = False
@property
def failed(self) -> bool:
return (self.mismatch and self.attempts >= self.MAX_ATTEMPTS
and self.thread is not None and not self.thread.is_alive() and not self.flashed)
def flash(self) -> None:
ret = subprocess.run(["sudo", sys.executable,
os.path.join(BASEDIR, "iqpilot/system/hardware/egpu_dock/flash.py")],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
cloudlog.event("egpu dock flash done", returncode=ret.returncode, output=ret.stdout[-1000:],
error=ret.returncode != 0)
self.flashed = ret.returncode == 0
def update(self, offroad: bool, usb_state: list[dict]) -> None:
self.mismatch = dock_needs_flash(usb_state)
if not self.mismatch:
self.flashed = False
return
if not offroad or self.flashed or self.attempts >= self.MAX_ATTEMPTS:
return
if self.thread is not None and self.thread.is_alive():
return
if time.monotonic() - self.last_attempt < self.RETRY_INTERVAL:
return
self.attempts += 1
self.last_attempt = time.monotonic()
cloudlog.warning(f"egpu dock firmware out of date, flashing (attempt {self.attempts})")
self.thread = threading.Thread(target=self.flash, daemon=True)
self.thread.start()
def set_offroad_alert_if_changed(offroad_alert: str, show_alert: bool, extra_text: str | None=None):
if prev_offroad_states.get(offroad_alert, None) == (show_alert, extra_text):
return
@@ -254,6 +311,9 @@ def hw_state_thread(end_event, hw_queue):
network_stats={'wwanTx': tx, 'wwanRx': rx},
network_metered=HARDWARE.get_network_metered(network_type),
modem_temps=modem_temps,
usb_state=get_usb_state(),
usb_link_errors=get_link_error_count(),
usb3_lane=usb3_lane(),
)
try:
@@ -280,7 +340,7 @@ def hw_state_thread(end_event, hw_queue):
def hardware_thread(end_event, hw_queue) -> None:
pm = messaging.PubMaster(['deviceState', 'iqPerfTrace'])
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "carState"], poll="pandaStates")
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "carState", "egpuDockState"], poll="pandaStates")
perf = PerfTraceEmitter("hardwared", pubmaster=pm)
count = 0
@@ -306,6 +366,9 @@ def hardware_thread(end_event, hw_queue) -> None:
network_strength=NetworkStrength.unknown,
network_stats={'wwanTx': -1, 'wwanRx': -1},
modem_temps=[],
usb_state=[],
usb_link_errors=0,
usb3_lane="unknown",
)
all_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_HW, initialized=False)
@@ -332,6 +395,8 @@ def hardware_thread(end_event, hw_queue) -> None:
thermal_config = HARDWARE.get_thermal_config()
fan_controller = FanController()
egpu_dock_flasher = EgpuDockFlasher()
egpu_dock_status = EgpuDockStatus()
while not end_event.is_set():
sm.update(PANDA_STATES_TIMEOUT)
@@ -427,6 +492,14 @@ def hardware_thread(end_event, hw_queue) -> None:
msg.deviceState.networkInfo = last_hw_state.network_info
msg.deviceState.modemTempC = last_hw_state.modem_temps
set_usb_state(msg.deviceState, last_hw_state.usb_state, last_hw_state.usb_link_errors,
last_hw_state.usb3_lane)
egpu_dock_flasher.update(started_ts is None, last_hw_state.usb_state)
egpu_valid = sm.alive["egpuDockState"] and sm.valid["egpuDockState"]
egpu_dock_status.update(started_ts is None, last_hw_state.usb_state, egpu_dock_flasher.failed,
params.get_bool("UsbGpuLoading"), params.get("UsbGpuActive"),
params.get_bool("UsbGpuCompiled"),
sm["egpuDockState"] if egpu_valid else None, set_offroad_alert_if_changed)
msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness()

View File

@@ -0,0 +1,132 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
The eGPU dock flasher writes SPI flash, so the parts that decide WHETHER and
WHAT to write are pinned here. The transfer path itself needs the hardware; the
image validator, product parsing, config preservation and the needs-flash
decision do not, and those are what stop a bad write.
"""
import os
import zlib
import pytest
from iqpilot.system.hardware.egpu_dock import flash as f
def _wrap(body: bytes, *, magic=0xA5, checksum=None, crc=None, body_len=None) -> bytes:
n = len(body) if body_len is None else body_len
cs = (sum(body) & 0xFF) if checksum is None else checksum
c = zlib.crc32(body).to_bytes(4, "little") if crc is None else crc
return n.to_bytes(4, "little") + body + bytes([magic, cs]) + c
def test_bundled_firmware_is_valid_and_named():
image = f.FIRMWARE_PATH.read_bytes()
f.validate_image(image) # raises if the shipped blob is corrupt
assert f.image_product(image).startswith("custom ")
assert f.image_product(image).endswith("-CLEAN")
assert f.bundled_version() == f.image_product(image)
def test_validate_rejects_corruption():
body = b"custom deadbeef-CLEAN" + bytes(64)
f.validate_image(_wrap(body)) # the good case
with pytest.raises(ValueError):
f.validate_image(b"\x00" * 4) # too short
with pytest.raises(ValueError):
f.validate_image(_wrap(body, magic=0x00)) # bad magic
with pytest.raises(ValueError):
f.validate_image(_wrap(body, checksum=0x00))
with pytest.raises(ValueError):
f.validate_image(_wrap(body, crc=b"\x00\x00\x00\x00"))
with pytest.raises(ValueError):
f.validate_image(_wrap(body, body_len=len(body) + 1)) # length disagrees
with pytest.raises(ValueError):
f.validate_image((f.MAX_CODE_SIZE + 1).to_bytes(4, "little") + bytes(16))
def test_image_product_requires_a_version_string():
with pytest.raises(ValueError):
f.image_product(b"no version here")
def test_saved_config_preserves_the_first_backup(tmp_path):
# the config page is per-unit; a reflash must rewrite the ORIGINAL, never the
# bytes read back from a half-written dock
p = str(tmp_path / "dock.bin")
original = bytes(range(256))
assert f.saved_config(p, original) == original
# later flash reads something different -> the stored original wins
assert f.saved_config(p, bytes(256)) == original
with open(p, "rb") as fh:
assert fh.read() == original
def test_saved_config_rejects_wrong_size_backup(tmp_path):
p = str(tmp_path / "dock.bin")
with open(p, "wb") as fh:
fh.write(b"\x00" * 8)
with pytest.raises(RuntimeError):
f.saved_config(p, bytes(256))
def test_needs_flash_only_for_a_dock_on_wrong_firmware():
expected = f.bundled_version()
vid, pid = (int(x, 16) for x in f.VID_PIDS[0])
rom_vid, rom_pid = (int(x, 16) for x in f.ROM_VID_PIDS[0])
assert not f.dock_needs_flash([])
assert not f.dock_needs_flash([{"vendorId": 0x1234, "productId": 0x5678, "product": "something else"}])
assert not f.dock_needs_flash([{"vendorId": vid, "productId": pid, "product": expected}])
assert f.dock_needs_flash([{"vendorId": vid, "productId": pid, "product": "custom 00000000-CLEAN"}])
# a ROM-mode board always needs flashing
assert f.dock_needs_flash([{"vendorId": rom_vid, "productId": rom_pid, "product": f.ROM_PRODUCT}])
def test_both_shipped_ids_trigger_the_check():
for pair in f.VID_PIDS:
vid, pid = (int(x, 16) for x in pair)
assert f.dock_needs_flash([{"vendorId": vid, "productId": pid, "product": "custom 00000000-CLEAN"}])
def test_rom_detection():
assert f.in_rom_bootloader(f.ROM_VID_PIDS[0], "anything")
assert f.in_rom_bootloader(("add1", "0001"), f.ROM_PRODUCT)
assert f.in_rom_bootloader(("add1", "0001"), "AS2462something")
assert not f.in_rom_bootloader(("add1", "0001"), f.bundled_version())
assert not f.in_rom_bootloader(("add1", "0001"), None)
def test_config_paths_are_per_host_and_have_a_legacy_fallback():
host = os.uname().nodename
assert f.config_path().endswith(f"{host}.bin")
assert f.config_path().startswith(f.CONFIG_DIR)
# a dock flashed on this device by stock openpilot left its backup elsewhere
assert f.legacy_config_path().startswith(f.LEGACY_CONFIG_DIR)
assert f.legacy_config_path() != f.config_path()
def test_we_do_not_autoflash():
# upstream flashes from hardwared automatically; ours must stay deliberate
# until it has been validated against a real dock
import subprocess
root = os.path.join(os.path.dirname(f.__file__), "..", "..", "..")
hits = subprocess.run(["grep", "-rnI", "--exclude-dir=__pycache__", "flash_dock", os.path.join(root, "system"),
os.path.join(root, "iqpilot")], capture_output=True, text=True).stdout
callers = [ln for ln in hits.splitlines() if "egpu_dock/flash.py" not in ln and "test_" not in ln]
assert callers == [], f"unexpected automatic flash caller: {callers}"
def test_runtime_fw_gate_is_pinned_to_the_bundled_firmware():
from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT
assert EGPU_DOCK_FW_PRODUCT == f.bundled_version()
def test_register_reads_default_to_superspeed_size():
assert f.MAX_REGISTER_READ_SIZE == 255
assert f.Flash().max_register_read_size == f.MAX_REGISTER_READ_SIZE
def test_link_up_is_false_without_a_dock():
assert f.link_up() is False

View File

@@ -1,57 +1,41 @@
import pytest
import numpy as np
from iqpilot.system.hardware.fan_controller import FanController
ALL_CONTROLLERS = [FanController]
def patched_controller(mocker, controller_class):
mocker.patch("os.system", new=mocker.Mock())
return controller_class()
class TestFanController:
def wind_up(self, controller, ignition=True):
for _ in range(1000):
controller.update(100, ignition)
def test_ramp_anchors(self):
c = FanController()
assert c.update(60, True) == 0
assert c.update(70, True) == 0
assert c.update(85, True) == 80
assert c.update(90, True) == 100
assert c.update(100, True) == 100
def wind_down(self, controller, ignition=False):
for _ in range(1000):
controller.update(10, ignition)
def test_ramp_is_monotonic_and_continuous(self):
c = FanController()
temps = np.arange(50.0, 105.0, 0.25)
outs = [c.update(t, True) for t in temps]
assert all(b >= a for a, b in zip(outs, outs[1:]))
# no step may exceed the steepest segment's slope (4 %/deg) over a 0.25 deg move
assert max(b - a for a, b in zip(outs, outs[1:])) <= 2
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
def test_hot_onroad(self, mocker, controller_class):
controller = patched_controller(mocker, controller_class)
self.wind_up(controller)
assert controller.update(100, True) >= 70
def test_hot_onroad(self):
assert FanController().update(100, True) >= 70
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
def test_offroad_limits(self, mocker, controller_class):
controller = patched_controller(mocker, controller_class)
self.wind_up(controller)
assert controller.update(100, False) <= 30
def test_offroad_capped(self):
c = FanController()
for t in (60, 75, 85, 100):
assert c.update(t, False) <= 30
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
def test_no_fan_wear(self, mocker, controller_class):
controller = patched_controller(mocker, controller_class)
self.wind_down(controller)
assert controller.update(10, False) == 0
def test_no_fan_wear(self):
assert FanController().update(10, False) == 0
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
def test_limited(self, mocker, controller_class):
controller = patched_controller(mocker, controller_class)
self.wind_up(controller, True)
assert controller.update(100, True) == 100
def test_max_cool(self):
c = FanController()
assert c.update(80, True, True) == 100
assert c.update(80, False, True) == 100
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
def test_max_cool(self, mocker, controller_class):
controller = patched_controller(mocker, controller_class)
self.wind_down(controller)
assert controller.update(80, True, True) == 100
assert controller.update(80, False, True) == 100
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
def test_windup_speed(self, mocker, controller_class):
controller = patched_controller(mocker, controller_class)
self.wind_down(controller, True)
for _ in range(10):
controller.update(90, True)
assert controller.update(90, True) >= 60
def test_target_band_has_airflow(self):
# the design centers on 75 C; the curve must actually move air there
assert 20 <= FanController().update(75, True) <= 40

View File

@@ -0,0 +1,267 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
deviceState.usbState carries a per-device USB bus snapshot plus the ssusb
link-error counter (IQ.OS 4.9.1+ `portli`) into every rlog. Drive it off a
synthetic sysfs tree so parsing, eGPU dock presence and the link-error
plumbing are pinned without hardware.
"""
from iqpilot.cereal import messaging
from iqpilot.system.hardware.usb import (
EGPU_DOCK_FW_PRODUCT, EGPU_DOCK_ROM_USB_IDS, EGPU_DOCK_USB_IDS, controller, egpu_dock_present,
egpu_dock_ready, get_link_error_count,
get_usb_topology, get_usb_state, link_controller, read_hex_counter, set_usb_state, usb3_lane,
)
def _mkctrl(root, name="a800000.ssusb", portli="0x00000000"):
"""Platform controller dir, mirroring /sys/devices/platform/soc/<x>.ssusb."""
ctrl = root / "soc" / name
ctrl.mkdir(parents=True)
if portli is not None:
(ctrl / "portli").write_text(portli + "\n")
return ctrl
def _mkdev(root, name, *, vid, pid, busnum=1, devnum=2, speed=5000,
manufacturer="ACME", product="Widget", ctrl=None):
"""USB device under the controller, symlinked into the bus view like sysfs."""
real = (ctrl / "usb1" / name) if ctrl is not None else (root / "bus" / name)
real.mkdir(parents=True)
(real / "idVendor").write_text(f"{vid:04x}\n")
(real / "idProduct").write_text(f"{pid:04x}\n")
(real / "busnum").write_text(f"{busnum}\n")
(real / "devnum").write_text(f"{devnum}\n")
(real / "speed").write_text(f"{speed}\n")
(real / "manufacturer").write_text(manufacturer + "\n")
(real / "product").write_text(product + "\n")
bus = root / "bus"
bus.mkdir(parents=True, exist_ok=True)
link = bus / name
if real != link:
link.symlink_to(real)
return real
def test_missing_sysfs_is_empty(tmp_path):
assert get_usb_state(tmp_path / "nope") == []
def test_entries_without_idvendor_are_skipped(tmp_path):
(tmp_path / "bus" / "usb1").mkdir(parents=True) # a root hub dir with no idVendor
_mkdev(tmp_path, "1-2", vid=0x1234, pid=0x5678)
state = get_usb_state(tmp_path / "bus")
assert len(state) == 1 and state[0]["vendorId"] == 0x1234
def test_fields_parsed_with_hex_ids(tmp_path):
_mkdev(tmp_path, "1-2", vid=0x0BDA, pid=0x8153, busnum=3, devnum=7,
speed=480, manufacturer="Realtek", product="USB 10/100 LAN")
(dev,) = get_usb_state(tmp_path / "bus")
assert dev == {
"busnum": 3, "devnum": 7,
"vendorId": 0x0BDA, "productId": 0x8153,
"speedMbps": 480,
"manufacturer": "Realtek", "product": "USB 10/100 LAN",
"linkErrorCount": 0, # no controller in this device's path
"usb3Lane": "unknown", # not on the type-C port's controller
}
def test_unreadable_strings_default_empty(tmp_path):
real = _mkdev(tmp_path, "1-2", vid=0x1, pid=0x2)
(real / "manufacturer").unlink()
(real / "product").unlink()
(dev,) = get_usb_state(tmp_path / "bus")
assert dev["manufacturer"] == "" and dev["product"] == ""
def test_hex_counter_parsing(tmp_path):
f = tmp_path / "portli"
f.write_text("0x00000000\n")
assert read_hex_counter(f) == 0
f.write_text("0x0000002a\n")
assert read_hex_counter(f) == 42
f.write_text("0000002a\n") # bare hex, no 0x prefix
assert read_hex_counter(f) == 42
f.write_text("garbage\n")
assert read_hex_counter(f) == 0
assert read_hex_counter(tmp_path / "absent") == 0 # pre-4.9.1 IQ.OS
def test_controller_resolved_from_device(tmp_path):
ctrl = _mkctrl(tmp_path)
_mkdev(tmp_path, "1-2", vid=0x1, pid=0x2, ctrl=ctrl)
assert controller(tmp_path / "bus" / "1-2") == ctrl.resolve()
def test_device_carries_its_controllers_link_errors(tmp_path):
ctrl = _mkctrl(tmp_path, portli="0x0000000c")
_mkdev(tmp_path, "1-2", vid=0x1234, pid=0x5678, ctrl=ctrl)
(dev,) = get_usb_state(tmp_path / "bus")
assert dev["linkErrorCount"] == 12
def test_controller_count_without_any_enumerated_device(tmp_path):
# peripheral mode (eMac gadget link): the peer never enumerates on our side,
# so the counter must still be readable off the controller
_mkctrl(tmp_path, portli="0x00000005")
assert get_usb_state(tmp_path / "bus") == []
assert get_link_error_count(tmp_path / "soc") == 5
def test_link_errors_summed_across_controllers(tmp_path):
_mkctrl(tmp_path, name="a800000.ssusb", portli="0x00000002")
_mkctrl(tmp_path, name="a600000.ssusb", portli="0x00000003")
assert get_link_error_count(tmp_path / "soc") == 5
def test_missing_portli_reads_zero(tmp_path):
_mkctrl(tmp_path, portli=None) # pre-4.9.1 kernel: file absent
assert get_link_error_count(tmp_path / "soc") == 0
def test_set_usb_state_populates_message_and_flags_dock(tmp_path):
ctrl = _mkctrl(tmp_path, portli="0x00000007")
_mkdev(tmp_path, "1-1", vid=0x1234, pid=0x5678, speed=480, ctrl=ctrl)
_mkdev(tmp_path, "1-2", vid=EGPU_DOCK_USB_IDS[0][0], pid=EGPU_DOCK_USB_IDS[0][1], speed=5000, ctrl=ctrl)
msg = messaging.new_message('deviceState')
set_usb_state(msg.deviceState, get_usb_state(tmp_path / "bus"), get_link_error_count(tmp_path / "soc"))
devices = list(msg.deviceState.usbState.devices)
assert len(devices) == 2
assert {d.speedMbps for d in devices} == {480, 5000}
assert all(d.linkErrorCount == 7 for d in devices)
assert msg.deviceState.usbState.linkErrorCount == 7
assert msg.deviceState.egpuDockPresent
def test_dock_absent_when_not_plugged(tmp_path):
_mkdev(tmp_path, "1-1", vid=0x1234, pid=0x5678)
msg = messaging.new_message('deviceState')
set_usb_state(msg.deviceState, get_usb_state(tmp_path / "bus"))
assert not msg.deviceState.egpuDockPresent
def test_both_shipped_dock_usb_ids_detected(tmp_path):
# comma ships the dock under two VID/PIDs; only the first was known before
for i, (vid, pid) in enumerate(EGPU_DOCK_USB_IDS):
root = tmp_path / f"v{i}"
_mkdev(root, "1-1", vid=vid, pid=pid)
assert egpu_dock_present(root / "bus"), f"{vid:#06x}:{pid:#06x} not detected"
def test_dock_in_rom_mode_is_not_present(tmp_path):
# bootloader/ROM state enumerates but cannot serve a GPU until flashed
vid, pid = EGPU_DOCK_ROM_USB_IDS[0]
_mkdev(tmp_path, "1-1", vid=vid, pid=pid)
assert not egpu_dock_present(tmp_path / "bus")
def test_empty_bus_clears_flag():
msg = messaging.new_message('deviceState')
set_usb_state(msg.deviceState, [])
assert len(msg.deviceState.usbState.devices) == 0
assert msg.deviceState.usbState.linkErrorCount == 0
assert not msg.deviceState.egpuDockPresent
def test_link_error_count_masked_to_16_bits(tmp_path):
# the per-device field is UInt16 upstream; a wrapped counter must not overflow it
ctrl = _mkctrl(tmp_path, portli="0x0001ffff")
_mkdev(tmp_path, "1-2", vid=0x1, pid=0x2, ctrl=ctrl)
(dev,) = get_usb_state(tmp_path / "bus")
assert dev["linkErrorCount"] == 0xFFFF
msg = messaging.new_message('deviceState')
set_usb_state(msg.deviceState, get_usb_state(tmp_path / "bus"))
assert list(msg.deviceState.usbState.devices)[0].linkErrorCount == 0xFFFF
def test_usb_topology_lists_bus_entries(tmp_path):
_mkdev(tmp_path, "1-1", vid=0x1, pid=0x2)
_mkdev(tmp_path, "1-2", vid=0x3, pid=0x4)
assert {"1-1", "1-2"} <= get_usb_topology(tmp_path / "bus")
assert get_usb_topology(tmp_path / "nope") == set()
def _mkudc(root, name="a600000.dwc3"):
udc = root / "udc" / name
udc.mkdir(parents=True, exist_ok=True)
(udc / "state").write_text("not attached\n")
return root / "udc"
def test_link_controller_derived_from_udc_not_hardcoded(tmp_path):
# comma pins "a600000.ssusb"; we derive it, so another board still resolves
assert link_controller(_mkudc(tmp_path)) == "a600000.ssusb"
assert link_controller(_mkudc(tmp_path / "other", "a800000.dwc3")) == "a800000.ssusb"
def test_link_controller_absent_udc_is_empty(tmp_path):
assert link_controller(tmp_path / "nope") == ""
def test_usb3_lane_mapping():
assert usb3_lane(1) == "a"
assert usb3_lane(2) == "b"
assert usb3_lane(0) == "unknown" # unattached
assert usb3_lane(None if False else 7) == "unknown"
def test_port_lane_survives_gadget_mode(tmp_path):
"""The case upstream cannot report: in peripheral mode nothing enumerates on
the link controller, so every Device row is 'unknown' while the eMac link is
up. The port-level field still carries it."""
ctrl = _mkctrl(tmp_path, name="a800000.ssusb")
_mkdev(tmp_path, "1-1", vid=0x1234, pid=0x5678, ctrl=ctrl) # panda, host controller
_mkudc(tmp_path) # gadget is a600000
devices = get_usb_state(tmp_path / "bus", tmp_path / "udc")
assert all(d["usb3Lane"] == "unknown" for d in devices), "no device sits on the gadget controller"
msg = messaging.new_message('deviceState')
set_usb_state(msg.deviceState, devices, 0, lane="b")
assert msg.deviceState.usbState.usb3Lane == "b"
assert all(d.usb3Lane == "unknown" for d in msg.deviceState.usbState.devices)
def test_device_on_link_controller_gets_the_lane(tmp_path):
# host mode on the type-C port (eGPU dock): upstream's per-device field populates
ctrl = _mkctrl(tmp_path, name="a600000.ssusb")
_mkdev(tmp_path, "1-1", vid=EGPU_DOCK_USB_IDS[0][0], pid=EGPU_DOCK_USB_IDS[0][1], ctrl=ctrl)
_mkudc(tmp_path)
import iqpilot.system.hardware.usb as usbmod
orig = usbmod.usb3_lane
usbmod.usb3_lane = lambda orientation=None: "a"
try:
devices = get_usb_state(tmp_path / "bus", tmp_path / "udc")
finally:
usbmod.usb3_lane = orig
assert devices[0]["usb3Lane"] == "a"
def test_dock_ready_requires_the_bundled_firmware_product(tmp_path):
root = tmp_path
vid, pid = EGPU_DOCK_USB_IDS[0]
_mkdev(root, "1-1", vid=vid, pid=pid, product=EGPU_DOCK_FW_PRODUCT)
assert egpu_dock_present(root / "bus")
assert egpu_dock_ready(root / "bus")
def test_dock_on_foreign_firmware_is_present_but_not_ready(tmp_path):
root = tmp_path
vid, pid = EGPU_DOCK_USB_IDS[0]
_mkdev(root, "1-1", vid=vid, pid=pid, product="custom deadbeef-CLEAN")
assert egpu_dock_present(root / "bus")
assert not egpu_dock_ready(root / "bus")
def test_rom_mode_dock_is_neither_present_nor_ready(tmp_path):
root = tmp_path
vid, pid = EGPU_DOCK_ROM_USB_IDS[0]
_mkdev(root, "1-1", vid=vid, pid=pid, product="USB 3.2 PCIe TinyEnclosure")
assert not egpu_dock_present(root / "bus")
assert not egpu_dock_ready(root / "bus")

View File

@@ -78,32 +78,38 @@ unbind() {
fi
}
set_attr() {
[ "$(cat "$1" 2>/dev/null)" = "$2" ] && return 0
echo "$2" | sudo tee "$1" >/dev/null 2>&1 || true
}
ensure_base() {
if ! mountpoint -q /config; then
sudo mount -t configfs none /config
fi
sudo mkdir -p "$GADGET/strings/0x409" "$GADGET/configs/c.1/strings/0x409"
cd "$GADGET"
[ -s idVendor ] || echo 0x04D8 | sudo tee idVendor >/dev/null
[ -s idProduct ] || echo 0x1235 | sudo tee idProduct >/dev/null
[ -s strings/0x409/serialnumber ] || echo "$(cat /proc/cmdline | sed -e 's/^.*androidboot.serialno=//' -e 's/ .*$//')" | sudo tee strings/0x409/serialnumber >/dev/null
[ -s strings/0x409/manufacturer ] || echo "comma.ai" | sudo tee strings/0x409/manufacturer >/dev/null
[ -s strings/0x409/product ] || echo "IQ.Pilot" | sudo tee strings/0x409/product >/dev/null
[ -s configs/c.1/MaxPower ] || echo 250 | sudo tee configs/c.1/MaxPower >/dev/null
[ -s configs/c.1/strings/0x409/configuration ] || echo "IQ.Pilot" | sudo tee configs/c.1/strings/0x409/configuration >/dev/null
# `[ -s ]` never guards a configfs attribute: an unset idVendor still reads back
# as "0x0000", so those writes were all skipped and the gadget stayed nameless
set_attr idVendor 0x04D8
set_attr idProduct 0x1235
set_attr strings/0x409/serialnumber "$(sed -e 's/^.*androidboot.serialno=//' -e 's/ .*$//' /proc/cmdline)"
set_attr strings/0x409/manufacturer "comma.ai"
set_attr strings/0x409/product "IQ.Pilot"
set_attr configs/c.1/MaxPower 250
set_attr configs/c.1/strings/0x409/configuration "IQ.Pilot"
}
add_adb() {
# same rationale as add_mass_storage: start from a clean slate to avoid stale busy attributes
remove_adb
cd "$GADGET"
sudo mkdir -p functions/ncm.0 functions/ffs.adb
sudo mkdir -p functions/ffs.adb
sudo mkdir -p /dev/usb-ffs/adb
if ! mountpoint -q /dev/usb-ffs/adb; then
sudo mount -t functionfs adb /dev/usb-ffs/adb
fi
sudo rm -f configs/c.1/ncm.0 configs/c.1/ffs.adb
sudo ln -s functions/ncm.0 configs/c.1/
sudo rm -f configs/c.1/ffs.adb
sudo ln -s functions/ffs.adb configs/c.1/
setprop service.adb.tcp.port -1 2>/dev/null || true
sudo systemctl start adbd
@@ -115,9 +121,42 @@ remove_adb() {
sudo systemctl stop adbd || true
if [ -d "$GADGET" ]; then
cd "$GADGET"
sudo rm -f configs/c.1/ncm.0 configs/c.1/ffs.adb
sudo rm -f configs/c.1/ffs.adb
sudo umount /dev/usb-ffs/adb 2>/dev/null || true
sudo rmdir functions/ncm.0 functions/ffs.adb 2>/dev/null || true
sudo rmdir functions/ffs.adb 2>/dev/null || true
fi
}
# ncm carries the usb0 ethernet link. ADB needs it, but so does the Mac-backed
# model worker with ADB off, so it is enabled independently of either.
# the kernel randomises the ncm MACs every boot, so macOS sees a new adapter each
# time and orphans the network service holding the link's static address
ncm_id() {
local id
id=$(tr -dc '0-9a-f' < /data/params/d/DongleId 2>/dev/null | tail -c 6)
[ ${#id} -eq 6 ] || id="000001"
echo "$id"
}
add_ncm() {
remove_ncm
cd "$GADGET"
sudo mkdir -p functions/ncm.0
local id
id=$(ncm_id)
# best effort: some kernels create the ncm netdev lazily and fail these writes
# with ENODEV, and a pinned MAC is never worth losing the whole gadget over
echo "02:49:51:${id:0:2}:${id:2:2}:${id:4:2}" | sudo tee functions/ncm.0/host_addr >/dev/null 2>&1 || true
echo "06:49:51:${id:0:2}:${id:2:2}:${id:4:2}" | sudo tee functions/ncm.0/dev_addr >/dev/null 2>&1 || true
sudo rm -f configs/c.1/ncm.0
sudo ln -s functions/ncm.0 configs/c.1/
}
remove_ncm() {
if [ -d "$GADGET" ]; then
cd "$GADGET"
sudo rm -f configs/c.1/ncm.0
sudo rmdir functions/ncm.0 2>/dev/null || true
fi
}
@@ -162,10 +201,18 @@ USB_STORAGE_ENABLE=0
read_bool_param "/data/params/d/UsbStorageEnabled" && USB_STORAGE_ENABLE=1
ADB_ENABLE=0
read_bool_param "/data/params/d/AdbEnabled" && ADB_ENABLE=1
EMAC_ENABLE=0
read_bool_param "/data/params/d/IQEmacEnabled" && EMAC_ENABLE=1
unbind
ensure_base
if [ "$ADB_ENABLE" == "1" ] || [ "$EMAC_ENABLE" == "1" ]; then
add_ncm
else
remove_ncm
fi
if [ "$ADB_ENABLE" == "1" ]; then
add_adb
else

View File

@@ -15,3 +15,57 @@ def apply_usb_storage_state(state: bool):
subprocess.Popen(args)
except OSError:
pass
NCM_TRIED_MARKER = "/tmp/.iqemac_ncm_provisioned"
MAX_NCM_ATTEMPTS = 3
def _ncm_attempts() -> int:
try:
with open(NCM_TRIED_MARKER) as f:
return int(f.read().strip() or 0)
except (OSError, ValueError):
return 0
def ensure_ncm_gadget() -> bool:
# configfs is RAM backed, so the gadget must be rebuilt every boot; binding it
# enumerates on the host, which must not happen mid-drive
if os.path.isdir("/sys/class/net/usb0"):
return True
params = Params()
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
if not (params.get_bool("IQEmacEnabled") or egpu_selected(params)):
return False
attempts = _ncm_attempts()
if attempts >= MAX_NCM_ATTEMPTS:
return False
try:
# stamped before the run: the gadget build can wedge configfs in
# uninterruptible D state, and retrying that forever helps nobody
with open(NCM_TRIED_MARKER, "w") as f:
f.write(str(attempts + 1))
subprocess.run(["sudo", "-n", SCRIPT_PATH], timeout=120, check=False)
except (OSError, subprocess.SubprocessError):
return False
return os.path.isdir("/sys/class/net/usb0")
INPUT_SUSPEND = "/sys/class/power_supply/battery/input_suspend"
def suspend_usb_input(suspend: bool = True) -> bool:
# a host on the data port makes the PMIC sink USB-PD while OBD-C feeds the same
# rail; the SOM browns out. This closes the charge path only, data is untouched
if not os.path.exists(INPUT_SUSPEND):
return False
try:
with open(INPUT_SUSPEND) as f:
if f.read().strip() == ("1" if suspend else "0"):
return True
except OSError:
pass
rc = subprocess.run(["sudo", "-n", "sh", "-c", f"echo {int(suspend)} > {INPUT_SUSPEND}"],
check=False, capture_output=True)
return rc.returncode == 0

View File

@@ -0,0 +1,184 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
USB bus snapshot for deviceState: every enumerated device with its negotiated
speed and its controller's link-error count. Landing this in every rlog makes
cable/hub/link regressions diagnosable from a recorded route instead of only
live.
Link errors come from `portli` on the ssusb controller (IQ.OS 4.9.1+); on older
builds the file is absent and the counts read 0.
The USB eGPU dock is identified by VID/PID only. comma's internal codename for
it is deliberately not used here: IQ.Pilot runs these models on several
backends (eGPU dock, eMac), so the naming stays about the role, not the vendor.
"""
from pathlib import Path
# comma's USB eGPU dock, both shipped USB IDs. The ROM ids are the same board
# sitting in its bootloader (ASMedia) before vendor firmware is flashed — it
# enumerates but cannot serve a GPU in that state.
EGPU_DOCK_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
EGPU_DOCK_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
# must equal image_product() of the bundled firmware; test_egpu_dock_flash pins them together
EGPU_DOCK_FW_PRODUCT = "custom ed4e39b7-CLEAN"
def is_egpu_usb_device(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
ids = EGPU_DOCK_USB_IDS + EGPU_DOCK_ROM_USB_IDS if include_bootloader else EGPU_DOCK_USB_IDS
return (vendor_id, product_id) in ids
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
UDC_PATH = Path("/sys/class/udc")
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
USB3_LANES = {1: "a", 2: "b"} # 0 = unattached
SOC_PLATFORM_PATH = Path("/sys/devices/platform/soc")
CONTROLLER_SUFFIX = ".ssusb"
LINK_ERRORS_FILE = "portli"
def read(path: Path) -> str | None:
# a controller in peripheral mode fails portli's show(); that surfaces as TypeError, not OSError
try:
return path.read_text().strip()
except Exception:
return None
def read_int(path: Path, base: int = 10) -> int:
try:
return int(path.read_text(), base)
except Exception:
return 0
def read_hex_counter(path: Path) -> int:
"""sysfs counter printed as '0x0000002a' (portli), tolerating a bare hex value."""
raw = read(path)
if raw is None:
return 0
try:
return int(raw, 0) if raw.lower().startswith("0x") else int(raw, 16)
except ValueError:
return 0
def get_usb_topology(root: Path = USB_DEVICES_PATH) -> set[str]:
"""Names of everything on the bus; a cheap way to detect hotplug without
re-reading every attribute."""
try:
return {p.name for p in root.iterdir()}
except Exception:
return set()
def usb_devices(root: Path = USB_DEVICES_PATH) -> list[Path]:
try:
return sorted((d for d in root.glob("*") if (d / "idVendor").exists()), key=lambda p: p.name)
except Exception:
return []
def controller(device: Path) -> Path | None:
"""The SuperSpeed controller a device hangs off (…/a800000.ssusb)."""
try:
return next((p for p in device.resolve().parents if p.name.endswith(CONTROLLER_SUFFIX)), None)
except Exception:
return None
def usb_controllers(soc: Path = SOC_PLATFORM_PATH) -> list[Path]:
try:
return sorted(soc.glob(f"*{CONTROLLER_SUFFIX}"))
except Exception:
return []
def link_controller(udc_root: Path = UDC_PATH) -> str:
"""Name of the Type-C port's controller, derived from the UDC rather than
hardcoded: the gadget exposes `<addr>.dwc3`, whose address prefix is the
`<addr>.ssusb` controller behind the same connector. comma pins the 3X value
directly, which would be wrong on any other board."""
try:
udc = next(iter(sorted(p.name for p in udc_root.iterdir())), "")
except Exception:
return ""
return f"{udc.split('.')[0]}{CONTROLLER_SUFFIX}" if udc else ""
def usb3_lane(orientation: int | None = None) -> str:
"""Which SuperSpeed lane the Type-C connector landed on. Unattached reads 0,
which is 'unknown' rather than a lane."""
if orientation is None:
orientation = read_int(TYPEC_CC_ORIENTATION_PATH)
return USB3_LANES.get(orientation, "unknown")
def link_errors(ctrl: Path | None) -> int:
return read_hex_counter(ctrl / LINK_ERRORS_FILE) if ctrl is not None else 0
def get_link_error_count(soc: Path = SOC_PLATFORM_PATH) -> int:
"""Cumulative SS port link errors, read off the controller rather than a
device: in peripheral mode (eMac gadget link) the peer never enumerates on
our side, so there is no device row to carry the count."""
return sum(link_errors(c) for c in usb_controllers(soc))
def egpu_dock_present(root: Path = USB_DEVICES_PATH) -> bool:
"""A dock in ROM/bootloader state is deliberately NOT counted as present: it
enumerates but cannot serve a GPU until vendor firmware is flashed."""
return any((read_int(d / "idVendor", 16), read_int(d / "idProduct", 16)) in EGPU_DOCK_USB_IDS
for d in usb_devices(root))
def egpu_dock_ready(root: Path = USB_DEVICES_PATH) -> bool:
"""Present AND running the exact firmware we ship. A dock on any other
firmware enumerates fine but has not been validated with this stack, so the
runtime refuses it; the flasher still sees it via egpu_dock_present."""
return any((read_int(d / "idVendor", 16), read_int(d / "idProduct", 16)) in EGPU_DOCK_USB_IDS
and (read(d / "product") or "").strip() == EGPU_DOCK_FW_PRODUCT
for d in usb_devices(root))
def get_usb_state(root: Path = USB_DEVICES_PATH, udc_root: Path = UDC_PATH) -> list[dict]:
devices = []
lane, link_ctrl = usb3_lane(), link_controller(udc_root)
for device in usb_devices(root):
ctrl = controller(device)
devices.append({
"usb3Lane": lane if ctrl is not None and ctrl.name == link_ctrl else "unknown",
"busnum": read_int(device / "busnum"),
"devnum": read_int(device / "devnum"),
"vendorId": read_int(device / "idVendor", 16),
"productId": read_int(device / "idProduct", 16),
"speedMbps": read_int(device / "speed"),
"manufacturer": read(device / "manufacturer") or "",
"product": read(device / "product") or "",
# 16-bit field upstream, so mask rather than let a wrapped counter overflow it
"linkErrorCount": link_errors(ctrl) & 0xFFFF,
})
return devices
def set_usb_state(device_state, devices: list[dict], link_error_count: int = 0,
lane: str | None = None) -> None:
entries = device_state.usbState.init('devices', len(devices))
dock_present = False
for entry, device in zip(entries, devices, strict=True):
entry.busnum = device["busnum"]
entry.devnum = device["devnum"]
entry.vendorId = device["vendorId"]
entry.productId = device["productId"]
entry.speedMbps = device["speedMbps"]
entry.manufacturer = device["manufacturer"]
entry.product = device["product"]
entry.linkErrorCount = device.get("linkErrorCount", 0) & 0xFFFF
entry.usb3Lane = device.get("usb3Lane", "unknown")
if (entry.vendorId, entry.productId) in EGPU_DOCK_USB_IDS:
dock_present = True
device_state.usbState.linkErrorCount = link_error_count
device_state.usbState.usb3Lane = lane if lane is not None else usb3_lane()
device_state.egpuDockPresent = dock_present

View File

@@ -53,6 +53,16 @@ def manager_init() -> None:
except Exception:
cloudlog.exception("recover_unclean_segments failed")
try:
from iqpilot.system.hardware import TICI
if TICI:
from iqpilot.system.hardware.tici.usb_storage import ensure_ncm_gadget, suspend_usb_input
ensure_ncm_gadget()
if Params().get_bool("IQEmacEnabled") or Params().get_bool("IQEgpuEnabled"):
suspend_usb_input(True)
except Exception:
cloudlog.exception("emac usb setup failed")
build_metadata = get_build_metadata()
params = Params()

View File

@@ -8,6 +8,7 @@ from iqpilot.system.hardware import HARDWARE, PC, TICI
from iqpilot.system.hardware.hw import Paths
from iqpilot.system.manager.process import PythonProcess, NativeProcess, BundleProcess
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected, resolve_backend, usbgpu_present
from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_model_runner
from iqpilot.konn3kt.service_health import hephaestus_ready
@@ -124,6 +125,22 @@ def is_tinygrad_model(started, params, CP: car.CarParams) -> bool:
"""Check if the active model runner is tinygrad."""
return bool(get_active_model_runner(params, not started) == custom.IQModelManager.Runner.tinygrad)
def _egpu_present(params) -> bool:
if params.get_bool("IQEgpuDisabled"):
return False
return usbgpu_present()
def emac_enabled(started, params, CP: car.CarParams) -> bool:
return resolve_backend(params.get_bool("IQEmacEnabled"), egpu_selected(params), _egpu_present(params)) == "emac"
def egpu_enabled(started, params, CP: car.CarParams) -> bool:
return (resolve_backend(params.get_bool("IQEmacEnabled"), egpu_selected(params), _egpu_present(params)) == "egpu"
and _egpu_present(params))
def big_model_enabled(started, params, CP: car.CarParams) -> bool:
return params.get_bool("IQEmacEnabled") or egpu_selected(params)
def hephaestus_ready_shim(started, params, CP: car.CarParams) -> bool:
return hephaestus_ready(params)
@@ -196,6 +213,15 @@ procs += [
# Models
BundleProcess("models_manager", "iqpilot_model_selector_private", "iqpilot_private.models.manager", and_(only_offroad, not_low_power)),
NativeProcess("iqmodeld", "iqpilot/selfdrive/iqmodeld", ["./iqmodeld"], and_(only_onroad, is_tinygrad_model), restart_if_crash=True),
# big-model backends: iqmodeld self-demotes to the small channel worker when
# either backend is enabled; the selector publishes, and exactly one big
# worker (Mac or eGPU, eMac wins) feeds the BIG channel
PythonProcess("modeld_selector", "iqpilot.selfdrive.iqmodeld.modeld_selector",
and_(only_onroad, and_(is_tinygrad_model, big_model_enabled)), restart_if_crash=True),
BundleProcess("maciqmodeld", "iqpilot_emac_private", "iqpilot_private.emac.maciqmodeld",
and_(only_onroad, and_(is_tinygrad_model, emac_enabled)), restart_if_crash=True),
PythonProcess("iqegpumodeld", "iqpilot.selfdrive.iqmodeld.iqegpumodeld",
and_(only_onroad, and_(is_tinygrad_model, egpu_enabled)), restart_if_crash=True),
BundleProcess("backup_manager_k3", "iqpilot_hephaestusd_private", "iqpilot_private.konn3kt.backups.backup_orchestrator",
and_(only_offroad, hephaestus_ready_shim, not_low_power)),