From 091e08e893aa600d129a65cdcf37e4fdb7031109 Mon Sep 17 00:00:00 2001 From: "IQ.Lvbs CI [bot]" Date: Mon, 31 Aug 2026 21:43:09 -0500 Subject: [PATCH] IQ.Pilot Release Commit @ 92b3bb6 --- iqpilot/selfdrive/iqmodeld/iqegpumodeld.py | 4 ++ iqpilot/selfdrive/pandad/panda.cc | 7 +- iqpilot/selfdrive/pandad/panda.h | 1 + iqpilot/selfdrive/pandad/pandad.py | 2 + .../pandad/tests/test_pandad_usbprotocol.cc | 8 +++ .../system/hardware/tests/test_usb_state.py | 12 ++++ iqpilot/system/hardware/usb.py | 21 ++++++ iqpilot/tools/iq.sh | 6 +- iqpilot/tools/tests/test_iq_command.py | 66 +++++++++++++++++++ 9 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 iqpilot/tools/tests/test_iq_command.py diff --git a/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py b/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py index 69cc190aa..bc5757c49 100644 --- a/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py +++ b/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py @@ -300,6 +300,10 @@ def main(demo: bool = False) -> None: cloudlog.error(f"iqegpumodeld giving up after {attempt} setup failures; exiting for a clean restart") sys.exit(1) if not usbgpu_present(): + from iqpilot.system.hardware.usb import ensure_host_role + if ensure_host_role(): + cloudlog.warning("iqegpumodeld: Type-C controller was out of host mode; restored") + time.sleep(2.0) _wait_for_egpu(params) time.sleep(min(SETUP_RETRY_MAX_S, SETUP_RETRY_BASE_S * attempt)) diff --git a/iqpilot/selfdrive/pandad/panda.cc b/iqpilot/selfdrive/pandad/panda.cc index 48df89222..458f09e49 100644 --- a/iqpilot/selfdrive/pandad/panda.cc +++ b/iqpilot/selfdrive/pandad/panda.cc @@ -12,6 +12,11 @@ const bool PANDAD_MAXOUT = getenv("PANDAD_MAXOUT") != nullptr; +std::string panda_firmware_path() { + const char *runtime_path = getenv("IQPILOT_PANDA_FW_PATH"); + return runtime_path != nullptr ? runtime_path : PANDA_FW_PATH; +} + Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) { // try USB first, then SPI try { @@ -138,7 +143,7 @@ std::optional Panda::get_serial() { bool Panda::up_to_date() { if (auto fw_sig = get_firmware_version()) { for (auto fn : { "panda.bin.signed", "panda_h7.bin.signed" }) { - auto content = util::read_file(std::string(PANDA_FW_PATH) + fn); + auto content = util::read_file(panda_firmware_path() + fn); if (content.size() >= fw_sig->size() && memcmp(content.data() + content.size() - fw_sig->size(), fw_sig->data(), fw_sig->size()) == 0) { return true; diff --git a/iqpilot/selfdrive/pandad/panda.h b/iqpilot/selfdrive/pandad/panda.h index 38b89aee9..b1b4807a4 100644 --- a/iqpilot/selfdrive/pandad/panda.h +++ b/iqpilot/selfdrive/pandad/panda.h @@ -42,6 +42,7 @@ struct can_frame { long src; }; +std::string panda_firmware_path(); class Panda { private: diff --git a/iqpilot/selfdrive/pandad/pandad.py b/iqpilot/selfdrive/pandad/pandad.py index d09753b81..ca7a7e47b 100755 --- a/iqpilot/selfdrive/pandad/pandad.py +++ b/iqpilot/selfdrive/pandad/pandad.py @@ -77,6 +77,8 @@ def check_panda_support(panda) -> bool: def main() -> None: + os.environ["IQPILOT_PANDA_FW_PATH"] = FW_PATH + # signal pandad to close the relay and exit def signal_handler(signum, frame): cloudlog.info(f"Caught signal {signum}, exiting") diff --git a/iqpilot/selfdrive/pandad/tests/test_pandad_usbprotocol.cc b/iqpilot/selfdrive/pandad/tests/test_pandad_usbprotocol.cc index 11f7184ef..0d702cf20 100644 --- a/iqpilot/selfdrive/pandad/tests/test_pandad_usbprotocol.cc +++ b/iqpilot/selfdrive/pandad/tests/test_pandad_usbprotocol.cc @@ -133,3 +133,11 @@ TEST_CASE("send/recv CAN FD packets") { test.test_can_recv(0x40); } } + +TEST_CASE("firmware path runtime override") { + unsetenv("IQPILOT_PANDA_FW_PATH"); + REQUIRE(panda_firmware_path() == PANDA_FW_PATH); + setenv("IQPILOT_PANDA_FW_PATH", "/runtime/panda/board/obj/", 1); + REQUIRE(panda_firmware_path() == "/runtime/panda/board/obj/"); + unsetenv("IQPILOT_PANDA_FW_PATH"); +} diff --git a/iqpilot/system/hardware/tests/test_usb_state.py b/iqpilot/system/hardware/tests/test_usb_state.py index ab4e6c148..8a536c22f 100644 --- a/iqpilot/system/hardware/tests/test_usb_state.py +++ b/iqpilot/system/hardware/tests/test_usb_state.py @@ -265,3 +265,15 @@ def test_rom_mode_dock_is_neither_present_nor_ready(tmp_path): _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") + + +class TestEnsureHostRole: + def test_already_host_needs_no_write(self, tmp_path): + from iqpilot.system.hardware.usb import ensure_host_role + mode = tmp_path / "mode" + mode.write_text("host\n") + assert ensure_host_role(mode) + + def test_missing_controller_is_false(self, tmp_path): + from iqpilot.system.hardware.usb import ensure_host_role + assert not ensure_host_role(tmp_path / "absent" / "mode") diff --git a/iqpilot/system/hardware/usb.py b/iqpilot/system/hardware/usb.py index 15be6f295..8c8a2d7e8 100644 --- a/iqpilot/system/hardware/usb.py +++ b/iqpilot/system/hardware/usb.py @@ -13,6 +13,7 @@ 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. """ +import subprocess from pathlib import Path # comma's USB eGPU dock, both shipped USB IDs. The ROM ids are the same board @@ -124,6 +125,26 @@ def get_link_error_count(soc: Path = SOC_PLATFORM_PATH) -> int: return sum(link_errors(c) for c in usb_controllers(soc)) +def host_role_controller(soc: Path = SOC_PLATFORM_PATH, udc_root: Path = UDC_PATH) -> Path | None: + ctrl = link_controller(udc_root) + return (soc / ctrl / "mode") if ctrl else None + + +def ensure_host_role(mode_path: Path | None = None) -> bool: + """A usbpd blip mid-drive can leave the Type-C controller in 'none'/'peripheral', and the + dock can never re-enumerate until something puts the port back into host mode.""" + path = mode_path if mode_path is not None else host_role_controller() + if path is None: + return False + current = read(path) + if current == "host": + return True + if current is None: + return False + rc = subprocess.run(["sudo", "-n", "sh", "-c", f"echo host > {path}"], check=False, capture_output=True) + return rc.returncode == 0 and read(path) == "host" + + 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.""" diff --git a/iqpilot/tools/iq.sh b/iqpilot/tools/iq.sh index e0b6d9edc..9426f26f0 100755 --- a/iqpilot/tools/iq.sh +++ b/iqpilot/tools/iq.sh @@ -154,7 +154,11 @@ iq_pkg() { # the venv python has the component packages (iqdbc etc.); bare python3 does not local py=python3 [[ -x "$IQ_ROOT/.venv/bin/python3" ]] && py="$IQ_ROOT/.venv/bin/python3" - (cd "$IQ_ROOT" && iq_run "$py" iqpilot/tools/scripts/setup_private_packages.py "$@") + if [[ -f "$IQ_ROOT/iqpilot/tools/scripts/setup_private_packages.py" ]]; then + (cd "$IQ_ROOT" && iq_run "$py" iqpilot/tools/scripts/setup_private_packages.py "$@") + else + iq_note 'private package sources are not present in this checkout' + fi if [[ -f "$IQ_ROOT/artifacts/runtime/ensure_private_installed.sh" ]]; then (cd "$IQ_ROOT" && iq_run bash artifacts/runtime/ensure_private_installed.sh) fi diff --git a/iqpilot/tools/tests/test_iq_command.py b/iqpilot/tools/tests/test_iq_command.py new file mode 100644 index 000000000..79f557d5d --- /dev/null +++ b/iqpilot/tools/tests/test_iq_command.py @@ -0,0 +1,66 @@ +# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos + +import os +from pathlib import Path +import subprocess + + +IQ_COMMAND = Path(__file__).parents[1] / "iq.sh" + + +def make_checkout(tmp_path: Path) -> Path: + checkout = tmp_path / "checkout" + (checkout / "iqpilot").mkdir(parents=True) + (checkout / "launch_iqpilot.sh").touch() + return checkout + + +def run_pkg(checkout: Path, path: Path) -> subprocess.CompletedProcess[str]: + env = os.environ.copy() + env["PATH"] = f"{path}:{env['PATH']}" + return subprocess.run( + ["bash", str(IQ_COMMAND), "--dir", str(checkout), "pkg"], + check=False, + capture_output=True, + env=env, + text=True, + ) + + +def test_public_checkout_skips_private_package_source_setup(tmp_path: Path): + checkout = make_checkout(tmp_path) + + result = run_pkg(checkout, tmp_path) + + assert result.returncode == 0 + assert "private package sources are not present" in result.stdout + assert "setup_private_packages.py" not in result.stderr + + +def test_internal_checkout_runs_private_package_source_setup(tmp_path: Path): + checkout = make_checkout(tmp_path) + setup_script = checkout / "iqpilot/tools/scripts/setup_private_packages.py" + setup_script.parent.mkdir(parents=True) + setup_script.touch() + python_log = tmp_path / "python.log" + python = tmp_path / "python3" + python.write_text(f"#!/usr/bin/env bash\nprintf '%s\\n' \"$*\" > {python_log!s}\n") + python.chmod(0o755) + + result = run_pkg(checkout, tmp_path) + + assert result.returncode == 0 + assert python_log.read_text().strip() == "iqpilot/tools/scripts/setup_private_packages.py" + + +def test_public_checkout_runs_bundled_package_installer(tmp_path: Path): + checkout = make_checkout(tmp_path) + installer_log = tmp_path / "installer.log" + installer = checkout / "artifacts/runtime/ensure_private_installed.sh" + installer.parent.mkdir(parents=True) + installer.write_text(f"printf installed > {installer_log!s}\n") + + result = run_pkg(checkout, tmp_path) + + assert result.returncode == 0 + assert installer_log.read_text() == "installed"