diff --git a/.gitignore b/.gitignore index c6b86f05..d733b470 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ compare_runtime*.html iqpilot/selfdrive/pandad/pandad iqpilot/selfdrive/pandad/tests/test_panda_safety +iqpilot/selfdrive/pandad/tests/test_ignition_hold iqpilot/cereal/services.h iqpilot/cereal/gen iqpilot/cereal/messaging/bridge @@ -180,3 +181,5 @@ iqpilot/tools/iqpilot/big_hud_preview.py artifacts/monrovia_runtime_packages/ artifacts/iqpilot_*_private/consumer.challenge /docs/ +/dist/ +nuitka-crash-report* diff --git a/LICENSE b/LICENSE index 688ca902..6a859020 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -IQ.Lvbs License v1.8.2 +IQ.Lvbs License v1.8.3 Copyright (c) 2026 IQ.Lvbs LLC, a part of Project Teal Lvbs Inc. All Rights Reserved. @@ -42,7 +42,8 @@ You may NOT: 5. Sublicense, sell, or transfer rights to the Software. 6. Use the Software and/or its source code to compete with or create a - substantially similar product. + substantially similar product that is not a personal non-commercial fork of IQ.Pilot that complies with + all other subsections of this license. 7. Use the Software in closed source software not licensed by IQ.Lvbs LLC. @@ -54,9 +55,8 @@ OWNERSHIP All rights, title, and interest in the Software remain exclusively with the Copyright Holder. Any modifications, improvements, or derivative works you -create based on the Software are owned by the Copyright Holder. By -contributing modifications, you irrevocably assign all rights to the -Copyright Holder. +create based on the Software are owned by you (licensing of that work is in no manner the liability of IQ Lvbs or its associates). +Any derivative in which IQ.Pilot source code is used and modified shall remain the property of IQ Lvbs with the exception of the modifications you have added. PROPRIETARY COMPONENTS diff --git a/artifacts/iqpilot_alc_private/.source_sha b/artifacts/iqpilot_alc_private/.source_sha deleted file mode 100644 index d3f47f5f..00000000 --- a/artifacts/iqpilot_alc_private/.source_sha +++ /dev/null @@ -1 +0,0 @@ -76a146832ea70711033c567d35af66df13221c2e diff --git a/artifacts/iqpilot_alc_private/manifest.json b/artifacts/iqpilot_alc_private/manifest.json deleted file mode 100644 index d97a74b5..00000000 --- a/artifacts/iqpilot_alc_private/manifest.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "python/_iqclosure/_vendored.json": { - "mode": 420, - "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", - "size": 3 - }, - "python/iqpilot_private/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/iqpilot_private/konn3kt/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/iqpilot_private/konn3kt/iqlvbs/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "16ce15884bfd765c17c05da0e2474aa1e7ddd8fcf68925b2169e19042235b4b6", - "size": 469760 - }, - "python/iqpilot_private/konn3kt/iqlvbs/vehicle_state.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "d5afc804c71ebd0c690035cbec639f5f07112b3a7080615e6430b27a1739984f", - "size": 69608 - }, - "runtime": { - "entries": { - "iqpilot_private.konn3kt.iqlvbs.alc": { - "path": "python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so", - "type": "python-module" - }, - "iqpilot_private.konn3kt.iqlvbs.vehicle_state": { - "path": "python/iqpilot_private/konn3kt/iqlvbs/vehicle_state.cpython-312-aarch64-linux-gnu.so", - "type": "python-module" - } - } - }, - "signatures": { - "python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so": "3OWEWh1t7VakRznr0JljvzvODcR9GG1lU9Fx0Bu+J+m7u+U8JdOeoBSjy0DQSKPXpZ4jgi4ieVGXihJME5AsCg==", - "python/iqpilot_private/konn3kt/iqlvbs/vehicle_state.cpython-312-aarch64-linux-gnu.so": "XzMSXupOCeoHX/pCV7X1kuDRw02qrCxJuuwEmSC50h9jy4ySWHk9vflvwRW3u+A6hZ9bljsK6wy0oFXZNoXcAg==" - } -} diff --git a/artifacts/iqpilot_alc_private/manifest.json.sig b/artifacts/iqpilot_alc_private/manifest.json.sig deleted file mode 100644 index 33a07343..00000000 --- a/artifacts/iqpilot_alc_private/manifest.json.sig +++ /dev/null @@ -1 +0,0 @@ -zfDjghpCSIizLkZR7YlPtvZQirhuMbIYJahB/pGnNaffGIWuXGM7NPoTol0XxKdoMYvCDCu7Kj5YcAdfYoFoBQ== diff --git a/artifacts/iqpilot_alc_private/python/_iqclosure/_vendored.json b/artifacts/iqpilot_alc_private/python/_iqclosure/_vendored.json deleted file mode 100644 index fe51488c..00000000 --- a/artifacts/iqpilot_alc_private/python/_iqclosure/_vendored.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index 6e22c91b..00000000 Binary files a/artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/iqlvbs/vehicle_state.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/iqlvbs/vehicle_state.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index a635d859..00000000 Binary files a/artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/iqlvbs/vehicle_state.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_commander_private/manifest.json b/artifacts/iqpilot_commander_private/manifest.json index ed3f58d9..58e5b6c1 100644 --- a/artifacts/iqpilot_commander_private/manifest.json +++ b/artifacts/iqpilot_commander_private/manifest.json @@ -1,8 +1,94 @@ { + "bundle_version": 1792551365, "python/_iqclosure/_vendored.json": { "mode": 420, - "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", - "size": 3 + "sha256": "ca67a05945042c0f65f1b48482d9f8effa71448326f7d6ef799ec91ead96a8c9", + "size": 454 + }, + "python/_iqclosure/iqpilot/__init__.py": { + "mode": 420, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0 + }, + "python/_iqclosure/iqpilot/common/__init__.py": { + "mode": 420, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0 + }, + "python/_iqclosure/iqpilot/common/gpio.py": { + "mode": 420, + "sha256": "84edc2cba235b774293cbb1c52c6da74856425ff8ad99198997e23a05e784bb7", + "size": 2166 + }, + "python/_iqclosure/iqpilot/common/i2c.py": { + "mode": 420, + "sha256": "4af8f9da7504a548d15f18b91d71c1379cb7c05c84be031448764cc49920ce68", + "size": 2449 + }, + "python/_iqclosure/iqpilot/common/time_helpers.py": { + "mode": 420, + "sha256": "c14cda1f84149d5ffa63a2adec5ca24126f77c1412a78412e5e3e68a6fd12bcf", + "size": 474 + }, + "python/_iqclosure/iqpilot/common/utils.py": { + "mode": 420, + "sha256": "c39b89a1ab34511dfa5f9efac783fde0e6d616963a416cf472bd1f604b57f84d", + "size": 5892 + }, + "python/_iqclosure/iqpilot/system/__init__.py": { + "mode": 420, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0 + }, + "python/_iqclosure/iqpilot/system/hardware/__init__.py": { + "mode": 420, + "sha256": "889391bffca493e74bfc108cae2976a53b644caca13ab600061fd29e9622c627", + "size": 695 + }, + "python/_iqclosure/iqpilot/system/hardware/base.py": { + "mode": 420, + "sha256": "a56293f7c23e503ff62d0d3ed39f1831b5544a65b37378165934361a5ddcd304", + "size": 4966 + }, + "python/_iqclosure/iqpilot/system/hardware/pc/__init__.py": { + "mode": 420, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0 + }, + "python/_iqclosure/iqpilot/system/hardware/pc/hardware.py": { + "mode": 420, + "sha256": "2520895597c375ae570bd04d5f189e874c89d642f3f07bdbeb20388cb6691433", + "size": 258 + }, + "python/_iqclosure/iqpilot/system/hardware/tici/__init__.py": { + "mode": 420, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0 + }, + "python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py": { + "mode": 420, + "sha256": "38e521356c56ac85f35543b9457d36639e55ed2ac916f30ea3b86948e806541f", + "size": 8016 + }, + "python/_iqclosure/iqpilot/system/hardware/tici/hardware.py": { + "mode": 420, + "sha256": "da5ecb280f7af14169903260dc941e3bfac033c93b184cb0abea0f1494c34582", + "size": 25731 + }, + "python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py": { + "mode": 420, + "sha256": "d22858982705531732d5a20a45a73795496e5bbd9e23028c24609a7e58ac7aed", + "size": 890 + }, + "python/_iqclosure/iqpilot/system/hardware/tici/lpa.py": { + "mode": 420, + "sha256": "2bffb84aef35892c4788e80b6d937bf2eebd15e226b72a82c4f39217667c1ca0", + "size": 53692 + }, + "python/_iqclosure/iqpilot/system/hardware/tici/pins.py": { + "mode": 420, + "sha256": "b57ba4262e6a6a79f2db15c27356acf3e54f579a7edd13b483b2e16d140aca6d", + "size": 678 }, "python/iqpilot_private/__init__.py": { "mode": 420, @@ -20,9 +106,9 @@ "size": 0 }, "python/iqpilot_private/konn3kt/iqlvbs/iqlvbs_commander.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "9b48803b46906943d7cb10b7e4b4f38c880f06a5c281f254a89e2254965b025f", - "size": 68080 + "mode": 416, + "sha256": "4d988d95e9de50403be4ad3f733fb4a49ee153f604736736f2fa20a44e419748", + "size": 1065272 }, "runtime": { "entries": { @@ -33,6 +119,6 @@ } }, "signatures": { - "python/iqpilot_private/konn3kt/iqlvbs/iqlvbs_commander.cpython-312-aarch64-linux-gnu.so": "RwstZkEGexV6fia/R9BRk+6CQ9Q6SGJopSJf04UpMpaw5fRtoPjLOrDFOt9kU8GPmCmW40gaXhJK+rimnBYnBw==" + "python/iqpilot_private/konn3kt/iqlvbs/iqlvbs_commander.cpython-312-aarch64-linux-gnu.so": "LZv8eAULL242KYGIwYc2uAfm3U2idwa3HW09LPfpVkPUK7o+sNd7RHNjUINR+tq/7QyQao9Ev8uUZCbRNxWUDA==" } } diff --git a/artifacts/iqpilot_commander_private/manifest.json.sig b/artifacts/iqpilot_commander_private/manifest.json.sig index 1384a15a..e6eba72d 100644 --- a/artifacts/iqpilot_commander_private/manifest.json.sig +++ b/artifacts/iqpilot_commander_private/manifest.json.sig @@ -1 +1 @@ -TV78NXdEYQiabAzLIqS2lsZbp5bO0y4EPGwLRUMyKeEH2BStZiFfpNPvlqSn5HGI+RjmTq+p5rRUDMNofbZGBw== ++0m3OFfYq3omyTvGD2hNN6MrCbY6lxnLEwQal9thza+Baxp6D5Wi38Def/7buznDfmIgJ5QenNBfP42H1NgnAg== diff --git a/artifacts/iqpilot_commander_private/python/_iqclosure/_vendored.json b/artifacts/iqpilot_commander_private/python/_iqclosure/_vendored.json index fe51488c..1a1b6690 100644 --- a/artifacts/iqpilot_commander_private/python/_iqclosure/_vendored.json +++ b/artifacts/iqpilot_commander_private/python/_iqclosure/_vendored.json @@ -1 +1,15 @@ -[] +[ + "iqpilot.common.gpio", + "iqpilot.common.i2c", + "iqpilot.common.time_helpers", + "iqpilot.common.utils", + "iqpilot.system.hardware", + "iqpilot.system.hardware.base", + "iqpilot.system.hardware.pc.hardware", + "iqpilot.system.hardware.tici", + "iqpilot.system.hardware.tici.amplifier", + "iqpilot.system.hardware.tici.hardware", + "iqpilot.system.hardware.tici.iwlist", + "iqpilot.system.hardware.tici.lpa", + "iqpilot.system.hardware.tici.pins" +] diff --git a/artifacts/iqpilot_alc_private/python/iqpilot_private/__init__.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/__init__.py similarity index 100% rename from artifacts/iqpilot_alc_private/python/iqpilot_private/__init__.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/__init__.py diff --git a/artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/__init__.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/__init__.py similarity index 100% rename from artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/__init__.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/__init__.py diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/gpio.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/gpio.py similarity index 89% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/gpio.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/gpio.py index 8f025a2d..5f4295e7 100644 --- a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/gpio.py +++ b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/gpio.py @@ -27,16 +27,6 @@ def gpio_read(pin: int) -> bool | None: return val -def gpio_export(pin: int) -> None: - if os.path.isdir(f"/sys/class/gpio/gpio{pin}"): - return - - try: - with open("/sys/class/gpio/export", 'w') as f: - f.write(str(pin)) - except Exception: - print(f"Failed to export gpio {pin}") - @cache def get_irq_action(irq: int) -> list[str]: try: diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/i2c.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/i2c.py similarity index 100% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/i2c.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/i2c.py diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/time_helpers.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/time_helpers.py similarity index 100% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/time_helpers.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/time_helpers.py diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/utils.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/utils.py similarity index 60% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/utils.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/utils.py index e0aa1605..005d75b8 100644 --- a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/utils.py +++ b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/common/utils.py @@ -5,7 +5,6 @@ import contextlib import subprocess import time import functools -from subprocess import Popen, PIPE, TimeoutExpired import zstandard as zstd LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change @@ -32,24 +31,44 @@ class Timer: realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" +_sudo_available: bool | None = None + + def sudo_write(val: str, path: str) -> None: + global _sudo_available try: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - os.system(f"sudo chmod a+w {path}") - try: + return + except OSError: + pass + # AGNOS 4.9.9.2 removes owner root; a privileged boot step opens these nodes to the stack so a direct write normally works. Only probe a non-interactive escalation once (interactive sudo blocks with no tty, and sudo -n itself can stall), and never let its failure or timeout propagate. + if _sudo_available is False: + return + try: + _sudo_available = subprocess.run(["sudo", "-n", "chmod", "a+w", path], timeout=2, capture_output=True).returncode == 0 + if _sudo_available: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - # fallback for debugfs files - os.system(f"sudo su -c 'echo {val} > {path}'") + except Exception: + _sudo_available = False def sudo_read(path: str) -> str: + global _sudo_available try: - return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + with open(path, encoding='utf8') as f: + return f.read().strip() + except OSError: + pass + if _sudo_available is False: + return "" + try: + result = subprocess.run(["sudo", "-n", "cat", path], timeout=2, capture_output=True, text=True) + _sudo_available = result.returncode == 0 + return result.stdout.strip() except Exception: + _sudo_available = False return "" @@ -153,105 +172,6 @@ def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> st return default -@contextlib.contextmanager -def managed_proc(cmd: list[str], env: dict[str, str]): - proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE) - try: - yield proc - finally: - if proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=5) - except TimeoutExpired: - proc.kill() - - -def tabulate(tabular_data, headers=(), tablefmt="simple", floatfmt="g", stralign="left", numalign=None): - rows = [list(row) for row in tabular_data] - - def fmt(val): - if isinstance(val, str): - return val - if isinstance(val, (bool, int)): - return str(val) - try: - return format(val, floatfmt) - except (TypeError, ValueError): - return str(val) - - formatted = [[fmt(c) for c in row] for row in rows] - hdrs = [str(h) for h in headers] if headers else None - - ncols = max((len(r) for r in formatted), default=0) - if hdrs: - ncols = max(ncols, len(hdrs)) - if ncols == 0: - return "" - - for r in formatted: - r.extend([""] * (ncols - len(r))) - if hdrs: - hdrs.extend([""] * (ncols - len(hdrs))) - - widths = [0] * ncols - if hdrs: - for i in range(ncols): - widths[i] = len(hdrs[i]) - for row in formatted: - for i in range(ncols): - widths[i] = max(widths[i], max(len(ln) for ln in row[i].split('\n'))) - - def _align(s, w): - if stralign == "center": - return s.center(w) - return s.ljust(w) - - if tablefmt == "html": - parts = [""] - if hdrs: - parts.append("") - parts.append("" + "".join(f"" for h in hdrs) + "") - parts.append("") - parts.append("") - for row in formatted: - parts.append("" + "".join(f"" for c in row) + "") - parts.append("") - parts.append("
{h}
{c}
") - return "\n".join(parts) - - if tablefmt == "simple_grid": - def _sep(left, mid, right): - return left + mid.join("─" * (w + 2) for w in widths) + right - - top, mid_sep, bot = _sep("┌", "┬", "┐"), _sep("├", "┼", "┤"), _sep("└", "┴", "┘") - - def _fmt_row(cells): - split = [c.split('\n') for c in cells] - nlines = max(len(s) for s in split) - for s in split: - s.extend([""] * (nlines - len(s))) - return ["│" + "│".join(f" {_align(split[i][li], widths[i])} " for i in range(ncols)) + "│" for li in range(nlines)] - - lines = [top] - if hdrs: - lines.extend(_fmt_row(hdrs)) - lines.append(mid_sep) - for ri, row in enumerate(formatted): - lines.extend(_fmt_row(row)) - lines.append(mid_sep if ri < len(formatted) - 1 else bot) - return "\n".join(lines) - - gap = " " - lines = [] - if hdrs: - lines.append(gap.join(h.ljust(w) for h, w in zip(hdrs, widths, strict=True))) - lines.append(gap.join("-" * w for w in widths)) - for row in formatted: - lines.append(gap.join(_align(row[i], widths[i]) for i in range(ncols))) - return "\n".join(lines) - - def retry(attempts=3, delay=1.0, ignore_failure=False): def decorator(func): @functools.wraps(func) diff --git a/artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/iqlvbs/__init__.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/__init__.py similarity index 100% rename from artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/iqlvbs/__init__.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/__init__.py diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/__init__.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/__init__.py similarity index 100% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/__init__.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/__init__.py diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/base.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/base.py similarity index 100% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/base.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/base.py diff --git a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/konn3kt/__init__.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/pc/__init__.py similarity index 100% rename from artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/konn3kt/__init__.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/pc/__init__.py diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py similarity index 100% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/__init__.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/__init__.py similarity index 100% rename from artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/__init__.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/__init__.py diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py similarity index 92% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py index 48a34ea4..c38e33b9 100644 --- a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py +++ b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py @@ -125,16 +125,32 @@ class Amplifier: print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") def set_configs(self, configs: list[AmpConfig]) -> bool: + import threading + tries = 15 backoff = 0. for i in range(tries): - try: - self._set_configs(configs) - return True - except OSError: + result = [None] + error = [None] + def _worker(): + try: + self._set_configs(configs) + result[0] = True + except OSError as e: + error[0] = e + t = threading.Thread(target=_worker, daemon=True) + t.start() + t.join(timeout=2) + if t.is_alive(): backoff += 0.1 time.sleep(backoff) - print(f"Failed to set amp config, {tries - i - 1} retries left") + print(f"Failed to set amp config (timeout), {tries - i - 1} retries left") + continue + if result[0]: + return True + backoff += 0.1 + time.sleep(backoff) + print(f"Failed to set amp config, {tries - i - 1} retries left") return False def set_global_shutdown(self, amp_disabled: bool) -> bool: diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py similarity index 98% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py index b9fa7fbf..2506fb9e 100644 --- a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py +++ b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py @@ -469,8 +469,10 @@ class Tici(HardwareBase): if self.amplifier is not None: self.amplifier.initialize_configuration(self.get_device_type()) - # Allow hardwared to write engagement status to kmsg - os.system("sudo chmod a+w /dev/kmsg") + try: + subprocess.run(["sudo", "-n", "chmod", "a+w", "/dev/kmsg"], timeout=2, capture_output=True, check=False) + except Exception: + pass # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL gpio_init(GPIO.SOM_ST_IO, True) @@ -514,10 +516,10 @@ class Tici(HardwareBase): affine_irq(3, "xhci-hcd:usb1") try: pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() - subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) - subprocess.call(["sudo", "taskset", "-pc", "3", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessException as e: - print(str(e)) + subprocess.run(["sudo", "-n", "chrt", "-f", "-p", "1", pid], timeout=2, capture_output=True, check=False) + subprocess.run(["sudo", "-n", "taskset", "-pc", "3", pid], timeout=2, capture_output=True, check=False) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + pass def configure_modem(self): from iqpilot.common.params import Params @@ -557,7 +559,7 @@ class Tici(HardwareBase): if self.get_device_type() in ("tici", "tizi"): if initial_eps_apn: subprocess.call(["mmcli", "-m", "any", f'--3gpp-set-initial-eps-bearer-settings=apn={initial_eps_apn}']) - else: + elif is_comma_profile: subprocess.call(["mmcli", "-m", "any", '--3gpp-set-initial-eps-bearer-settings=apn=']) cmds += [ diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py similarity index 100% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py similarity index 100% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py b/artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py similarity index 100% rename from artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py rename to artifacts/iqpilot_commander_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py diff --git a/artifacts/iqpilot_commander_private/python/iqpilot_private/konn3kt/iqlvbs/iqlvbs_commander.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_commander_private/python/iqpilot_private/konn3kt/iqlvbs/iqlvbs_commander.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 6d872d06..72acfd9c Binary files a/artifacts/iqpilot_commander_private/python/iqpilot_private/konn3kt/iqlvbs/iqlvbs_commander.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_commander_private/python/iqpilot_private/konn3kt/iqlvbs/iqlvbs_commander.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_context_private/manifest.json b/artifacts/iqpilot_context_private/manifest.json index 27118a3e..632e98a7 100644 --- a/artifacts/iqpilot_context_private/manifest.json +++ b/artifacts/iqpilot_context_private/manifest.json @@ -1,8 +1,9 @@ { + "bundle_version": 1792550965, "python/_iqclosure/_vendored.json": { "mode": 420, - "sha256": "74f679d0ffd3a8e3225496dec9ac060c5505503c94fa04acabd0de4539c57871", - "size": 1072 + "sha256": "ae4c187fb594e87be0b64ff050e3ab39cff5dace25e8f5766e3f11be6736992f", + "size": 1137 }, "python/_iqclosure/iqpilot/__init__.py": { "mode": 420, @@ -16,13 +17,18 @@ }, "python/_iqclosure/iqpilot/common/api/__init__.py": { "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 + "sha256": "1dd848ec6e867836a321a93f9ac218415c19528aa356a3e80969503285316b54", + "size": 900 }, "python/_iqclosure/iqpilot/common/api/base.py": { "mode": 420, - "sha256": "86b834e02e24a005c903f1ca059b1a572e37c17a87271726c90918cd04b6133b", - "size": 3451 + "sha256": "5b312ec45e673d9337a89ea82a157152d1f64e5e48518807a592247787087609", + "size": 4921 + }, + "python/_iqclosure/iqpilot/common/api/konn3kt.py": { + "mode": 420, + "sha256": "2282e19909079bfd77a089f480df5a229191c35f8523f005d20e2ea468a6b0bc", + "size": 313 }, "python/_iqclosure/iqpilot/common/basedir.py": { "mode": 420, @@ -61,18 +67,8 @@ }, "python/_iqclosure/iqpilot/common/utils.py": { "mode": 420, - "sha256": "103fa6c24f8b963576c482a34d7ecb020168fd3e85156493ae4432ee3b7b2c06", - "size": 5159 - }, - "python/_iqclosure/iqpilot/konn3kt/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/konn3kt/cloud_client.py": { - "mode": 420, - "sha256": "4d58e62f8d628a8570881f2b87c82c801b46928fbb84f8dab8a81a9b55ba4e15", - "size": 493 + "sha256": "c39b89a1ab34511dfa5f9efac783fde0e6d616963a416cf472bd1f604b57f84d", + "size": 5892 }, "python/_iqclosure/iqpilot/selfdrive/__init__.py": { "mode": 420, @@ -116,8 +112,13 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py": { "mode": 420, - "sha256": "2fad977c3816ff824c126a92605f866f3bc0aa989ed8397e818bed481a41192f", - "size": 14051 + "sha256": "9628f88e71f7fae2c3d62dd17618367b3c7d12749cfe2a4737410288888cd3bd", + "size": 15008 + }, + "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/iqc.py": { + "mode": 420, + "sha256": "54ec8e50513edae74ddb1c28015868183cb0c103de48386292d0b7fd83bcbb44", + "size": 391 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/speed_limit_input.py": { "mode": 420, @@ -166,13 +167,13 @@ }, "python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py": { "mode": 420, - "sha256": "98f46f8705259b00efae7274186b4e68201aad467a19de4f16d5f850d10ab0b5", - "size": 7585 + "sha256": "38e521356c56ac85f35543b9457d36639e55ed2ac916f30ea3b86948e806541f", + "size": 8016 }, "python/_iqclosure/iqpilot/system/hardware/tici/hardware.py": { "mode": 420, - "sha256": "e8eae4e8a75e1751525471cf54a8555b7bfd7f08ad84bf2e75642b5745f222db", - "size": 25601 + "sha256": "da5ecb280f7af14169903260dc941e3bfac033c93b184cb0abea0f1494c34582", + "size": 25731 }, "python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py": { "mode": 420, @@ -204,43 +205,52 @@ "sha256": "b6d70f9628b596bef9eb6509e829d9cf94b4635c6e46bb8febde4a067203a222", "size": 113 }, + "python/iqpilot_private/context/aux_inputs.cpython-312-aarch64-linux-gnu.so": { + "mode": 416, + "sha256": "a6d54e9ef8773e31e28411012a447ef4880ed7f8d43751bb16666adceee5e0e0", + "size": 1065336 + }, "python/iqpilot_private/context/cot_decode.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "506f732da9133ba5fdf8ffcb5d7557b28de44b8b789a4994fb1bd9a7c2240641", - "size": 138936 + "mode": 416, + "sha256": "47e0652a7f87850cfc9cdc28ed88b18cd36adb4da00d5e5b4c2181df7ae09089", + "size": 1130696 }, "python/iqpilot_private/context/drive_profile.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "33358560c99c961e172a1643fb4a0ac6048f90d85551c0dea595fc1793372199", - "size": 202256 + "mode": 416, + "sha256": "a5a4c36f9aa116467c10e4fe9f07b4fde58d98b97e0950a799b9ce1c83190a08", + "size": 1130920 }, "python/iqpilot_private/context/feature_inputs.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "e23916b1c8ce0dd6fe28e07d381e8545c14ab9851cc3c501cc8f08e2002df6ee", - "size": 135368 + "mode": 416, + "sha256": "c518443db5595c542ec1dec089be97ee79d3a1e7e0f661dbf8d069cf243da402", + "size": 1130872 }, "python/iqpilot_private/context/model_context.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "cf6a559498981b3bebb4e50ef276e8c6f0816db68534c43b3372f2d88fec9f7d", - "size": 473440 + "mode": 416, + "sha256": "c7bfa17d75afa08c0f85256b461c1dd0318fadf7ca422b704673613a8ba54949", + "size": 1524504 }, "python/iqpilot_private/context/reasoning_relay.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "674bb7de81a76911f536cd684b384f384b4271bdedf0591ff18c574c7378d6c7", - "size": 137560 + "mode": 416, + "sha256": "b4bd7aa63d69c0db66731eff1fb668551bb5eea17f9d0a8baba01fbb1ef05ae8", + "size": 1130728 }, "python/iqpilot_private/context/speed_limit_input.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "b6b8c578e98f584ee221d35e08233f4a96afee3d3d765bc8ce5229040626f9c9", - "size": 135872 + "mode": 416, + "sha256": "9737473c0727f844593bd539cc106aa9358e6b62288e4ceb7a44c78425fec08b", + "size": 1065272 }, "python/iqpilot_private/context/temporal_state.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "a08e7b38bdd666bf2aa38e7528574f2bb3650cd8439edc9689b1e36b08fcf5c2", - "size": 205456 + "mode": 416, + "sha256": "87c0e5b83e1f53b1984aa9debe7e6f5f8a6432b6de6c4f5d82cef01e31eb5ed8", + "size": 1130776 }, "runtime": { "entries": { + "iqpilot_private.context.aux_inputs": { + "path": "python/iqpilot_private/context/aux_inputs.cpython-312-aarch64-linux-gnu.so", + "type": "python-module" + }, "iqpilot_private.context.cot_decode": { "path": "python/iqpilot_private/context/cot_decode.cpython-312-aarch64-linux-gnu.so", "type": "python-module" @@ -272,12 +282,13 @@ } }, "signatures": { - "python/iqpilot_private/context/cot_decode.cpython-312-aarch64-linux-gnu.so": "3XxFsaC/nx8fXtDtpWs+7oPABYmwlIEj1Ex30U8kTDzguB6P5RJVmkFbstAiWOnVCYfLT47sMlvHeU3VTHehDA==", - "python/iqpilot_private/context/drive_profile.cpython-312-aarch64-linux-gnu.so": "9SRCLPJAzPMXMJRyxmfzIR3B2uTfpRm4uzUi1UBnSufzR+ppaRxfCtLestlXWDf2nkQiiaHjF3JdPv3VhjjmBg==", - "python/iqpilot_private/context/feature_inputs.cpython-312-aarch64-linux-gnu.so": "KdG3SA295ptxX5WcOmdjeHUkE+f+DTRkPGcJFEXgLMPPMWSgo8aVYILT9z46JTUHd+va82ijRIAJZtJHE+wqAg==", - "python/iqpilot_private/context/model_context.cpython-312-aarch64-linux-gnu.so": "yNV0YdFkYGTRyiBFwejPolZHv5ueyQ3XwBjKAoDEB5c80ps6dEDDoXzJBi+Pe1PS/hhVZq3rb5NqVzuJEBlPCA==", - "python/iqpilot_private/context/reasoning_relay.cpython-312-aarch64-linux-gnu.so": "OD6pPW3nNwopVqsY0UXVyGrAREG7KNCEO59c5j6uIPuKHaSCu2z59tNG9WZ/mgfJCyhLCAN6K9BULmX2kwRTDA==", - "python/iqpilot_private/context/speed_limit_input.cpython-312-aarch64-linux-gnu.so": "B4Pc4T9sPIuF9g35pB43YbaEzwX9h4IhPrvLzwf1HUmkPMT7knz2B1lgU0z8atz96F5OUpJSE2mG5usq5BzjAA==", - "python/iqpilot_private/context/temporal_state.cpython-312-aarch64-linux-gnu.so": "KYtRL67X5HZ2vVmQfqW8XhnmDVK1vxpo2geVcZ7NXGZ3FFn5vcIweK2ZFVZoIgPfGvK5QMSONnLXPUNSfc9iCw==" + "python/iqpilot_private/context/aux_inputs.cpython-312-aarch64-linux-gnu.so": "1CTTb0TqkzHb+66U+mwhtiB0xa5ETlJmr7Hzdl7HJjEAhZVp8LImLWJm785MsaSCkC1mr2wI81FTTCM+6KOtBg==", + "python/iqpilot_private/context/cot_decode.cpython-312-aarch64-linux-gnu.so": "X1Tb8xcrygS+L0a/f0os516Q8mTPIQuCkVUNkChn77lDLc+B3UaHYiIeI7iyomzW3958NNlXo9lq+lRYw4TZBQ==", + "python/iqpilot_private/context/drive_profile.cpython-312-aarch64-linux-gnu.so": "afh0sJAKVrv2p8wGD2USTgvpme2/DgFV+mb2YAchPNGtBDL+S+y5KaxKsKXUVHVN7wU36AKCj/6T777jIVz2Dg==", + "python/iqpilot_private/context/feature_inputs.cpython-312-aarch64-linux-gnu.so": "ewMXJE1p5pz0uADnJROdarY8Cw85Uis5rAJvpotH+rVOoVoTaw/nO4MRbCeDj6Ko042QiCo7p/91cO5VT/OjDw==", + "python/iqpilot_private/context/model_context.cpython-312-aarch64-linux-gnu.so": "4DeG7yQsLSC1kkOTusrCebb3yxQyTXZ4BbL4YDyIk9qKtGBPmPYj0x2J6b9HpAwchheqq9uNuko2nUkW3sMvCA==", + "python/iqpilot_private/context/reasoning_relay.cpython-312-aarch64-linux-gnu.so": "1an09hj/ok9Fk4vted3GXczjV8v+qfUsjexRpKXnFeKdQNltv6oWosjjQvtEwzkikc43mtgIKAYoAxLBiS9lDw==", + "python/iqpilot_private/context/speed_limit_input.cpython-312-aarch64-linux-gnu.so": "zyrasLvljreehe00C+GuDffQOE2OL3RDFy3dyCofB2WfB2EMcUyIWOFk+Ybtn+jJ9DQTHH063bDkURT5yviqDw==", + "python/iqpilot_private/context/temporal_state.cpython-312-aarch64-linux-gnu.so": "hgeXamMIvzc3BLu9PtmlvI2jqi+x+g37TWvbrmfd78/z8Nzw68KoVIju3rKZcHU0BaXgnhc5sxlQV0wswk+3Cg==" } } diff --git a/artifacts/iqpilot_context_private/manifest.json.sig b/artifacts/iqpilot_context_private/manifest.json.sig index b6d35d04..11e697eb 100644 --- a/artifacts/iqpilot_context_private/manifest.json.sig +++ b/artifacts/iqpilot_context_private/manifest.json.sig @@ -1 +1 @@ -NcjiBJcKitajD4QhhgJZWkc7JRypOIeWKlHvcc0gRMfLAHU7K3ogMd/ZYHUwWPaTZw0JS5zkUIPuZ7hN+chqDA== +ODnEFb70mSlY+uG5Kl/aS6lYQ1fmNusxlf3O/ed2G/lPuA21fbzKmU+9XT9a83g9oAp5sLZuBV2zECPLewWiDg== diff --git a/artifacts/iqpilot_context_private/python/_iqclosure/_vendored.json b/artifacts/iqpilot_context_private/python/_iqclosure/_vendored.json index fe911233..fc4ccee7 100644 --- a/artifacts/iqpilot_context_private/python/_iqclosure/_vendored.json +++ b/artifacts/iqpilot_context_private/python/_iqclosure/_vendored.json @@ -1,5 +1,7 @@ [ + "iqpilot.common.api", "iqpilot.common.api.base", + "iqpilot.common.api.konn3kt", "iqpilot.common.basedir", "iqpilot.common.gpio", "iqpilot.common.i2c", @@ -8,13 +10,13 @@ "iqpilot.common.swaglog", "iqpilot.common.time_helpers", "iqpilot.common.utils", - "iqpilot.konn3kt.cloud_client", "iqpilot.selfdrive.iqmodeld.config", "iqpilot.selfdrive.iqmodeld.drive_profile", "iqpilot.selfdrive.iqmodeld.emac_model_meta", "iqpilot.selfdrive.iqmodeld.model_map_tile", "iqpilot.selfdrive.iqmodeld.models.fetcher", "iqpilot.selfdrive.iqmodeld.models.helpers", + "iqpilot.selfdrive.iqmodeld.models.iqc", "iqpilot.selfdrive.iqmodeld.speed_limit_input", "iqpilot.selfdrive.iqmodeld.temporal_state", "iqpilot.system.hardware", diff --git a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/__init__.py b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/__init__.py index e69de29b..b7e90dac 100644 --- a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/__init__.py +++ b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/__init__.py @@ -0,0 +1,26 @@ +import iqpilot.common.api.konn3kt + + +class Api: + def __init__(self, dongle_id): + self.service = iqpilot.common.api.konn3kt.Konn3ktApi(dongle_id) + + def request(self, method, endpoint, **params): + return self.service.request(method, endpoint, **params) + + def get(self, *args, **kwargs): + return self.service.get(*args, **kwargs) + + def post(self, *args, **kwargs): + return self.service.post(*args, **kwargs) + + def get_token(self, payload_extra=None, expiry_hours=1): + return self.service.get_token(payload_extra, expiry_hours) + + +def api_get(endpoint, method='GET', timeout=None, access_token=None, session=None, **params): + return iqpilot.common.api.konn3kt.Konn3ktApi(None).api_get(endpoint, method, timeout, access_token, session, **params) + + +def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: + return iqpilot.common.api.konn3kt.Konn3ktApi(None).get_key_pair() diff --git a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/base.py b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/base.py index 371a8ab4..0aef9c35 100644 --- a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/base.py +++ b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/base.py @@ -7,10 +7,44 @@ from functools import lru_cache from iqpilot.system.hardware.hw import Paths from iqpilot.system.version import get_version -# name: jwt signature algorithm KEYS = {"id_rsa": "RS256", "id_ecdsa": "ES256"} +_CUSTODIAN_KEY = object() +_IQKEEPER_SOCKET = "/run/iqkeeper/iqkeeper.sock" + + +def _custodian_available() -> bool: + import stat + try: + return stat.S_ISSOCK(os.stat(_IQKEEPER_SOCKET).st_mode) + except OSError: + return False + + +def _custodian_sign_jwt(dongle_id: str, payload: dict, alg: str) -> str: + import base64 + import json + import socket + nonce = base64.b64encode(os.urandom(16)).decode() + packet = json.dumps({ + "v": 1, + "op": "sign", + "dongle_id": dongle_id, + "payload": payload, + "alg": alg, + "nonce": nonce, + }).encode() + with socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET) as sock: + sock.settimeout(25.0) + sock.connect(_IQKEEPER_SOCKET) + sock.send(packet) + reply_data = sock.recv(8192) + reply = json.loads(reply_data) if reply_data else {} + if reply.get("nonce") != nonce or not reply.get("ok"): + raise PermissionError(f"custodian sign failed: {reply.get('error', 'unknown')}") + return reply["token"] + @lru_cache(maxsize=4) def load_signing_key(private_key: str): @@ -60,6 +94,9 @@ class BaseApi: } if payload_extra is not None: payload.update(payload_extra) + if self.private_key is _CUSTODIAN_KEY: + serializable = {k: v.isoformat() if isinstance(v, datetime) else v for k, v in payload.items()} + return _custodian_sign_jwt(self.dongle_id, serializable, self.jwt_algorithm) key = load_signing_key(self.private_key) if self.private_key else self.private_key token = jwt.encode(payload, key, algorithm=self.jwt_algorithm) if isinstance(token, bytes): @@ -73,9 +110,25 @@ class BaseApi: return self.request(method, endpoint, timeout, access_token, session=session, json=json, **params) @staticmethod - def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: + def get_key_pair() -> tuple[str, str | object, str] | tuple[None, None, None]: for key in KEYS: - if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'): - with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public: - return KEYS[key], private.read(), public.read() + priv_path = Paths.persist_root() + f'/comma/{key}' + pub_path = priv_path + '.pub' + if not os.path.isfile(priv_path) or not os.path.isfile(pub_path): + continue + try: + with open(pub_path) as public: + pub = public.read() + except OSError: + continue + try: + with open(priv_path) as private: + return KEYS[key], private.read(), pub + except PermissionError: + import time + for _attempt in range(10): + if _custodian_available(): + return KEYS[key], _CUSTODIAN_KEY, pub + time.sleep(0.5) + continue return None, None, None diff --git a/iqpilot/common/api/comma_connect.py b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/konn3kt.py similarity index 75% rename from iqpilot/common/api/comma_connect.py rename to artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/konn3kt.py index bc16d40e..cae7ea46 100644 --- a/iqpilot/common/api/comma_connect.py +++ b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/api/konn3kt.py @@ -1,3 +1,4 @@ +# Copyright (c) 2026 IQ.Lvbs. All rights reserved. import os from iqpilot.common.api.base import BaseApi @@ -5,7 +6,7 @@ from iqpilot.common.api.base import BaseApi API_HOST = os.getenv('API_HOST', 'https://api-iqlabs.konn3kt.com') -class CommaConnectApi(BaseApi): +class Konn3ktApi(BaseApi): def __init__(self, dongle_id): super().__init__(dongle_id, API_HOST) self.user_agent = "openpilot-" diff --git a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/utils.py b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/utils.py index a80270bb..005d75b8 100644 --- a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/utils.py +++ b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/common/utils.py @@ -31,24 +31,44 @@ class Timer: realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" +_sudo_available: bool | None = None + + def sudo_write(val: str, path: str) -> None: + global _sudo_available try: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - os.system(f"sudo chmod a+w {path}") - try: + return + except OSError: + pass + # AGNOS 4.9.9.2 removes owner root; a privileged boot step opens these nodes to the stack so a direct write normally works. Only probe a non-interactive escalation once (interactive sudo blocks with no tty, and sudo -n itself can stall), and never let its failure or timeout propagate. + if _sudo_available is False: + return + try: + _sudo_available = subprocess.run(["sudo", "-n", "chmod", "a+w", path], timeout=2, capture_output=True).returncode == 0 + if _sudo_available: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - # fallback for debugfs files - os.system(f"sudo su -c 'echo {val} > {path}'") + except Exception: + _sudo_available = False def sudo_read(path: str) -> str: + global _sudo_available try: - return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + with open(path, encoding='utf8') as f: + return f.read().strip() + except OSError: + pass + if _sudo_available is False: + return "" + try: + result = subprocess.run(["sudo", "-n", "cat", path], timeout=2, capture_output=True, text=True) + _sudo_available = result.returncode == 0 + return result.stdout.strip() except Exception: + _sudo_available = False return "" diff --git a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py deleted file mode 100644 index 2897609e..00000000 --- a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos -""" - - -import os -from iqpilot.common.api.base import BaseApi -API_HOST = os.getenv('KONN3KT_API_HOST', 'https://api-iqlabs.konn3kt.com') - -class Konn3ktApi(BaseApi): - - def __init__(self, dongle_id): - super().__init__(dongle_id, API_HOST) - self.user_agent = "konn3kt-device-" - - def get_token(self, expiry_hours=1): - return super()._get_token(expiry_hours=expiry_hours) diff --git a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py index 05cce298..ee2eb77f 100644 --- a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py +++ b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py @@ -305,7 +305,40 @@ def get_runtime_bundle_upgrade(bundle, params: Params = None, available_bundles= return _find_runtime_upgrade(bundle, params, available_bundles) +_forced_bundle = None + + +def load_default_model_session(): + """Stage the shipped default model files and return a decoded default bundle + WITHOUT persisting it as the active selection, so the next boot retries the + user's own model instead of staying on the default forever.""" + bundle_dict = _load_default_bundle_dict() + ensure_default_model_files(bundle_dict) + return ModelBundle(**bundle_dict) + + +class forced_active_bundle: + """Make get_active_bundle return `bundle` for the duration of the block, used to + build a runner for a session-only fallback bundle without touching Params.""" + def __init__(self, bundle): + self._bundle = bundle + + def __enter__(self): + global _forced_bundle + self._prev = _forced_bundle + _forced_bundle = self._bundle + return self._bundle + + def __exit__(self, *exc): + global _forced_bundle + _forced_bundle = self._prev + return False + + def get_active_bundle(params: Params = None): + if _forced_bundle is not None: + return _forced_bundle + params = Params() if params is None else params try: diff --git a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/iqc.py b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/iqc.py new file mode 100644 index 00000000..da84740e --- /dev/null +++ b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/iqc.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.models.iqc") +except ProprietaryModuleMissing: + from iqpilot.models_private_src.iqc import * # noqa: F403 diff --git a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py index 48a34ea4..c38e33b9 100644 --- a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py +++ b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py @@ -125,16 +125,32 @@ class Amplifier: print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") def set_configs(self, configs: list[AmpConfig]) -> bool: + import threading + tries = 15 backoff = 0. for i in range(tries): - try: - self._set_configs(configs) - return True - except OSError: + result = [None] + error = [None] + def _worker(): + try: + self._set_configs(configs) + result[0] = True + except OSError as e: + error[0] = e + t = threading.Thread(target=_worker, daemon=True) + t.start() + t.join(timeout=2) + if t.is_alive(): backoff += 0.1 time.sleep(backoff) - print(f"Failed to set amp config, {tries - i - 1} retries left") + print(f"Failed to set amp config (timeout), {tries - i - 1} retries left") + continue + if result[0]: + return True + backoff += 0.1 + time.sleep(backoff) + print(f"Failed to set amp config, {tries - i - 1} retries left") return False def set_global_shutdown(self, amp_disabled: bool) -> bool: diff --git a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py index 2f71ed63..2506fb9e 100644 --- a/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py +++ b/artifacts/iqpilot_context_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py @@ -469,8 +469,10 @@ class Tici(HardwareBase): if self.amplifier is not None: self.amplifier.initialize_configuration(self.get_device_type()) - # Allow hardwared to write engagement status to kmsg - os.system("sudo chmod a+w /dev/kmsg") + try: + subprocess.run(["sudo", "-n", "chmod", "a+w", "/dev/kmsg"], timeout=2, capture_output=True, check=False) + except Exception: + pass # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL gpio_init(GPIO.SOM_ST_IO, True) @@ -514,10 +516,10 @@ class Tici(HardwareBase): affine_irq(3, "xhci-hcd:usb1") try: pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() - subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) - subprocess.call(["sudo", "taskset", "-pc", "3", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessException as e: - print(str(e)) + subprocess.run(["sudo", "-n", "chrt", "-f", "-p", "1", pid], timeout=2, capture_output=True, check=False) + subprocess.run(["sudo", "-n", "taskset", "-pc", "3", pid], timeout=2, capture_output=True, check=False) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + pass def configure_modem(self): from iqpilot.common.params import Params diff --git a/artifacts/iqpilot_context_private/python/iqpilot_private/context/aux_inputs.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_context_private/python/iqpilot_private/context/aux_inputs.cpython-312-aarch64-linux-gnu.so new file mode 100644 index 00000000..64c68349 Binary files /dev/null and b/artifacts/iqpilot_context_private/python/iqpilot_private/context/aux_inputs.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_context_private/python/iqpilot_private/context/cot_decode.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_context_private/python/iqpilot_private/context/cot_decode.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 31026b7b..91347616 Binary files a/artifacts/iqpilot_context_private/python/iqpilot_private/context/cot_decode.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_context_private/python/iqpilot_private/context/cot_decode.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_context_private/python/iqpilot_private/context/drive_profile.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_context_private/python/iqpilot_private/context/drive_profile.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index b4db0672..950aed58 Binary files a/artifacts/iqpilot_context_private/python/iqpilot_private/context/drive_profile.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_context_private/python/iqpilot_private/context/drive_profile.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_context_private/python/iqpilot_private/context/feature_inputs.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_context_private/python/iqpilot_private/context/feature_inputs.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 65f06d3a..4ed3f1b9 Binary files a/artifacts/iqpilot_context_private/python/iqpilot_private/context/feature_inputs.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_context_private/python/iqpilot_private/context/feature_inputs.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_context_private/python/iqpilot_private/context/model_context.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_context_private/python/iqpilot_private/context/model_context.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index ef676926..e64bdbdb Binary files a/artifacts/iqpilot_context_private/python/iqpilot_private/context/model_context.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_context_private/python/iqpilot_private/context/model_context.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_context_private/python/iqpilot_private/context/reasoning_relay.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_context_private/python/iqpilot_private/context/reasoning_relay.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index fa8f7c2f..a0d4c386 Binary files a/artifacts/iqpilot_context_private/python/iqpilot_private/context/reasoning_relay.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_context_private/python/iqpilot_private/context/reasoning_relay.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_context_private/python/iqpilot_private/context/speed_limit_input.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_context_private/python/iqpilot_private/context/speed_limit_input.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 54bce54e..5111fa21 Binary files a/artifacts/iqpilot_context_private/python/iqpilot_private/context/speed_limit_input.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_context_private/python/iqpilot_private/context/speed_limit_input.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_context_private/python/iqpilot_private/context/temporal_state.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_context_private/python/iqpilot_private/context/temporal_state.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 9e332d5d..313ba458 Binary files a/artifacts/iqpilot_context_private/python/iqpilot_private/context/temporal_state.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_context_private/python/iqpilot_private/context/temporal_state.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_emac_private/manifest.json b/artifacts/iqpilot_emac_private/manifest.json index 3e863019..7a2838d0 100644 --- a/artifacts/iqpilot_emac_private/manifest.json +++ b/artifacts/iqpilot_emac_private/manifest.json @@ -1,8 +1,9 @@ { + "bundle_version": 1792551322, "python/_iqclosure/_vendored.json": { "mode": 420, - "sha256": "c4e45105dedcba6f7a4abf84a6e012517ca82cd615165e162c2025ad59020fa1", - "size": 3823 + "sha256": "00a3c1ea8d86878488abd7ee5ae5dce065c4b3d0fcf43072999303067807bc63", + "size": 3668 }, "python/_iqclosure/iqpilot/__init__.py": { "mode": 420, @@ -16,18 +17,18 @@ }, "python/_iqclosure/iqpilot/common/api/__init__.py": { "mode": 420, - "sha256": "97d6e0d8cbd3e47797d734322b42d04efea4d95a2dae5a56f1baef3fd847aa0d", - "size": 939 + "sha256": "1dd848ec6e867836a321a93f9ac218415c19528aa356a3e80969503285316b54", + "size": 900 }, "python/_iqclosure/iqpilot/common/api/base.py": { "mode": 420, - "sha256": "86b834e02e24a005c903f1ca059b1a572e37c17a87271726c90918cd04b6133b", - "size": 3451 + "sha256": "5b312ec45e673d9337a89ea82a157152d1f64e5e48518807a592247787087609", + "size": 4921 }, - "python/_iqclosure/iqpilot/common/api/comma_connect.py": { + "python/_iqclosure/iqpilot/common/api/konn3kt.py": { "mode": 420, - "sha256": "e7c2dd0b56fb8533c2cbc614e728a9ffc5ce7360524cfa62a02f5235bc23b9a8", - "size": 267 + "sha256": "2282e19909079bfd77a089f480df5a229191c35f8523f005d20e2ea468a6b0bc", + "size": 313 }, "python/_iqclosure/iqpilot/common/atlas_alerts.py": { "mode": 420, @@ -81,13 +82,8 @@ }, "python/_iqclosure/iqpilot/common/realtime.py": { "mode": 420, - "sha256": "016be524079ef32df03b2c135eab8f83da60a46fc7f143620b836c65f33e39a3", - "size": 4423 - }, - "python/_iqclosure/iqpilot/common/spinner.py": { - "mode": 420, - "sha256": "33a2eb2aa3148492414035c5dc7150b3a69467905d7a3e58c8f65cfe70d3552c", - "size": 1371 + "sha256": "0190cf1f239f8a2bda573524a72a4b6a74e7ed70aa4c1e5f72d1f1159d59cd60", + "size": 4926 }, "python/_iqclosure/iqpilot/common/steer_delay.py": { "mode": 420, @@ -106,23 +102,18 @@ }, "python/_iqclosure/iqpilot/common/utils.py": { "mode": 420, - "sha256": "103fa6c24f8b963576c482a34d7ecb020168fd3e85156493ae4432ee3b7b2c06", - "size": 5159 + "sha256": "c39b89a1ab34511dfa5f9efac783fde0e6d616963a416cf472bd1f604b57f84d", + "size": 5892 }, "python/_iqclosure/iqpilot/konn3kt/__init__.py": { "mode": 420, "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0 }, - "python/_iqclosure/iqpilot/konn3kt/cloud_client.py": { - "mode": 420, - "sha256": "4d58e62f8d628a8570881f2b87c82c801b46928fbb84f8dab8a81a9b55ba4e15", - "size": 493 - }, "python/_iqclosure/iqpilot/konn3kt/registration.py": { "mode": 420, - "sha256": "e9c37e8ae0a70c3a19ca1a33b290e94658b255fab153a941f50f8a29168d75c3", - "size": 7816 + "sha256": "5a2b06d919adce513fb8a7e4e6f26d48e502e92dbd89f9af7a0e67e9114ab3be", + "size": 2683 }, "python/_iqclosure/iqpilot/selfdrive/__init__.py": { "mode": 420, @@ -139,36 +130,16 @@ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0 }, - "python/_iqclosure/iqpilot/selfdrive/controls/lib/desire_helper.py": { + "python/_iqclosure/iqpilot/selfdrive/controls/lib/action_engine.py": { "mode": 420, - "sha256": "5d06908d9a5358d8b27e2a37b9f234f618966004aacb1761cf16c1a1acc74ab0", - "size": 15285 + "sha256": "c3e33ae73bcdab89ba32b09e18712fe91018338907e8c81af056d447c5ef04c6", + "size": 815 }, "python/_iqclosure/iqpilot/selfdrive/controls/lib/drive_helpers.py": { "mode": 420, "sha256": "94e202959eb80d841720ecd0581206fc4ac9d50ad2e2a6cf19f0de9914913988", "size": 3159 }, - "python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_change.py": { - "mode": 420, - "sha256": "0e4b4c54856d651d89e1c8896ebf8bb4f0a2efc0b8788a585094e06ecd6616d7", - "size": 8461 - }, - "python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py": { - "mode": 420, - "sha256": "d27bd9010967dd51dccc18ec005ff446ae965379b8e828ff7173331818723ec8", - "size": 4667 - }, - "python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lateral_edge_guard.py": { - "mode": 420, - "sha256": "6bb6bcb8e5323480bf1b992549279a7d4c17a1bd1bd260863cf7b61b887fa735", - "size": 10227 - }, "python/_iqclosure/iqpilot/selfdrive/iqlocd/__init__.py": { "mode": 420, "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", @@ -189,6 +160,11 @@ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0 }, + "python/_iqclosure/iqpilot/selfdrive/iqmodeld/aux_inputs.py": { + "mode": 420, + "sha256": "e80699014729784ff72c64ac2cdf317a97e0f069ef99676581b1d179db5b4a0a", + "size": 493 + }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/big_catalog.py": { "mode": 420, "sha256": "7ec5df43ce10b47ce925467e61100123e828b8e72bdab20bef4c8ac0ffd3cc7a", @@ -211,8 +187,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/daemon.py": { "mode": 420, - "sha256": "62f51d922f6883bc094590a9d95c2c0849d858bbce30f35c2eba00ec9d3d9bad", - "size": 36325 + "sha256": "2f94719d06907c4ac729b8aba063ad43079fde966813d55b6190af8d6c7da679", + "size": 37375 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/drive_profile.py": { "mode": 420, @@ -221,13 +197,13 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/driving_action.py": { "mode": 420, - "sha256": "46a46ea0675ce2f815021c63f3b1ab171c7a453c499a827f2a82b2f2e55d6f58", - "size": 2484 + "sha256": "83c1155d90607a2ca119ce0797474e79d741aa0399ae86a22843ad580b5c8ef7", + "size": 2644 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py": { "mode": 420, - "sha256": "a9f2bc8477b58a3df7196f4b40cdddf631e28051809e9bbfdd4c78148444c474", - "size": 10891 + "sha256": "af83d52609ec4f2abe82f4e5f788ce4788bdc41cbf7b3de36dc63f1f598ef46c", + "size": 12107 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_model.py": { "mode": 420, @@ -256,8 +232,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py": { "mode": 420, - "sha256": "83b0190f19e3f037c937a03b1988931729ed43d4dac92223aba0745697611ed8", - "size": 12835 + "sha256": "62b13a05afbe9b5fb5d9e3d09c1491b37826ff840126c5e317a7ebf1edc28762", + "size": 12060 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_channel.py": { "mode": 420, @@ -281,8 +257,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py": { "mode": 420, - "sha256": "321e7223249dc7dba93effe8e834f225fb6067abf49da17fe9a6faeb71ba8e49", - "size": 2786 + "sha256": "170639c92d69e4ae2493ca961fb95e75d42be9cedb2442521fe3ac7c25cb977c", + "size": 2891 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/fetcher.py": { "mode": 420, @@ -291,14 +267,19 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py": { "mode": 420, - "sha256": "2fad977c3816ff824c126a92605f866f3bc0aa989ed8397e818bed481a41192f", - "size": 14051 + "sha256": "9628f88e71f7fae2c3d62dd17618367b3c7d12749cfe2a4737410288888cd3bd", + "size": 15008 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/inference_state.py": { "mode": 420, "sha256": "bdfd1c1b538e2342b17d08aa6ebe0c041c2f721025bcd7334434d1139fe6063d", "size": 268 }, + "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/iqc.py": { + "mode": 420, + "sha256": "54ec8e50513edae74ddb1c28015868183cb0c103de48386292d0b7fd83bcbb44", + "size": 391 + }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/__init__.py": { "mode": 420, "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", @@ -306,8 +287,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py": { "mode": 420, - "sha256": "e0eb9d7bff1da89d52b09596f7e7109fc25c3b9b86a89674382a7dde9e0a4199", - "size": 8555 + "sha256": "c9645f9f377020a44d4a785de5930dcfc1b0028489461a94a3a1ddd10e7c7be3", + "size": 8566 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/__init__.py": { "mode": 420, @@ -316,13 +297,13 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py": { "mode": 420, - "sha256": "621968a20fec17cc69d1d68735c317251dde33168e343f0ecb692aef310861c8", - "size": 10894 + "sha256": "3d6cd89b8a45b246c0fea6dfc760d22cfdfc91557f95fff115bd186da7b76678", + "size": 10897 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py": { "mode": 420, - "sha256": "25304fe7ae034d1bb8c9c86a2757704944539be6a0c69c322e75adc84f66e221", - "size": 7885 + "sha256": "314023341e6fb0201c49975f1e3ed11ff074348e78bd42e893d2317e5c36ea14", + "size": 7896 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/model_types.py": { "mode": 420, @@ -331,8 +312,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py": { "mode": 420, - "sha256": "120904be1407395a0d88d28fcba1fe426a2b84e381c3c2a0e6d9665868f492fe", - "size": 10304 + "sha256": "9f4a0ab397ca4c10cf72318ee39072cb38258afe0a6d31950639fcad80387434", + "size": 10315 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runtime.py": { "mode": 420, @@ -341,8 +322,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py": { "mode": 420, - "sha256": "58ffe0afb2cd9527d2a1d5bbebf0471af8c8210534e266ab80488a7f89bb06ba", - "size": 7684 + "sha256": "175410433d287cfd3ac93e8fae19a5da409af61dab1d1a48731eb8aa7043c18c", + "size": 7702 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/split_model_constants.py": { "mode": 420, @@ -516,13 +497,13 @@ }, "python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py": { "mode": 420, - "sha256": "98f46f8705259b00efae7274186b4e68201aad467a19de4f16d5f850d10ab0b5", - "size": 7585 + "sha256": "38e521356c56ac85f35543b9457d36639e55ed2ac916f30ea3b86948e806541f", + "size": 8016 }, "python/_iqclosure/iqpilot/system/hardware/tici/hardware.py": { "mode": 420, - "sha256": "e8eae4e8a75e1751525471cf54a8555b7bfd7f08ad84bf2e75642b5745f222db", - "size": 25601 + "sha256": "da5ecb280f7af14169903260dc941e3bfac033c93b184cb0abea0f1494c34582", + "size": 25731 }, "python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py": { "mode": 420, @@ -575,49 +556,49 @@ "size": 119 }, "python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "0dcf7516014011de4b56eee175e4e299fa5fee748f6e0b72169605e098c080bc", - "size": 400040 + "mode": 416, + "sha256": "fddf1c1e66a06c6815c7b3bf3b2d35f209bc245073db60542a76a56b28c28497", + "size": 1327832 }, "python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "39263f739980829ebf32ba43abdf32dffa2c015944808ea64cb6ff1dc210a043", - "size": 68152 + "mode": 416, + "sha256": "8f552e5100f29394362210ced30a1e44a4b8227edb3f66fcf2b7dde2f3001e69", + "size": 999816 }, "python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "6787635f4b79687dc54fd5e3c25b72285a5a8e04e81a28b5764deac6ce2492ed", - "size": 333704 + "mode": 416, + "sha256": "17dc02560735d0adf518df30d0fceb5432a1528807e44b2924d22bc4b9f61561", + "size": 1393240 }, "python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "a3bab600ccec9e9edb5817d5a177cd6638a8a727a8d4b25c6e722d4ad038188b", - "size": 675944 + "mode": 416, + "sha256": "e99eed14d07496da9f4d085a03ae3380de93a58fadc9c8bf5247e6f07b0f57d2", + "size": 1655512 }, "python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "9476c1b5934125917bc0a0dceca95274ebf47d45a1ed66122cb34ba050f40e34", - "size": 597936 + "mode": 416, + "sha256": "64904b6215365c7db4b2df51d1706f9483b411768bfaedd888a0e3a1701e0302", + "size": 1655480 }, "python/iqpilot_private/emac/model_context.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "f47a9a61624b9cb5e71bfe979fbae403e04cee27db61f0ccf9b30effd1bad626", - "size": 473440 + "mode": 416, + "sha256": "80ae92f06aa2562acdb6d5c88be004bbc1bf731351640f926f42a30cbde43d39", + "size": 1393208 }, "python/iqpilot_private/emac/native_launcher.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "b68e259978df7824d5e992653aa1861c6fbd746e97f67c753b5f3ca5c4a93f7b", - "size": 268000 + "mode": 416, + "sha256": "41fe239760108a0a2f1c3e96fb0dec4801bcbdad5b6ed5d4b92c10120715ed22", + "size": 1196408 }, "python/iqpilot_private/emac/rh_images.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "ab3543fc0fe22a5778cb79d63b3ff48031a36ce6e873c79dbfe54d73277d7f16", - "size": 206032 + "mode": 416, + "sha256": "dbcb1f6103caecf536d6becfad9351381b0a9eef8742ea7df4c03c817c34e000", + "size": 1262024 }, "python/iqpilot_private/emac/wire_codec.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "fe91bb1f156c49cecb4a4707a37affdab70b7d9125ff23e4081ca9494ba2c356", - "size": 203152 + "mode": 416, + "sha256": "22ef20fc1e70b73f135beb8abeb2831770cc39e0e2a44f3fc7bb44a5da811f93", + "size": 1130760 }, "runtime": { "entries": { @@ -628,14 +609,14 @@ } }, "signatures": { - "python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so": "nSEzB01BtGt3un5WVEbn1E5IaHnj7/zLt4lXIYOfBd/yqMBpuUCZ48SUhJU5hTFKMyiuyk3HG/72iwDWMCo1BA==", - "python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so": "kUADWHWJJcjYKyumfG2b+XtgDgCQTMtz371MDjnngEv4zn0BnW3AlD+WfoggzBcd6FZ5DYNB8oyjea6BkQgMDA==", - "python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so": "CbUzdCkb7mORLJaOMCeyLXEkeZzr3EYA1b5doki+q9AshoXNoSOkso+Uh7fApTNr1nqSy+QNfxBm8IMtNQt8Aw==", - "python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so": "80cJCpENHXIyhxGAHRgqBEfyN7KBQ112YQMdmPe1LOiXASSTWTsu5QMUzzV3u+X5kk+1UqRC0NZ5PjoF/sFADg==", - "python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so": "x4CkpFtIxtZpt3de1im0hi7jABTdJ3mB3OSN4apkeQALCdYdhFzbVasfSrtkpN6t+/CUEx3g5cLhslEUBt9lCw==", - "python/iqpilot_private/emac/model_context.cpython-312-aarch64-linux-gnu.so": "ISg4GM8peP3ER4alhIe3+3ogJ39dNDq+prLO70AtEf/wqSfATPN09rt/kmIrveqgyTlGD2/v3uOvICnLotKlBQ==", - "python/iqpilot_private/emac/native_launcher.cpython-312-aarch64-linux-gnu.so": "ttGirAmlJmDTRy2MseGmR/xSK9KkjXIe9eevQ+EJ2P2itPr1XKN8rOoFXaJdUSQaVlsXi/w/yAtLcNEBf0oABA==", - "python/iqpilot_private/emac/rh_images.cpython-312-aarch64-linux-gnu.so": "/v2I9Y7/OT0GhFGw8MjKWsz4hk4R/l02V5VCxD5iIYZkGBKdy9UPFcBYWjg/zpfwF+cYqwp0SmlrZOBFmykbDw==", - "python/iqpilot_private/emac/wire_codec.cpython-312-aarch64-linux-gnu.so": "jFsJk4sFSEtSamxV6f0dBS1kWYcFQ2KEPA+0UbDqfXf4I3+KSaJpr0CQTmfGhgIWYGxAnetunoEJMKsVrNPNDA==" + "python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so": "c7oPgelyaOkjpN3BDwHkt1UZCGuXa97ocZ4CkNtOyeBxW+7R26hldG7nOaif+nuNmnYNE/ya7ucuPwPkWJXYDA==", + "python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so": "6ypZpUAkpit8UhYLh0XqceQan9EduF4yQ4MA4GqmPvFUw7u/muSOaopK+lBaw1Xtg8yPbfR8DoGZu0uj3fsPDw==", + "python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so": "LrhYCVk6U7yOKNcVJKEBQuOfxC6cJfeUn4IA8gpwmX+ZyaV/oUvv+nDYB37MZD72KRAarhLoTXgpAU9yG7AWCw==", + "python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so": "zm5GDcrKM6wB3mr8bmmjF65QUCTjj8XtvLo1+NrP5wClZKAtVrc90Glfau8YApntZwK+uSKBeHRi2xqg4V14BQ==", + "python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so": "HgA5ik+z5dVMtSJolJQ7ERbW53vlcbG9GoDKfLN+ANluWrd4q1ezQUDvJJ5JZERQEw8N8tho6du2HsPgY65mAQ==", + "python/iqpilot_private/emac/model_context.cpython-312-aarch64-linux-gnu.so": "50yzbrZZAOWidHTH6nZ+2LMvJGJ2LV60GXN2yXdKfVERrd8BGZH8j/oOrG71lW3wf/htiZmZRLgIvEKXcUJSCw==", + "python/iqpilot_private/emac/native_launcher.cpython-312-aarch64-linux-gnu.so": "suTF6e1oz3Lki7qhFQ5wdHa2m5ZccMmCVEy/BZ/6wqem2fxt8C3hd0OjXiM0Xyg7R+pumDpGOiL23uh2EZDcAw==", + "python/iqpilot_private/emac/rh_images.cpython-312-aarch64-linux-gnu.so": "y0YKu5/ezBo04HGeD4mQVlBpRBJdHn4Y8mBt3HNo6f5mcWg9NhJ7T0u3hmvXl1JcMUnTeDDwNkVIi52VrK+5DA==", + "python/iqpilot_private/emac/wire_codec.cpython-312-aarch64-linux-gnu.so": "P+fIzA2ScQzFeulAfgkx5favc2mzvlTybL+e59hBEwF+5bFE3V12DFUOEBga5Rbcv9VrvfG52dMIeysNmHHHCg==" } } diff --git a/artifacts/iqpilot_emac_private/manifest.json.sig b/artifacts/iqpilot_emac_private/manifest.json.sig index cb2a261d..439b35cc 100644 --- a/artifacts/iqpilot_emac_private/manifest.json.sig +++ b/artifacts/iqpilot_emac_private/manifest.json.sig @@ -1 +1 @@ -gmKHJty4j+pkzT4cISfbob5EVVaY0wTii9yUBeuFC4ZF5ERST59xBav/C/5rCnBhk6ednbwTWq68l0uqgc4XDA== +wRI3NynFc/uIF9F5+7fv4xjZh+64osm4CRaZhUM4JAnJXUaYYl5VDp+kZCDJw8cg/i/VKCRZMGNHq8+XE9eLAQ== diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/_vendored.json b/artifacts/iqpilot_emac_private/python/_iqclosure/_vendored.json index a4b369ca..5db6542b 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/_vendored.json +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/_vendored.json @@ -1,7 +1,7 @@ [ "iqpilot.common.api", "iqpilot.common.api.base", - "iqpilot.common.api.comma_connect", + "iqpilot.common.api.konn3kt", "iqpilot.common.atlas_alerts", "iqpilot.common.basedir", "iqpilot.common.constants", @@ -13,19 +13,15 @@ "iqpilot.common.issue_debug", "iqpilot.common.logging_extra", "iqpilot.common.realtime", - "iqpilot.common.spinner", "iqpilot.common.steer_delay", "iqpilot.common.swaglog", "iqpilot.common.time_helpers", "iqpilot.common.utils", - "iqpilot.konn3kt.cloud_client", "iqpilot.konn3kt.registration", - "iqpilot.selfdrive.controls.lib.desire_helper", + "iqpilot.selfdrive.controls.lib.action_engine", "iqpilot.selfdrive.controls.lib.drive_helpers", - "iqpilot.selfdrive.controls.lib.helpers.lane_change", - "iqpilot.selfdrive.controls.lib.helpers.lane_turn", - "iqpilot.selfdrive.controls.lib.helpers.lateral_edge_guard", "iqpilot.selfdrive.iqlocd.models.constants", + "iqpilot.selfdrive.iqmodeld.aux_inputs", "iqpilot.selfdrive.iqmodeld.big_catalog", "iqpilot.selfdrive.iqmodeld.camera", "iqpilot.selfdrive.iqmodeld.config", @@ -47,6 +43,7 @@ "iqpilot.selfdrive.iqmodeld.models.fetcher", "iqpilot.selfdrive.iqmodeld.models.helpers", "iqpilot.selfdrive.iqmodeld.models.inference_state", + "iqpilot.selfdrive.iqmodeld.models.iqc", "iqpilot.selfdrive.iqmodeld.models.runners.model_runner", "iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.combined_split_runner", "iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.fused_runner", diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/__init__.py index 7c7b26fc..b7e90dac 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/__init__.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/__init__.py @@ -1,9 +1,9 @@ -import iqpilot.common.api.comma_connect +import iqpilot.common.api.konn3kt class Api: def __init__(self, dongle_id): - self.service = iqpilot.common.api.comma_connect.CommaConnectApi(dongle_id) + self.service = iqpilot.common.api.konn3kt.Konn3ktApi(dongle_id) def request(self, method, endpoint, **params): return self.service.request(method, endpoint, **params) @@ -19,8 +19,8 @@ class Api: def api_get(endpoint, method='GET', timeout=None, access_token=None, session=None, **params): - return iqpilot.common.api.comma_connect.CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, session, **params) + return iqpilot.common.api.konn3kt.Konn3ktApi(None).api_get(endpoint, method, timeout, access_token, session, **params) def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: - return iqpilot.common.api.comma_connect.CommaConnectApi(None).get_key_pair() + return iqpilot.common.api.konn3kt.Konn3ktApi(None).get_key_pair() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/base.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/base.py index 371a8ab4..0aef9c35 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/base.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/base.py @@ -7,10 +7,44 @@ from functools import lru_cache from iqpilot.system.hardware.hw import Paths from iqpilot.system.version import get_version -# name: jwt signature algorithm KEYS = {"id_rsa": "RS256", "id_ecdsa": "ES256"} +_CUSTODIAN_KEY = object() +_IQKEEPER_SOCKET = "/run/iqkeeper/iqkeeper.sock" + + +def _custodian_available() -> bool: + import stat + try: + return stat.S_ISSOCK(os.stat(_IQKEEPER_SOCKET).st_mode) + except OSError: + return False + + +def _custodian_sign_jwt(dongle_id: str, payload: dict, alg: str) -> str: + import base64 + import json + import socket + nonce = base64.b64encode(os.urandom(16)).decode() + packet = json.dumps({ + "v": 1, + "op": "sign", + "dongle_id": dongle_id, + "payload": payload, + "alg": alg, + "nonce": nonce, + }).encode() + with socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET) as sock: + sock.settimeout(25.0) + sock.connect(_IQKEEPER_SOCKET) + sock.send(packet) + reply_data = sock.recv(8192) + reply = json.loads(reply_data) if reply_data else {} + if reply.get("nonce") != nonce or not reply.get("ok"): + raise PermissionError(f"custodian sign failed: {reply.get('error', 'unknown')}") + return reply["token"] + @lru_cache(maxsize=4) def load_signing_key(private_key: str): @@ -60,6 +94,9 @@ class BaseApi: } if payload_extra is not None: payload.update(payload_extra) + if self.private_key is _CUSTODIAN_KEY: + serializable = {k: v.isoformat() if isinstance(v, datetime) else v for k, v in payload.items()} + return _custodian_sign_jwt(self.dongle_id, serializable, self.jwt_algorithm) key = load_signing_key(self.private_key) if self.private_key else self.private_key token = jwt.encode(payload, key, algorithm=self.jwt_algorithm) if isinstance(token, bytes): @@ -73,9 +110,25 @@ class BaseApi: return self.request(method, endpoint, timeout, access_token, session=session, json=json, **params) @staticmethod - def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: + def get_key_pair() -> tuple[str, str | object, str] | tuple[None, None, None]: for key in KEYS: - if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'): - with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public: - return KEYS[key], private.read(), public.read() + priv_path = Paths.persist_root() + f'/comma/{key}' + pub_path = priv_path + '.pub' + if not os.path.isfile(priv_path) or not os.path.isfile(pub_path): + continue + try: + with open(pub_path) as public: + pub = public.read() + except OSError: + continue + try: + with open(priv_path) as private: + return KEYS[key], private.read(), pub + except PermissionError: + import time + for _attempt in range(10): + if _custodian_available(): + return KEYS[key], _CUSTODIAN_KEY, pub + time.sleep(0.5) + continue return None, None, None diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/comma_connect.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/konn3kt.py similarity index 75% rename from artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/comma_connect.py rename to artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/konn3kt.py index bc16d40e..cae7ea46 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/comma_connect.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/konn3kt.py @@ -1,3 +1,4 @@ +# Copyright (c) 2026 IQ.Lvbs. All rights reserved. import os from iqpilot.common.api.base import BaseApi @@ -5,7 +6,7 @@ from iqpilot.common.api.base import BaseApi API_HOST = os.getenv('API_HOST', 'https://api-iqlabs.konn3kt.com') -class CommaConnectApi(BaseApi): +class Konn3ktApi(BaseApi): def __init__(self, dongle_id): super().__init__(dongle_id, API_HOST) self.user_agent = "openpilot-" diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/realtime.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/realtime.py index 72578964..ca79bbab 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/realtime.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/realtime.py @@ -62,10 +62,23 @@ def config_background_thread() -> None: set_core_affinity(list(range(os.cpu_count() or 1))) +def _swap_active() -> bool: + try: + with open("/proc/swaps") as f: + return len(f.read().splitlines()) > 1 + except Exception: + return False + + def lock_memory() -> None: - """mlockall this process so memory reclaim/compaction can't stall it. RT control - procs only (locking ui/modeld would worsen pressure). Best-effort.""" - if sys.platform != 'linux' or PC: + """mlockall this process so swap can't evict its pages and stall the RT loop. + RT control procs only (locking ui/modeld would worsen pressure). Best-effort. + + No-op without active swap: with none, anonymous pages (thread stacks, heap) + are already unreclaimable, so mlockall buys nothing — it only pins every 8 MB + thread stack and MCL_FUTURE allocation in full (~400 MB/proc here) and, being + non-migratable, starves CMA. The device runs swapless by design (iqos-zram).""" + if sys.platform != 'linux' or PC or not _swap_active(): return try: import ctypes diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/spinner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/spinner.py deleted file mode 100644 index b823d09a..00000000 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/spinner.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import subprocess -from iqpilot.common.basedir import BASEDIR - - -class Spinner: - def __init__(self): - try: - self.spinner_proc = subprocess.Popen(["./spinner.py"], - stdin=subprocess.PIPE, - cwd=os.path.join(BASEDIR, "iqpilot", "system", "ui"), - close_fds=True) - except OSError: - self.spinner_proc = None - - def __enter__(self): - return self - - def update(self, spinner_text: str): - if self.spinner_proc is not None: - self.spinner_proc.stdin.write(spinner_text.encode('utf8') + b"\n") - try: - self.spinner_proc.stdin.flush() - except BrokenPipeError: - pass - - def update_progress(self, cur: float, total: float): - self.update(str(round(100 * cur / total))) - - def close(self): - if self.spinner_proc is not None: - self.spinner_proc.kill() - try: - self.spinner_proc.communicate(timeout=2.) - except subprocess.TimeoutExpired: - print("WARNING: failed to kill spinner") - self.spinner_proc = None - - def __del__(self): - self.close() - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - -if __name__ == "__main__": - import time - with Spinner() as s: - s.update("Spinner text") - time.sleep(5.0) - print("gone") - time.sleep(5.0) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/utils.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/utils.py index a80270bb..005d75b8 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/utils.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/utils.py @@ -31,24 +31,44 @@ class Timer: realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" +_sudo_available: bool | None = None + + def sudo_write(val: str, path: str) -> None: + global _sudo_available try: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - os.system(f"sudo chmod a+w {path}") - try: + return + except OSError: + pass + # AGNOS 4.9.9.2 removes owner root; a privileged boot step opens these nodes to the stack so a direct write normally works. Only probe a non-interactive escalation once (interactive sudo blocks with no tty, and sudo -n itself can stall), and never let its failure or timeout propagate. + if _sudo_available is False: + return + try: + _sudo_available = subprocess.run(["sudo", "-n", "chmod", "a+w", path], timeout=2, capture_output=True).returncode == 0 + if _sudo_available: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - # fallback for debugfs files - os.system(f"sudo su -c 'echo {val} > {path}'") + except Exception: + _sudo_available = False def sudo_read(path: str) -> str: + global _sudo_available try: - return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + with open(path, encoding='utf8') as f: + return f.read().strip() + except OSError: + pass + if _sudo_available is False: + return "" + try: + result = subprocess.run(["sudo", "-n", "cat", path], timeout=2, capture_output=True, text=True) + _sudo_available = result.returncode == 0 + return result.stdout.strip() except Exception: + _sudo_available = False return "" diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py deleted file mode 100644 index 2897609e..00000000 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos -""" - - -import os -from iqpilot.common.api.base import BaseApi -API_HOST = os.getenv('KONN3KT_API_HOST', 'https://api-iqlabs.konn3kt.com') - -class Konn3ktApi(BaseApi): - - def __init__(self, dongle_id): - super().__init__(dongle_id, API_HOST) - self.user_agent = "konn3kt-device-" - - def get_token(self, expiry_hours=1): - return super()._get_token(expiry_hours=expiry_hours) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/registration.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/registration.py index 11e4fc30..a3720bfa 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/registration.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/registration.py @@ -1,18 +1,12 @@ #!/usr/bin/env python3 -import os -import time -import json -import jwt +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" import re import secrets -from typing import cast from pathlib import Path -from datetime import datetime, timedelta, UTC -from iqpilot.common.api import api_get, get_key_pair from iqpilot.common.params import Params -from iqpilot.common.spinner import Spinner -from iqpilot.system.hardware import HARDWARE from iqpilot.system.hardware.hw import Paths from iqpilot.common.swaglog import cloudlog @@ -35,6 +29,10 @@ def _read_persist_dongle_id() -> str | None: return None +def is_valid_dongle_id(dongle_id: str | None) -> bool: + return bool(dongle_id and _DONGLE_ID_RE.fullmatch(dongle_id)) + + def get_cached_dongle_id(params: Params | None = None, prefer_readonly: bool = True) -> str | None: ro = _read_persist_dongle_id() if is_valid_dongle_id(ro): @@ -46,8 +44,8 @@ def get_cached_dongle_id(params: Params | None = None, prefer_readonly: bool = T if v and v != UNREGISTERED_DONGLE_ID: return v.lower() if is_valid_dongle_id(v) else v return ro or None -def is_valid_dongle_id(dongle_id: str | None) -> bool: - return bool(dongle_id and _DONGLE_ID_RE.fullmatch(dongle_id)) + + def get_or_create_dongle_id(params: Params | None = None, prefer_readonly: bool = True) -> str: p = Params() if params is None else params dongle_id = get_cached_dongle_id(p, prefer_readonly=prefer_readonly) @@ -57,164 +55,32 @@ def get_or_create_dongle_id(params: Params | None = None, prefer_readonly: bool p.put("DongleId", dongle_id) cloudlog.warning(f"generated new DongleId={dongle_id} (no readonly dongle_id found)") return dongle_id + + +_impl = None + + +def _registration_impl(): + global _impl + if _impl is None: + from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + try: + _impl = load_private_module(__name__, "iqpilot_private.konn3kt.registration.reg_core") + except ProprietaryModuleMissing: + from konn3kt_private.registration import reg_core as _impl + return _impl + + def ensure_dev_pairing_identity(params: Params | None = None, force_reset: bool = False) -> dict[str, str]: - p = Params() if params is None else params - - persist_dir = Path(Paths.persist_root()) / "comma" - persist_dir.mkdir(parents=True, exist_ok=True) - - dongle_path = persist_dir / "dongle_id" - priv_path = persist_dir / "id_rsa" - pub_path = persist_dir / "id_rsa.pub" - - if force_reset: - for fp in (dongle_path, priv_path, pub_path): - try: - fp.unlink(missing_ok=True) - except Exception: - cloudlog.exception(f"failed to remove {fp}") - try: - (persist_dir / "konn3kt_prime_type").unlink(missing_ok=True) - except Exception: - pass - try: - p.remove("PrimeType") - except Exception: - pass - forced_dongle = os.getenv("KONN3KT_DEV_DONGLE_ID") - dongle_id = forced_dongle.strip().lower() if forced_dongle else None - if dongle_id and not is_valid_dongle_id(dongle_id): - cloudlog.error("KONN3KT_DEV_DONGLE_ID must be 16 hex chars") - dongle_id = None - if dongle_id is None: - existing = None - try: - existing = dongle_path.read_text().strip().lower() if dongle_path.is_file() else None - except Exception: - cloudlog.exception("failed reading existing dev dongle_id") - dongle_id = existing if is_valid_dongle_id(existing) else secrets.token_hex(8) - try: - dongle_path.write_text(dongle_id) - except Exception: - cloudlog.exception("failed writing dev dongle_id") - p.put("DongleId", dongle_id) - p.put("HardwareSerial", p.get("HardwareSerial") or f"DEV-{dongle_id}") - if force_reset or (not priv_path.is_file()) or (not pub_path.is_file()): - try: - from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import rsa - key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - priv_bytes = key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - pub_bytes = key.public_key().public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - priv_path.write_bytes(priv_bytes) - pub_path.write_bytes(pub_bytes) - except Exception: - cloudlog.exception("failed generating dev RSA keys") - raise - return { - "dongle_id": dongle_id, - "serial": p.get("HardwareSerial") or f"DEV-{dongle_id}", - "persist_dir": str(persist_dir), - } -def _normalize_imei(value: str | None) -> str: - return value or "" + return _registration_impl().ensure_dev_pairing_identity(params, force_reset) def get_registration_identifiers(wait_timeout: float = IMEI_WAIT_TIMEOUT, show_spinner: bool = False) -> tuple[str, str, str]: - serial = HARDWARE.get_serial() - spinner = Spinner() if show_spinner else None - start_time = time.monotonic() - imei1: str | None = None - imei2: str | None = None - - while time.monotonic() - start_time < wait_timeout: - try: - imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) - if imei1 or imei2: - break - except RuntimeError as e: - if "no modems" in str(e).lower(): - cloudlog.warning("No cellular modem available, proceeding without IMEI") - break - cloudlog.exception("Error getting imei, trying again...") - except Exception: - cloudlog.exception("Error getting imei, trying again...") - time.sleep(1) - - imei1 = _normalize_imei(imei1) - imei2 = _normalize_imei(imei2) - - if not imei1 and not imei2: - cloudlog.warning(f"proceeding with serial-only registration for serial={serial}") - if spinner is not None: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1 or None}, {imei2 or None})") - spinner.close() - - return serial, imei1, imei2 + return _registration_impl().get_registration_identifiers(wait_timeout, show_spinner) def register(show_spinner=False) -> str | None: - params = Params() - - dongle_id: str | None = get_cached_dongle_id(params, prefer_readonly=True) - if dongle_id in ("", UNREGISTERED_DONGLE_ID): - dongle_id = None - - jwt_algo, private_key, public_key = get_key_pair() - - if not public_key: - dongle_id = UNREGISTERED_DONGLE_ID - cloudlog.warning("missing public key") - elif dongle_id is None: - if show_spinner: - spinner = Spinner() - spinner.update("registering device") - - serial, imei1, imei2 = get_registration_identifiers(wait_timeout=IMEI_WAIT_TIMEOUT, show_spinner=False) - - backoff = 0 - start_time = time.monotonic() - while True: - try: - register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, - cast(str, private_key), algorithm=jwt_algo) - cloudlog.info("getting pilotauth") - cloudlog.info("getting pilotauth") - resp = api_get("v2/pilotauth/", method='POST', timeout=15, - imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token) - - if resp.status_code in (400, 402, 403): - cloudlog.info(f"Unable to register device, got {resp.status_code}") - dongle_id = UNREGISTERED_DONGLE_ID - else: - dongleauth = json.loads(resp.text) - dongle_id = dongleauth["dongle_id"] - break - except Exception: - cloudlog.exception("failed to authenticate") - backoff = min(backoff + 1, 15) - time.sleep(backoff) - - if time.monotonic() - start_time > 60 and show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") - return UNREGISTERED_DONGLE_ID - - if show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1 or None}, {imei2 or None})") - spinner.close() - - if dongle_id: - params.put("DongleId", dongle_id) - from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert - set_offroad_alert("Offroad_UnregisteredHardware", False) - return dongle_id + return _registration_impl().register(show_spinner) if __name__ == "__main__": diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/action_engine.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/action_engine.py new file mode 100644 index 00000000..053a5ed9 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/action_engine.py @@ -0,0 +1,18 @@ +# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.navd.action_engine") +except ProprietaryModuleMissing: + from iqpilot.cereal import custom, log + + class ActionEngine: + def __init__(self): + self.desire = log.Desire.none + self.lane_change_state = log.LaneChangeState.off + self.lane_change_direction = log.LaneChangeDirection.none + self.lane_turn_direction = custom.IQTurnSignalDirection.none + self.lateral_edge_block = custom.IQLateralEdgeBlock.none + + def update(self, carstate, lateral_active, lane_change_prob, nav_state=None, modeldata=None, radar_state=None): + return None diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/desire_helper.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/desire_helper.py deleted file mode 100644 index 0d0f09b9..00000000 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/desire_helper.py +++ /dev/null @@ -1,360 +0,0 @@ -""" -Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ -""" - -from __future__ import annotations - -import math - -from iqpilot.cereal import car, custom, log - -from iqpilot.common.constants import CV -from iqpilot.common.params import Params -from iqpilot.common.realtime import DT_MDL -from iqpilot.selfdrive.controls.lib.helpers.lane_change import ( - IQLaneSwapController, - AutoLaneChangeMode, - NavExitLaneChangeController, -) -from iqpilot.selfdrive.controls.lib.helpers.lateral_edge_guard import LateralEdgeGuard -from iqpilot.selfdrive.controls.lib.helpers.lane_turn import IQNavTurnController - -LaneChangeState = log.LaneChangeState -LaneChangeDirection = log.LaneChangeDirection -TurnDirection = custom.IQTurnSignalDirection -LateralEdgeBlock = custom.IQLateralEdgeBlock -NavManeuverPhase = custom.IQNavState.ManeuverPhase - -LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS -LANE_CHANGE_TIME_MAX = 10.0 -LANE_CHANGE_CANCEL_TORQUE_FRAMES = round(0.25 / DT_MDL) -# only the rising edge reaches the model and it stays visible for the 5 s the history spans, so one -# edge per 5 s keeps the turn alive through the maneuver without stacking edges that cut the corner -TURN_DESIRE_PULSE_CYCLE_FRAMES = round(5.0 / DT_MDL) -TURN_DESIRE_PULSE_HOLD_FRAMES = TURN_DESIRE_PULSE_CYCLE_FRAMES - 1 -TURN_RECOIL_HEADING_RAD = math.radians(55.0) -TURN_DESIRE_MAX_SECONDS = 25.0 -TURN_DESIRE_MOVING_SPEED = 0.5 - -_LANE_CHANGE_DESIRES = { - (LaneChangeDirection.none, LaneChangeState.off): log.Desire.none, - (LaneChangeDirection.none, LaneChangeState.preLaneChange): log.Desire.none, - (LaneChangeDirection.none, LaneChangeState.laneChangeStarting): log.Desire.none, - (LaneChangeDirection.none, LaneChangeState.laneChangeFinishing): log.Desire.none, - (LaneChangeDirection.left, LaneChangeState.off): log.Desire.none, - (LaneChangeDirection.left, LaneChangeState.preLaneChange): log.Desire.none, - (LaneChangeDirection.left, LaneChangeState.laneChangeStarting): log.Desire.laneChangeLeft, - (LaneChangeDirection.left, LaneChangeState.laneChangeFinishing): log.Desire.laneChangeLeft, - (LaneChangeDirection.right, LaneChangeState.off): log.Desire.none, - (LaneChangeDirection.right, LaneChangeState.preLaneChange): log.Desire.none, - (LaneChangeDirection.right, LaneChangeState.laneChangeStarting): log.Desire.laneChangeRight, - (LaneChangeDirection.right, LaneChangeState.laneChangeFinishing): log.Desire.laneChangeRight, -} - -_TURN_DESIRES = { - TurnDirection.none: log.Desire.none, - TurnDirection.turnLeft: log.Desire.turnLeft, - TurnDirection.turnRight: log.Desire.turnRight, -} - -_PULSED_TURN_DESIRES = { - log.Desire.turnLeft, - log.Desire.turnRight, -} - - -def turn_desire(turn_direction) -> log.Desire: - return _TURN_DESIRES[getattr(turn_direction, "raw", turn_direction)] - - -def _direction_from_blinkers(carstate) -> int: - if carstate.leftBlinker: - return LaneChangeDirection.left - if carstate.rightBlinker: - return LaneChangeDirection.right - return LaneChangeDirection.none - - -def _steering_nudge_matches(carstate, direction: int) -> bool: - if not carstate.steeringPressed: - return False - return ( - (direction == LaneChangeDirection.left and carstate.steeringTorque > 0) or - (direction == LaneChangeDirection.right and carstate.steeringTorque < 0) - ) - - -def _opposite_direction(direction: int) -> int: - if direction == LaneChangeDirection.left: - return LaneChangeDirection.right - if direction == LaneChangeDirection.right: - return LaneChangeDirection.left - return LaneChangeDirection.none - - -def _blinker_matches(carstate, direction: int) -> bool: - return ( - (direction == LaneChangeDirection.left and carstate.leftBlinker and not carstate.rightBlinker) or - (direction == LaneChangeDirection.right and carstate.rightBlinker and not carstate.leftBlinker) - ) - - -def _blindspot_matches(carstate, direction: int) -> bool: - return ( - (direction == LaneChangeDirection.left and carstate.leftBlindspot) or - (direction == LaneChangeDirection.right and carstate.rightBlindspot) - ) - - -def _read_enable_bsm() -> bool: - try: - with car.CarParams.from_bytes(Params().get("CarParams")) as cp: - return bool(cp.enableBsm) - except Exception: - return False - - -class DesireHelper: - def __init__(self): - self.lane_change_state = LaneChangeState.off - self.lane_change_direction = LaneChangeDirection.none - self.lane_change_timer = 0.0 - self.lane_change_ll_prob = 1.0 - self.lane_change_counter_torque_frames = 0 - self.prev_one_blinker = False - self.prev_nav_exit_active = False - self.nav_exit_rearmed = False - self.desire = log.Desire.none - - self.alc = IQLaneSwapController(self) - self.lane_turn_controller = IQNavTurnController(self) - self.nav_exit = NavExitLaneChangeController(_read_enable_bsm()) - self.lateral_edge_guard = LateralEdgeGuard() - self.lateral_edge_block = LateralEdgeBlock.none - self.lane_turn_direction = TurnDirection.none - self.nav_turn_direction = TurnDirection.none - self.turn_desire_frames = 0 - self.turn_desire_cycle_input = log.Desire.none - self.turn_heading_rad = 0.0 - self.turn_desire_seconds = 0.0 - self.turn_recoiled = False - - @staticmethod - def get_lane_change_direction(carstate): - return _direction_from_blinkers(carstate) - - @staticmethod - def _nav_turn_desire(nav_state): - if nav_state is None or not getattr(nav_state, "active", False): - return TurnDirection.none - if getattr(nav_state, "maneuverPhase", NavManeuverPhase.none) != NavManeuverPhase.turnActive: - return TurnDirection.none - if not getattr(nav_state, "shouldSendTurnDesire", False): - return TurnDirection.none - return getattr(nav_state, "turnDesireDirection", TurnDirection.none) - - def _clear_lane_change(self) -> None: - self.lane_change_state = LaneChangeState.off - self.lane_change_direction = LaneChangeDirection.none - - def _refresh_turn_overrides(self, carstate, nav_state) -> bool: - speed_mps = carstate.vEgo - self.lane_turn_controller.update_params() - self.lane_turn_controller.update_lane_turn( - blindspot_left=carstate.leftBlindspot, - blindspot_right=carstate.rightBlindspot, - left_blinker=carstate.leftBlinker, - right_blinker=carstate.rightBlinker, - v_ego=speed_mps, - ) - self.lane_turn_direction = self.lane_turn_controller.get_turn_direction() - self.nav_turn_direction = self._nav_turn_desire(nav_state) - - self.nav_exit.update_params() - self.nav_exit.update(nav_state, carstate) - return bool(self.nav_exit.active) - - def _reset_required(self, lateral_active: bool, nav_exit_active: bool) -> bool: - timed_out = self.lane_change_timer > LANE_CHANGE_TIME_MAX - feature_disabled = self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF and not nav_exit_active - return (not lateral_active) or timed_out or feature_disabled - - def _begin_from_idle(self, one_blinker: bool, nav_exit_active: bool, below_speed: bool) -> None: - if below_speed: - return - if one_blinker and not self.prev_one_blinker: - self.lane_change_state = LaneChangeState.preLaneChange - self.lane_change_direction = _direction_from_blinkers(self._last_carstate) - self.lane_change_ll_prob = 1.0 - return - if nav_exit_active and not self.prev_nav_exit_active: - self.lane_change_state = LaneChangeState.preLaneChange - self.lane_change_direction = self.nav_exit.direction - self.lane_change_ll_prob = 1.0 - - def _refresh_requested_direction(self, one_blinker: bool, nav_exit_active: bool) -> None: - if one_blinker: - self.lane_change_direction = _direction_from_blinkers(self._last_carstate) - elif nav_exit_active: - self.lane_change_direction = self.nav_exit.direction - - def _step_pre_lane_change(self, one_blinker: bool, nav_exit_active: bool, below_speed: bool) -> None: - self._refresh_requested_direction(one_blinker, nav_exit_active) - blindspot_detected = _blindspot_matches(self._last_carstate, self.lane_change_direction) - self.lateral_edge_block = self.lateral_edge_guard.block_for_direction(self.lane_change_direction) - lateral_edge_blocked = self.lateral_edge_block != LateralEdgeBlock.none - steering_ready = _steering_nudge_matches(self._last_carstate, self.lane_change_direction) - # an exit can need more than one lane, but only the first move may start without a driver nudge - nav_auto_start = nav_exit_active and self.nav_exit.auto_allowed and not self.nav_exit_rearmed - - self.alc.update_lane_change(blindspot_detected=blindspot_detected, brake_pressed=self._last_carstate.brakePressed) - allowed_to_launch = steering_ready or self.alc.auto_lane_change_allowed or nav_auto_start - - if (not (one_blinker or nav_exit_active)) or below_speed: - self._clear_lane_change() - elif allowed_to_launch and not blindspot_detected and not lateral_edge_blocked: - self.lane_change_state = LaneChangeState.laneChangeStarting - - def _step_lane_change_starting(self, lane_change_prob: float) -> None: - self.lane_change_ll_prob = max(self.lane_change_ll_prob - (2.0 * DT_MDL), 0.0) - if lane_change_prob < 0.02 and self.lane_change_ll_prob < 0.01: - self.lane_change_state = LaneChangeState.laneChangeFinishing - - def _step_lane_change_finishing(self, one_blinker: bool, nav_exit_active: bool) -> None: - self.lane_change_ll_prob = min(self.lane_change_ll_prob + DT_MDL, 1.0) - if self.lane_change_ll_prob <= 0.99: - return - self.lane_change_direction = LaneChangeDirection.none - if one_blinker: - self.lane_change_state = LaneChangeState.preLaneChange - elif nav_exit_active: - self.lane_change_state = LaneChangeState.preLaneChange - self.lane_change_direction = self.nav_exit.direction - self.nav_exit_rearmed = True - else: - self.lane_change_state = LaneChangeState.off - - def _driver_cancels_lane_change(self) -> bool: - if self.lane_change_state not in (LaneChangeState.laneChangeStarting, LaneChangeState.laneChangeFinishing): - self.lane_change_counter_torque_frames = 0 - return False - opposite = _opposite_direction(self.lane_change_direction) - if _steering_nudge_matches(self._last_carstate, opposite): - self.lane_change_counter_torque_frames += 1 - else: - self.lane_change_counter_torque_frames = 0 - return (_blinker_matches(self._last_carstate, opposite) or - self.lane_change_counter_torque_frames >= LANE_CHANGE_CANCEL_TORQUE_FRAMES) - - def _cancel_lane_change(self) -> None: - self._clear_lane_change() - self.lane_change_ll_prob = 1.0 - self.lane_change_counter_torque_frames = 0 - - def _advance_lane_change_machine(self, one_blinker: bool, nav_exit_active: bool, below_speed: bool, lane_change_prob: float) -> None: - if self._driver_cancels_lane_change(): - self._cancel_lane_change() - return - if self.lane_change_state == LaneChangeState.off: - self._begin_from_idle(one_blinker, nav_exit_active, below_speed) - return - if self.lane_change_state == LaneChangeState.preLaneChange: - self._step_pre_lane_change(one_blinker, nav_exit_active, below_speed) - return - if self.lane_change_state == LaneChangeState.laneChangeStarting: - self._step_lane_change_starting(lane_change_prob) - return - if self.lane_change_state == LaneChangeState.laneChangeFinishing: - self._step_lane_change_finishing(one_blinker, nav_exit_active) - - def _update_timer(self) -> None: - if self.lane_change_state in (LaneChangeState.off, LaneChangeState.preLaneChange): - self.lane_change_timer = 0.0 - else: - self.lane_change_timer += DT_MDL - - def _clear_turn_desire_pulse(self) -> None: - self.turn_desire_frames = 0 - self.turn_desire_cycle_input = log.Desire.none - self.turn_heading_rad = 0.0 - self.turn_desire_seconds = 0.0 - self.turn_recoiled = False - - def _measured_yaw_rate(self) -> float: - # carState.yawRate is left unpopulated by many ports, so the model pose is the primary source - rate = getattr(getattr(getattr(self, "_last_modeldata", None), "orientationRate", None), "z", None) - try: - if rate is not None and len(rate) and math.isfinite(rate[0]): - return float(rate[0]) - except (TypeError, IndexError): - pass - try: - return float(getattr(self._last_carstate, "yawRate", 0.0) or 0.0) - except (TypeError, ValueError): - return 0.0 - - def _turn_recoiled(self, desired_output: log.Desire) -> bool: - # blinkers keep running well past the apex, so the executed heading change has to end the - # desire: one that outlives the turn keeps steering the plan into the far side of the road - if desired_output != self.turn_desire_cycle_input: - self.turn_heading_rad = 0.0 - self.turn_desire_seconds = 0.0 - self.turn_recoiled = False - self.turn_heading_rad += abs(self._measured_yaw_rate()) * DT_MDL - # a red light at the junction must not use up the turn before the car moves - if float(getattr(self._last_carstate, "vEgo", 0.0) or 0.0) > TURN_DESIRE_MOVING_SPEED: - self.turn_desire_seconds += DT_MDL - if self.turn_heading_rad >= TURN_RECOIL_HEADING_RAD or self.turn_desire_seconds >= TURN_DESIRE_MAX_SECONDS: - self.turn_recoiled = True - return self.turn_recoiled - - def _pulse_turn_desire(self, desired_output: log.Desire) -> log.Desire: - if desired_output not in _PULSED_TURN_DESIRES: - self._clear_turn_desire_pulse() - return desired_output - - recoiled = self._turn_recoiled(desired_output) - - if desired_output != self.turn_desire_cycle_input: - self.turn_desire_frames = 0 - self.turn_desire_cycle_input = desired_output - - cycle_frame = self.turn_desire_frames % TURN_DESIRE_PULSE_CYCLE_FRAMES - self.turn_desire_frames += 1 - if recoiled or cycle_frame >= TURN_DESIRE_PULSE_HOLD_FRAMES: - return log.Desire.none - return desired_output - - def _pick_desire_output(self) -> None: - desired_output = log.Desire.none - if self.nav_turn_direction != TurnDirection.none: - desired_output = turn_desire(self.nav_turn_direction) - elif self.lane_turn_direction != TurnDirection.none: - desired_output = turn_desire(self.lane_turn_direction) - else: - desired_output = _LANE_CHANGE_DESIRES[(self.lane_change_direction, self.lane_change_state)] - - self.desire = self._pulse_turn_desire(desired_output) - - def update(self, carstate, lateral_active, lane_change_prob, nav_state=None, modeldata=None, radar_state=None): - self._last_carstate = carstate - self._last_modeldata = modeldata - self.lateral_edge_guard.update(modeldata, carstate.vEgo, DT_MDL) - self.lateral_edge_block = LateralEdgeBlock.none - one_blinker = carstate.leftBlinker != carstate.rightBlinker - below_speed = carstate.vEgo < LANE_CHANGE_SPEED_MIN - nav_exit_active = self._refresh_turn_overrides(carstate, nav_state) - if not nav_exit_active: - self.nav_exit_rearmed = False - - self.alc.update_params() - if self._reset_required(lateral_active, nav_exit_active): - self._clear_lane_change() - else: - self._advance_lane_change_machine(one_blinker, nav_exit_active, below_speed, lane_change_prob) - - self._update_timer() - self.prev_one_blinker = one_blinker and lateral_active - self.prev_nav_exit_active = nav_exit_active - self.alc.update_state() - self._pick_desire_output() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/aux_inputs.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/aux_inputs.py new file mode 100644 index 00000000..28b599ad --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/aux_inputs.py @@ -0,0 +1,17 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.context.aux_inputs") +except ProprietaryModuleMissing: + class AuxModelInputs: + def __init__(self, input_spec: dict): + self.active = False + + def poll(self) -> None: + pass + + def extras(self) -> dict: + return {} diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/daemon.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/daemon.py index e8c1a9ee..104b102a 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/daemon.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/daemon.py @@ -23,7 +23,7 @@ from iqpilot.common.realtime import DT_MDL, config_realtime_process from iqpilot.common.swaglog import cloudlog from iqpilot.common.transformations.camera import DEVICE_CAMERAS from iqpilot.common.transformations.model import get_warp_matrix -from iqpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from iqpilot.selfdrive.controls.lib.action_engine import ActionEngine from iqpilot.selfdrive.controls.lib.drive_helpers import ( MODEL_SMOOTHING_MAX_TOTAL_SEC, dynamic_lat_smooth_extra_seconds, @@ -34,7 +34,7 @@ from iqpilot.selfdrive.locationd.calibration_helpers import get_calibrated_rpy from iqpilot.system import sentry from iqpilot.common.steer_delay import lateral_action_delay -from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle +from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle, load_default_model_session, forced_active_bundle from iqpilot.selfdrive.iqmodeld.models.inference_state import InferenceStateBase from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import get_model_runner from iqpilot.selfdrive.iqmodeld.camera import CameraOffsetHelper @@ -243,13 +243,30 @@ def _merged_plan(runtime_state: "NeuralEngineState", outputs: dict[str, np.ndarr return base_plan + (runtime_state.PLANPLUS_CONTROL * _planplus_gain(vehicle_speed)) * outputs["planplus"][0] +def _set_default_fallback_alert(active: bool) -> None: + try: + from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert + set_offroad_alert("Offroad_ModelFallbackDefault", active) + except Exception: + cloudlog.exception("could not update the default-model fallback alert") + + class NeuralEngineState(InferenceStateBase): frames: dict[str, RoadProjector] def __init__(self, gpu_context: WarpContext): super().__init__() - runner = get_model_runner() - bundle = get_active_bundle() + try: + runner = get_model_runner() + bundle = get_active_bundle() + _set_default_fallback_alert(False) + except Exception: + cloudlog.exception("active model failed to load; running the default model this session so pose stays valid") + default_bundle = load_default_model_session() + with forced_active_bundle(default_bundle): + runner = get_model_runner() + bundle = get_active_bundle() + _set_default_fallback_alert(True) self.model_runner = runner self.constants = runner.constants @@ -354,7 +371,10 @@ class NeuralEngineState(InferenceStateBase): def get_action_from_model(self, outputs: dict[str, np.ndarray], previous_action: log.ModelDataV2.Action, lat_action_t: float, long_action_t: float, vehicle_speed: float, - lat_smooth_seconds: float | None = None) -> log.ModelDataV2.Action: + lat_smooth_seconds: float | None = None, *, + lat_active: bool = True, current_curvature: float = 0.0) -> log.ModelDataV2.Action: + if not lat_active: + previous_action.desiredCurvature = current_curvature if lat_smooth_seconds is None: lat_smooth_seconds = self.LAT_SMOOTH_SECONDS @@ -568,7 +588,7 @@ class InferenceDaemon: self._pub = PubMaster(pub_services) self._sub = SubMaster([ "deviceState", "carState", "roadCameraState", "extrinsicsCalibration", - "driverMonitoringState", "carControl", "lateralDelay", "iqNavState", "navInstruction", "radarState", + "driverMonitoringState", "carControl", "controlsState", "lateralDelay", "iqNavState", "navInstruction", "radarState", ] + self._feature_inputs.services) self._message_memory = DrivePacketMemory() self._params = Params() @@ -580,7 +600,7 @@ class InferenceDaemon: self._car_params = self._load_car_params(demo) self._long_action_delay = self._car_params.longitudinalActuatorDelay + self._runtime.LONG_SMOOTH_SECONDS self._previous_action = log.ModelDataV2.Action() - self._desire_logic = DesireHelper() + self._desire_logic = ActionEngine() self._lat_smooth_extra_sec = 0.0 self._drive_profile_state = DriveProfileState() self._memory_resets = MemoryResetTracker() @@ -664,7 +684,8 @@ class InferenceDaemon: ) lat_smooth_total_sec = min(self._runtime.LAT_SMOOTH_SECONDS + self._lat_smooth_extra_sec, MODEL_SMOOTHING_MAX_TOTAL_SEC) action = self._runtime.get_action_from_model( - outputs, self._previous_action, lat_horizon, long_horizon, vehicle_speed, lat_smooth_total_sec + outputs, self._previous_action, lat_horizon, long_horizon, vehicle_speed, lat_smooth_total_sec, + lat_active=self._sub["carControl"].latActive, current_curvature=self._sub["controlsState"].curvature, ) self._previous_action = action diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/driving_action.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/driving_action.py index 67a38f68..d994f868 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/driving_action.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/driving_action.py @@ -17,7 +17,10 @@ DESIRE_LEN = 8 def get_action_from_model(outputs: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, v_ego: float, lat_action_t: float, long_action_t: float, - lat_smooth_seconds: float | None = None) -> log.ModelDataV2.Action: + lat_smooth_seconds: float | None = None, *, + lat_active: bool = True, current_curvature: float = 0.0) -> log.ModelDataV2.Action: + if not lat_active: + prev_action.desiredCurvature = current_curvature if "action" in outputs: desired_accel = float(outputs["action"][0, 1]) desired_curvature = float(outputs["action"][0, 0]) / (max(1.0, v_ego)) ** 2 diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py index 1749bfe0..6ba1c352 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py @@ -139,7 +139,18 @@ def egpu_oob_pkl_path(meta: dict) -> str: def egpu_model_oob_pkl_path(meta: dict) -> str: from iqpilot.system.hardware.hw import Paths - return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_model_oob.pkl") + return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_model_oob2.pkl") + + +def drop_frozen_model_oob(meta: dict) -> None: + from iqpilot.system.hardware.hw import Paths + # warp-on-dock bundles saved under the old name were built before the prune fix and replay capture-time feeds + stale = os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_model_oob.pkl") + try: + os.remove(stale) + cloudlog.warning(f"removed frozen-feed warp-on-dock artifact {stale}") + except OSError: + pass def onnx_cache_path(meta: dict) -> str: @@ -288,6 +299,28 @@ def patch_tinygrad_fetch_fw() -> None: helpers.fetch_fw = fetch_fw +def patch_tinygrad_prune_views() -> None: + from tinygrad.engine import jit + from tinygrad.uop.ops import Ops + if getattr(jit.prune_linear, "_iq_patched", False): + return + + # upstream prune only sees bare BUFFER args, so a kernel reading an input through a SLICE view ran once at capture and froze that input + def prune_linear(linear, needed): + kept, onetime = [], [] + for si in linear.src: + si_bufs = {b for src in si.src[1:] for b in src.toposort() if b.op is Ops.BUFFER} + if not si_bufs.isdisjoint(needed): + kept.append(si) + needed |= si_bufs + else: + onetime.append(si) + return linear.replace(src=tuple(kept)), linear.replace(src=tuple(onetime)) + + prune_linear._iq_patched = True + jit.prune_linear = prune_linear + + def patch_usb_signal_wait() -> None: import time diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py index b8f4ccfc..7b75718a 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py @@ -3,7 +3,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed """ from __future__ import annotations -import functools import hashlib import json import os @@ -24,26 +23,29 @@ RANGE_RETRIES = 4 PARALLEL_PARTS = max(1, int(os.environ.get("IQ_DOWNLOAD_PARTS", "3"))) -def _requests_auth(): +def _git_auth(): import importlib for mod in ("iqpilot_private.models.git_auth", "iqpilot.models_private_src.git_auth", "iqpilot.selfdrive.iqmodeld.models.git_auth"): try: - return importlib.import_module(mod).get_requests_auth() + return importlib.import_module(mod) except Exception: continue return None -def _hf(): - import importlib - for mod in ("iqpilot_private.models.git_auth", "iqpilot.selfdrive.iqmodeld.models.git_auth"): - try: - m = importlib.import_module(mod) - return m.get_hf_headers(), m.hf_resolve_url - except Exception: - continue - return None, None +def _resolve_hf(hf_path: str) -> tuple[str | None, dict]: + m = _git_auth() + if m is None: + return None, {} + return m.resolve_hf(hf_path) + + +def requests_get(url: str, **kwargs): + m = _git_auth() + if m is None: + raise RuntimeError("no asset broker available") + return m.requests_get(url, **kwargs) class RangesNotHonoured(Exception): @@ -189,10 +191,9 @@ def _verify_file(tmp: str, sha256: str, size: int) -> None: def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb=None) -> str: import requests - headers, resolve = _hf() - if resolve is None: + url, headers = _resolve_hf(hf_path) + if url is None: raise RuntimeError("no HF credentials available") - url = resolve(hf_path) os.makedirs(os.path.dirname(dst), exist_ok=True) tmp = dst + ".hfpart" last_error: Exception | None = None @@ -213,8 +214,10 @@ def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb os.remove(leftover) except Exception as e: last_error = e - for _attempt in range(STREAM_RETRIES): + for attempt in range(STREAM_RETRIES): try: + if attempt: + url, headers = _resolve_hf(hf_path) have = os.path.getsize(tmp) if os.path.isfile(tmp) else 0 if size and have > size: os.remove(tmp) @@ -242,26 +245,6 @@ def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb raise RuntimeError(f"HF download failed: {last_error}") -def _lfs_endpoint(base_url: str) -> str: - return base_url.split("/raw/", 1)[0] + ".git/info/lfs" - - -def _resolve_oid(session, base_url: str, oid: str, size: int, auth): - import requests - batch = session.post(f"{_lfs_endpoint(base_url)}/objects/batch", - data=json.dumps({"operation": "download", "transfers": ["basic"], - "objects": [{"oid": oid, "size": size}]}), - headers={"Content-Type": "application/vnd.git-lfs+json", - "Accept": "application/vnd.git-lfs+json"}, - auth=auth, timeout=HTTP_TIMEOUT_S) - batch.raise_for_status() - entry = batch.json()["objects"][0] - if "actions" not in entry: - raise requests.RequestException(f"LFS object unavailable: {entry.get('error', oid)}") - action = entry["actions"]["download"] - return action["href"], action.get("header", {}) - - def _part_path(dst: str, oid: str) -> str: return os.path.join(dst + ".parts", oid) @@ -276,19 +259,28 @@ def _part_complete(path: str, oid: str, size: int) -> bool: return digest.hexdigest() == oid -def _fetch_part(session, base_url: str, obj: dict, path: str, auth, progress) -> None: +def _fetch_part(session, obj: dict, path: str, progress) -> None: size = int(obj["size"]) have = os.path.getsize(path) if os.path.isfile(path) else 0 if have > size: os.remove(path) have = 0 - href, headers = _resolve_oid(session, base_url, obj["oid"], size, auth) - obj_auth = None if headers.get("Authorization") else auth + m = _git_auth() + if m is None: + raise RuntimeError("no asset broker available for LFS parts") + target = m.broker_lfs_url(obj["oid"], size) + headers = m.broker_headers() if target else {} + if not (target and headers): + raise RuntimeError("no device identity for the konn3kt asset broker") + _stream_part(session, target, headers, path, have, progress) + + +def _stream_part(session, href: str, headers: dict, path: str, have: int, progress) -> None: # LFS parts are content-addressed (oid == sha256), so a half-written part can be resumed with a # Range request and verified afterwards instead of being thrown away on every restart. if have: headers = {**headers, "Range": f"bytes={have}-"} - with session.get(href, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S, auth=obj_auth) as r: + with session.get(href, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S) as r: r.raise_for_status() if have and r.status_code != 206: have = 0 @@ -300,7 +292,6 @@ def _fetch_part(session, base_url: str, obj: dict, path: str, auth, progress) -> def download_lfs_bundle(objects: list, dst: str, sha256: str, size: int, progress_cb=None) -> str: import requests - auth = _requests_auth() os.makedirs(dst + ".parts", exist_ok=True) total = int(size) or sum(int(o["size"]) for o in objects) done_bytes = sum(int(o["size"]) for o in objects if _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"]))) @@ -318,30 +309,26 @@ def download_lfs_bundle(objects: list, dst: str, sha256: str, size: int, progres with lock: progress(n) - def fetch(base_url: str, obj: dict) -> None: + def fetch(obj: dict) -> None: path = _part_path(dst, obj["oid"]) if _part_complete(path, obj["oid"], int(obj["size"])): return - _fetch_part(requests.Session(), base_url, obj, path, auth, locked_progress) + _fetch_part(requests.Session(), obj, path, locked_progress) if not _part_complete(path, obj["oid"], int(obj["size"])): if os.path.getsize(path) >= int(obj["size"]): os.remove(path) raise RuntimeError(f"part {obj['oid'][:12]} incomplete or failed verification") - for base_url in MODELS_BASE_URLS: - for _attempt in range(STREAM_RETRIES): - try: - pending = [o for o in objects if not _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"]))] - got[0] = total - sum(int(o["size"]) for o in pending) - with ThreadPoolExecutor(max_workers=max(1, min(PARALLEL_PARTS, len(pending) or 1))) as pool: - for _ in pool.map(functools.partial(fetch, base_url), pending): - pass - break - except Exception as e: - last_error = e - else: - continue - break + for _attempt in range(STREAM_RETRIES): + try: + pending = [o for o in objects if not _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"]))] + got[0] = total - sum(int(o["size"]) for o in pending) + with ThreadPoolExecutor(max_workers=max(1, min(PARALLEL_PARTS, len(pending) or 1))) as pool: + for _ in pool.map(fetch, pending): + pass + break + except Exception as e: + last_error = e else: raise RuntimeError(f"model bundle download failed: {last_error}") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py index bf17800a..ea1dff01 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py @@ -17,7 +17,8 @@ _OVERRIDE_KEYS = ( "combinedSplitArtifact", "iqCombinedArtifact", ) -_SPLIT_ROLE_PATTERN = re.compile(r"^driving_(vision|policy|off_policy|on_policy)_(.+)_tinygrad\.pkl$") +_SPLIT_ROLE_PATTERN = re.compile(r"^driving_(vision|policy|off_policy|on_policy)_(.+)_tinygrad\.pkl(?:\.iqc)?$") +_IQC_SUFFIX = ".iqc" def _bundle_models(bundle) -> list: @@ -91,8 +92,9 @@ def combined_split_artifact_candidates(bundle) -> list[Path]: def resolve_combined_split_artifact(bundle) -> Path | None: for candidate in combined_split_artifact_candidates(bundle): - if candidate.is_file(): - return candidate + for path in (candidate, candidate.with_name(candidate.name + _IQC_SUFFIX)): + if path.is_file(): + return path return None diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py index 05cce298..ee2eb77f 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py @@ -305,7 +305,40 @@ def get_runtime_bundle_upgrade(bundle, params: Params = None, available_bundles= return _find_runtime_upgrade(bundle, params, available_bundles) +_forced_bundle = None + + +def load_default_model_session(): + """Stage the shipped default model files and return a decoded default bundle + WITHOUT persisting it as the active selection, so the next boot retries the + user's own model instead of staying on the default forever.""" + bundle_dict = _load_default_bundle_dict() + ensure_default_model_files(bundle_dict) + return ModelBundle(**bundle_dict) + + +class forced_active_bundle: + """Make get_active_bundle return `bundle` for the duration of the block, used to + build a runner for a session-only fallback bundle without touching Params.""" + def __init__(self, bundle): + self._bundle = bundle + + def __enter__(self): + global _forced_bundle + self._prev = _forced_bundle + _forced_bundle = self._bundle + return self._bundle + + def __exit__(self, *exc): + global _forced_bundle + _forced_bundle = self._prev + return False + + def get_active_bundle(params: Params = None): + if _forced_bundle is not None: + return _forced_bundle + params = Params() if params is None else params try: diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/iqc.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/iqc.py new file mode 100644 index 00000000..da84740e --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/iqc.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.models.iqc") +except ProprietaryModuleMissing: + from iqpilot.models_private_src.iqc import * # noqa: F403 diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py index 8ede5f6c..48d990f0 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py @@ -2,7 +2,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos """ import os -import pickle as _pk from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any @@ -13,6 +12,7 @@ from iqpilot.system.hardware import TICI from iqpilot.system.hardware.hw import Paths as _hw_paths from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle as _fetch_bundle from iqpilot.selfdrive.iqmodeld.models.combined_artifact import has_combined_split_artifact +from iqpilot.selfdrive.iqmodeld.models.iqc import load_pickle # ---- runtime type surface (native OpenCL/frame handles resolve to Any off-device) ---- if TYPE_CHECKING: @@ -73,8 +73,7 @@ def load_artifact_metadata(metadata_filename): if os.path.getsize(path) > _META_MAX_BYTES: cloudlog.error(f"metadata pkl {metadata_filename} is artifact-sized, refusing to unpickle it") return tuple({} for _ in _META_FIELDS) - with open(path, 'rb') as fh: - blob = _pk.load(fh) + blob = load_pickle(path) return tuple(blob.get(field, {}) for field in _META_FIELDS) except Exception: cloudlog.exception(f"unreadable metadata pkl {metadata_filename}, continuing without it") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py index 9acb0ad3..37f1233f 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py @@ -4,7 +4,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed from __future__ import annotations -import pickle from typing import Any import numpy as np @@ -15,6 +14,7 @@ from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import NumpyDict, Sh from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants from iqpilot.selfdrive.iqmodeld.parser import PhaseParser +from iqpilot.selfdrive.iqmodeld.models.iqc import load_pickle def _tinygrad_imports(): @@ -64,8 +64,7 @@ class TinygradCombinedSplitRunner(ModelRunner): if self._artifact_path is None: raise FileNotFoundError("No IQ combined split artifact is available for the active bundle") - with open(self._artifact_path, "rb") as artifact: - runtime_package: dict[Any, Any] = pickle.load(artifact) + runtime_package: dict[Any, Any] = load_pickle(self._artifact_path) self._meta_by_role = runtime_package.get("meta_by_role", runtime_package.get("metadata", {})) self._policy_roles = runtime_package.get("roles", _phase_roles(self._meta_by_role)) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py index 33beea42..b8783dc8 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py @@ -4,7 +4,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed from __future__ import annotations import os -import pickle from typing import Any import numpy as np @@ -15,6 +14,7 @@ from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ( from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants from iqpilot.selfdrive.iqmodeld.parser import PhaseParser +from iqpilot.selfdrive.iqmodeld.models.iqc import load_pickle def _tinygrad_imports(): @@ -40,8 +40,7 @@ class TinygradFusedRunner(ModelRunner): self._model_data = next(iter(self.models.values())) pkl_path = os.path.join(CUSTOM_MODEL_PATH, self._model_data.model.artifact.fileName) - with open(pkl_path, 'rb') as f: - self._fused: dict[Any, Any] = pickle.load(f) + self._fused: dict[Any, Any] = load_pickle(pkl_path) self._vision_meta = self._fused['metadata']['vision'] self._on_meta = self._fused['metadata']['on_policy'] diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py index ea35728a..e457a5f8 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py @@ -5,7 +5,6 @@ from __future__ import annotations import hashlib import os -import pickle import re from typing import Any @@ -17,6 +16,7 @@ from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants from iqpilot.selfdrive.iqmodeld.parser import PhaseParser from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runtime import TinygradSupercomboRuntime +from iqpilot.selfdrive.iqmodeld.models.iqc import load_pickle def _captured_queue_depth(warp_jit: Any) -> int | None: @@ -77,8 +77,7 @@ class TinygradSupercomboRunner(TinygradSupercomboRuntime, ModelRunner): self._pkl_path = pkl_path self._expected_sha256 = getattr(getattr(self._model_data.model.artifact, "downloadUri", None), "sha256", "") or "" self._verify_artifact_file() - with open(pkl_path, 'rb') as f: - self._m: dict[Any, Any] = pickle.load(f) + self._m: dict[Any, Any] = load_pickle(pkl_path) self._meta = self._m['metadata'] self._ish = self._meta['input_shapes'] diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py index 2d91c3df..7bde2165 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py @@ -4,7 +4,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed from __future__ import annotations -import pickle from dataclasses import dataclass import numpy as np @@ -32,6 +31,7 @@ from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelCo from iqpilot.selfdrive.iqmodeld.config import ModelConstants from iqpilot.selfdrive.iqmodeld.runtime.tinygrad import qcom_tensor_from_opencl_address from iqpilot.system.hardware import TICI +from iqpilot.selfdrive.iqmodeld.models.iqc import load_pickle @dataclass(frozen=True) @@ -45,12 +45,11 @@ def _artifact_path(filename: str) -> str: def _load_program_blob(filename: str): - with open(_artifact_path(filename), "rb") as artifact: - try: - return pickle.load(artifact) - except FileNotFoundError as exc: - assert "/dev/kgsl-3d0" not in str(exc), "Model was built on C3 or C3X, but is being loaded on PC" - raise + try: + return load_pickle(_artifact_path(filename)) + except FileNotFoundError as exc: + assert "/dev/kgsl-3d0" not in str(exc), "Model was built on C3 or C3X, but is being loaded on PC" + raise def _compile_input_plan(captured) -> dict[str, _TensorShapePlan]: @@ -81,7 +80,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny raise ValueError(f"Model data for type {model_type} not available.") asset_name = self._model_data.model.artifact.fileName - assert asset_name.endswith("_tinygrad.pkl"), f"Invalid model file {asset_name} for TinygradRunner" + assert asset_name.removesuffix(".iqc").endswith("_tinygrad.pkl"), f"Invalid model file {asset_name} for TinygradRunner" self.model_run = _load_program_blob(asset_name) self._input_plan = _compile_input_plan(self.model_run.captured) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py index 48a34ea4..c38e33b9 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py @@ -125,16 +125,32 @@ class Amplifier: print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") def set_configs(self, configs: list[AmpConfig]) -> bool: + import threading + tries = 15 backoff = 0. for i in range(tries): - try: - self._set_configs(configs) - return True - except OSError: + result = [None] + error = [None] + def _worker(): + try: + self._set_configs(configs) + result[0] = True + except OSError as e: + error[0] = e + t = threading.Thread(target=_worker, daemon=True) + t.start() + t.join(timeout=2) + if t.is_alive(): backoff += 0.1 time.sleep(backoff) - print(f"Failed to set amp config, {tries - i - 1} retries left") + print(f"Failed to set amp config (timeout), {tries - i - 1} retries left") + continue + if result[0]: + return True + backoff += 0.1 + time.sleep(backoff) + print(f"Failed to set amp config, {tries - i - 1} retries left") return False def set_global_shutdown(self, amp_disabled: bool) -> bool: diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py index 2f71ed63..2506fb9e 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py @@ -469,8 +469,10 @@ class Tici(HardwareBase): if self.amplifier is not None: self.amplifier.initialize_configuration(self.get_device_type()) - # Allow hardwared to write engagement status to kmsg - os.system("sudo chmod a+w /dev/kmsg") + try: + subprocess.run(["sudo", "-n", "chmod", "a+w", "/dev/kmsg"], timeout=2, capture_output=True, check=False) + except Exception: + pass # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL gpio_init(GPIO.SOM_ST_IO, True) @@ -514,10 +516,10 @@ class Tici(HardwareBase): affine_irq(3, "xhci-hcd:usb1") try: pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() - subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) - subprocess.call(["sudo", "taskset", "-pc", "3", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessException as e: - print(str(e)) + subprocess.run(["sudo", "-n", "chrt", "-f", "-p", "1", pid], timeout=2, capture_output=True, check=False) + subprocess.run(["sudo", "-n", "taskset", "-pc", "3", pid], timeout=2, capture_output=True, check=False) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + pass def configure_modem(self): from iqpilot.common.params import Params diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 2c9b40a6..e0659500 Binary files a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index b61a4a7d..38e34598 Binary files a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 3fcc68de..4db554d7 Binary files a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 0d682d50..15de8c4c Binary files a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 6e65ece6..b3026557 Binary files a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/model_context.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/model_context.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index e25f03bf..eb3a7727 Binary files a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/model_context.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/model_context.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/native_launcher.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/native_launcher.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 2a6bc486..b3deb3ed Binary files a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/native_launcher.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/native_launcher.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/rh_images.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/rh_images.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 22bbfdad..dbf2ad46 Binary files a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/rh_images.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/rh_images.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/wire_codec.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/wire_codec.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 1a186207..c1c0dcf3 Binary files a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/wire_codec.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/wire_codec.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/.source_sha b/artifacts/iqpilot_hephaestusd_private/.source_sha deleted file mode 100644 index 123c7c9a..00000000 --- a/artifacts/iqpilot_hephaestusd_private/.source_sha +++ /dev/null @@ -1 +0,0 @@ -f217b0f040cfa83f51d6e935f1646946f5992b58 diff --git a/artifacts/iqpilot_hephaestusd_private/manifest.json b/artifacts/iqpilot_hephaestusd_private/manifest.json index fb36ab39..101b3a09 100644 --- a/artifacts/iqpilot_hephaestusd_private/manifest.json +++ b/artifacts/iqpilot_hephaestusd_private/manifest.json @@ -1,8 +1,9 @@ { + "bundle_version": 1792395046, "python/_iqclosure/_vendored.json": { "mode": 420, - "sha256": "8cbd4406c95aeca668d07f6714cffdfc00d7143988901bfe86c1bb1381019996", - "size": 2050 + "sha256": "93d71b5f76734cb6ceff3ead9218b21727bf47814a1a25d6be5a44994b9528fb", + "size": 1717 }, "python/_iqclosure/iqpilot/__init__.py": { "mode": 420, @@ -16,34 +17,24 @@ }, "python/_iqclosure/iqpilot/common/api/__init__.py": { "mode": 420, - "sha256": "97d6e0d8cbd3e47797d734322b42d04efea4d95a2dae5a56f1baef3fd847aa0d", - "size": 939 + "sha256": "1dd848ec6e867836a321a93f9ac218415c19528aa356a3e80969503285316b54", + "size": 900 }, "python/_iqclosure/iqpilot/common/api/base.py": { "mode": 420, - "sha256": "86b834e02e24a005c903f1ca059b1a572e37c17a87271726c90918cd04b6133b", - "size": 3451 + "sha256": "5b312ec45e673d9337a89ea82a157152d1f64e5e48518807a592247787087609", + "size": 4921 }, - "python/_iqclosure/iqpilot/common/api/comma_connect.py": { + "python/_iqclosure/iqpilot/common/api/konn3kt.py": { "mode": 420, - "sha256": "e7c2dd0b56fb8533c2cbc614e728a9ffc5ce7360524cfa62a02f5235bc23b9a8", - "size": 267 - }, - "python/_iqclosure/iqpilot/common/atlas_alerts.py": { - "mode": 420, - "sha256": "a8b47e01f604d41d4959d4b0f5ed594c1ab90e5b0f7fb2962b5a4a71ec766598", - "size": 9411 + "sha256": "2282e19909079bfd77a089f480df5a229191c35f8523f005d20e2ea468a6b0bc", + "size": 313 }, "python/_iqclosure/iqpilot/common/basedir.py": { "mode": 420, "sha256": "58cfa5d6405903ee1acecc4544463bb8f47a66921b120c382143aa80d8539b28", "size": 106 }, - "python/_iqclosure/iqpilot/common/constants.py": { - "mode": 420, - "sha256": "fc439d59c24b95ed349b84f1a5862df3ea50e0891dbec4ea3cf5e1e0ccff6513", - "size": 434 - }, "python/_iqclosure/iqpilot/common/file_helpers.py": { "mode": 420, "sha256": "3c661d05dea697fae741b3e65a93385e4e0f5941eae8e8f4ea64da8da90e2f52", @@ -64,11 +55,6 @@ "sha256": "4af8f9da7504a548d15f18b91d71c1379cb7c05c84be031448764cc49920ce68", "size": 2449 }, - "python/_iqclosure/iqpilot/common/issue_debug.py": { - "mode": 420, - "sha256": "6fadd02538b85f92963f247a43dcc6f50cf4c65cdc4171f3c7e03646a76fcd1b", - "size": 1019 - }, "python/_iqclosure/iqpilot/common/logging_extra.py": { "mode": 420, "sha256": "ce9b3b3d8498d76e1c4c0352a3e98e72759bb7f367ff8629c1329c46040595c3", @@ -79,11 +65,6 @@ "sha256": "016be524079ef32df03b2c135eab8f83da60a46fc7f143620b836c65f33e39a3", "size": 4423 }, - "python/_iqclosure/iqpilot/common/spinner.py": { - "mode": 420, - "sha256": "33a2eb2aa3148492414035c5dc7150b3a69467905d7a3e58c8f65cfe70d3552c", - "size": 1371 - }, "python/_iqclosure/iqpilot/common/swaglog.py": { "mode": 420, "sha256": "0a133b4ef49f80b8890f115e81273f3f3e21351ae79f92dc02abc033e6d24027", @@ -96,8 +77,8 @@ }, "python/_iqclosure/iqpilot/common/utils.py": { "mode": 420, - "sha256": "103fa6c24f8b963576c482a34d7ecb020168fd3e85156493ae4432ee3b7b2c06", - "size": 5159 + "sha256": "c39b89a1ab34511dfa5f9efac783fde0e6d616963a416cf472bd1f604b57f84d", + "size": 5892 }, "python/_iqclosure/iqpilot/common/version.py": { "mode": 420, @@ -116,8 +97,8 @@ }, "python/_iqclosure/iqpilot/konn3kt/registration.py": { "mode": 420, - "sha256": "e9c37e8ae0a70c3a19ca1a33b290e94658b255fab153a941f50f8a29168d75c3", - "size": 7816 + "sha256": "5a2b06d919adce513fb8a7e4e6f26d48e502e92dbd89f9af7a0e67e9114ab3be", + "size": 2683 }, "python/_iqclosure/iqpilot/konn3kt/service_health.py": { "mode": 420, @@ -141,8 +122,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/car/vehicle_catalog.json": { "mode": 420, - "sha256": "9226a2475a10088d7f472af2de9e9e3884522562db9432376c4a0def6142fe38", - "size": 122493 + "sha256": "ac6ffed7e5374df5add04646af383980d251d400b0617755bd54a1fc85d21325", + "size": 124301 }, "python/_iqclosure/iqpilot/selfdrive/car/vehicle_catalog.py": { "mode": 420, @@ -161,8 +142,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py": { "mode": 420, - "sha256": "a9f2bc8477b58a3df7196f4b40cdddf631e28051809e9bbfdd4c78148444c474", - "size": 10891 + "sha256": "af83d52609ec4f2abe82f4e5f788ce4788bdc41cbf7b3de36dc63f1f598ef46c", + "size": 12107 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_model.py": { "mode": 420, @@ -171,8 +152,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py": { "mode": 420, - "sha256": "83b0190f19e3f037c937a03b1988931729ed43d4dac92223aba0745697611ed8", - "size": 12835 + "sha256": "62b13a05afbe9b5fb5d9e3d09c1491b37826ff840126c5e317a7ebf1edc28762", + "size": 12060 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/__init__.py": { "mode": 420, @@ -186,48 +167,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py": { "mode": 420, - "sha256": "2fad977c3816ff824c126a92605f866f3bc0aa989ed8397e818bed481a41192f", - "size": 14051 - }, - "python/_iqclosure/iqpilot/selfdrive/locationd/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/selfdrive/locationd/calibrationd.py": { - "mode": 420, - "sha256": "6bceebb5a15895d2b6d20a28cbd14b9112f9422e0a0cf466314a7e8779da39dc", - "size": 15698 - }, - "python/_iqclosure/iqpilot/selfdrive/selfdrived/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/selfdrive/selfdrived/alertmanager.py": { - "mode": 420, - "sha256": "c601bac30951a750d3ebf3d30f06c597bacd148210b1c405d5d3eca79bb7c4e3", - "size": 2045 - }, - "python/_iqclosure/iqpilot/selfdrive/selfdrived/events.py": { - "mode": 420, - "sha256": "e1b113565c45271ec5310dd83940dd338ff7fa56a2bf121f864dd58cc0ba1343", - "size": 32024 - }, - "python/_iqclosure/iqpilot/selfdrive/ui/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/selfdrive/ui/feedback/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/selfdrive/ui/feedback/feedbackd.py": { - "mode": 420, - "sha256": "7c1e6cb2c3c2c6aae91210f6cbc5c372f33c28cc3edb292ef71439b83844615d", - "size": 2615 + "sha256": "9628f88e71f7fae2c3d62dd17618367b3c7d12749cfe2a4737410288888cd3bd", + "size": 15008 }, "python/_iqclosure/iqpilot/system/__init__.py": { "mode": 420, @@ -266,8 +207,8 @@ }, "python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py": { "mode": 420, - "sha256": "98f46f8705259b00efae7274186b4e68201aad467a19de4f16d5f850d10ab0b5", - "size": 7585 + "sha256": "38e521356c56ac85f35543b9457d36639e55ed2ac916f30ea3b86948e806541f", + "size": 8016 }, "python/_iqclosure/iqpilot/system/hardware/tici/esim_manager.py": { "mode": 420, @@ -276,8 +217,8 @@ }, "python/_iqclosure/iqpilot/system/hardware/tici/hardware.py": { "mode": 420, - "sha256": "e8eae4e8a75e1751525471cf54a8555b7bfd7f08ad84bf2e75642b5745f222db", - "size": 25601 + "sha256": "da5ecb280f7af14169903260dc941e3bfac033c93b184cb0abea0f1494c34582", + "size": 25731 }, "python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py": { "mode": 420, @@ -316,18 +257,13 @@ }, "python/_iqclosure/iqpilot/system/manager/process.py": { "mode": 420, - "sha256": "04bf7d945e43ed67362916884caf9ac6011ddd5dacc985f2adb282eacbdf225b", - "size": 10130 + "sha256": "703663ef081e8786b0071df02d95b404b9236c6162d044a0766eb34ef3181875", + "size": 9926 }, "python/_iqclosure/iqpilot/system/manager/process_config.py": { "mode": 420, - "sha256": "f7a4f10168ce71e490578164c08328e1e983bd9e72df955c7e34d4eda6be7086", - "size": 15379 - }, - "python/_iqclosure/iqpilot/system/micd.py": { - "mode": 420, - "sha256": "1a59cbe6b72fcca5723ab56dc09f52039817299071a8d8da37f04acea58776bc", - "size": 5977 + "sha256": "5d5f9354ac81904097cd7c25f6ea9ee2c5dcf74ed63a3400530dddcc6a1f1660", + "size": 15410 }, "python/_iqclosure/iqpilot/system/phonegps/__init__.py": { "mode": 420, @@ -366,8 +302,8 @@ }, "python/_iqclosure/iqpilot/system/ui/lib/wifi_manager.py": { "mode": 420, - "sha256": "c9fe91484344f154726b0cab62a19f58a172e4f06dfd6341ca9b179c0c4646a8", - "size": 41618 + "sha256": "859d3eb2f56eb456f1d6d2df18d93c5d4990a7f4a6e75be495930a19efcb5151", + "size": 42121 }, "python/_iqclosure/iqpilot/system/version.py": { "mode": 420, @@ -400,29 +336,29 @@ "size": 0 }, "python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "7afc7118cb0e8fcfcdc552a7df224cd02dedda078d79e3484cc7c2c87209582c", - "size": 135480 + "mode": 416, + "sha256": "97137b00af89cc21ec2b9c07f5887304ff0a7379619b37837384c3e6d49a96a2", + "size": 1130808 }, "python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "bd195bcc71dd7f15975ef2bbb28317fd7518f68c73a3d056d214b8b3446a8852", - "size": 67680 + "mode": 416, + "sha256": "0674dcaf72b8137be6c75ac220001f2ea440fea92c93c5211a01e0d513772df7", + "size": 1065192 }, "python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "53fe8078e5388290dc36fd765c3d923b9617ac35cb7cb94c4faf8234afa69766", - "size": 204616 + "mode": 416, + "sha256": "3e8e9d8d2ac2282cda1bb02e7ae366c373191bd49c49de47e09effa2f000a4f2", + "size": 1130968 }, "python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "c077e27d89e8b1704b4a179654690c2e0d2db08941bf8b55e511a3e524e88508", - "size": 69752 + "mode": 416, + "sha256": "2135582d5a3cedf8247dce06242619e90e1d519a44548891de071b7cab1ec694", + "size": 1065144 }, "python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "b014f9ddb6ad90a40877ff1bcf74f00f493b8b128e32025aff876cae6458ab03", - "size": 203064 + "mode": 416, + "sha256": "d36c107d46199f3efc6f10b266f2658119212e184dbcdbecff09c450a6dfc381", + "size": 1065368 }, "python/iqpilot_private/konn3kt/flockd/__init__.py": { "mode": 420, @@ -430,14 +366,14 @@ "size": 0 }, "python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "3a066b70e0f2755f97fa9886389dd0e50392a9a01f0dc6c8eda2655cdf44d010", - "size": 268240 + "mode": 416, + "sha256": "c801140079340b2e4ab00ad925ad7bc8b5184ea7f19601643ad03f65c2dd5f09", + "size": 1196424 }, "python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "15d8e8b6a620c41dc6fd597ff65e6545ba4da13297355a12efabca92107e1a23", - "size": 68104 + "mode": 416, + "sha256": "66129d17569ca6678ba81dd7bda5d06fd68c1a7a84c0b4e6a2a610cbaf640686", + "size": 1065144 }, "python/iqpilot_private/konn3kt/hephaestus/__init__.py": { "mode": 420, @@ -446,118 +382,133 @@ }, "python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip": { "mode": 420, - "sha256": "17a7e20afa7a49c43883f54a7b7f2dd0319eac0235a2d87ee1f6b70b37ef5af3", - "size": 1379463 + "sha256": "8562471927037cfce082e59054ffcdd39ca67519e8bad3f2583418f28f898960", + "size": 1379541 }, "python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "330b1a62d6ddc23a20c493945dcf1e68bb0144cfbe3b44f9d3e06c65ad438c02", - "size": 268072 + "mode": 416, + "sha256": "16a9c7dd5865cc62f67af7f7af931adc31a82a3292f4e952c153a68b3e557e47", + "size": 1262120 }, "python/iqpilot_private/konn3kt/hephaestus/ble_connection_parameters.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "ae55af9d22773bcb5b497fdc7da051345341c2699590d61dae0a6ba0ab6b8072", - "size": 68104 + "mode": 416, + "sha256": "8ec415342cdb92e468f477f36117ec9e9e3ed3c4e10625a133fbf1b6ffc7db7b", + "size": 999768 }, "python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "b16d70ff35a87b2707d00de69179d672f89a8cc8c355344fa0bd395df5a85d4f", - "size": 472568 + "mode": 416, + "sha256": "d940e5fa03e70641c613eb4d0a1392c4564d9393dee7226112564f6041ccc396", + "size": 1327640 }, "python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "9f42acb2fd579d548f05eabd6f892a27ccd0b5224e052dbe0a1caa8bcae1e15a", - "size": 68080 + "mode": 416, + "sha256": "8cef60cba9aa38e086c585c786b60223d7d7b858a02ad88d19a1f434d73db9d9", + "size": 1065272 }, "python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "9b3de54dc169aed94abed3620e5ddc58865317c1c4865777d249e0056e459cf8", - "size": 402424 + "mode": 416, + "sha256": "8e080bac97b176d2a7b271bedc3ac735c11079e8ac2a03cd2c19b4761155ccde", + "size": 1393080 }, "python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "987e5afd63f14b691fbc313b4ccd55d03be8f378a06829dde4cbe05d5f9d6fbc", - "size": 337616 + "mode": 416, + "sha256": "bae356c2438f4c779b72c829deda319e043ce5792c624864a72b0883d6196d4a", + "size": 1262056 }, "python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "c2e296298df67271d0a113fd36f5af5d9fe2dd645decaffe3b8af685c8e2266e", - "size": 134424 + "mode": 416, + "sha256": "a1bb5651a3f2bba40cca75070b721fb8c3e0108475e15227f4c2e218d201b4d0", + "size": 1065320 }, "python/iqpilot_private/konn3kt/hephaestus/driving_settings.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "3f3fd10b7394e9534738c3cfc2ae6c6a3dad8d0dd7953f009e46d017f66ef190", - "size": 68152 + "mode": 416, + "sha256": "e1184e9e8d5fee473e99e0edd504784d1edbbf4b5898f1eeb14794e25d2ccb7d", + "size": 999800 }, "python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "d2f5d3004195e660b86639e4c82402b299204db9deb161558dad9f2907abf9ed", - "size": 4311240 + "mode": 416, + "sha256": "89a1ee80f07a7547ecc81e1293300969079095046b859fafd9a9871f5fe51687", + "size": 5065800 }, "python/iqpilot_private/konn3kt/hephaestus/iqsentryd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "fab6aec7f68e57b725ec68f049c23af734502b2bbd31e0c582ad8147ed9fa64c", - "size": 402296 + "mode": 416, + "sha256": "690af279a513a1491ec2639d6aa6bf391a5a690456458ab1c122e90664c0848f", + "size": 1393128 }, "python/iqpilot_private/konn3kt/hephaestus/iqsentryvisiond.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "df46250fca25ef5e87c50c3a0caad434dd92fa893f8c9e77300d331d63cb2fc5", - "size": 271008 + "mode": 416, + "sha256": "92a8bc5e07148a44920f90ed6de201c81011f77a7d38675ad8ae60ac263f3bde", + "size": 1196504 }, "python/iqpilot_private/konn3kt/hephaestus/k3wgd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "0430935f4dea6b17d1eea372e07203b74b2842cef69bcc149db5921b99ce5499", - "size": 338912 + "mode": 416, + "sha256": "f68dac3600056ba38f65894285b89d266c494b210d8f40d754f6883bf42439e5", + "size": 1262040 }, "python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "81b0e7ee271a24ac155731b02e5d3fdc1b00e6f86afedcfb657eb3331006ad56", - "size": 136384 + "mode": 416, + "sha256": "60842713081ba77c08871ddf37799b26c0934615d1e3d49d22dd677be8eaf830", + "size": 1065288 }, "python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "c4f596d81eee41ac1d11bbe74ff388c37ea88eab61d51148280cdfa2648af8a9", - "size": 133752 + "mode": 416, + "sha256": "1b468f0e37f7647d4b3597f945b13bcdb6261277c9ee82dde1e4db0c01ae1777", + "size": 1065400 }, "python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "d76998ffe1a7250545dc2dd79c7f0463a16c1370000273dd6eea3acd6be7b154", - "size": 67872 + "mode": 416, + "sha256": "dc3f21fdd7dfc4baa6acfc884716a56de0f66974a51df1fbb8ba977b5f103ed8", + "size": 999720 }, "python/iqpilot_private/konn3kt/hephaestus/sentry_body.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "2c50d0a84df01de778666ea92969e28226a91b87d6da7e6ac0d419ba25743598", - "size": 137120 + "mode": 416, + "sha256": "bdcb85c47639922960d380b6dd2fbf68883b4fc76de48da4d9fd93668b38cb45", + "size": 1065224 }, "python/iqpilot_private/konn3kt/hephaestus/sentry_imu.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "673812cb76a3dc353e7726bf940d68ee8c2b9ff3316dbddce0de972e72233150", - "size": 135816 + "mode": 416, + "sha256": "0d69eab9b5c27d88d0c99a0cb6e9bfb9aa50310d4b823753644b3e3eb200ad3d", + "size": 1196440 }, "python/iqpilot_private/konn3kt/hephaestus/sentry_media.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "734054caeac7fe5b17f69ee5d2ec917a93463a562cac50d584b8b6da5a7ab4ed", - "size": 204080 + "mode": 416, + "sha256": "cda5e46e4746b54d25012ea5966da9778e396d74ef4bb6eac762fba4c17b6fae", + "size": 1065224 }, "python/iqpilot_private/konn3kt/hephaestus/sentry_policy.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "3ba641937f57ce36a3663eb85df9facb97bfb5c637a776d81b63e7e74482c2d5", - "size": 201912 + "mode": 416, + "sha256": "4f3666dc01af607a8860b7d26689f4790797b25633bbe5049f7a2cc3f75ea68f", + "size": 1196472 }, "python/iqpilot_private/konn3kt/hephaestus/sentry_upload.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "1aa10dad0406f6d71298fb017d1cb32f4516db52eb5fe97018d220171f81048e", - "size": 205248 + "mode": 416, + "sha256": "b836c3f257c530f56882c15ea0b836c47bc270e57eb38424cfb763a2509a1f70", + "size": 1130696 + }, + "python/iqpilot_private/konn3kt/hephaestus/session_bridge.cpython-312-aarch64-linux-gnu.so": { + "mode": 416, + "sha256": "e4805ad4be15e8ba8fd3856b87161e95e627687275b97dd68f8ac8a84ae1c38d", + "size": 1065352 }, "python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "1fbb0bb01ce46805ca383b3ba207bf40b08647a047336fe0d00936518f241945", - "size": 135584 + "mode": 416, + "sha256": "4c27b4ad1ee41a5fbd854392fc5541f9532071d87a333e2fef43a253f1c78cc1", + "size": 1065368 }, "python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "2ae7c4618618edc31b56a8720ffcf5dea7772fd6c00ce5a68703a58990757732", - "size": 269536 + "mode": 416, + "sha256": "badeebe1aea6c0be071bebc7bffa3368d528a48b517e5a079b40f9c8579803d7", + "size": 1130920 + }, + "python/iqpilot_private/konn3kt/registration/__init__.py": { + "mode": 420, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0 + }, + "python/iqpilot_private/konn3kt/registration/reg_core.cpython-312-aarch64-linux-gnu.so": { + "mode": 416, + "sha256": "338b657e8e87de063ca1d04600315f4cf30ffae34be5029e6415e52d6cd4b786", + "size": 1065304 }, "python/iqpilot_private/konn3kt/telemetry/__init__.py": { "mode": 420, @@ -565,9 +516,9 @@ "size": 0 }, "python/iqpilot_private/konn3kt/telemetry/iqtelemetryd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "be7294d1034251717952a68a6831902d5ce8ea903f69b16fc3bbf7d232043d05", - "size": 540624 + "mode": 416, + "sha256": "11cee4ff4e4041bbbb8ba76a0846a5eedbaee7fc172c7abba855a09e394915a1", + "size": 1393240 }, "python/iqpilot_private/konn3kt/uploaderd/__init__.py": { "mode": 420, @@ -575,9 +526,9 @@ "size": 0 }, "python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "ea3c6f1c89a3411773fcbab54e73f5b6997765a42c5fbe6d12249eccc321d31e", - "size": 336264 + "mode": 416, + "sha256": "2b6d4963a7370a2cac157b3ab44c02f251d7be221fbf7ec33df0b096a0ad3e3d", + "size": 1262072 }, "runtime": { "entries": { @@ -601,6 +552,14 @@ "path": "python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so", "type": "python-module" }, + "iqpilot_private.konn3kt.hephaestus.iqsentryd": { + "path": "python/iqpilot_private/konn3kt/hephaestus/iqsentryd.cpython-312-aarch64-linux-gnu.so", + "type": "python-module" + }, + "iqpilot_private.konn3kt.hephaestus.k3wgd": { + "path": "python/iqpilot_private/konn3kt/hephaestus/k3wgd.cpython-312-aarch64-linux-gnu.so", + "type": "python-module" + }, "iqpilot_private.konn3kt.hephaestus.manage_hephaestusd": { "path": "python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so", "type": "python-module" @@ -624,36 +583,38 @@ } }, "signatures": { - "python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": "YpZLvLgHCf+QVH7Yo1fml62OiJu7oUVZkvT/4VScxphS4ul8GjBe+wQ+r2pFE+E1DkZ3h+QLQoI9WgqeC0HRDg==", - "python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": "TVUDTKJVz5koYuF2SxNScKNChgHO3adhQdhpZtPE3IVC+zmOr/BVxGJpykyYaFx1J541DydjM3pYeiVbkBtDCA==", - "python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": "zFIzelMLy+b9fNs3EDCwnc5/9R4KxGQyhACAbjqGogKDuo18t1FfDnQHzoa4vYqrkLcvVTRwgVP27/CWR5sNAA==", - "python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": "RImusUqqBvtJLKEgQbzOLZRn8izo9Pzll7BaFbqqfeJCaqRGBcWzNc70YcJQ5wodEaQzaY2DLIsu07l/o0ScAQ==", - "python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": "MCSdZIEfeTNKhHrL/vQChDUxRx51miixM0UAMJJ4SKkvbvmVwN56Y34B6A/aLRJvVDH8YXyUZnUCQaR8+UhgCg==", - "python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": "YP/Op7Ol87glLrYv77mMifZyBM6d4Dj/X3wiNKIcB8ECaDCtbhzTmOwlh2eO+HZIWdxT57PCu1FEhdWlb7RaCA==", - "python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": "uNYw3cE1Ffda+71eLKrU3sjFrpG7xNuhfTNf8X8ubGYFhp3tmAf2sp1+hTyvef/Mi+QjtF00CE0rnZ/dra98CQ==", - "python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": "xibUheiHQQs9Cx7ygWhYYjr7/CGBMvsSv1E0IhLyejZifAnUQQ1tK0wWcfdwFrzw3iFTgO9hp55fDHXPJV2qDw==", - "python/iqpilot_private/konn3kt/hephaestus/ble_connection_parameters.cpython-312-aarch64-linux-gnu.so": "WguojStOM6IoIAZ3IFCmUcIJowx8UtuL8+qgn61BzvfnF2/AVS/9a+vdVvKZXwXMuZsAkKAd+GZWt4rLPUFXBw==", - "python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": "NqiUDgwj60ZdB6SYVIPkGh0HwFIQv2z0p0BKKingy5EyFThcKfbELMk86wYRbXAZnjkCUdTWrhxsxp9sIrSEAw==", - "python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": "+xL+bgAX904lJa3+fesZC3e0FAf+CbXByelJpRIjlz94eGG3h5qVgozeFalGUZYdhZpJb42SyAze4DcpaxPUAg==", - "python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": "kmmmMdiMMIIslmInzMjniLfmp/VP5scfuAbRAvNA9QRbmxxw4ko5nnya0VhTjS7d8hM0rRZ/McI4qAJByPpYCw==", - "python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": "LLGPCnUzDdyF22dJ/qhI6gYPU8hx3xeL9LeyOEqP//gzoa4jNCJeI+HWYLz0teOSoJssiGwLJ+ZuAtuT7MIpCQ==", - "python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": "kHEBEwEVZAodATQ/+/wSNyKJ5S7p2AiTqZ+kfPAmmb3ihr4xVyuy94fAkV87WIzaO1SzInB8x6SGg6UghsZiBQ==", - "python/iqpilot_private/konn3kt/hephaestus/driving_settings.cpython-312-aarch64-linux-gnu.so": "03FhZYwHUlbfbOxYDX8CwC2gIwl6KDGRaYmCGdIx6gcg4IdtAdVyrgIw7OWDIqKuHXqlckDnf+Jh3KOClpTOAw==", - "python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": "v6huZjXXSNqSNprbiFaUgC30HDcoIiyEC7A105rut+dM3KCBisvYe/adu4p4PQVS8T2qNbqnHGboZd2NHtKQCA==", - "python/iqpilot_private/konn3kt/hephaestus/iqsentryd.cpython-312-aarch64-linux-gnu.so": "aFGz1KAWpY4O/+4xXZ+/Yfq9GLQ7IEZqNNQjEtRVjxDE7XEukXK3s4dXWpeZ9pg5j1NhmcCn/bnGaR+p9JWUBQ==", - "python/iqpilot_private/konn3kt/hephaestus/iqsentryvisiond.cpython-312-aarch64-linux-gnu.so": "gmK55AoOUqaRL4qlpQFtSYGrPkaZ+2VtN+0XqHhwY8NH8gXZUEevFhNi/yne5qdw9TPW7k1xM0kE43cNpKAEBA==", - "python/iqpilot_private/konn3kt/hephaestus/k3wgd.cpython-312-aarch64-linux-gnu.so": "+0CBcRpRn/sXdSxnPRIV8Hv/HO0/3+dsoIbkGePdunOxWujxmPbaFYh6jADa8vrhh4AL+UDaaJQGfMjy/3CzCg==", - "python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": "8CbpnBxEs/4IEKRupI+K2b//RlL8wGmmR99RUIa40XtOrq0Y2oZkk4qQWqWx0Leth3tj2euz3eSdm0OwrYerAg==", - "python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": "+b6HQ5D++9rppi1E1mHVQjaMXy0tB0KLb036r4sX8JslK3pXuiP1HYP4kqpugNEBhUTvI5shcsYndKi0PBRvDA==", - "python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": "1qSKc1Ge2UVIevFR4gCR+f8N3zlRoK+kRU+Db56lfzEB0f7S72mrMQpE3QD0ZRfQ+IngyYj71TueRAYTuUOZCA==", - "python/iqpilot_private/konn3kt/hephaestus/sentry_body.cpython-312-aarch64-linux-gnu.so": "FMnBSL3lt83BfmjqfHGGdGl3Ivq4D5GwCXoULBsiq4uEKBO7v2ADru0poTJzjRCrPTTv8nnteWT+lGffEXO4DQ==", - "python/iqpilot_private/konn3kt/hephaestus/sentry_imu.cpython-312-aarch64-linux-gnu.so": "Qp3+AE9b6G1bw6IhSGAPY9NZJeqqzsVf0vLVhdQ+CWkIesNIBAWTNixW9xLZ6bxVnw4Jvk4k5n404iaOP2iPDw==", - "python/iqpilot_private/konn3kt/hephaestus/sentry_media.cpython-312-aarch64-linux-gnu.so": "yK5QgYja6wGUAEDAjvnhZr7kvS72pekXRGKuDfKPFUSehFVJVx+HNwWvKNLZDE60UNyeNzCf0XG40zxsN+ufDQ==", - "python/iqpilot_private/konn3kt/hephaestus/sentry_policy.cpython-312-aarch64-linux-gnu.so": "DvORZ1fUs4uW/eRe6RiN8jp2tUJU2Mnn/y9M9XbNSXieKKWFsPgyvYAQS4TvhZc2+S1+cNbgoqqihu8EcFu7BA==", - "python/iqpilot_private/konn3kt/hephaestus/sentry_upload.cpython-312-aarch64-linux-gnu.so": "Hu5AB59owBNkFqKmvfshrOXBSDa0I02ydUifnHjiq+7tocZNrOmjnsBPbcQgSuHYPgaux5W1r0fBmwpGLs9PAA==", - "python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": "CEXfY5TOPY7JyHubxBeyW+WS0mrCU6nH3HB2hglnA/xdIWrRxNo0DzkegS1LRDhNCvwqThMLX3c6DY8yR8urCw==", - "python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": "Wo9GEpl8kt2tPyy/7lX9d7a2gMI0gL1XrLKtTIkhJbq78qqp/TtzZOuXty2y9NvGnoXmJdRSCRfOMBFZkY/mAA==", - "python/iqpilot_private/konn3kt/telemetry/iqtelemetryd.cpython-312-aarch64-linux-gnu.so": "gYuyE8q9g3GsKXMfMoXrLCYZvz2XJDicuriUutLPgz1KBKEZDgsAVNUxfR5oztOV+OSAb11by5nb9gi9vo62BA==", - "python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": "ZCpdDtvv/E9G2tkoFT027BhNaA6ZBtlwU9m7lyKiY3nXiD0hk5m2mkVnbwKXolNXBCyCiQa3YmL5fKmPDqUqBg==" + "python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": "0qFnstW83THKbKdU/6x895gXoJLMF5kp9PhxhSoja0t/D/kQmp1QI0u7zhi3idsk8pwYzZCN8YMIRBrVALFsCg==", + "python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": "eFqA9ShbE8zeqXxEaEqV0KOXKbCP/9SQifNXvRR3Y+of29oxKT+CEK5OyOdNnEjaEG+u+gTXMOd3oSsk/L+wAQ==", + "python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": "Qx5fS+scNnelKqICvs1iRFY8httSCmDCRQLpB95hK4Riakydm5pwLTJF1YUR0Oz0cf4it+INwKcoUmVkAG9kBw==", + "python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": "R9j9s8waY7kThuL5491x9r7C/9ROkVZWigxKaThgKC8s0Je9xIb/EFuO58reyJUVx6P37rXVU2XxSm+MvZdLCg==", + "python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": "UkT0VaaRxweoTRjfzP3E38fwVRgEI1Fj/HMl6HPxuiBrOJhOSVWlJHwHKtYgFrzVBhUwNqnHrmAbdLy6ZN9LBg==", + "python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": "2AnnmUawhW1vWt76WabuiXHM779pEE8hC5xHVEdZ7cHN34RJDg+Q1ZyO/5arv2ntek2Vp5v3F8dlRm6FpfQSBA==", + "python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": "Aq7CLbLUNrZlTq6XWlexzlKS/31+9H+XRKzZRI7VVBpiD9VEtlcB7u49RRdcEH5gfpGZFLDIMrfS1J01yoggAg==", + "python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": "gXWjTUUp8r9dDyUMaXBpEUBZCTFH70Gwb8SEj+rWFV69iB1wOijDeW9P7danSn8iw3y68l6AcVV7u3y8F+trDg==", + "python/iqpilot_private/konn3kt/hephaestus/ble_connection_parameters.cpython-312-aarch64-linux-gnu.so": "HqaR/WsSKt1UgAFo9cg4plqPGK7719drj6AE6GwZriK6vrLZrC2KZRwNTul7RT39RsatksN3J4oNw85I5LCPAw==", + "python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": "ygAT4jof7Gu4vS2X+8GMic0D2QCTXCAUdU9waYi8vlyi+6hmrgbWMM/BDjDdIJoYMxjnn3zRYdN0Z6BD9PrcBg==", + "python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": "1yQX0yOs4u8K08p5nlyQT+1UwpH96PgMTYZlpCvNwbC8o5xex4lQJyI6lirgpHXfYltVmEgSgw8N1i0GA7yGCQ==", + "python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": "s83uFeK7C19GQKrq5R+L3poMHRo3VCj4NMq46gWrC+ofOoRmPRgkaGSqGrDGr8m/EIMbj4bpYInkxqbjqy3AAQ==", + "python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": "ETryY+pFucqOyF5iTwE4g6mnf/luxyM3VljIioWLLppDpf8WN9WVv2Qj0r4GVsAg7iwf37HOW/dvj5EtfEiPBA==", + "python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": "HK2d7XJdB/NScUdK0kcpOOEwlehCjXKDiz0t3qul7TSGTw/TP7fQRfottuWhynWk3WseDpDruerOM9XZNJQzDw==", + "python/iqpilot_private/konn3kt/hephaestus/driving_settings.cpython-312-aarch64-linux-gnu.so": "u8BScZvFva8KW03yPS1XI1VwGjLGcjMy0UMLL7X9GgksKdBKEJ/DQLU9wx3FunBk8yRQ8L/K5mq3TTGUpLn+BQ==", + "python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": "6V6EGUYF2qEA6tfQeUs9oTMLDuAOkx1OpDrS3LJNNHuyw+QbK33iYGAiH07eNs2GjL8psL3zS1tbn2x4p6udAw==", + "python/iqpilot_private/konn3kt/hephaestus/iqsentryd.cpython-312-aarch64-linux-gnu.so": "QUrRj3RbxO8WaTZGn5AZBTAGQYrviI8Q9IKlX7DRyVd+kq1GwAgftTS6tDLimQNHHhLXj1ZN/wnxVqKoBrGrAg==", + "python/iqpilot_private/konn3kt/hephaestus/iqsentryvisiond.cpython-312-aarch64-linux-gnu.so": "e0UybV5I5fAtvmF2L+1059qtu9dXCnkWUb+kdOpnUl+YXdWRwOL7TdR9TSitdNfGFRmxPfh5DwdNSToe2ZlwAw==", + "python/iqpilot_private/konn3kt/hephaestus/k3wgd.cpython-312-aarch64-linux-gnu.so": "GKxP/VjKIdlFW7EIDu/83MOOjKhcgjk7GqsIJzuEwlsCIWlir9cOjbJB/pgwE/mnW+BPZwKMzYeeVdUXCK+rDw==", + "python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": "7TUFhgkXVyx4JT5aFzlK+DIm7WxF4taRsSVabHHITFbon7XXBv7YjhTsaBTZL4d83eSfj4B7qU+1oHzFclTOBQ==", + "python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": "9CBMXV4OHwtRfxXpoXvq6xJ0rb4jE54xMZUEJz47JjOBctahb3xBKpo02pNbVKnZqZEiNmPKIDqAoHCkwtbODA==", + "python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": "2mJaygzUnf3XXy8z3/yrFc6OtZf8MDnRE2bn3gXx3EFo/otVuBHjyVu1Mv0H8SymIRN7I/Fn+ao+lpM7ILTWCQ==", + "python/iqpilot_private/konn3kt/hephaestus/sentry_body.cpython-312-aarch64-linux-gnu.so": "ZeAj6MuMrsp+mlzimai1v31Jalb7yuClrRSsDIL2Hyh76efNxajwPOx5evu0Zk1jVznu3GLtXIE8qFwWqrXXBQ==", + "python/iqpilot_private/konn3kt/hephaestus/sentry_imu.cpython-312-aarch64-linux-gnu.so": "EJiZdjHFhyFThk1YZAYRa28cjbEa0jkSDmjNoMonT2yGyMVaxfkdU5OI86GR+LY+kEFxhRPsIlO8YSAp7PpqDA==", + "python/iqpilot_private/konn3kt/hephaestus/sentry_media.cpython-312-aarch64-linux-gnu.so": "IaXakjeDlvwMDS7NUG0f6E/3WTqv8FwgryUtRuXYXP67bmQfrzBI/kT8e+7FH4i49fu+CyrGbS42+JhsXEpwAA==", + "python/iqpilot_private/konn3kt/hephaestus/sentry_policy.cpython-312-aarch64-linux-gnu.so": "30KviVv+3hNyDLKPF+IvFbWoSH7JGgL8MKQLLR7Rrndxff6xsIw9fjFrjJuWA8q/RAkO/zw/ptJOCBFLGgTQDw==", + "python/iqpilot_private/konn3kt/hephaestus/sentry_upload.cpython-312-aarch64-linux-gnu.so": "Qw7U90kMvbmWcIaQl4AwLY/NesGZmFEDT5DpRw2t+IC//yr1aZqH5NteA8oEcLnnVxlqmVkP1DMu/5Dn4aBiDA==", + "python/iqpilot_private/konn3kt/hephaestus/session_bridge.cpython-312-aarch64-linux-gnu.so": "8xFCNu6XKY52fe6xOxPM0jDOC1fkn6G/dDVct9t/WsnScSLBb7tOaPJKl+G3zk4xlttrztdK9haPoDOFlApuBw==", + "python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": "xBhqIugWTyGqS1Qd8hXTcL2BX4dwY5EhO2taJEBgwKhU/o17zv7+7GufJiIsAHambrVWI06fnmUA9mZXEBSRDw==", + "python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": "cHJU8JNjRlQ9mdxUsW7VU8hr7yFohd5S2xRjIF8n4mLtfA+IiwTBPWj58HOHK9QbMG2O5uyiQnGd+pINcaF2Cg==", + "python/iqpilot_private/konn3kt/registration/reg_core.cpython-312-aarch64-linux-gnu.so": "Pbz/pYFEjYy2Ywx8HLxYfSZt447S+X7of2ADDrPDjWR4qwdy3mUSUgmOR20jm0SeDlhe9GtI3kbc5I3F6GrdBw==", + "python/iqpilot_private/konn3kt/telemetry/iqtelemetryd.cpython-312-aarch64-linux-gnu.so": "hMIg0GAGId13kARPXKz3kUDi20oF707izEkG2CWymliKdG+1L/8AYn6ib4whIl9NBszjy7vnmAzznbjTbmfgBA==", + "python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": "Vj5Encl2ybZctTrlmZ9f5puYEBIm6ESgscF9Ee414GN88pyQnsUUhqzCeN6IaAJ9T/2tt7n/gEKSyptHCVOUCg==" } } diff --git a/artifacts/iqpilot_hephaestusd_private/manifest.json.sig b/artifacts/iqpilot_hephaestusd_private/manifest.json.sig index 60541a10..1c57efdc 100644 --- a/artifacts/iqpilot_hephaestusd_private/manifest.json.sig +++ b/artifacts/iqpilot_hephaestusd_private/manifest.json.sig @@ -1 +1 @@ -+bka3mSd+pxAt1QcXidY5AAFJXqrazscCuWg0zNSJmjsEk7/LaSXKaHcTCOpBOLN/oMF5nQCsmHnoJUwOEVsDQ== +FdPsL8Lon52Th92plPCF4TsZpz+7qfhZ7yFhrfKKVDknEnF3n+t9LchpGq3n4KuCnhfCO+dszLtoNQfSq/qkDA== diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/_vendored.json b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/_vendored.json index 08cc6239..7403813f 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/_vendored.json +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/_vendored.json @@ -1,18 +1,14 @@ [ "iqpilot.common.api", "iqpilot.common.api.base", - "iqpilot.common.api.comma_connect", - "iqpilot.common.atlas_alerts", + "iqpilot.common.api.konn3kt", "iqpilot.common.basedir", - "iqpilot.common.constants", "iqpilot.common.file_helpers", "iqpilot.common.git_creds", "iqpilot.common.gpio", "iqpilot.common.i2c", - "iqpilot.common.issue_debug", "iqpilot.common.logging_extra", "iqpilot.common.realtime", - "iqpilot.common.spinner", "iqpilot.common.swaglog", "iqpilot.common.time_helpers", "iqpilot.common.utils", @@ -28,10 +24,6 @@ "iqpilot.selfdrive.iqmodeld.model_bundle_downloader", "iqpilot.selfdrive.iqmodeld.models.fetcher", "iqpilot.selfdrive.iqmodeld.models.helpers", - "iqpilot.selfdrive.locationd.calibrationd", - "iqpilot.selfdrive.selfdrived.alertmanager", - "iqpilot.selfdrive.selfdrived.events", - "iqpilot.selfdrive.ui.feedback.feedbackd", "iqpilot.system.hardware", "iqpilot.system.hardware.base", "iqpilot.system.hardware.hw", @@ -47,7 +39,6 @@ "iqpilot.system.loggerd.xattr_cache", "iqpilot.system.manager.process", "iqpilot.system.manager.process_config", - "iqpilot.system.micd", "iqpilot.system.phonegps.bridge", "iqpilot.system.sentry", "iqpilot.system.ui.lib.networkmanager", diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/__init__.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/__init__.py index 7c7b26fc..b7e90dac 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/__init__.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/__init__.py @@ -1,9 +1,9 @@ -import iqpilot.common.api.comma_connect +import iqpilot.common.api.konn3kt class Api: def __init__(self, dongle_id): - self.service = iqpilot.common.api.comma_connect.CommaConnectApi(dongle_id) + self.service = iqpilot.common.api.konn3kt.Konn3ktApi(dongle_id) def request(self, method, endpoint, **params): return self.service.request(method, endpoint, **params) @@ -19,8 +19,8 @@ class Api: def api_get(endpoint, method='GET', timeout=None, access_token=None, session=None, **params): - return iqpilot.common.api.comma_connect.CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, session, **params) + return iqpilot.common.api.konn3kt.Konn3ktApi(None).api_get(endpoint, method, timeout, access_token, session, **params) def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: - return iqpilot.common.api.comma_connect.CommaConnectApi(None).get_key_pair() + return iqpilot.common.api.konn3kt.Konn3ktApi(None).get_key_pair() diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/base.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/base.py index 371a8ab4..0aef9c35 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/base.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/base.py @@ -7,10 +7,44 @@ from functools import lru_cache from iqpilot.system.hardware.hw import Paths from iqpilot.system.version import get_version -# name: jwt signature algorithm KEYS = {"id_rsa": "RS256", "id_ecdsa": "ES256"} +_CUSTODIAN_KEY = object() +_IQKEEPER_SOCKET = "/run/iqkeeper/iqkeeper.sock" + + +def _custodian_available() -> bool: + import stat + try: + return stat.S_ISSOCK(os.stat(_IQKEEPER_SOCKET).st_mode) + except OSError: + return False + + +def _custodian_sign_jwt(dongle_id: str, payload: dict, alg: str) -> str: + import base64 + import json + import socket + nonce = base64.b64encode(os.urandom(16)).decode() + packet = json.dumps({ + "v": 1, + "op": "sign", + "dongle_id": dongle_id, + "payload": payload, + "alg": alg, + "nonce": nonce, + }).encode() + with socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET) as sock: + sock.settimeout(25.0) + sock.connect(_IQKEEPER_SOCKET) + sock.send(packet) + reply_data = sock.recv(8192) + reply = json.loads(reply_data) if reply_data else {} + if reply.get("nonce") != nonce or not reply.get("ok"): + raise PermissionError(f"custodian sign failed: {reply.get('error', 'unknown')}") + return reply["token"] + @lru_cache(maxsize=4) def load_signing_key(private_key: str): @@ -60,6 +94,9 @@ class BaseApi: } if payload_extra is not None: payload.update(payload_extra) + if self.private_key is _CUSTODIAN_KEY: + serializable = {k: v.isoformat() if isinstance(v, datetime) else v for k, v in payload.items()} + return _custodian_sign_jwt(self.dongle_id, serializable, self.jwt_algorithm) key = load_signing_key(self.private_key) if self.private_key else self.private_key token = jwt.encode(payload, key, algorithm=self.jwt_algorithm) if isinstance(token, bytes): @@ -73,9 +110,25 @@ class BaseApi: return self.request(method, endpoint, timeout, access_token, session=session, json=json, **params) @staticmethod - def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: + def get_key_pair() -> tuple[str, str | object, str] | tuple[None, None, None]: for key in KEYS: - if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'): - with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public: - return KEYS[key], private.read(), public.read() + priv_path = Paths.persist_root() + f'/comma/{key}' + pub_path = priv_path + '.pub' + if not os.path.isfile(priv_path) or not os.path.isfile(pub_path): + continue + try: + with open(pub_path) as public: + pub = public.read() + except OSError: + continue + try: + with open(priv_path) as private: + return KEYS[key], private.read(), pub + except PermissionError: + import time + for _attempt in range(10): + if _custodian_available(): + return KEYS[key], _CUSTODIAN_KEY, pub + time.sleep(0.5) + continue return None, None, None diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/comma_connect.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/konn3kt.py similarity index 75% rename from artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/comma_connect.py rename to artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/konn3kt.py index bc16d40e..cae7ea46 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/comma_connect.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/api/konn3kt.py @@ -1,3 +1,4 @@ +# Copyright (c) 2026 IQ.Lvbs. All rights reserved. import os from iqpilot.common.api.base import BaseApi @@ -5,7 +6,7 @@ from iqpilot.common.api.base import BaseApi API_HOST = os.getenv('API_HOST', 'https://api-iqlabs.konn3kt.com') -class CommaConnectApi(BaseApi): +class Konn3ktApi(BaseApi): def __init__(self, dongle_id): super().__init__(dongle_id, API_HOST) self.user_agent = "openpilot-" diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/atlas_alerts.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/atlas_alerts.py deleted file mode 100644 index e2d28626..00000000 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/atlas_alerts.py +++ /dev/null @@ -1,277 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from bisect import insort -from collections.abc import Callable, Iterable -from dataclasses import dataclass, field -from enum import IntEnum - -import iqpilot.cereal.messaging as messaging -from iqpilot.cereal import car, log -from iqpilot.common.realtime import DT_CTRL -from iqpilot.system.hardware import HARDWARE - -AlertSize = log.SelfdriveState.AlertSize -AlertStatus = log.SelfdriveState.AlertStatus -VisualAlert = car.CarControl.HUDControl.VisualAlert -AudibleAlert = car.CarControl.HUDControl.AudibleAlert - - -def _frames_for(seconds: float) -> int: - return int(seconds / DT_CTRL) - - -class Tier(IntEnum): - LOWEST = 0 - LOWER = 1 - LOW = 2 - MID = 3 - HIGH = 4 - HIGHEST = 5 - - -class Tags: - ENABLE = "enable" - PRE_ENABLE = "preEnable" - OVERRIDE_LATERAL = "overrideLateral" - OVERRIDE_LONGITUDINAL = "overrideLongitudinal" - NO_ENTRY = "noEntry" - WARNING = "warning" - USER_DISABLE = "userDisable" - SOFT_DISABLE = "softDisable" - IMMEDIATE_DISABLE = "immediateDisable" - PERMANENT = "permanent" - - -@dataclass(slots=True) -class AlertCard: - alert_text_1: str - alert_text_2: str - alert_status: log.SelfdriveState.AlertStatus - alert_size: log.SelfdriveState.AlertSize - priority: Tier - visual_alert: car.CarControl.HUDControl.VisualAlert - audible_alert: car.CarControl.HUDControl.AudibleAlert - duration: int - creation_delay: float = 0.0 - alert_type: str = field(default="", init=False) - event_type: str | None = field(default=None, init=False) - - def __init__(self, - alert_text_1: str, - alert_text_2: str, - alert_status: log.SelfdriveState.AlertStatus, - alert_size: log.SelfdriveState.AlertSize, - priority: Tier, - visual_alert: car.CarControl.HUDControl.VisualAlert, - audible_alert: car.CarControl.HUDControl.AudibleAlert, - duration: float, - creation_delay: float = 0.0): - self.alert_text_1 = alert_text_1 - self.alert_text_2 = alert_text_2 - self.alert_status = alert_status - self.alert_size = alert_size - self.priority = priority - self.visual_alert = visual_alert - self.audible_alert = audible_alert - self.duration = _frames_for(duration) - self.creation_delay = creation_delay - self.alert_type = "" - self.event_type = None - - def __str__(self) -> str: - return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}" - - -AlertFactory = Callable[[car.CarParams, car.CarState, messaging.SubMaster, bool, int, log.ControlsState], AlertCard] - - -def car_mode_entry_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> AlertCard: - del CS, sm, metric, soft_disable_time, personality - headline = "Enable Adaptive Cruise to Engage" - if CP.brand == "honda": - headline = "Enable Main Switch to Engage" - return NoEntryCard(headline) - - -class EventBook(ABC): - def __init__(self): - self._live_names: list[int] = [] - self._latched_names: list[int] = [] - self.event_counters: dict[int, int] = {} - - @property - def events(self) -> list[int]: - return self._live_names - - @events.setter - def events(self, values: list[int]) -> None: - self._live_names = values - - @property - def static_events(self) -> list[int]: - return self._latched_names - - @static_events.setter - def static_events(self, values: list[int]) -> None: - self._latched_names = values - - @property - def names(self) -> list[int]: - return list(self._live_names) - - def __len__(self) -> int: - return len(self._live_names) - - def add(self, event_name: int, static: bool = False) -> None: - if static: - insort(self._latched_names, event_name) - insort(self._live_names, event_name) - - def clear(self) -> None: - refreshed: dict[int, int] = {} - for event_name, frames_seen in self.event_counters.items(): - refreshed[event_name] = frames_seen + 1 if event_name in self._live_names else 0 - self.event_counters = refreshed - self._live_names = list(self._latched_names) - - def contains(self, event_type: str) -> bool: - board = self.get_events_mapping() - return any(event_type in board.get(event_name, {}) for event_name in self._live_names) - - def has(self, event_name: int) -> bool: - return event_name in self._live_names - - def contains_in_list(self, events_list: list[int]) -> bool: - return any(event_name in self._live_names for event_name in events_list) - - def remove(self, event_name: int, static: bool = False) -> None: - if static and event_name in self._latched_names: - self._latched_names.remove(event_name) - - if event_name in self._live_names: - self.event_counters[event_name] = self.event_counters.get(event_name, 0) + 1 - self._live_names.remove(event_name) - - def add_from_msg(self, events: Iterable) -> None: - for event in events: - insort(self._live_names, event.name.raw) - - def to_msg(self): - board = self.get_events_mapping() - outbound = [] - for event_name in self._live_names: - msg = self.get_event_msg_type().new_message() - msg.name = event_name - for event_kind in board.get(event_name, {}): - setattr(msg, event_kind, True) - outbound.append(msg) - return outbound - - def create_alerts(self, event_types: list[str], callback_args=None): - callback_args = [] if callback_args is None else callback_args - board = self.get_events_mapping() - spawned: list[AlertCard] = [] - for event_name in self._live_names: - variants = board.get(event_name, {}) - for event_type in event_types: - chosen = variants.get(event_type) - if chosen is None: - continue - alert = self._realize(chosen, callback_args) - age_frames = self.event_counters.get(event_name, 0) + 1 - if age_frames * DT_CTRL < alert.creation_delay: - continue - alert.alert_type = f"{self.get_event_name(event_name)}/{event_type}" - alert.event_type = event_type - spawned.append(alert) - return spawned - - @staticmethod - def _realize(candidate: AlertCard | AlertFactory, callback_args: list) -> AlertCard: - return candidate if isinstance(candidate, AlertCard) else candidate(*callback_args) - - @abstractmethod - def get_events_mapping(self) -> dict[int, dict[str, AlertCard | AlertFactory]]: - raise NotImplementedError - - @abstractmethod - def get_event_name(self, event: int) -> str: - raise NotImplementedError - - @abstractmethod - def get_event_msg_type(self): - raise NotImplementedError - - -def _mici_reframe(primary: str, secondary: str) -> tuple[str, str, log.SelfdriveState.AlertSize]: - if HARDWARE.get_device_type() == "mici": - return secondary, primary, AlertSize.small - return primary, secondary, AlertSize.mid - - -class NoEntryCard(AlertCard): - def __init__(self, - alert_text_2: str, - alert_text_1: str = "IQ.Pilot Unavailable", - visual_alert: car.CarControl.HUDControl.VisualAlert = VisualAlert.none, - priority: Tier = Tier.LOW): - primary, secondary, size = _mici_reframe(alert_text_1, alert_text_2) - super().__init__(primary, secondary, AlertStatus.normal, size, priority, visual_alert, AudibleAlert.refuse, 3.0) - - -class GentleDisableCard(AlertCard): - def __init__(self, alert_text_2: str): - super().__init__( - "TAKE CONTROL IMMEDIATELY", - alert_text_2, - AlertStatus.userPrompt, - AlertSize.full, - Tier.MID, - VisualAlert.steerRequired, - AudibleAlert.warningSoft, - 2.0, - ) - - -class PendingDisableCard(GentleDisableCard): - def __init__(self, alert_text_2: str): - super().__init__(alert_text_2) - self.alert_text_1 = "IQ.Pilot will disengage" - - -class HardDisableCard(AlertCard): - def __init__(self, alert_text_2: str): - super().__init__( - "TAKE CONTROL IMMEDIATELY", - alert_text_2, - AlertStatus.critical, - AlertSize.full, - Tier.HIGHEST, - VisualAlert.steerRequired, - AudibleAlert.warningImmediate, - 4.0, - ) - - -class ChimeCard(AlertCard): - def __init__(self, audible_alert: car.CarControl.HUDControl.AudibleAlert): - super().__init__("", "", AlertStatus.normal, AlertSize.none, Tier.MID, VisualAlert.none, audible_alert, 0.2) - - -class BannerCard(AlertCard): - def __init__(self, alert_text_1: str, alert_text_2: str = "", duration: float = 0.2, priority: Tier = Tier.LOWER, creation_delay: float = 0.0): - size = AlertSize.mid if alert_text_2 else AlertSize.small - super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, size, priority, VisualAlert.none, AudibleAlert.none, duration, creation_delay) - - -class BootCard(AlertCard): - def __init__(self, alert_text_1: str, alert_text_2: str = "Always keep hands on wheel and eyes on road", alert_status=AlertStatus.normal): - if HARDWARE.get_device_type() == "mici": - compact_secondary = "" if alert_text_2 == "Always keep hands on wheel and eyes on road" else alert_text_2 - super().__init__(alert_text_1, compact_secondary, alert_status, AlertSize.small, Tier.LOWER, VisualAlert.none, AudibleAlert.none, 5.0) - else: - super().__init__(alert_text_1, alert_text_2, alert_status, AlertSize.mid, Tier.LOWER, VisualAlert.none, AudibleAlert.none, 5.0) - - -NULL_ALERT = AlertCard("", "", AlertStatus.normal, AlertSize.none, Tier.LOWEST, VisualAlert.none, AudibleAlert.none, 0.0) diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/constants.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/constants.py deleted file mode 100644 index 7ca425c4..00000000 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/constants.py +++ /dev/null @@ -1,23 +0,0 @@ -import numpy as np - -# conversions -class CV: - # Speed - MPH_TO_KPH = 1.609344 - KPH_TO_MPH = 1. / MPH_TO_KPH - MS_TO_KPH = 3.6 - KPH_TO_MS = 1. / MS_TO_KPH - MS_TO_MPH = MS_TO_KPH * KPH_TO_MPH - MPH_TO_MS = MPH_TO_KPH * KPH_TO_MS - MS_TO_KNOTS = 1.9438 - KNOTS_TO_MS = 1. / MS_TO_KNOTS - - # Angle - DEG_TO_RAD = np.pi / 180. - RAD_TO_DEG = 1. / DEG_TO_RAD - - # Mass - LB_TO_KG = 0.453592 - - -ACCELERATION_DUE_TO_GRAVITY = 9.81 # m/s^2 diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/issue_debug.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/issue_debug.py deleted file mode 100644 index 85ce98fc..00000000 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/issue_debug.py +++ /dev/null @@ -1,36 +0,0 @@ -import threading -import time -from datetime import datetime -from pathlib import Path - -from iqpilot.system.hardware import PC -from iqpilot.system.hardware.hw import Paths - - -DEBUG_FILENAME = "iqpilot_issue_debug.txt" -DEBUG_PATH = Path(Paths.comma_home()) / "community" / DEBUG_FILENAME if PC else Path("/data/community") / DEBUG_FILENAME - -_lock = threading.Lock() -_last_log_times: dict[str, float] = {} - - -def log_issue(tag: str, message: str) -> None: - try: - DEBUG_PATH.parent.mkdir(parents=True, exist_ok=True) - with _lock: - with open(DEBUG_PATH, "a", encoding="utf-8") as f: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] - f.write(f"[{timestamp}] [{tag}] {message}\n") - except OSError: - pass - - -def log_issue_limited(key: str, tag: str, message: str, interval_sec: float = 1.0) -> None: - now = time.monotonic() - with _lock: - last = _last_log_times.get(key, 0.0) - if now - last < interval_sec: - return - _last_log_times[key] = now - - log_issue(tag, message) diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/spinner.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/spinner.py deleted file mode 100644 index b823d09a..00000000 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/spinner.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import subprocess -from iqpilot.common.basedir import BASEDIR - - -class Spinner: - def __init__(self): - try: - self.spinner_proc = subprocess.Popen(["./spinner.py"], - stdin=subprocess.PIPE, - cwd=os.path.join(BASEDIR, "iqpilot", "system", "ui"), - close_fds=True) - except OSError: - self.spinner_proc = None - - def __enter__(self): - return self - - def update(self, spinner_text: str): - if self.spinner_proc is not None: - self.spinner_proc.stdin.write(spinner_text.encode('utf8') + b"\n") - try: - self.spinner_proc.stdin.flush() - except BrokenPipeError: - pass - - def update_progress(self, cur: float, total: float): - self.update(str(round(100 * cur / total))) - - def close(self): - if self.spinner_proc is not None: - self.spinner_proc.kill() - try: - self.spinner_proc.communicate(timeout=2.) - except subprocess.TimeoutExpired: - print("WARNING: failed to kill spinner") - self.spinner_proc = None - - def __del__(self): - self.close() - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - -if __name__ == "__main__": - import time - with Spinner() as s: - s.update("Spinner text") - time.sleep(5.0) - print("gone") - time.sleep(5.0) diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/utils.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/utils.py index a80270bb..005d75b8 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/utils.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/common/utils.py @@ -31,24 +31,44 @@ class Timer: realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" +_sudo_available: bool | None = None + + def sudo_write(val: str, path: str) -> None: + global _sudo_available try: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - os.system(f"sudo chmod a+w {path}") - try: + return + except OSError: + pass + # AGNOS 4.9.9.2 removes owner root; a privileged boot step opens these nodes to the stack so a direct write normally works. Only probe a non-interactive escalation once (interactive sudo blocks with no tty, and sudo -n itself can stall), and never let its failure or timeout propagate. + if _sudo_available is False: + return + try: + _sudo_available = subprocess.run(["sudo", "-n", "chmod", "a+w", path], timeout=2, capture_output=True).returncode == 0 + if _sudo_available: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - # fallback for debugfs files - os.system(f"sudo su -c 'echo {val} > {path}'") + except Exception: + _sudo_available = False def sudo_read(path: str) -> str: + global _sudo_available try: - return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + with open(path, encoding='utf8') as f: + return f.read().strip() + except OSError: + pass + if _sudo_available is False: + return "" + try: + result = subprocess.run(["sudo", "-n", "cat", path], timeout=2, capture_output=True, text=True) + _sudo_available = result.returncode == 0 + return result.stdout.strip() except Exception: + _sudo_available = False return "" diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/konn3kt/registration.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/konn3kt/registration.py index 11e4fc30..a3720bfa 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/konn3kt/registration.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/konn3kt/registration.py @@ -1,18 +1,12 @@ #!/usr/bin/env python3 -import os -import time -import json -import jwt +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" import re import secrets -from typing import cast from pathlib import Path -from datetime import datetime, timedelta, UTC -from iqpilot.common.api import api_get, get_key_pair from iqpilot.common.params import Params -from iqpilot.common.spinner import Spinner -from iqpilot.system.hardware import HARDWARE from iqpilot.system.hardware.hw import Paths from iqpilot.common.swaglog import cloudlog @@ -35,6 +29,10 @@ def _read_persist_dongle_id() -> str | None: return None +def is_valid_dongle_id(dongle_id: str | None) -> bool: + return bool(dongle_id and _DONGLE_ID_RE.fullmatch(dongle_id)) + + def get_cached_dongle_id(params: Params | None = None, prefer_readonly: bool = True) -> str | None: ro = _read_persist_dongle_id() if is_valid_dongle_id(ro): @@ -46,8 +44,8 @@ def get_cached_dongle_id(params: Params | None = None, prefer_readonly: bool = T if v and v != UNREGISTERED_DONGLE_ID: return v.lower() if is_valid_dongle_id(v) else v return ro or None -def is_valid_dongle_id(dongle_id: str | None) -> bool: - return bool(dongle_id and _DONGLE_ID_RE.fullmatch(dongle_id)) + + def get_or_create_dongle_id(params: Params | None = None, prefer_readonly: bool = True) -> str: p = Params() if params is None else params dongle_id = get_cached_dongle_id(p, prefer_readonly=prefer_readonly) @@ -57,164 +55,32 @@ def get_or_create_dongle_id(params: Params | None = None, prefer_readonly: bool p.put("DongleId", dongle_id) cloudlog.warning(f"generated new DongleId={dongle_id} (no readonly dongle_id found)") return dongle_id + + +_impl = None + + +def _registration_impl(): + global _impl + if _impl is None: + from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + try: + _impl = load_private_module(__name__, "iqpilot_private.konn3kt.registration.reg_core") + except ProprietaryModuleMissing: + from konn3kt_private.registration import reg_core as _impl + return _impl + + def ensure_dev_pairing_identity(params: Params | None = None, force_reset: bool = False) -> dict[str, str]: - p = Params() if params is None else params - - persist_dir = Path(Paths.persist_root()) / "comma" - persist_dir.mkdir(parents=True, exist_ok=True) - - dongle_path = persist_dir / "dongle_id" - priv_path = persist_dir / "id_rsa" - pub_path = persist_dir / "id_rsa.pub" - - if force_reset: - for fp in (dongle_path, priv_path, pub_path): - try: - fp.unlink(missing_ok=True) - except Exception: - cloudlog.exception(f"failed to remove {fp}") - try: - (persist_dir / "konn3kt_prime_type").unlink(missing_ok=True) - except Exception: - pass - try: - p.remove("PrimeType") - except Exception: - pass - forced_dongle = os.getenv("KONN3KT_DEV_DONGLE_ID") - dongle_id = forced_dongle.strip().lower() if forced_dongle else None - if dongle_id and not is_valid_dongle_id(dongle_id): - cloudlog.error("KONN3KT_DEV_DONGLE_ID must be 16 hex chars") - dongle_id = None - if dongle_id is None: - existing = None - try: - existing = dongle_path.read_text().strip().lower() if dongle_path.is_file() else None - except Exception: - cloudlog.exception("failed reading existing dev dongle_id") - dongle_id = existing if is_valid_dongle_id(existing) else secrets.token_hex(8) - try: - dongle_path.write_text(dongle_id) - except Exception: - cloudlog.exception("failed writing dev dongle_id") - p.put("DongleId", dongle_id) - p.put("HardwareSerial", p.get("HardwareSerial") or f"DEV-{dongle_id}") - if force_reset or (not priv_path.is_file()) or (not pub_path.is_file()): - try: - from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import rsa - key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - priv_bytes = key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - pub_bytes = key.public_key().public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - priv_path.write_bytes(priv_bytes) - pub_path.write_bytes(pub_bytes) - except Exception: - cloudlog.exception("failed generating dev RSA keys") - raise - return { - "dongle_id": dongle_id, - "serial": p.get("HardwareSerial") or f"DEV-{dongle_id}", - "persist_dir": str(persist_dir), - } -def _normalize_imei(value: str | None) -> str: - return value or "" + return _registration_impl().ensure_dev_pairing_identity(params, force_reset) def get_registration_identifiers(wait_timeout: float = IMEI_WAIT_TIMEOUT, show_spinner: bool = False) -> tuple[str, str, str]: - serial = HARDWARE.get_serial() - spinner = Spinner() if show_spinner else None - start_time = time.monotonic() - imei1: str | None = None - imei2: str | None = None - - while time.monotonic() - start_time < wait_timeout: - try: - imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) - if imei1 or imei2: - break - except RuntimeError as e: - if "no modems" in str(e).lower(): - cloudlog.warning("No cellular modem available, proceeding without IMEI") - break - cloudlog.exception("Error getting imei, trying again...") - except Exception: - cloudlog.exception("Error getting imei, trying again...") - time.sleep(1) - - imei1 = _normalize_imei(imei1) - imei2 = _normalize_imei(imei2) - - if not imei1 and not imei2: - cloudlog.warning(f"proceeding with serial-only registration for serial={serial}") - if spinner is not None: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1 or None}, {imei2 or None})") - spinner.close() - - return serial, imei1, imei2 + return _registration_impl().get_registration_identifiers(wait_timeout, show_spinner) def register(show_spinner=False) -> str | None: - params = Params() - - dongle_id: str | None = get_cached_dongle_id(params, prefer_readonly=True) - if dongle_id in ("", UNREGISTERED_DONGLE_ID): - dongle_id = None - - jwt_algo, private_key, public_key = get_key_pair() - - if not public_key: - dongle_id = UNREGISTERED_DONGLE_ID - cloudlog.warning("missing public key") - elif dongle_id is None: - if show_spinner: - spinner = Spinner() - spinner.update("registering device") - - serial, imei1, imei2 = get_registration_identifiers(wait_timeout=IMEI_WAIT_TIMEOUT, show_spinner=False) - - backoff = 0 - start_time = time.monotonic() - while True: - try: - register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, - cast(str, private_key), algorithm=jwt_algo) - cloudlog.info("getting pilotauth") - cloudlog.info("getting pilotauth") - resp = api_get("v2/pilotauth/", method='POST', timeout=15, - imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token) - - if resp.status_code in (400, 402, 403): - cloudlog.info(f"Unable to register device, got {resp.status_code}") - dongle_id = UNREGISTERED_DONGLE_ID - else: - dongleauth = json.loads(resp.text) - dongle_id = dongleauth["dongle_id"] - break - except Exception: - cloudlog.exception("failed to authenticate") - backoff = min(backoff + 1, 15) - time.sleep(backoff) - - if time.monotonic() - start_time > 60 and show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") - return UNREGISTERED_DONGLE_ID - - if show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1 or None}, {imei2 or None})") - spinner.close() - - if dongle_id: - params.put("DongleId", dongle_id) - from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert - set_offroad_alert("Offroad_UnregisteredHardware", False) - return dongle_id + return _registration_impl().register(show_spinner) if __name__ == "__main__": diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/car/vehicle_catalog.json b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/car/vehicle_catalog.json index e7048978..79367200 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/car/vehicle_catalog.json +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/car/vehicle_catalog.json @@ -3941,6 +3941,86 @@ ], "req": "All" }, + "TESLA_MODEL_S_HW1|2014-2016": { + "label": "Tesla Model S (with HW1) 2014-16", + "id": "TESLA_MODEL_S_HW1", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model S (with HW1)", + "yrs": [ + "2014", + "2015", + "2016" + ], + "req": "All" + }, + "TESLA_MODEL_S_HW2|2017-2019": { + "label": "Tesla Model S (with HW2) 2017-19", + "id": "TESLA_MODEL_S_HW2", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model S (with HW2)", + "yrs": [ + "2017", + "2018", + "2019" + ], + "req": "All" + }, + "TESLA_MODEL_S_HW3|2020-2023": { + "label": "Tesla Model S (with HW3) 2020-23", + "id": "TESLA_MODEL_S_HW3", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model S (with HW3)", + "yrs": [ + "2020", + "2021", + "2022", + "2023" + ], + "req": "All" + }, + "TESLA_MODEL_X_HW1|2014-2016": { + "label": "Tesla Model X (with HW1) 2014-16", + "id": "TESLA_MODEL_X_HW1", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model X (with HW1)", + "yrs": [ + "2014", + "2015", + "2016" + ], + "req": "All" + }, + "TESLA_MODEL_X_HW2|2016-2019": { + "label": "Tesla Model X (with HW2) 2016-19", + "id": "TESLA_MODEL_X_HW2", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model X (with HW2)", + "yrs": [ + "2016", + "2017", + "2018", + "2019" + ], + "req": "All" + }, + "TESLA_MODEL_X_HW3|2021-2023": { + "label": "Tesla Model X (with HW3) 2021-23", + "id": "TESLA_MODEL_X_HW3", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model X (with HW3)", + "yrs": [ + "2021", + "2022", + "2023" + ], + "req": "All" + }, "TESLA_MODEL_X|2024": { "label": "Tesla Model X (with HW4) 2024", "id": "TESLA_MODEL_X", diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py index 1749bfe0..6ba1c352 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py @@ -139,7 +139,18 @@ def egpu_oob_pkl_path(meta: dict) -> str: def egpu_model_oob_pkl_path(meta: dict) -> str: from iqpilot.system.hardware.hw import Paths - return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_model_oob.pkl") + return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_model_oob2.pkl") + + +def drop_frozen_model_oob(meta: dict) -> None: + from iqpilot.system.hardware.hw import Paths + # warp-on-dock bundles saved under the old name were built before the prune fix and replay capture-time feeds + stale = os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_model_oob.pkl") + try: + os.remove(stale) + cloudlog.warning(f"removed frozen-feed warp-on-dock artifact {stale}") + except OSError: + pass def onnx_cache_path(meta: dict) -> str: @@ -288,6 +299,28 @@ def patch_tinygrad_fetch_fw() -> None: helpers.fetch_fw = fetch_fw +def patch_tinygrad_prune_views() -> None: + from tinygrad.engine import jit + from tinygrad.uop.ops import Ops + if getattr(jit.prune_linear, "_iq_patched", False): + return + + # upstream prune only sees bare BUFFER args, so a kernel reading an input through a SLICE view ran once at capture and froze that input + def prune_linear(linear, needed): + kept, onetime = [], [] + for si in linear.src: + si_bufs = {b for src in si.src[1:] for b in src.toposort() if b.op is Ops.BUFFER} + if not si_bufs.isdisjoint(needed): + kept.append(si) + needed |= si_bufs + else: + onetime.append(si) + return linear.replace(src=tuple(kept)), linear.replace(src=tuple(onetime)) + + prune_linear._iq_patched = True + jit.prune_linear = prune_linear + + def patch_usb_signal_wait() -> None: import time diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py index b8f4ccfc..7b75718a 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py @@ -3,7 +3,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed """ from __future__ import annotations -import functools import hashlib import json import os @@ -24,26 +23,29 @@ RANGE_RETRIES = 4 PARALLEL_PARTS = max(1, int(os.environ.get("IQ_DOWNLOAD_PARTS", "3"))) -def _requests_auth(): +def _git_auth(): import importlib for mod in ("iqpilot_private.models.git_auth", "iqpilot.models_private_src.git_auth", "iqpilot.selfdrive.iqmodeld.models.git_auth"): try: - return importlib.import_module(mod).get_requests_auth() + return importlib.import_module(mod) except Exception: continue return None -def _hf(): - import importlib - for mod in ("iqpilot_private.models.git_auth", "iqpilot.selfdrive.iqmodeld.models.git_auth"): - try: - m = importlib.import_module(mod) - return m.get_hf_headers(), m.hf_resolve_url - except Exception: - continue - return None, None +def _resolve_hf(hf_path: str) -> tuple[str | None, dict]: + m = _git_auth() + if m is None: + return None, {} + return m.resolve_hf(hf_path) + + +def requests_get(url: str, **kwargs): + m = _git_auth() + if m is None: + raise RuntimeError("no asset broker available") + return m.requests_get(url, **kwargs) class RangesNotHonoured(Exception): @@ -189,10 +191,9 @@ def _verify_file(tmp: str, sha256: str, size: int) -> None: def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb=None) -> str: import requests - headers, resolve = _hf() - if resolve is None: + url, headers = _resolve_hf(hf_path) + if url is None: raise RuntimeError("no HF credentials available") - url = resolve(hf_path) os.makedirs(os.path.dirname(dst), exist_ok=True) tmp = dst + ".hfpart" last_error: Exception | None = None @@ -213,8 +214,10 @@ def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb os.remove(leftover) except Exception as e: last_error = e - for _attempt in range(STREAM_RETRIES): + for attempt in range(STREAM_RETRIES): try: + if attempt: + url, headers = _resolve_hf(hf_path) have = os.path.getsize(tmp) if os.path.isfile(tmp) else 0 if size and have > size: os.remove(tmp) @@ -242,26 +245,6 @@ def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb raise RuntimeError(f"HF download failed: {last_error}") -def _lfs_endpoint(base_url: str) -> str: - return base_url.split("/raw/", 1)[0] + ".git/info/lfs" - - -def _resolve_oid(session, base_url: str, oid: str, size: int, auth): - import requests - batch = session.post(f"{_lfs_endpoint(base_url)}/objects/batch", - data=json.dumps({"operation": "download", "transfers": ["basic"], - "objects": [{"oid": oid, "size": size}]}), - headers={"Content-Type": "application/vnd.git-lfs+json", - "Accept": "application/vnd.git-lfs+json"}, - auth=auth, timeout=HTTP_TIMEOUT_S) - batch.raise_for_status() - entry = batch.json()["objects"][0] - if "actions" not in entry: - raise requests.RequestException(f"LFS object unavailable: {entry.get('error', oid)}") - action = entry["actions"]["download"] - return action["href"], action.get("header", {}) - - def _part_path(dst: str, oid: str) -> str: return os.path.join(dst + ".parts", oid) @@ -276,19 +259,28 @@ def _part_complete(path: str, oid: str, size: int) -> bool: return digest.hexdigest() == oid -def _fetch_part(session, base_url: str, obj: dict, path: str, auth, progress) -> None: +def _fetch_part(session, obj: dict, path: str, progress) -> None: size = int(obj["size"]) have = os.path.getsize(path) if os.path.isfile(path) else 0 if have > size: os.remove(path) have = 0 - href, headers = _resolve_oid(session, base_url, obj["oid"], size, auth) - obj_auth = None if headers.get("Authorization") else auth + m = _git_auth() + if m is None: + raise RuntimeError("no asset broker available for LFS parts") + target = m.broker_lfs_url(obj["oid"], size) + headers = m.broker_headers() if target else {} + if not (target and headers): + raise RuntimeError("no device identity for the konn3kt asset broker") + _stream_part(session, target, headers, path, have, progress) + + +def _stream_part(session, href: str, headers: dict, path: str, have: int, progress) -> None: # LFS parts are content-addressed (oid == sha256), so a half-written part can be resumed with a # Range request and verified afterwards instead of being thrown away on every restart. if have: headers = {**headers, "Range": f"bytes={have}-"} - with session.get(href, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S, auth=obj_auth) as r: + with session.get(href, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S) as r: r.raise_for_status() if have and r.status_code != 206: have = 0 @@ -300,7 +292,6 @@ def _fetch_part(session, base_url: str, obj: dict, path: str, auth, progress) -> def download_lfs_bundle(objects: list, dst: str, sha256: str, size: int, progress_cb=None) -> str: import requests - auth = _requests_auth() os.makedirs(dst + ".parts", exist_ok=True) total = int(size) or sum(int(o["size"]) for o in objects) done_bytes = sum(int(o["size"]) for o in objects if _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"]))) @@ -318,30 +309,26 @@ def download_lfs_bundle(objects: list, dst: str, sha256: str, size: int, progres with lock: progress(n) - def fetch(base_url: str, obj: dict) -> None: + def fetch(obj: dict) -> None: path = _part_path(dst, obj["oid"]) if _part_complete(path, obj["oid"], int(obj["size"])): return - _fetch_part(requests.Session(), base_url, obj, path, auth, locked_progress) + _fetch_part(requests.Session(), obj, path, locked_progress) if not _part_complete(path, obj["oid"], int(obj["size"])): if os.path.getsize(path) >= int(obj["size"]): os.remove(path) raise RuntimeError(f"part {obj['oid'][:12]} incomplete or failed verification") - for base_url in MODELS_BASE_URLS: - for _attempt in range(STREAM_RETRIES): - try: - pending = [o for o in objects if not _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"]))] - got[0] = total - sum(int(o["size"]) for o in pending) - with ThreadPoolExecutor(max_workers=max(1, min(PARALLEL_PARTS, len(pending) or 1))) as pool: - for _ in pool.map(functools.partial(fetch, base_url), pending): - pass - break - except Exception as e: - last_error = e - else: - continue - break + for _attempt in range(STREAM_RETRIES): + try: + pending = [o for o in objects if not _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"]))] + got[0] = total - sum(int(o["size"]) for o in pending) + with ThreadPoolExecutor(max_workers=max(1, min(PARALLEL_PARTS, len(pending) or 1))) as pool: + for _ in pool.map(fetch, pending): + pass + break + except Exception as e: + last_error = e else: raise RuntimeError(f"model bundle download failed: {last_error}") diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py index 05cce298..ee2eb77f 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py @@ -305,7 +305,40 @@ def get_runtime_bundle_upgrade(bundle, params: Params = None, available_bundles= return _find_runtime_upgrade(bundle, params, available_bundles) +_forced_bundle = None + + +def load_default_model_session(): + """Stage the shipped default model files and return a decoded default bundle + WITHOUT persisting it as the active selection, so the next boot retries the + user's own model instead of staying on the default forever.""" + bundle_dict = _load_default_bundle_dict() + ensure_default_model_files(bundle_dict) + return ModelBundle(**bundle_dict) + + +class forced_active_bundle: + """Make get_active_bundle return `bundle` for the duration of the block, used to + build a runner for a session-only fallback bundle without touching Params.""" + def __init__(self, bundle): + self._bundle = bundle + + def __enter__(self): + global _forced_bundle + self._prev = _forced_bundle + _forced_bundle = self._bundle + return self._bundle + + def __exit__(self, *exc): + global _forced_bundle + _forced_bundle = self._prev + return False + + def get_active_bundle(params: Params = None): + if _forced_bundle is not None: + return _forced_bundle + params = Params() if params is None else params try: diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibrationd.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibrationd.py deleted file mode 100644 index 3e3ec6d4..00000000 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibrationd.py +++ /dev/null @@ -1,367 +0,0 @@ -#!/usr/bin/env python3 -''' -This process finds calibration values. More info on what these calibration values -are can be found here https://github.com/commaai/openpilot/tree/master/common/transformations -While the roll calibration is a real value that can be estimated, here we assume it's zero, -and the image input into the neural network is not corrected for roll. -''' - -import os -import capnp -import numpy as np -from typing import NoReturn - -from iqpilot.cereal import log, car -import iqpilot.cereal.messaging as messaging -from iqpilot.common.constants import CV -from iqpilot.common.params import Params -from iqpilot.common.issue_debug import log_issue_limited -from iqpilot.common.realtime import config_realtime_process -from iqpilot.common.transformations.orientation import rot_from_euler, euler_from_rot -from iqpilot.common.swaglog import cloudlog -from iqpilot.system.hardware import HARDWARE - -MIN_SPEED_FILTER = 15 * CV.MPH_TO_MS -MAX_VEL_ANGLE_STD = np.radians(0.25) -MAX_YAW_RATE_FILTER = np.radians(2) # per second - -MAX_HEIGHT_STD = np.exp(-3.5) - -# This is at model frequency, blocks needed for efficiency -SMOOTH_CYCLES = 10 -BLOCK_SIZE = 100 -INPUTS_NEEDED = 5 # Minimum blocks needed for valid calibration -INPUTS_WANTED = 50 # We want a little bit more than we need for stability -MAX_ALLOWED_YAW_SPREAD = np.radians(2) -MAX_ALLOWED_PITCH_SPREAD = np.radians(4) -TICI_FAMILY_PITCH_SPREAD_RESET = np.radians(3) -RPY_INIT = np.array([0.0,0.0,0.0]) -WIDE_FROM_DEVICE_EULER_INIT = np.array([0.0, 0.0, 0.0]) -HEIGHT_INIT = np.array([1.22]) -HEIGHT_SANE_MIN, HEIGHT_SANE_MAX = 0.9, 2.0 -DEVICE_IS_TICI_FAMILY = HARDWARE.get_device_type() in ("tici", "tizi") - -# These values are needed to accommodate the model frame in the narrow cam -if HARDWARE.get_device_type() == 'mici': - PITCH_LIMITS = np.array([-0.143101, 0.22235988]) -else: - PITCH_LIMITS = np.array([-0.09074112085129739, 0.17]) -YAW_LIMITS = np.array([-0.06912048084718224, 0.06912048084718235]) -DEBUG = os.getenv("DEBUG") is not None - -def is_calibration_valid(rpy: np.ndarray) -> bool: - return (PITCH_LIMITS[0] < rpy[1] < PITCH_LIMITS[1]) and (YAW_LIMITS[0] < rpy[2] < YAW_LIMITS[1]) - - -def sanity_clip(rpy: np.ndarray) -> np.ndarray: - if np.isnan(rpy).any(): - rpy = RPY_INIT - return np.array([rpy[0], - np.clip(rpy[1], PITCH_LIMITS[0] - .005, PITCH_LIMITS[1] + .005), - np.clip(rpy[2], YAW_LIMITS[0] - .005, YAW_LIMITS[1] + .005)]) - -def moving_avg_with_linear_decay(prev_mean: np.ndarray, new_val: np.ndarray, idx: int, block_size: float) -> np.ndarray: - return (idx*prev_mean + (block_size - idx) * new_val) / block_size - -class Calibrator: - def __init__(self, param_put: bool = False): - self.param_put = param_put - - self.not_car = False - self.stable_rpy = RPY_INIT.copy() - self.stable_wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT.copy() - self.stable_height = HEIGHT_INIT.copy() - self.has_stable_snapshot = False - - # Read saved calibration - self.params = Params() - calibration_params = self.params.get("CalibrationParams") - rpy_init = RPY_INIT - wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT - height = HEIGHT_INIT - valid_blocks = 0 - self.cal_status = log.ExtrinsicsCalibration.Status.uncalibrated - - if param_put and calibration_params: - try: - with log.Event.from_bytes(calibration_params) as msg: - rpy_init = np.array(msg.extrinsicsCalibration.rpyCalib) - valid_blocks = msg.extrinsicsCalibration.validBlocks - wide_from_device_euler = np.array(msg.extrinsicsCalibration.wideFromDeviceEuler) - height = np.array(msg.extrinsicsCalibration.height) - except Exception: - cloudlog.exception("Error reading cached CalibrationParams") - - self.reset(rpy_init, valid_blocks, wide_from_device_euler, height) - self.update_status() - - # If saved calibration is immediately invalid (e.g. bad params from a previous - # bootstrap bug or device remount), auto-clear it so we recalibrate from scratch - # instead of getting permanently stuck in the "Calibration Invalid" state. - if self.cal_status == log.ExtrinsicsCalibration.Status.invalid: - cloudlog.warning("calibrationd: saved CalibrationParams are invalid, clearing and starting fresh") - if param_put: - self.params.remove("CalibrationParams") - self.reset() - self.update_status() - - def _remember_stable_solution(self) -> None: - self.stable_rpy = self.rpy.copy() - self.stable_wide_from_device_euler = self.wide_from_device_euler.copy() - self.stable_height = self.height.copy() - self.has_stable_snapshot = True - - def reset(self, rpy_init: np.ndarray = RPY_INIT, - valid_blocks: int = 0, - wide_from_device_euler_init: np.ndarray = WIDE_FROM_DEVICE_EULER_INIT, - height_init: np.ndarray = HEIGHT_INIT, - smooth_from: np.ndarray | None = None) -> None: - if not np.isfinite(rpy_init).all(): - self.rpy = RPY_INIT.copy() - else: - self.rpy = rpy_init.copy() - - if not np.isfinite(height_init).all() or len(height_init) != 1: - self.height = HEIGHT_INIT.copy() - else: - self.height = height_init.copy() - - if not np.isfinite(wide_from_device_euler_init).all() or len(wide_from_device_euler_init) != 3: - self.wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT.copy() - else: - self.wide_from_device_euler = wide_from_device_euler_init.copy() - - if not np.isfinite(valid_blocks) or valid_blocks < 0: - self.valid_blocks = 0 - else: - self.valid_blocks = valid_blocks - - self.rpys = np.tile(self.rpy, (INPUTS_WANTED, 1)) - self.wide_from_device_eulers = np.tile(self.wide_from_device_euler, (INPUTS_WANTED, 1)) - self.heights = np.tile(self.height, (INPUTS_WANTED, 1)) - - self.idx = 0 - self.block_idx = 0 - self.v_ego = 0.0 - - if smooth_from is None: - self.old_rpy = RPY_INIT - self.old_rpy_weight = 0.0 - else: - self.old_rpy = smooth_from - self.old_rpy_weight = 1.0 - - def get_valid_idxs(self) -> list[int]: - # exclude current block_idx from validity window - before_current = list(range(self.block_idx)) - after_current = list(range(min(self.valid_blocks, self.block_idx + 1), self.valid_blocks)) - return before_current + after_current - - def update_status(self) -> None: - valid_idxs = self.get_valid_idxs() - if valid_idxs: - self.wide_from_device_euler = np.mean(self.wide_from_device_eulers[valid_idxs], axis=0) - self.height = np.mean(self.heights[valid_idxs], axis=0) - rpys = self.rpys[valid_idxs] - self.rpy = np.mean(rpys, axis=0) - max_rpy_calib = np.array(np.max(rpys, axis=0)) - min_rpy_calib = np.array(np.min(rpys, axis=0)) - self.calib_spread = np.abs(max_rpy_calib - min_rpy_calib) - else: - self.calib_spread = np.zeros(3) - - if self.valid_blocks < INPUTS_NEEDED: - if self.cal_status == log.ExtrinsicsCalibration.Status.recalibrating: - self.cal_status = log.ExtrinsicsCalibration.Status.recalibrating - else: - self.cal_status = log.ExtrinsicsCalibration.Status.uncalibrated - elif is_calibration_valid(self.rpy): - self.cal_status = log.ExtrinsicsCalibration.Status.calibrated - else: - self.cal_status = log.ExtrinsicsCalibration.Status.invalid - - # If spread is too high, assume mounting was changed and reset to last block. - # Make the transition smooth. Abrupt transitions are not good for feedback loop through supercombo model. - # TODO: add height spread check with smooth transition too - pitch_spread_limit = TICI_FAMILY_PITCH_SPREAD_RESET if DEVICE_IS_TICI_FAMILY else MAX_ALLOWED_PITCH_SPREAD - spread_too_high = self.calib_spread[1] > pitch_spread_limit or self.calib_spread[2] > MAX_ALLOWED_YAW_SPREAD - if self.cal_status == log.ExtrinsicsCalibration.Status.calibrated and not spread_too_high: - self._remember_stable_solution() - - if spread_too_high and self.cal_status == log.ExtrinsicsCalibration.Status.calibrated: - use_stable_snapshot = DEVICE_IS_TICI_FAMILY and self.has_stable_snapshot - if use_stable_snapshot: - reset_rpy = self.stable_rpy - reset_wide = self.stable_wide_from_device_euler - reset_height = self.stable_height - else: - reset_rpy = self.rpys[self.block_idx - 1] - reset_wide = self.wide_from_device_eulers[self.block_idx - 1] - reset_height = self.heights[self.block_idx - 1] - - log_issue_limited( - "calibrationd_reset_spread", - "calibration", - f"calibrationd reset unstable solution pitchSpread={self.calib_spread[1]:.6f} " - f"yawSpread={self.calib_spread[2]:.6f} pitchLimit={pitch_spread_limit:.6f} " - f"use_stable_snapshot={use_stable_snapshot} rpy={self.rpy.tolist()}", - interval_sec=0.5, - ) - self.reset(reset_rpy, valid_blocks=1, wide_from_device_euler_init=reset_wide, - height_init=reset_height, smooth_from=self.stable_rpy if use_stable_snapshot else self.rpy) - self.cal_status = log.ExtrinsicsCalibration.Status.recalibrating - - write_this_cycle = (self.idx == 0) and (self.block_idx % (INPUTS_WANTED//5) == 5) - if self.param_put and write_this_cycle: - self.params.put_nonblocking("CalibrationParams", self.get_msg(True).to_bytes()) - - def handle_v_ego(self, v_ego: float) -> None: - self.v_ego = v_ego - - def get_smooth_rpy(self) -> np.ndarray: - if self.old_rpy_weight > 0: - return self.old_rpy_weight * self.old_rpy + (1.0 - self.old_rpy_weight) * self.rpy - else: - return self.rpy - - def handle_cam_odom(self, trans: list[float], - rot: list[float], - wide_from_device_euler: list[float], - trans_std: list[float], - road_transform_trans: list[float], - road_transform_trans_std: list[float]) -> np.ndarray | None: - self.old_rpy_weight = max(0.0, self.old_rpy_weight - 1/SMOOTH_CYCLES) - - fast_enough = self.v_ego > MIN_SPEED_FILTER - motion_speed = max(float(self.v_ego), float(trans[0])) - cam_fast_enough = motion_speed > MIN_SPEED_FILTER - yaw_ok = abs(rot[2]) < MAX_YAW_RATE_FILTER - straight_and_fast = fast_enough and cam_fast_enough and yaw_ok - angle_std_threshold = MAX_VEL_ANGLE_STD - height_std_threshold = MAX_HEIGHT_STD - rpy_certain = np.arctan2(trans_std[1], motion_speed) < angle_std_threshold - if len(road_transform_trans_std) == 3: - height_certain = road_transform_trans_std[2] < height_std_threshold - else: - height_certain = True - - certain_if_calib = rpy_certain - if not (straight_and_fast and certain_if_calib): - log_issue_limited( - "calibrationd_rejected_sample", - "calibration", - f"calibrationd rejected sample vEgo={self.v_ego:.2f} trans0={trans[0]:.2f} yawRate={rot[2]:.4f} " - f"fast_enough={fast_enough} cam_fast_enough={cam_fast_enough} motion_speed={motion_speed:.2f} yaw_ok={yaw_ok} " - f"rpy_certain={rpy_certain} height_certain={height_certain} valid_blocks={self.valid_blocks} idx={self.idx}", - interval_sec=1.0, - ) - return None - - observed_rpy = np.array([0, - -np.arctan2(trans[2], trans[0]), - np.arctan2(trans[1], trans[0])]) - new_rpy = euler_from_rot(rot_from_euler(self.get_smooth_rpy()).dot(rot_from_euler(observed_rpy))) - new_rpy = sanity_clip(new_rpy) - - if len(wide_from_device_euler) == 3: - new_wide_from_device_euler = np.array(wide_from_device_euler) - else: - new_wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT - - if len(road_transform_trans) == 3 and HEIGHT_SANE_MIN <= road_transform_trans[2] <= HEIGHT_SANE_MAX: - new_height = np.array([road_transform_trans[2]]) - else: - new_height = HEIGHT_INIT - - self.rpys[self.block_idx] = moving_avg_with_linear_decay(self.rpys[self.block_idx], new_rpy, self.idx, float(BLOCK_SIZE)) - self.wide_from_device_eulers[self.block_idx] = moving_avg_with_linear_decay(self.wide_from_device_eulers[self.block_idx], - new_wide_from_device_euler, self.idx, float(BLOCK_SIZE)) - self.heights[self.block_idx] = moving_avg_with_linear_decay(self.heights[self.block_idx], new_height, self.idx, float(BLOCK_SIZE)) - - self.idx = (self.idx + 1) % BLOCK_SIZE - if self.idx == 0: - self.block_idx += 1 - self.valid_blocks = max(self.block_idx, self.valid_blocks) - self.block_idx = self.block_idx % INPUTS_WANTED - - self.update_status() - - if self.idx == 0: - log_issue_limited( - "calibrationd_progress_block", - "calibration", - f"calibrationd progress status={int(self.cal_status)} valid_blocks={self.valid_blocks} " - f"calPerc={min(100 * (self.valid_blocks * BLOCK_SIZE + self.idx) // (INPUTS_NEEDED * BLOCK_SIZE), 100)} " - f"rpy={self.rpy.tolist()} spread={self.calib_spread.tolist()}", - interval_sec=0.5, - ) - - return new_rpy - - def get_msg(self, valid: bool) -> capnp.lib.capnp._DynamicStructBuilder: - smooth_rpy = self.get_smooth_rpy() - - msg = messaging.new_message('extrinsicsCalibration') - msg.valid = valid - - extrinsicsCalibration = msg.extrinsicsCalibration - extrinsicsCalibration.validBlocks = self.valid_blocks - extrinsicsCalibration.calStatus = self.cal_status - extrinsicsCalibration.calPerc = min(100 * (self.valid_blocks * BLOCK_SIZE + self.idx) // (INPUTS_NEEDED * BLOCK_SIZE), 100) - extrinsicsCalibration.rpyCalib = smooth_rpy.tolist() - extrinsicsCalibration.rpyCalibSpread = self.calib_spread.tolist() - extrinsicsCalibration.wideFromDeviceEuler = self.wide_from_device_euler.tolist() - extrinsicsCalibration.height = self.height.tolist() - - return msg - - def send_data(self, pm: messaging.PubMaster, valid: bool) -> None: - pm.send('extrinsicsCalibration', self.get_msg(valid)) - - -def main() -> NoReturn: - config_realtime_process([0, 1, 2, 3], 5) - - pm = messaging.PubMaster(['extrinsicsCalibration']) - sm = messaging.SubMaster(['cameraOdometry', 'carState'], poll='cameraOdometry') - - params_reader = Params() - CP = messaging.log_from_bytes(params_reader.get("CarParams", block=True), car.CarParams) - - calibrator = Calibrator(param_put=True) - calibrator.not_car = CP.notCar - - while 1: - timeout = 0 if sm.frame == -1 else 100 - sm.update(timeout) - - if sm.updated['cameraOdometry']: - calibrator.handle_v_ego(sm['carState'].vEgo) - new_rpy = calibrator.handle_cam_odom(sm['cameraOdometry'].trans, - sm['cameraOdometry'].rot, - sm['cameraOdometry'].wideFromDeviceEuler, - sm['cameraOdometry'].transStd, - sm['cameraOdometry'].roadTransformTrans, - sm['cameraOdometry'].roadTransformTransStd) - - if DEBUG and new_rpy is not None: - print('got new rpy', new_rpy) - - # 4Hz driven by cameraOdometry - if sm.frame % 5 == 0: - checks_ok = sm.all_checks() - if not checks_ok: - ft = sm.freq_tracker - recv_hz = {s: (round(1.0 / ft[s].avg_dt.get_average(), 2) if ft[s].avg_dt.count else None) for s in sm.services} - log_issue_limited( - "calibrationd_checks_failed", - "calibration", - f"calibrationd all_checks failed alive={sm.alive} freq_ok={sm.freq_ok} valid={sm.valid} " - f"seen={sm.seen} recv_hz={recv_hz}", - interval_sec=5.0, - ) - calibrator.send_data(pm, checks_ok) - - -if __name__ == "__main__": - main() diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/alertmanager.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/alertmanager.py deleted file mode 100644 index 0ffe2943..00000000 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/alertmanager.py +++ /dev/null @@ -1,68 +0,0 @@ -import copy -import os -import json -from collections import defaultdict -from dataclasses import dataclass - -from iqpilot.common.basedir import BASEDIR -from iqpilot.common.params import Params -from iqpilot.selfdrive.selfdrived.events import Alert -from iqpilot.common.atlas_alerts import NULL_ALERT as EmptyAlert - - -with open(os.path.join(BASEDIR, "iqpilot/selfdrive/selfdrived/alerts_offroad.json")) as f: - OFFROAD_ALERTS = json.load(f) - - -def set_offroad_alert(alert: str, show_alert: bool, extra_text: str | None = None) -> None: - if show_alert: - a = copy.copy(OFFROAD_ALERTS[alert]) - a['extra'] = extra_text or '' - Params().put(alert, a) - else: - Params().remove(alert) - - -@dataclass -class AlertEntry: - alert: Alert | None = None - start_frame: int = -1 - end_frame: int = -1 - added_frame: int = -1 - - def active(self, frame: int) -> bool: - return frame <= self.end_frame - - def just_added(self, frame: int) -> bool: - return self.active(frame) and frame == (self.added_frame + 1) - -class AlertManager: - def __init__(self): - self.alerts: dict[str, AlertEntry] = defaultdict(AlertEntry) - self.current_alert = EmptyAlert - - def add_many(self, frame: int, alerts: list[Alert]) -> None: - for alert in alerts: - entry = self.alerts[alert.alert_type] - entry.alert = alert - if not entry.just_added(frame): - entry.start_frame = frame - min_end_frame = entry.start_frame + alert.duration - entry.end_frame = max(frame + 1, min_end_frame) - entry.added_frame = frame - - def process_alerts(self, frame: int, clear_event_types: set): - ae = AlertEntry() - for v in self.alerts.values(): - if not v.alert: - continue - - if v.alert.event_type in clear_event_types: - v.end_frame = -1 - - # sort by priority first and then by start_frame - greater = ae.alert is None or (v.alert.priority, v.start_frame) > (ae.alert.priority, ae.start_frame) - if v.active(frame) and greater: - ae = v - - self.current_alert = ae.alert if ae.alert is not None else EmptyAlert diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/events.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/events.py deleted file mode 100644 index aeaa9eb9..00000000 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/events.py +++ /dev/null @@ -1,892 +0,0 @@ -#!/usr/bin/env python3 -import math - -from iqpilot.cereal import log, car -import iqpilot.cereal.messaging as messaging -from iqpilot.common.constants import CV -from iqpilot.common.realtime import DT_CTRL -from iqpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER -from iqpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER -from iqpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION -from iqpilot.system.hardware import HARDWARE - -from iqpilot.common.atlas_alerts import EventBook as EventsBase, Tier as Priority, Tags as ET, AlertCard as Alert, \ - NoEntryCard as NoEntryAlert, GentleDisableCard as SoftDisableAlert, PendingDisableCard as UserSoftDisableAlert, \ - HardDisableCard as ImmediateDisableAlert, ChimeCard as EngagementAlert, BannerCard as NormalPermanentAlert, \ - BootCard as StartupAlert, AlertFactory as AlertCallbackType, car_mode_entry_alert as wrong_car_mode_alert - -AlertSize = log.SelfdriveState.AlertSize -AlertStatus = log.SelfdriveState.AlertStatus -VisualAlert = car.CarControl.HUDControl.VisualAlert -AudibleAlert = car.CarControl.HUDControl.AudibleAlert -EventName = log.OnroadEvent.EventName - -EVENT_NAME = {v: k for k, v in EventName.schema.enumerants.items()} - -class Events(EventsBase): - def __init__(self): - super().__init__() - self.event_counters = dict.fromkeys(EVENTS.keys(), 0) - - def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: - return EVENTS - - def get_event_name(self, event: int): - return EVENT_NAME[event] - - def get_event_msg_type(self): - return log.OnroadEvent - -def get_display_speed(speed_ms: float, metric: bool) -> str: - speed = int(round(speed_ms * (CV.MS_TO_KPH if metric else CV.MS_TO_MPH))) - unit = 'km/h' if metric else 'mph' - return f"{speed} {unit}" - -def soft_disable_alert(alert_text_2: str) -> AlertCallbackType: - def func(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - if soft_disable_time < int(0.5 / DT_CTRL): - return ImmediateDisableAlert(alert_text_2) - return SoftDisableAlert(alert_text_2) - return func - -def user_soft_disable_alert(alert_text_2: str) -> AlertCallbackType: - def func(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - if soft_disable_time < int(0.5 / DT_CTRL): - return ImmediateDisableAlert(alert_text_2) - return UserSoftDisableAlert(alert_text_2) - return func - -def below_engage_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - return NoEntryAlert(f"Drive above {get_display_speed(CP.minEnableSpeed, metric)} to engage") - - -def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - return Alert( - f"Steer Assist Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}", - "", - AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4) - - -def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - first_word = 'Recalibrating' if sm['extrinsicsCalibration'].calStatus == log.ExtrinsicsCalibration.Status.recalibrating else 'Calibrating' - return Alert( - f"{first_word}: {sm['extrinsicsCalibration'].calPerc:.0f}%", - f"Drive Above {get_display_speed(MIN_SPEED_FILTER, metric)}", - AlertStatus.normal, AlertSize.mid, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2) - - -def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE) - return NormalPermanentAlert( - "Recording Audio Feedback", - f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.", - priority=Priority.LOW) - -def out_of_space_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - full_perc = round(100. - sm['deviceState'].freeSpacePercent) - return NormalPermanentAlert("Out of Storage", f"{full_perc}% full") - - -def posenet_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - if sm.frame * DT_CTRL < 10.: - return NoEntryAlert("IQModel is starting up", alert_text_1="Please Wait") - mdl = sm['modelV2'].velocity.x[0] if len(sm['modelV2'].velocity.x) else math.nan - err = CS.vEgo - mdl - msg = f"Speed Error: {err:.1f} m/s" - return NoEntryAlert(msg, alert_text_1="Posenet Speed Invalid") - - -def process_not_running_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - not_running = [p.name for p in sm['managerState'].processes if not p.running and p.shouldBeRunning] - msg = ', '.join(not_running) - return NoEntryAlert(msg, alert_text_1="Process Not Running") - - -def camera_malfunction_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - all_cams = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState') - bad_cams = [s.replace('State', '') for s in all_cams if s in sm.data.keys() and not sm.all_checks([s, ])] - return NormalPermanentAlert("Camera Malfunction", ', '.join(bad_cams)) - - -def calibration_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - rpy = sm['extrinsicsCalibration'].rpyCalib - yaw = math.degrees(rpy[2] if len(rpy) == 3 else math.nan) - pitch = math.degrees(rpy[1] if len(rpy) == 3 else math.nan) - angles = f"Remount Device (Pitch: {pitch:.1f}°, Yaw: {yaw:.1f}°)" - return NormalPermanentAlert("Calibration Invalid", angles) - - -def paramsd_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - if not sm['vehicleParameters'].angleOffsetValid: - angle_offset_deg = sm['vehicleParameters'].angleOffsetDeg - title = "Steering misalignment detected" - text = f"Angle offset too high (Offset: {angle_offset_deg:.1f}°)" - elif not sm['vehicleParameters'].steerRatioValid: - steer_ratio = sm['vehicleParameters'].steerRatio - title = "Steer ratio mismatch" - text = f"Steering rack geometry may be off (Ratio: {steer_ratio:.1f})" - elif not sm['vehicleParameters'].stiffnessFactorValid: - stiffness_factor = sm['vehicleParameters'].stiffnessFactor - title = "Abnormal tire stiffness" - text = f"Check tires, pressure, or alignment (Factor: {stiffness_factor:.1f})" - else: - return NoEntryAlert("paramsd Temporary Error") - - return NoEntryAlert(alert_text_1=title, alert_text_2=text) - -def overheat_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - cpu = max(sm['deviceState'].cpuTempC, default=0.) - gpu = max(sm['deviceState'].gpuTempC, default=0.) - temp = max((cpu, gpu, sm['deviceState'].memoryTempC)) - return NormalPermanentAlert("System Overheated", f"{temp:.0f} °C") - - -def low_memory_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - return NormalPermanentAlert("Low Memory", f"{sm['deviceState'].memoryUsagePercent}% used") - - -def modeld_lagging_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - return NormalPermanentAlert("Driving Model Lagging", f"{sm['modelV2'].frameDropPerc:.1f}% frames dropped") - - -def joystick_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - gb = sm['carControl'].actuators.accel / 4. - if CP.steerControlType in (car.CarParams.SteerControlType.angle, car.CarParams.SteerControlType.curvatureDEPRECATED): - steer = sm['carControl'].actuators.steeringAngleDeg - vals = f"Gas: {round(gb * 100.)}%, Angle: {round(steer, 1)}°" - else: - steer = sm['carControl'].actuators.torque - vals = f"Gas: {round(gb * 100.)}%, Steer: {round(steer * 100.)}%" - return NormalPermanentAlert("Joystick Mode", vals) - - -def longitudinal_maneuver_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - ad = sm['alertDebug'] - audible_alert = AudibleAlert.prompt if 'Active' in ad.alertText1 else AudibleAlert.none - alert_status = AlertStatus.userPrompt if 'Active' in ad.alertText1 else AlertStatus.normal - alert_size = AlertSize.mid if ad.alertText2 else AlertSize.small - return Alert(ad.alertText1, ad.alertText2, - alert_status, alert_size, - Priority.LOW, VisualAlert.none, audible_alert, 0.2) - - -def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - personality = str(personality).title() - return NormalPermanentAlert(f"Driving Personality: {personality}", duration=1.5) - - -def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - title = "Invalid LKAS setting" - text = "Toggle stock LKAS on or off to engage" - if CP.brand == "tesla": - title = "Dashcam Mode" - text = "FSD / Autosteer is active" - elif CP.brand == "mazda": - text = "Enable your car's LKAS to engage" - elif CP.brand == "nissan": - text = "Disable your car's stock LKAS to engage" - return NormalPermanentAlert(title, text) - - -def invalid_lkas_setting_no_entry_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, - metric: bool, soft_disable_time: int, personality) -> Alert: - if CP.brand == "tesla": - return NoEntryAlert("FSD / Autosteer is active", alert_text_1="Dashcam Mode") - return NoEntryAlert("Invalid LKAS setting") - - - -EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { - EventName.stockFcw: {}, - EventName.actuatorsApiUnavailable: {}, - - EventName.joystickDebug: { - ET.WARNING: joystick_alert, - ET.PERMANENT: NormalPermanentAlert("Joystick Mode"), - }, - - EventName.longitudinalManeuver: { - ET.WARNING: longitudinal_maneuver_alert, - ET.PERMANENT: NormalPermanentAlert("Longitudinal Maneuver Mode", - "Ensure road ahead is clear"), - }, - - EventName.bigModelLoading: { - ET.NO_ENTRY: NoEntryAlert("Big Model Loading"), - }, - - EventName.bigModelFailed: { - ET.SOFT_DISABLE: soft_disable_alert("Big Model Failed"), - ET.PERMANENT: NormalPermanentAlert("Big Model Failed ", "Restart the car to retry,\nsmall model is still available", duration=20.), - }, - - EventName.lateralManeuver: { - ET.WARNING: longitudinal_maneuver_alert, - ET.PERMANENT: NormalPermanentAlert("Lateral Maneuver Mode"), - }, - - EventName.selfdriveInitializing: { - ET.NO_ENTRY: NoEntryAlert("IQ.Pilot Initializing"), - }, - - EventName.startup: { - ET.PERMANENT: StartupAlert("Welcome to IQ.Pilot!") - }, - - EventName.startupMaster: { - ET.PERMANENT: StartupAlert("Welcome to IQ.Pilot!"), - }, - - EventName.startupNoControl: { - ET.PERMANENT: StartupAlert("Dashcam mode"), - ET.NO_ENTRY: NoEntryAlert("Dashcam mode"), - }, - - EventName.startupNoCar: { - ET.PERMANENT: StartupAlert("IQ.Pilot Dashcam mode: car unrecognized"), - }, - - EventName.startupNoSecOcKey: { - ET.PERMANENT: NormalPermanentAlert("Dashcam Mode", - "TSK Security Key Not Available", - priority=Priority.HIGH), - }, - - EventName.dashcamMode: { - ET.PERMANENT: NormalPermanentAlert("Dashcam Mode", - priority=Priority.LOWEST), - }, - - EventName.invalidLkasSetting: { - ET.PERMANENT: invalid_lkas_setting_alert, - ET.NO_ENTRY: invalid_lkas_setting_no_entry_alert, - }, - - EventName.cruiseMismatch: { - #ET.PERMANENT: ImmediateDisableAlert("openpilot failed to cancel cruise"), - }, - - EventName.carUnrecognized: { - ET.PERMANENT: NormalPermanentAlert("Dashcam Mode", - "Car Unrecognized", - priority=Priority.LOWEST), - }, - - EventName.aeb: { - ET.PERMANENT: Alert( - "BRAKE!", - "Emergency Braking: Risk of Collision", - AlertStatus.critical, AlertSize.full, - Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.none, 2.), - ET.NO_ENTRY: NoEntryAlert("AEB: Risk of Collision"), - }, - - EventName.stockAeb: { - ET.PERMANENT: Alert( - "BRAKE!", - "Stock AEB: Risk of Collision", - AlertStatus.critical, AlertSize.full, - Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.none, 2.), - ET.NO_ENTRY: NoEntryAlert("Stock AEB: Risk of Collision"), - }, - - EventName.stockLkas: { - ET.PERMANENT: Alert( - "Stock LKAS: Lane Departure Detected", - "", - AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.), - ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"), - }, - - EventName.fcw: { - ET.PERMANENT: Alert( - "BRAKE!", - "Risk of Collision", - AlertStatus.critical, AlertSize.full, - Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.warningSoft, 2.), - }, - - EventName.ldw: { - ET.PERMANENT: Alert( - "Lane Departure Detected", - "", - AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.), - }, - EventName.steerTempUnavailableSilent: { - ET.WARNING: Alert( - "Steering Assist Temporarily Unavailable", - "", - AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8), - }, - - EventName.preDriverDistracted: { - ET.PERMANENT: Alert( - "Pay Attention", - "", - AlertStatus.normal, AlertSize.small, - Priority.MID, VisualAlert.none, AudibleAlert.none, .1), - }, - - EventName.promptDriverDistracted: { - ET.PERMANENT: Alert( - "Pay Attention", - "Driver Distracted", - AlertStatus.userPrompt, AlertSize.mid, - Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1), - }, - - EventName.driverDistracted: { - ET.PERMANENT: Alert( - "DISENGAGE IMMEDIATELY", - "Driver Distracted", - AlertStatus.critical, AlertSize.full, - Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1), - }, - - EventName.preDriverUnresponsive: { - ET.PERMANENT: Alert( - "Touch Steering Wheel: No Face Detected", - "", - AlertStatus.normal, AlertSize.small, - Priority.LOW, VisualAlert.steerRequired, AudibleAlert.none, .1), - }, - - EventName.promptDriverUnresponsive: { - ET.PERMANENT: Alert( - "Touch Steering Wheel", - "Driver Unresponsive", - AlertStatus.userPrompt, AlertSize.mid, - Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1), - }, - - EventName.driverUnresponsive: { - ET.PERMANENT: Alert( - "DISENGAGE IMMEDIATELY", - "Driver Unresponsive", - AlertStatus.critical, AlertSize.full, - Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1), - }, - - EventName.manualRestart: { - ET.WARNING: Alert( - "TAKE CONTROL", - "Resume Driving Manually", - AlertStatus.userPrompt, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), - }, - - EventName.resumeRequired: { - ET.WARNING: Alert( - "Press Resume to Exit Standstill", - "", - AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), - }, - - EventName.belowSteerSpeed: { - ET.WARNING: below_steer_speed_alert, - }, - - EventName.preLaneChangeLeft: { - ET.WARNING: Alert( - "Steer Left to Start Lane Change Once Safe", - "", - AlertStatus.normal, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), - }, - - EventName.preLaneChangeRight: { - ET.WARNING: Alert( - "Steer Right to Start Lane Change Once Safe", - "", - AlertStatus.normal, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), - }, - - EventName.laneChangeBlocked: { - ET.WARNING: Alert( - "Car Detected in Blindspot", - "", - AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1), - }, - - EventName.laneChange: { - ET.WARNING: Alert( - "Changing Lanes", - "", - AlertStatus.normal, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), - }, - - EventName.steerSaturated: { - ET.WARNING: Alert( - "Take Control", - "Turn Exceeds Steering Limit", - AlertStatus.userPrompt, AlertSize.mid, - Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.), - }, - - EventName.fanMalfunction: { - ET.PERMANENT: NormalPermanentAlert("Fan Malfunction", "Likely Hardware Issue"), - }, - - EventName.cameraMalfunction: { - ET.PERMANENT: camera_malfunction_alert, - ET.SOFT_DISABLE: soft_disable_alert("Camera Malfunction"), - ET.NO_ENTRY: NoEntryAlert("Camera Malfunction: Reboot Your Device"), - }, - EventName.cameraFrameRate: { - ET.PERMANENT: NormalPermanentAlert("Camera Frame Rate Low", "Reboot your Device"), - ET.SOFT_DISABLE: soft_disable_alert("Camera Frame Rate Low"), - ET.NO_ENTRY: NoEntryAlert("Camera Frame Rate Low: Reboot Your Device"), - }, - EventName.locationdTemporaryError: { - ET.NO_ENTRY: NoEntryAlert("locationd Temporary Error"), - ET.SOFT_DISABLE: soft_disable_alert("locationd Temporary Error"), - }, - - EventName.locationdPermanentError: { - ET.NO_ENTRY: NoEntryAlert("locationd Permanent Error"), - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("locationd Permanent Error"), - ET.PERMANENT: NormalPermanentAlert("locationd Permanent Error"), - }, - - EventName.paramsdTemporaryError: { - ET.NO_ENTRY: paramsd_invalid_alert, - ET.SOFT_DISABLE: soft_disable_alert("paramsd Temporary Error"), - }, - - EventName.paramsdPermanentError: { - ET.NO_ENTRY: NoEntryAlert("paramsd Permanent Error"), - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("paramsd Permanent Error"), - ET.PERMANENT: NormalPermanentAlert("paramsd Permanent Error"), - }, - EventName.pcmEnable: { - ET.ENABLE: EngagementAlert(AudibleAlert.engage), - }, - - EventName.buttonEnable: { - ET.ENABLE: EngagementAlert(AudibleAlert.engage), - }, - - EventName.pcmDisable: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), - }, - - EventName.buttonCancel: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), - ET.NO_ENTRY: NoEntryAlert("Cancel Pressed"), - }, - - EventName.brakeHold: { - ET.WARNING: Alert( - "Press Resume to Exit Brake Hold", - "", - AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), - }, - - EventName.parkBrake: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), - ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"), - }, - - EventName.pedalPressed: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), - ET.NO_ENTRY: NoEntryAlert("Pedal Pressed", - visual_alert=VisualAlert.brakePressed), - }, - - EventName.steerDisengage: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), - ET.NO_ENTRY: NoEntryAlert("Steering Pressed"), - }, - - EventName.preEnableStandstill: { - ET.PRE_ENABLE: Alert( - "Release Brake to Engage", - "", - AlertStatus.normal, AlertSize.small, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .1, creation_delay=1.), - }, - - EventName.gasPressedOverride: { - ET.OVERRIDE_LONGITUDINAL: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .1), - }, - - EventName.steerOverride: { - ET.OVERRIDE_LATERAL: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .1), - }, - - EventName.wrongCarMode: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), - ET.NO_ENTRY: wrong_car_mode_alert, - }, - - EventName.resumeBlocked: { - ET.NO_ENTRY: NoEntryAlert("Press Set to Engage"), - }, - - EventName.wrongCruiseMode: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), - ET.NO_ENTRY: NoEntryAlert("Adaptive Cruise Disabled"), - }, - - EventName.steerTempUnavailable: { - ET.SOFT_DISABLE: soft_disable_alert("Steering Assist Temporarily Unavailable"), - ET.NO_ENTRY: NoEntryAlert("Steering Temporarily Unavailable"), - }, - - EventName.steerTimeLimit: { - ET.SOFT_DISABLE: soft_disable_alert("Vehicle Steering Time Limit"), - ET.NO_ENTRY: NoEntryAlert("Vehicle Steering Time Limit"), - }, - - EventName.outOfSpace: { - ET.PERMANENT: out_of_space_alert, - ET.NO_ENTRY: NoEntryAlert("Out of Storage"), - }, - - EventName.belowEngageSpeed: { - ET.NO_ENTRY: below_engage_speed_alert, - }, - - EventName.sensorDataInvalid: { - ET.PERMANENT: Alert( - "Sensor Data Invalid", - "Possible Hardware Issue", - AlertStatus.normal, AlertSize.mid, - Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=1.), - ET.NO_ENTRY: NoEntryAlert("Sensor Data Invalid"), - ET.SOFT_DISABLE: soft_disable_alert("Sensor Data Invalid"), - }, - - EventName.noGps: { - }, - - EventName.tooDistracted: { - ET.NO_ENTRY: NoEntryAlert("Distraction Level Too High"), - }, - - EventName.excessiveActuation: { - ET.SOFT_DISABLE: soft_disable_alert("Excessive Actuation"), - ET.NO_ENTRY: NoEntryAlert("Excessive Actuation"), - }, - - EventName.overheat: { - ET.PERMANENT: overheat_alert, - ET.SOFT_DISABLE: soft_disable_alert("System Overheated"), - ET.NO_ENTRY: NoEntryAlert("System Overheated"), - }, - - EventName.wrongGear: { - ET.SOFT_DISABLE: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - }, - - EventName.calibrationInvalid: { - ET.PERMANENT: calibration_invalid_alert, - ET.SOFT_DISABLE: soft_disable_alert("Calibration Invalid: Remount Device & Recalibrate"), - ET.NO_ENTRY: NoEntryAlert("Calibration Invalid: Remount Device & Recalibrate"), - }, - - EventName.calibrationIncomplete: { - ET.PERMANENT: calibration_incomplete_alert, - ET.SOFT_DISABLE: soft_disable_alert("Calibration Incomplete"), - ET.NO_ENTRY: NoEntryAlert("Calibration in Progress"), - }, - - EventName.calibrationRecalibrating: { - ET.PERMANENT: calibration_incomplete_alert, - ET.SOFT_DISABLE: soft_disable_alert("Device Remount Detected: Recalibrating"), - ET.NO_ENTRY: NoEntryAlert("Remount Detected: Recalibrating"), - }, - - EventName.doorOpen: { - ET.SOFT_DISABLE: user_soft_disable_alert("Door Open"), - ET.NO_ENTRY: NoEntryAlert("Door Open"), - }, - - EventName.seatbeltNotLatched: { - ET.SOFT_DISABLE: user_soft_disable_alert("Seatbelt Unlatched"), - ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"), - }, - - EventName.espDisabled: { - ET.SOFT_DISABLE: soft_disable_alert("Electronic Stability Control Disabled"), - ET.NO_ENTRY: NoEntryAlert("Electronic Stability Control Disabled"), - }, - - EventName.lowBattery: { - ET.SOFT_DISABLE: soft_disable_alert("Low Battery"), - ET.NO_ENTRY: NoEntryAlert("Low Battery"), - }, - - EventName.commIssue: { - }, - EventName.commIssueAvgFreq: { - }, - - EventName.selfdrivedLagging: { - }, - - EventName.processNotRunning: { - ET.NO_ENTRY: process_not_running_alert, - ET.SOFT_DISABLE: soft_disable_alert("Process Not Running"), - }, - - EventName.radarFault: { - ET.SOFT_DISABLE: soft_disable_alert("Radar Error: Restart the Car"), - ET.NO_ENTRY: NoEntryAlert("Radar Error: Restart the Car"), - }, - - EventName.radarTempUnavailable: { - ET.SOFT_DISABLE: soft_disable_alert("Radar Temporarily Unavailable"), - ET.NO_ENTRY: NoEntryAlert("Radar Temporarily Unavailable"), - }, - - EventName.modeldLagging: { - ET.SOFT_DISABLE: soft_disable_alert("Driving Model Lagging"), - ET.NO_ENTRY: NoEntryAlert("Driving Model Lagging"), - ET.PERMANENT: modeld_lagging_alert, - }, - - EventName.posenetInvalid: { - ET.SOFT_DISABLE: soft_disable_alert("Posenet Speed Invalid"), - ET.NO_ENTRY: posenet_invalid_alert, - }, - - EventName.deviceFalling: { - ET.SOFT_DISABLE: soft_disable_alert("Device Fell Off Mount"), - ET.NO_ENTRY: NoEntryAlert("Device Fell Off Mount"), - }, - - EventName.lowMemory: { - ET.SOFT_DISABLE: soft_disable_alert("Low Memory: Reboot Your Device"), - ET.PERMANENT: low_memory_alert, - ET.NO_ENTRY: NoEntryAlert("Low Memory: Reboot Your Device"), - }, - - EventName.accFaulted: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Cruise Fault: Restart the Car"), - ET.PERMANENT: NormalPermanentAlert("Cruise Fault: Restart the car to engage"), - ET.NO_ENTRY: NoEntryAlert("Cruise Fault: Restart the Car"), - }, - - EventName.cruiseFaultLateralAllowed: { - ET.PERMANENT: NormalPermanentAlert("Cruise Faulted", "Lane Assist remains available", duration=5.0, priority=Priority.LOW), - }, - - EventName.espActive: { - ET.SOFT_DISABLE: soft_disable_alert("Electronic Stability Control Active"), - ET.NO_ENTRY: NoEntryAlert("Electronic Stability Control Active"), - }, - - EventName.controlsMismatch: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch"), - ET.NO_ENTRY: NoEntryAlert("Controls Mismatch"), - }, - - EventName.usbError: { - ET.SOFT_DISABLE: soft_disable_alert("USB Error: Reboot Your Device"), - ET.PERMANENT: NormalPermanentAlert("USB Error: Reboot Your Device"), - ET.NO_ENTRY: NoEntryAlert("USB Error: Reboot Your Device"), - }, - - EventName.canError: { - ET.PERMANENT: Alert( - "CAN Error", - "", - AlertStatus.normal, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.), - }, - - EventName.canBusMissing: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN Bus Disconnected"), - ET.PERMANENT: Alert( - "CAN Bus Disconnected: Likely Faulty Cable", - "", - AlertStatus.normal, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.), - ET.NO_ENTRY: NoEntryAlert("CAN Bus Disconnected: Check Connections"), - }, - - EventName.steerUnavailable: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("LKAS Fault: Restart the Car"), - ET.PERMANENT: NormalPermanentAlert("LKAS Fault: Restart the car to engage"), - ET.NO_ENTRY: NoEntryAlert("LKAS Fault: Restart the Car"), - }, - - EventName.reverseGear: { - ET.PERMANENT: Alert( - "Reverse\nGear", - "", - AlertStatus.normal, AlertSize.full, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), - ET.USER_DISABLE: ImmediateDisableAlert("Reverse Gear"), - ET.NO_ENTRY: NoEntryAlert("Reverse Gear"), - }, - - EventName.cruiseDisabled: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Cruise Is Off"), - }, - - EventName.relayMalfunction: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Harness Relay Malfunction"), - ET.PERMANENT: NormalPermanentAlert("Harness Relay Malfunction", "Check Hardware"), - ET.NO_ENTRY: NoEntryAlert("Harness Relay Malfunction"), - }, - - EventName.speedTooLow: { - ET.IMMEDIATE_DISABLE: Alert( - "IQ.Pilot Canceled", - "Speed too low", - AlertStatus.normal, AlertSize.mid, - Priority.HIGH, VisualAlert.none, AudibleAlert.disengage, 3.), - }, - - EventName.speedTooHigh: { - ET.WARNING: Alert( - "Speed Too High", - "Model uncertain at this speed", - AlertStatus.userPrompt, AlertSize.mid, - Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 4.), - ET.NO_ENTRY: NoEntryAlert("Slow down to engage"), - }, - - EventName.vehicleSensorsInvalid: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Vehicle Sensors Invalid"), - ET.PERMANENT: NormalPermanentAlert("Vehicle Sensors Calibrating", "Drive to Calibrate"), - ET.NO_ENTRY: NoEntryAlert("Vehicle Sensors Calibrating"), - }, - - EventName.personalityChanged: { - ET.WARNING: personality_changed_alert, - }, - - EventName.userBookmark: { - ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5), - }, - - EventName.audioFeedback: { - ET.PERMANENT: audio_feedback_alert, - }, -} - - -if HARDWARE.get_device_type() == 'mici': - EVENTS.update({ - EventName.preDriverDistracted: { - ET.PERMANENT: Alert( - "Pay Attention", - "", - AlertStatus.normal, AlertSize.small, - Priority.MID, VisualAlert.none, AudibleAlert.none, 2), - }, - EventName.promptDriverDistracted: { - ET.PERMANENT: Alert( - "Pay Attention", - "Driver Distracted", - AlertStatus.userPrompt, AlertSize.mid, - Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, 1), - }, - EventName.resumeRequired: { - ET.WARNING: Alert( - "Press Resume", - "", - AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), - }, - EventName.preLaneChangeLeft: { - ET.WARNING: Alert( - "Steer Left", - "Confirm Lane Change", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), - }, - EventName.preLaneChangeRight: { - ET.WARNING: Alert( - "Steer Right", - "Confirm Lane Change", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), - }, - EventName.laneChangeBlocked: { - ET.WARNING: Alert( - "Car in Blindspot", - "", - AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1), - }, - EventName.steerSaturated: { - ET.WARNING: Alert( - "take control", - "turn exceeds limit", - AlertStatus.userPrompt, AlertSize.mid, - Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.), - }, - EventName.calibrationIncomplete: { - ET.PERMANENT: calibration_incomplete_alert, - ET.SOFT_DISABLE: soft_disable_alert("Calibration Incomplete"), - ET.NO_ENTRY: NoEntryAlert("Calibrating"), - }, - EventName.reverseGear: { - ET.PERMANENT: Alert( - "Reverse", - "", - AlertStatus.normal, AlertSize.full, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), - ET.USER_DISABLE: ImmediateDisableAlert("Reverse"), - ET.NO_ENTRY: NoEntryAlert("Reverse"), - }, - }) - - -if __name__ == '__main__': - from iqpilot.cereal.services import SERVICE_LIST - from collections import defaultdict - - event_names = {v: k for k, v in EventName.schema.enumerants.items()} - alerts_by_type: dict[str, dict[Priority, list[str]]] = defaultdict(lambda: defaultdict(list)) - - CP = car.CarParams.new_message() - CS = car.CarState.new_message() - sm = messaging.SubMaster(list(SERVICE_LIST.keys())) - - for i, alerts in EVENTS.items(): - for et, alert in alerts.items(): - if callable(alert): - alert = alert(CP, CS, sm, False, 1, log.LongitudinalPersonality.standard) - alerts_by_type[et][alert.priority].append(event_names[i]) - - all_alerts: dict[str, list[tuple[Priority, list[str]]]] = {} - for et, priority_alerts in alerts_by_type.items(): - all_alerts[et] = sorted(priority_alerts.items(), key=lambda x: x[0], reverse=True) - - for status, evs in sorted(all_alerts.items(), key=lambda x: x[0]): - print(f"**** {status} ****") - for p, alert_list in evs: - print(f" {repr(p)}:") - print(" ", ', '.join(alert_list), "\n") diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/feedbackd.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/feedbackd.py deleted file mode 100644 index 0412edd3..00000000 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/feedbackd.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -import iqpilot.cereal.messaging as messaging -from iqpilot.common.params import Params -from iqpilot.common.swaglog import cloudlog -from iqpilot.cereal import car -from iqpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER - -FEEDBACK_MAX_DURATION = 10.0 -ButtonType = car.CarState.ButtonEvent.Type - - -def main(): - params = Params() - pm = messaging.PubMaster(['userBookmark', 'audioFeedback']) - sm = messaging.SubMaster(['rawAudioData', 'bookmarkButton']) - should_record_audio = False - block_num = 0 - waiting_for_release = False - early_stop_triggered = False - - while True: - sm.update() - should_send_bookmark = False - - if False and sm.updated['carState'] and sm['carState'].canValid and not sm['iqState'].aol.available: - for be in sm['carState'].buttonEvents: - if be.type == ButtonType.lkas: - if be.pressed: - if not should_record_audio: - if params.get_bool("RecordAudioFeedback"): - should_record_audio = True - block_num = 0 - waiting_for_release = False - early_stop_triggered = False - cloudlog.info("LKAS button pressed - starting 10-second audio feedback") - else: - should_send_bookmark = True - cloudlog.info("LKAS button pressed - bookmarking") - elif should_record_audio and not waiting_for_release: - waiting_for_release = True - elif waiting_for_release: - waiting_for_release = False - early_stop_triggered = True - cloudlog.info("LKAS button released - ending recording early") - - if should_record_audio and sm.updated['rawAudioData']: - raw_audio = sm['rawAudioData'] - msg = messaging.new_message('audioFeedback', valid=True) - msg.audioFeedback.audio.data = raw_audio.data - msg.audioFeedback.audio.sampleRate = raw_audio.sampleRate - msg.audioFeedback.blockNum = block_num - block_num += 1 - if (block_num * SAMPLE_BUFFER / SAMPLE_RATE) >= FEEDBACK_MAX_DURATION or early_stop_triggered: - should_send_bookmark = True - should_record_audio = False - early_stop_triggered = False - cloudlog.info("10-second recording completed or second button press - stopping audio feedback") - pm.send('audioFeedback', msg) - - if sm.updated['bookmarkButton']: - cloudlog.info("Bookmark button pressed!") - should_send_bookmark = True - - if should_send_bookmark: - msg = messaging.new_message('userBookmark', valid=True) - pm.send('userBookmark', msg) - - -if __name__ == '__main__': - main() diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py index 48a34ea4..c38e33b9 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py @@ -125,16 +125,32 @@ class Amplifier: print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") def set_configs(self, configs: list[AmpConfig]) -> bool: + import threading + tries = 15 backoff = 0. for i in range(tries): - try: - self._set_configs(configs) - return True - except OSError: + result = [None] + error = [None] + def _worker(): + try: + self._set_configs(configs) + result[0] = True + except OSError as e: + error[0] = e + t = threading.Thread(target=_worker, daemon=True) + t.start() + t.join(timeout=2) + if t.is_alive(): backoff += 0.1 time.sleep(backoff) - print(f"Failed to set amp config, {tries - i - 1} retries left") + print(f"Failed to set amp config (timeout), {tries - i - 1} retries left") + continue + if result[0]: + return True + backoff += 0.1 + time.sleep(backoff) + print(f"Failed to set amp config, {tries - i - 1} retries left") return False def set_global_shutdown(self, amp_disabled: bool) -> bool: diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py index 2f71ed63..2506fb9e 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py @@ -469,8 +469,10 @@ class Tici(HardwareBase): if self.amplifier is not None: self.amplifier.initialize_configuration(self.get_device_type()) - # Allow hardwared to write engagement status to kmsg - os.system("sudo chmod a+w /dev/kmsg") + try: + subprocess.run(["sudo", "-n", "chmod", "a+w", "/dev/kmsg"], timeout=2, capture_output=True, check=False) + except Exception: + pass # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL gpio_init(GPIO.SOM_ST_IO, True) @@ -514,10 +516,10 @@ class Tici(HardwareBase): affine_irq(3, "xhci-hcd:usb1") try: pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() - subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) - subprocess.call(["sudo", "taskset", "-pc", "3", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessException as e: - print(str(e)) + subprocess.run(["sudo", "-n", "chrt", "-f", "-p", "1", pid], timeout=2, capture_output=True, check=False) + subprocess.run(["sudo", "-n", "taskset", "-pc", "3", pid], timeout=2, capture_output=True, check=False) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + pass def configure_modem(self): from iqpilot.common.params import Params diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/manager/process.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/manager/process.py index 31434b81..17d97196 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/manager/process.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/manager/process.py @@ -34,36 +34,33 @@ except ModuleNotFoundError: return _VERIFIED_RUNNER_PATH -def launcher(proc: str, name: str) -> None: +def launcher(proc: str, name: str, env: dict[str, str] | None = None) -> None: try: - # import the process + if env: + os.environ.update(env) + mod = importlib.import_module(proc) - # rename the process setproctitle(proc) - # create new context since we forked messaging.reset_context() - # add daemon name tag to logs cloudlog.bind(daemon=name) sentry.set_tag("daemon", name) - # exec the process mod.main() except KeyboardInterrupt: cloudlog.warning(f"child {proc} got SIGINT") except Exception: - # can't install the crash handler because sys.excepthook doesn't play nice - # with threads, so catch it here. sentry.capture_exception() raise -def nativelauncher(pargs: list[str], cwd: str, name: str) -> None: +def nativelauncher(pargs: list[str], cwd: str, name: str, env: dict[str, str] | None = None) -> None: + if env: + os.environ.update(env) os.environ['MANAGER_DAEMON'] = name - # exec the process os.chdir(cwd) os.environ['PWD'] = cwd os.execvp(pargs[0], pargs) @@ -162,7 +159,7 @@ class ManagerProcess(ABC): class NativeProcess(ManagerProcess): - def __init__(self, name, cwd, cmdline, should_run, enabled=True, sigkill=False, restart_if_crash=False): + def __init__(self, name, cwd, cmdline, should_run, enabled=True, sigkill=False, restart_if_crash=False, env=None): self.name = name self.cwd = cwd self.cmdline = cmdline @@ -171,12 +168,12 @@ class NativeProcess(ManagerProcess): self.sigkill = sigkill self.launcher = nativelauncher self.restart_if_crash = restart_if_crash + self.env = env def prepare(self) -> None: pass def start(self) -> None: - # In case we only tried a non blocking stop we need to stop it before restarting if self.shutting_down: self.stop() @@ -185,7 +182,7 @@ class NativeProcess(ManagerProcess): cwd = os.path.join(BASEDIR, self.cwd) cloudlog.info(f"starting process {self.name}") - self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name)) + self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name, self.env)) self.proc.start() self.shutting_down = False @@ -217,7 +214,7 @@ def _normalize_bundle_modes(bundle: str) -> None: class BundleProcess(NativeProcess): - def __init__(self, name, bundle, entry, should_run, enabled=True, sigkill=False, restart_if_crash=False): + def __init__(self, name, bundle, entry, should_run, enabled=True, sigkill=False, restart_if_crash=False, env=None): self.bundle = bundle self.entry = entry runner_path = preferred_runner_path() @@ -237,6 +234,7 @@ class BundleProcess(NativeProcess): enabled=enabled, sigkill=sigkill, restart_if_crash=restart_if_crash, + env=env, ) def start(self) -> None: @@ -249,7 +247,7 @@ class BundleProcess(NativeProcess): class PythonProcess(ManagerProcess): - def __init__(self, name, module, should_run, enabled=True, sigkill=False, restart_if_crash=False): + def __init__(self, name, module, should_run, enabled=True, sigkill=False, restart_if_crash=False, env=None): self.name = name self.module = module self.should_run = should_run @@ -257,6 +255,7 @@ class PythonProcess(ManagerProcess): self.sigkill = sigkill self.launcher = launcher self.restart_if_crash = restart_if_crash + self.env = env def prepare(self) -> None: if self.enabled: @@ -264,7 +263,6 @@ class PythonProcess(ManagerProcess): importlib.import_module(self.module) def start(self) -> None: - # In case we only tried a non blocking stop we need to stop it before restarting if self.shutting_down: self.stop() @@ -272,7 +270,7 @@ class PythonProcess(ManagerProcess): return cloudlog.info(f"starting python {self.module}") - self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name)) + self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name, self.env)) self.proc.start() self.shutting_down = False diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/manager/process_config.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/manager/process_config.py index 6908a78d..d5a8b37e 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/manager/process_config.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/manager/process_config.py @@ -166,6 +166,8 @@ def big_model_enabled(started, params, CP: car.CarParams) -> bool: def hephaestus_ready_shim(started, params, CP: car.CarParams) -> bool: return hephaestus_ready(params) +_IQCORE_ARMED_ENV = {"IQCORE_ARMED": "1"} + def not_low_power(started: bool, params: Params, CP: car.CarParams) -> bool: # FastSleep deep standby: heavy processes are shed offroad while DevicePowerState is low_power return started or params.get("DevicePowerState") != "low_power" @@ -185,7 +187,8 @@ def and_(*fns): procs = [ NativeProcess("loggerd", "iqpilot/system/loggerd", ["./loggerd"], logging), NativeProcess("encoderd", "iqpilot/system/loggerd", ["./encoderd"], only_onroad), - NativeProcess("stream_encoderd", "iqpilot/system/loggerd", ["./encoderd", "--stream"], or_(notcar, livestream, sentry_recording)), + NativeProcess("stream_encoderd", "iqpilot/system/loggerd", ["./encoderd", "--stream"], or_(notcar, livestream, sentry_recording), + restart_if_crash=True), PythonProcess("logmessaged", "iqpilot.system.logmessaged", always_run, restart_if_crash=True), NativeProcess("camerad", "iqpilot/system/camerad", ["./camerad"], or_(driverview, livestream, sentry_recording), restart_if_crash=True), @@ -194,13 +197,11 @@ procs = [ PythonProcess("micd", "iqpilot.system.micd", or_(iscar, livestream)), PythonProcess("timed", "iqpilot.system.timed", always_run, enabled=not PC), PythonProcess("wireguardd", "iqpilot.system.wireguard.wireguardd", wireguard_installed, enabled=TICI, restart_if_crash=True), - BundleProcess("k3wgd", "iqpilot_hephaestusd_private", "iqpilot_private.konn3kt.hephaestus.k3wgd", always_run, enabled=TICI, - restart_if_crash=True), BundleProcess("iqsentryd", "iqpilot_hephaestusd_private", "iqpilot_private.konn3kt.hephaestus.iqsentryd", sentry_enabled, enabled=TICI, restart_if_crash=True), BundleProcess("navassistd", "iqpilot_navassist_private", "iqpilot_private.navassist.daemon", nav_assist, enabled=TICI, restart_if_crash=True), - PythonProcess("dmonitoringmodeld", "iqpilot.selfdrive.dmonitoringmodeld.dmonitoringmodeld", driver_monitoring, enabled=not PC), + PythonProcess("dmonitoringmodeld", "iqpilot.selfdrive.dmonitoringmodeld.dmonitoringmodeld", driver_monitoring, enabled=not PC, env=_IQCORE_ARMED_ENV), PythonProcess("sensord", "iqpilot.system.sensord.sensord", only_onroad, enabled=not PC), PythonProcess("ui", "iqpilot.selfdrive.ui.ui", not_low_power, restart_if_crash=True), @@ -235,7 +236,7 @@ procs = [ # debug procs NativeProcess("bridge", "iqpilot/cereal/messaging", ["./bridge"], notcar), - PythonProcess("webrtcd", "iqpilot.system.webrtc.webrtcd", or_(iscar, livestream)), + PythonProcess("webrtcd", "iqpilot.system.webrtc.webrtcd", or_(iscar, livestream), restart_if_crash=True), PythonProcess("canlived", "iqpilot.konn3kt.canlive.canlived", canlive), ] @@ -243,16 +244,16 @@ procs = [ procs += [ # Models BundleProcess("models_manager", "iqpilot_model_selector_private", "iqpilot_private.models.manager", and_(only_offroad, not_low_power), restart_if_crash=True), - NativeProcess("iqmodeld", "iqpilot/selfdrive/iqmodeld", ["./iqmodeld"], and_(only_onroad, is_tinygrad_model), restart_if_crash=True), + NativeProcess("iqmodeld", "iqpilot/selfdrive/iqmodeld", ["./iqmodeld"], and_(only_onroad, is_tinygrad_model), restart_if_crash=True, env=_IQCORE_ARMED_ENV), # 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), + and_(only_onroad, and_(is_tinygrad_model, emac_enabled)), restart_if_crash=True, env=_IQCORE_ARMED_ENV), PythonProcess("iqegpumodeld", "iqpilot.selfdrive.iqmodeld.iqegpumodeld", - and_(only_onroad, and_(is_tinygrad_model, egpu_enabled)), restart_if_crash=True), + and_(only_onroad, and_(is_tinygrad_model, egpu_enabled)), restart_if_crash=True, env=_IQCORE_ARMED_ENV), PythonProcess("egpu_prefetch", "iqpilot.selfdrive.iqmodeld.egpu_prefetch", and_(only_offroad, and_(is_tinygrad_model, egpu_prefetch_enabled)), restart_if_crash=True), diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/micd.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/micd.py deleted file mode 100644 index 5c8d0559..00000000 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/micd.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 -import numpy as np -import os -import time -from functools import cache -import threading - -from iqpilot.cereal import messaging -from iqpilot.common.params import Params -from iqpilot.common.realtime import Ratekeeper -from iqpilot.common.utils import retry -from iqpilot.common.swaglog import cloudlog - -RATE = 10 -FFT_SAMPLES = 1600 # 100ms -REFERENCE_SPL = 2e-5 # newtons/m^2 -SAMPLE_RATE = 16000 -SAMPLE_BUFFER = 800 # 50ms - - -@cache -def get_a_weighting_filter(): - # Calculate the A-weighting filter - # https://en.wikipedia.org/wiki/A-weighting - freqs = np.fft.fftfreq(FFT_SAMPLES, d=1 / SAMPLE_RATE) - A = 12194 ** 2 * freqs ** 4 / ((freqs ** 2 + 20.6 ** 2) * (freqs ** 2 + 12194 ** 2) * np.sqrt((freqs ** 2 + 107.7 ** 2) * (freqs ** 2 + 737.9 ** 2))) - return A / np.max(A) - - -def calculate_spl(measurements): - # https://www.engineeringtoolbox.com/sound-pressure-d_711.html - sound_pressure = np.sqrt(np.mean(measurements ** 2)) # RMS of amplitudes - if sound_pressure > 0: - sound_pressure_level = 20 * np.log10(sound_pressure / REFERENCE_SPL) # dB - else: - sound_pressure_level = 0 - return sound_pressure, sound_pressure_level - - -def apply_a_weighting(measurements: np.ndarray) -> np.ndarray: - # Generate a Hanning window of the same length as the audio measurements - measurements_windowed = measurements * np.hanning(len(measurements)) - - # Apply the A-weighting filter to the signal - return np.abs(np.fft.ifft(np.fft.fft(measurements_windowed) * get_a_weighting_filter())) - - -class Mic: - def __init__(self): - self.rk = Ratekeeper(RATE) - self.pm = messaging.PubMaster(['soundPressure', 'rawAudioData']) - self.params = Params() - - self.measurements = np.empty(0) - - self.sound_pressure = 0 - self.sound_pressure_weighted = 0 - self.sound_pressure_level_weighted = 0 - - self.lock = threading.Lock() - self.callback_count = 0 - self.last_audio_rms = 0.0 - self.last_audio_peak = 0.0 - self.last_device = None - self.last_status = None - - def update(self): - with self.lock: - sound_pressure = self.sound_pressure - sound_pressure_weighted = self.sound_pressure_weighted - sound_pressure_level_weighted = self.sound_pressure_level_weighted - callback_count = self.callback_count - audio_rms = self.last_audio_rms - audio_peak = self.last_audio_peak - device_name = self.last_device - status = self.last_status - - msg = messaging.new_message('soundPressure', valid=True) - msg.soundPressure.soundPressure = float(sound_pressure) - msg.soundPressure.soundPressureWeighted = float(sound_pressure_weighted) - msg.soundPressure.soundPressureWeightedDb = float(sound_pressure_level_weighted) - - self.pm.send('soundPressure', msg) - if callback_count % RATE == 0: - cloudlog.info( - f"micd health: callbacks={callback_count} rms={audio_rms:.6f} peak={audio_peak:.6f} " - f"device={device_name} status={status!r} livestream={self.params.get_bool('IsLiveStreaming')}" - ) - self.rk.keep_time() - - def callback(self, indata, frames, time, status): - """ - Using amplitude measurements, calculate an uncalibrated sound pressure and sound pressure level. - Then apply A-weighting to the raw amplitudes and run the same calculations again. - - Logged A-weighted equivalents are rough approximations of the human-perceived loudness. - """ - msg = messaging.new_message('rawAudioData', valid=True) - audio_data_int_16 = (indata[:, 0] * 32767).astype(np.int16) - msg.rawAudioData.data = audio_data_int_16.tobytes() - msg.rawAudioData.sampleRate = SAMPLE_RATE - self.pm.send('rawAudioData', msg) - - with self.lock: - self.callback_count += 1 - self.last_audio_rms = float(np.sqrt(np.mean(np.square(indata[:, 0])))) - self.last_audio_peak = float(np.max(np.abs(indata[:, 0]))) - self.last_status = str(status) if status else None - self.measurements = np.concatenate((self.measurements, indata[:, 0])) - - while self.measurements.size >= FFT_SAMPLES: - measurements = self.measurements[:FFT_SAMPLES] - - self.sound_pressure, _ = calculate_spl(measurements) - measurements_weighted = apply_a_weighting(measurements) - self.sound_pressure_weighted, self.sound_pressure_level_weighted = calculate_spl(measurements_weighted) - - self.measurements = self.measurements[FFT_SAMPLES:] - - @retry(attempts=10, delay=3) - def get_stream(self, sd): - # reload sounddevice to reinitialize portaudio - sd._terminate() - sd._initialize() - requested_device = os.environ.get("MICD_DEVICE") - device = int(requested_device) if requested_device is not None else None - return sd.InputStream(channels=1, samplerate=SAMPLE_RATE, callback=self.callback, blocksize=SAMPLE_BUFFER, device=device) - - def micd_thread(self): - # sounddevice must be imported after forking processes - import sounddevice as sd - - device = sd.default.device - if os.environ.get("MICD_DEVICE") is not None: - device = int(os.environ["MICD_DEVICE"]) - sd.default.device = (device, device) - self.last_device = f"{device}: {sd.query_devices(device)['name']}" if isinstance(device, int) else str(device) - cloudlog.info(f"micd selecting input device {self.last_device}") - - while True: - try: - with self.get_stream(sd) as stream: - cloudlog.info(f"micd stream started: {stream.samplerate=} {stream.channels=} {stream.dtype=} {stream.device=}, {stream.blocksize=}") - while True: - self.update() - except Exception: - # Some A1s wedge the audio DSP (ALSA EINVAL / ADSP_EFAILED until reboot). Dying here - # crash-loops the process and selfdrived raises a takeover alert mid-drive over a - # microphone - stay alive and keep retrying instead; recovers if the DSP comes back. - cloudlog.exception("micd: audio stream unavailable, retrying") - time.sleep(10) - - -def main(): - mic = Mic() - mic.micd_thread() - - -if __name__ == "__main__": - main() diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/ui/lib/wifi_manager.py b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/ui/lib/wifi_manager.py index a52cdab8..ae32de15 100644 --- a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/ui/lib/wifi_manager.py +++ b/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/system/ui/lib/wifi_manager.py @@ -177,7 +177,7 @@ class WifiManager: # State self._connecting_to_ssid: str = "" self._ipv4_address: str = "" - self._current_network_metered: MeteredType = MeteredType.UNKNOWN + self._current_network_metered: MeteredType = MeteredType.NO self._tethering_password: str = "" self._ipv4_forward = False self._tethering_transition = False @@ -568,6 +568,7 @@ class WifiManager: 'type': ('s', '802-11-wireless'), 'uuid': ('s', str(uuid.uuid4())), 'id': ('s', f'iqpilot connection {ssid}'), + 'metered': ('i', int(MeteredType.NO)), 'autoconnect-retries': ('i', 0), }, '802-11-wireless': { @@ -802,7 +803,7 @@ class WifiManager: cloudlog.warning("No WiFi device found") return - self._current_network_metered = MeteredType.UNKNOWN + self._current_network_metered = MeteredType.NO for active_conn in self._get_active_connections(): conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1] @@ -821,8 +822,14 @@ class WifiManager: metered_prop = settings['connection'].get('metered', ('i', 0))[1] if metered_prop == MeteredType.YES: self._current_network_metered = MeteredType.YES - elif metered_prop == MeteredType.NO: + else: self._current_network_metered = MeteredType.NO + if metered_prop != MeteredType.NO: + settings['connection']['metered'] = ('i', int(MeteredType.NO)) + settings_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + reply = self._router_main.send_and_get_reply(new_method_call(settings_addr, 'Update', 'a{sa{sv}}', (settings,))) + if reply.header.message_type == MessageType.error: + cloudlog.warning(f'Failed to default Wi-Fi connection to unmetered: {reply}') return def set_current_network_metered(self, metered: MeteredType): diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 4f1e13fe..708c3562 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 2483f835..9343868a Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index e9048d77..c4de6837 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 78010022..01756052 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index e69b12e4..3f6b662b Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 18f45c78..fe14357b Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index b0ac025e..0f9eee0c Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip index 53a8424c..db9c9ffb 100644 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 5efa11d6..c7c2849a Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_connection_parameters.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_connection_parameters.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index fe55babe..6f511566 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_connection_parameters.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_connection_parameters.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index be25b936..1ad12c26 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 04398ed6..e957340e Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 0c1d4991..2ecb5b40 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index f5f4f6c9..fb13f5b3 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index bd376df0..34709087 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/driving_settings.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/driving_settings.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 1b637aca..a042db3b Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/driving_settings.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/driving_settings.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 8211b261..b807c376 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/iqsentryd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/iqsentryd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index a5695239..7a93c8ab Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/iqsentryd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/iqsentryd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/iqsentryvisiond.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/iqsentryvisiond.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index c7234da0..4d56b10b Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/iqsentryvisiond.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/iqsentryvisiond.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/k3wgd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/k3wgd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 6e65f8db..39f3fb6f Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/k3wgd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/k3wgd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index cd4baf5c..30528cc7 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index eab513bb..34288596 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 2c67e071..6bac9663 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_body.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_body.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 57e74985..c294e80f Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_body.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_body.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_imu.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_imu.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 8e91068b..3b5c0054 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_imu.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_imu.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_media.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_media.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 7dc6ffd4..0e9a325d Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_media.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_media.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_policy.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_policy.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 990f9bed..e454658e Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_policy.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_policy.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_upload.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_upload.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 7e744602..41273130 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_upload.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/sentry_upload.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/session_bridge.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/session_bridge.cpython-312-aarch64-linux-gnu.so new file mode 100644 index 00000000..fe54cde2 Binary files /dev/null and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/session_bridge.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 12f40270..5d37480b Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 57ba3ffe..69adbd5c Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/locationd/__init__.py b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/registration/__init__.py similarity index 100% rename from artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/locationd/__init__.py rename to artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/registration/__init__.py diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/registration/reg_core.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/registration/reg_core.cpython-312-aarch64-linux-gnu.so new file mode 100644 index 00000000..43349d79 Binary files /dev/null and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/registration/reg_core.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/telemetry/iqtelemetryd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/telemetry/iqtelemetryd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 2d7c379d..e96b93e3 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/telemetry/iqtelemetryd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/telemetry/iqtelemetryd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 30f7583d..178d4773 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_iqvd_private/manifest.json b/artifacts/iqpilot_iqvd_private/manifest.json index 9a480028..d648e1ed 100644 --- a/artifacts/iqpilot_iqvd_private/manifest.json +++ b/artifacts/iqpilot_iqvd_private/manifest.json @@ -1,4 +1,5 @@ { + "bundle_version": 1792551438, "python/_iqclosure/_vendored.json": { "mode": 420, "sha256": "b5eb6863ec038e9d3dbd166ebd4ebce4e1bfdba80e399cc818660daa39121105", @@ -41,8 +42,8 @@ }, "python/_iqclosure/iqpilot/common/utils.py": { "mode": 420, - "sha256": "103fa6c24f8b963576c482a34d7ecb020168fd3e85156493ae4432ee3b7b2c06", - "size": 5159 + "sha256": "c39b89a1ab34511dfa5f9efac783fde0e6d616963a416cf472bd1f604b57f84d", + "size": 5892 }, "python/_iqclosure/iqpilot/system/__init__.py": { "mode": 420, @@ -81,13 +82,13 @@ }, "python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py": { "mode": 420, - "sha256": "98f46f8705259b00efae7274186b4e68201aad467a19de4f16d5f850d10ab0b5", - "size": 7585 + "sha256": "38e521356c56ac85f35543b9457d36639e55ed2ac916f30ea3b86948e806541f", + "size": 8016 }, "python/_iqclosure/iqpilot/system/hardware/tici/hardware.py": { "mode": 420, - "sha256": "e8eae4e8a75e1751525471cf54a8555b7bfd7f08ad84bf2e75642b5745f222db", - "size": 25601 + "sha256": "da5ecb280f7af14169903260dc941e3bfac033c93b184cb0abea0f1494c34582", + "size": 25731 }, "python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py": { "mode": 420, @@ -109,25 +110,15 @@ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0 }, - "python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "ebe6862e0d71e64c708adcf0b30980d68a881fae903270451513c3c36ac19cec", - "size": 67640 - }, "python/iqpilot_private/iqvd/__init__.py": { "mode": 420, "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0 }, "python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "35a3ff4fc87a93e7b83b5abefdb3b25aff40f55c57e1d6c94b706580da97472a", - "size": 201088 - }, - "python/iqpilot_private/iqvd/offload/__init__.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "4aa8810cb7deb908a17727fe9070caa6e89fad22ace74ea66cab52cf3a9d8ee3", - "size": 67640 + "mode": 416, + "sha256": "56ce5ad31b607b188eb8cf8e7b512953484420d2cb7a27568b78e139cc92df74", + "size": 1130920 }, "python/iqpilot_private/iqvd/offload/__init__.py": { "mode": 420, @@ -135,29 +126,29 @@ "size": 0 }, "python/iqpilot_private/iqvd/offload/client.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "2819b633eada9ca81ebf5c2b70b09c59e75fc39ad5fe1d3354e6937af2ebf671", - "size": 135248 + "mode": 416, + "sha256": "fdd2f2f8d6008780de923a81dfd2a855c6974f8630250e368a278f677d966fd7", + "size": 1065224 }, "python/iqpilot_private/iqvd/offload/geometry.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "24520726197b90091a7be02751425a2d443a8030d9fdcaf2cbf9753cb54712e1", - "size": 68232 + "mode": 416, + "sha256": "ea45fc96d4f8ae45961799d7ed57f927fb16106c9db8307db6d395dac3d0b509", + "size": 1065224 }, "python/iqpilot_private/iqvd/offload/perception.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "4743a191fc66df0ab47c912dd91f66db2a52eb5602bbeacbe958045477f97074", - "size": 135928 + "mode": 416, + "sha256": "3e8f2c8fda7666847d407daf8cc8ca6a7005cb9f6fd870b860ddf1f53060bbd5", + "size": 1130840 }, "python/iqpilot_private/iqvd/offload/protocol.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "c960f9ea3d8d418979173f2f38af254cbe5f7215bc730ab32d00867c972e2b78", - "size": 68120 + "mode": 416, + "sha256": "c9621673c8d4f1dd32dace6a8c4e95f51cadc21d683208254474e9f080b5c4ac", + "size": 1130904 }, "python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "40009c25a3870406c2d525b89f9dd1d3a4a2b5d87167aaaf40539f0a04e2d154", - "size": 270104 + "mode": 416, + "sha256": "8d8e37fcc2f64c3d6c198626479d3cca36dd959e568ab64355c65362fc5ff360", + "size": 1196344 }, "runtime": { "entries": { @@ -168,13 +159,11 @@ } }, "signatures": { - "python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so": "O0ruQnmy0kYo9kDP497IqO1qv8vbu7MxQPJ3NYOtzE7wGMVFXNPY7XYsuwLSeMWpvyulR84VyESKdw1XxAJ+Cg==", - "python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so": "eC8ltU6gkLw2jtoMNaE1rlWZQvR41aUJC2UwRXJzpzPzMdwvDWwmQ6pjdM+NMrlAH9+hTfspmzl/I+41hl+EDg==", - "python/iqpilot_private/iqvd/offload/__init__.cpython-312-aarch64-linux-gnu.so": "51199f2smtzQKSMN2O1P2RqIKxcTusT/tHSImFHuluWr9Ln9EeoY52tFkY8o8ghKvRAwyRKhd8aBtXVWDk5lDw==", - "python/iqpilot_private/iqvd/offload/client.cpython-312-aarch64-linux-gnu.so": "rHFp8fRDzB4p8BALQU3H8lIdgfWiDZB9hLhaPTt7VxBRa/8LqYb0J2Nn9rPAVKlE/Ge+CpWsDvjZRSvI1gGxAw==", - "python/iqpilot_private/iqvd/offload/geometry.cpython-312-aarch64-linux-gnu.so": "v30o1QIBwUNtguaChR0gIvZc31sxsm5sggTvwFj5fPc15Gdn2hV4xSpvwny2AzhgXMIsQRodoP/beMUJUR/lBA==", - "python/iqpilot_private/iqvd/offload/perception.cpython-312-aarch64-linux-gnu.so": "W6ikO5pIopJ6hF5/7TH8wscx3SpLdnzbTvqqiaUJnph+y3VazBXRppg/+hW5cMf1aTUSKXQvMEPb9eM5VHMcAQ==", - "python/iqpilot_private/iqvd/offload/protocol.cpython-312-aarch64-linux-gnu.so": "JUDqK6O2H7/nSx7uLBetzvzenw5fQepaxeAnKVuxL7K5T8jUhWsTCVxbwtzTB/VajwBcQcLUUqv4qxtbr6lLBQ==", - "python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so": "UyYQcqQ9VPw8Met6ec2peNVz19jSbo3mHPygM8NbKQ4ghc134lpvco1J0paNKx4m1O4wf0jxH2wgjn/kfmwqDg==" + "python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so": "nQeUBh5VBNq1B4ygfO7q78KCI9kadSY/PlCco1Dpm9+VwIIsnBCRsr1rLN6wdb4pKACizXei1tuquFnKfDklCg==", + "python/iqpilot_private/iqvd/offload/client.cpython-312-aarch64-linux-gnu.so": "VHHNoJPyndUJRKnkl7gCvmBa03UG8zLmqBL5hChQkPrwIxGTJVy6VIpQirxD5d/8tCUkolL9MtTudyzVcr5oAw==", + "python/iqpilot_private/iqvd/offload/geometry.cpython-312-aarch64-linux-gnu.so": "4pN2rWVV39iTQ4NCSotKGvdcjsyv2nebQiiWVhz8eg/i0TYT+rglCiK/K/CRvRz6TtN8p+gvO6kvTNX8qhsuDw==", + "python/iqpilot_private/iqvd/offload/perception.cpython-312-aarch64-linux-gnu.so": "Xeu//0EZs72tK76JU1miYeLSSxHu+jGZGK0gZfB8fosX3o4LaECdoMFpxCmvsiWNiWpcHvyU1l80N0ZIMoyiAg==", + "python/iqpilot_private/iqvd/offload/protocol.cpython-312-aarch64-linux-gnu.so": "DUHqu0XBxEFlZxMJ+haVhC/J8rS9S5QYoIx5MOOh9CxExSwRQalCnWJ5nUmyS66VNLs2LRXa5XUPZQ+wqxDtDw==", + "python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so": "P/Kyh/f8spyn1p/Rq17lKu2IfYv5Y1boFJw6UEXB0vD13ra69GLShOoqDs4L7RR8S2QByFuCfLm1ifmqkuiPCQ==" } } diff --git a/artifacts/iqpilot_iqvd_private/manifest.json.sig b/artifacts/iqpilot_iqvd_private/manifest.json.sig index fd71d4c6..90266fe1 100644 --- a/artifacts/iqpilot_iqvd_private/manifest.json.sig +++ b/artifacts/iqpilot_iqvd_private/manifest.json.sig @@ -1 +1 @@ -4i8PuAK3YOHd51HjPmUofy1Va2zgK8QsJWSY9zkROZpkNBOudH+xv/3nizVt+3v74p9lPP9xzMo+RpV72ZFMBg== +RASbMiHfyPugPQ0ry9f9syHR2qjdE1JQ8X8rcFTMwuJjgvQewwTyclwZ2i54V50lvhvi60RpHtfvXETO4UjSDQ== diff --git a/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/common/utils.py b/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/common/utils.py index a80270bb..005d75b8 100644 --- a/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/common/utils.py +++ b/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/common/utils.py @@ -31,24 +31,44 @@ class Timer: realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" +_sudo_available: bool | None = None + + def sudo_write(val: str, path: str) -> None: + global _sudo_available try: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - os.system(f"sudo chmod a+w {path}") - try: + return + except OSError: + pass + # AGNOS 4.9.9.2 removes owner root; a privileged boot step opens these nodes to the stack so a direct write normally works. Only probe a non-interactive escalation once (interactive sudo blocks with no tty, and sudo -n itself can stall), and never let its failure or timeout propagate. + if _sudo_available is False: + return + try: + _sudo_available = subprocess.run(["sudo", "-n", "chmod", "a+w", path], timeout=2, capture_output=True).returncode == 0 + if _sudo_available: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - # fallback for debugfs files - os.system(f"sudo su -c 'echo {val} > {path}'") + except Exception: + _sudo_available = False def sudo_read(path: str) -> str: + global _sudo_available try: - return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + with open(path, encoding='utf8') as f: + return f.read().strip() + except OSError: + pass + if _sudo_available is False: + return "" + try: + result = subprocess.run(["sudo", "-n", "cat", path], timeout=2, capture_output=True, text=True) + _sudo_available = result.returncode == 0 + return result.stdout.strip() except Exception: + _sudo_available = False return "" diff --git a/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py b/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py index 48a34ea4..c38e33b9 100644 --- a/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py +++ b/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py @@ -125,16 +125,32 @@ class Amplifier: print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") def set_configs(self, configs: list[AmpConfig]) -> bool: + import threading + tries = 15 backoff = 0. for i in range(tries): - try: - self._set_configs(configs) - return True - except OSError: + result = [None] + error = [None] + def _worker(): + try: + self._set_configs(configs) + result[0] = True + except OSError as e: + error[0] = e + t = threading.Thread(target=_worker, daemon=True) + t.start() + t.join(timeout=2) + if t.is_alive(): backoff += 0.1 time.sleep(backoff) - print(f"Failed to set amp config, {tries - i - 1} retries left") + print(f"Failed to set amp config (timeout), {tries - i - 1} retries left") + continue + if result[0]: + return True + backoff += 0.1 + time.sleep(backoff) + print(f"Failed to set amp config, {tries - i - 1} retries left") return False def set_global_shutdown(self, amp_disabled: bool) -> bool: diff --git a/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py b/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py index 2f71ed63..2506fb9e 100644 --- a/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py +++ b/artifacts/iqpilot_iqvd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py @@ -469,8 +469,10 @@ class Tici(HardwareBase): if self.amplifier is not None: self.amplifier.initialize_configuration(self.get_device_type()) - # Allow hardwared to write engagement status to kmsg - os.system("sudo chmod a+w /dev/kmsg") + try: + subprocess.run(["sudo", "-n", "chmod", "a+w", "/dev/kmsg"], timeout=2, capture_output=True, check=False) + except Exception: + pass # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL gpio_init(GPIO.SOM_ST_IO, True) @@ -514,10 +516,10 @@ class Tici(HardwareBase): affine_irq(3, "xhci-hcd:usb1") try: pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() - subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) - subprocess.call(["sudo", "taskset", "-pc", "3", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessException as e: - print(str(e)) + subprocess.run(["sudo", "-n", "chrt", "-f", "-p", "1", pid], timeout=2, capture_output=True, check=False) + subprocess.run(["sudo", "-n", "taskset", "-pc", "3", pid], timeout=2, capture_output=True, check=False) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + pass def configure_modem(self): from iqpilot.common.params import Params diff --git a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index 27500f5a..00000000 Binary files a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 894f46ab..b123d3f3 Binary files a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/__init__.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/__init__.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index c2f67e69..00000000 Binary files a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/__init__.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/client.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/client.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index ed964de5..cfa073f2 Binary files a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/client.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/client.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/geometry.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/geometry.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index b107df75..0df741b4 Binary files a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/geometry.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/geometry.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/perception.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/perception.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index e9cd5d1b..f2c250c6 Binary files a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/perception.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/perception.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/protocol.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/protocol.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index e12e4c81..fa7a84b9 Binary files a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/protocol.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/offload/protocol.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 967ea97f..bd535ee6 Binary files a/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_iqvd_private/python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_maps_private/manifest.json b/artifacts/iqpilot_maps_private/manifest.json index d09b8d50..909be7c0 100644 --- a/artifacts/iqpilot_maps_private/manifest.json +++ b/artifacts/iqpilot_maps_private/manifest.json @@ -1,4 +1,5 @@ { + "bundle_version": 1792551456, "python/_iqclosure/_vendored.json": { "mode": 420, "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", @@ -9,20 +10,15 @@ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0 }, - "python/iqpilot_private/maps/__init__.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "b4f12341a5ebeb6263226cd03391c6505c708279c9c4ff4c0057326f6325e8ab", - "size": 67616 - }, "python/iqpilot_private/maps/__init__.py": { "mode": 420, "sha256": "5128931b54d4f1ff131185e7711d56a788b5e2cb71e3c8154a2f79fef184b452", "size": 118 }, "python/iqpilot_private/maps/git_auth.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "84600544b6ca61637b1d332d4881a00083ef9001efba65ac45cca79a2a526c33", - "size": 67856 + "mode": 416, + "sha256": "8626c26d5b891985bc2a0ea84e3e51169447aebc6a88a154e82e962ab2c93c10", + "size": 1130840 }, "runtime": { "entries": { @@ -33,7 +29,6 @@ } }, "signatures": { - "python/iqpilot_private/maps/__init__.cpython-312-aarch64-linux-gnu.so": "f9iG6goRpO0HKZpoQUoZ7KGcO7YBqV3oBtZxtES4IjeOqUl7zPaVAMxGYD0jkkjtC5HKBH2fldpGGOAoQvhbBw==", - "python/iqpilot_private/maps/git_auth.cpython-312-aarch64-linux-gnu.so": "63rRKgkKdIVzRaDNRFYj+X/NRNnZrwra43QGZF8VcafRopvwXnXrSgjxmgLPIRZQVk5YPcn9cgHXAuU2UJjdDw==" + "python/iqpilot_private/maps/git_auth.cpython-312-aarch64-linux-gnu.so": "rAXhCCEeW9flkDTlDRHAGiU/wrQ6YWtdWaUdPjoEDUgTkdvvA5a4QW+I/ongdPkO55y+J2crx/LXOz8KPtHHAg==" } } diff --git a/artifacts/iqpilot_maps_private/manifest.json.sig b/artifacts/iqpilot_maps_private/manifest.json.sig index 18a66bbb..35e0bca9 100644 --- a/artifacts/iqpilot_maps_private/manifest.json.sig +++ b/artifacts/iqpilot_maps_private/manifest.json.sig @@ -1 +1 @@ -UrGrwApvCiaSCaGrD+6aKwLNTm5TaQ02X2dS4DVbM9Gw6qvQ3w6/ql/mfbr/i0Y1SgPIDfKx8fCOdcCWIwJtBA== +Y4lco3biglBKOOqFOD1X1894CCwb2uPKgYMRTV2om+t1vTXYwZ1ge8pjRMbZfXdLewmxQ2pNqkffWNqUITymDg== diff --git a/artifacts/iqpilot_maps_private/python/iqpilot_private/maps/__init__.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_maps_private/python/iqpilot_private/maps/__init__.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index f9dc9ccb..00000000 Binary files a/artifacts/iqpilot_maps_private/python/iqpilot_private/maps/__init__.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_maps_private/python/iqpilot_private/maps/git_auth.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_maps_private/python/iqpilot_private/maps/git_auth.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 2be9e39c..476383b8 Binary files a/artifacts/iqpilot_maps_private/python/iqpilot_private/maps/git_auth.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_maps_private/python/iqpilot_private/maps/git_auth.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_model_selector_private/manifest.json b/artifacts/iqpilot_model_selector_private/manifest.json index ee2bf108..a38b5f45 100644 --- a/artifacts/iqpilot_model_selector_private/manifest.json +++ b/artifacts/iqpilot_model_selector_private/manifest.json @@ -1,8 +1,9 @@ { + "bundle_version": 1792551178, "python/_iqclosure/_vendored.json": { "mode": 420, - "sha256": "1b0c676a7151287ba1aaaf1f80fbb49de1eed48bd6e979a3dffa23f01230af86", - "size": 920 + "sha256": "8756c85acb0d6b34f059386aee6177bf9a5d8fb01b509dd81c8848441ef7929b", + "size": 942 }, "python/_iqclosure/iqpilot/__init__.py": { "mode": 420, @@ -16,13 +17,18 @@ }, "python/_iqclosure/iqpilot/common/api/__init__.py": { "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 + "sha256": "1dd848ec6e867836a321a93f9ac218415c19528aa356a3e80969503285316b54", + "size": 900 }, "python/_iqclosure/iqpilot/common/api/base.py": { "mode": 420, - "sha256": "86b834e02e24a005c903f1ca059b1a572e37c17a87271726c90918cd04b6133b", - "size": 3451 + "sha256": "5b312ec45e673d9337a89ea82a157152d1f64e5e48518807a592247787087609", + "size": 4921 + }, + "python/_iqclosure/iqpilot/common/api/konn3kt.py": { + "mode": 420, + "sha256": "2282e19909079bfd77a089f480df5a229191c35f8523f005d20e2ea468a6b0bc", + "size": 313 }, "python/_iqclosure/iqpilot/common/basedir.py": { "mode": 420, @@ -46,8 +52,8 @@ }, "python/_iqclosure/iqpilot/common/realtime.py": { "mode": 420, - "sha256": "016be524079ef32df03b2c135eab8f83da60a46fc7f143620b836c65f33e39a3", - "size": 4423 + "sha256": "0190cf1f239f8a2bda573524a72a4b6a74e7ed70aa4c1e5f72d1f1159d59cd60", + "size": 4926 }, "python/_iqclosure/iqpilot/common/swaglog.py": { "mode": 420, @@ -61,18 +67,8 @@ }, "python/_iqclosure/iqpilot/common/utils.py": { "mode": 420, - "sha256": "103fa6c24f8b963576c482a34d7ecb020168fd3e85156493ae4432ee3b7b2c06", - "size": 5159 - }, - "python/_iqclosure/iqpilot/konn3kt/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/konn3kt/cloud_client.py": { - "mode": 420, - "sha256": "4d58e62f8d628a8570881f2b87c82c801b46928fbb84f8dab8a81a9b55ba4e15", - "size": 493 + "sha256": "c39b89a1ab34511dfa5f9efac783fde0e6d616963a416cf472bd1f604b57f84d", + "size": 5892 }, "python/_iqclosure/iqpilot/selfdrive/__init__.py": { "mode": 420, @@ -91,8 +87,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_cache.py": { "mode": 420, - "sha256": "1838b6ab0e9b71729397f0feda85a959e7796d3196aa68de9ca774f8fe3c473c", - "size": 2559 + "sha256": "2b3664ae0cfbab6ddbc624d4f0376dee74ccf0b43b6089b022aa7c9362009908", + "size": 2560 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/__init__.py": { "mode": 420, @@ -106,8 +102,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py": { "mode": 420, - "sha256": "2fad977c3816ff824c126a92605f866f3bc0aa989ed8397e818bed481a41192f", - "size": 14051 + "sha256": "9628f88e71f7fae2c3d62dd17618367b3c7d12749cfe2a4737410288888cd3bd", + "size": 15008 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/temporal_state.py": { "mode": 420, @@ -151,13 +147,13 @@ }, "python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py": { "mode": 420, - "sha256": "98f46f8705259b00efae7274186b4e68201aad467a19de4f16d5f850d10ab0b5", - "size": 7585 + "sha256": "38e521356c56ac85f35543b9457d36639e55ed2ac916f30ea3b86948e806541f", + "size": 8016 }, "python/_iqclosure/iqpilot/system/hardware/tici/hardware.py": { "mode": 420, - "sha256": "e8eae4e8a75e1751525471cf54a8555b7bfd7f08ad84bf2e75642b5745f222db", - "size": 25601 + "sha256": "da5ecb280f7af14169903260dc941e3bfac033c93b184cb0abea0f1494c34582", + "size": 25731 }, "python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py": { "mode": 420, @@ -184,50 +180,60 @@ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0 }, - "python/iqpilot_private/models/__init__.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "4256dfb6e41630d1650932bda2b0b34249a4f68cc66038c8c4880f02758ecc0c", - "size": 67640 - }, "python/iqpilot_private/models/__init__.py": { "mode": 420, "sha256": "5128931b54d4f1ff131185e7711d56a788b5e2cb71e3c8154a2f79fef184b452", "size": 118 }, + "python/iqpilot_private/models/_enroll.cpython-312-aarch64-linux-gnu.so": { + "mode": 416, + "sha256": "d230702120b8a84b0b122844d17741269e9c04c963d0a2d50949c53e7e4a0392", + "size": 999928 + }, + "python/iqpilot_private/models/_iqcore_binding.cpython-312-aarch64-linux-gnu.so": { + "mode": 416, + "sha256": "fe3b6c3827b9a17a200cf289f8cb8592e472b5e7e1829e8a09bc8cac78d2991b", + "size": 1065144 + }, "python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "c1af60adcc1e2bb13c12282fddcca774cc622269923a577ad46861147848f919", - "size": 133872 + "mode": 416, + "sha256": "b6c67773d2b22dc0c552f32791ff99c1c884ca6afad5f7cfc91434b8f3b5228b", + "size": 1065304 }, "python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "3ebab4e08c7c75f678a3c810bffc185092157b2a88e7ebc23e19eae8e6e129eb", - "size": 68144 + "mode": 416, + "sha256": "f096e15790592e54907f86e65df9c2302419189f5c43f8b88d3073527ae18238", + "size": 1065256 }, "python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "b6efc87125fb6d350f5b1346b1542d3e744c699fc45f75df99e5402edb9a21e9", - "size": 134096 + "mode": 416, + "sha256": "74ff49ceb972b20578e66a2853f15e180fa9b428cdbf37c8f647c08c8aa15892", + "size": 1065368 }, "python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "a024d244743cd899ad359c1177aee2315ba9e5a12b1f8708f4d76c9cc46bea2c", - "size": 201752 + "mode": 416, + "sha256": "888aeabbe88c05c1e1d11def322f32bd399e5f558d49303eabae19b3c09fca37", + "size": 1130920 }, "python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "ec73d0435e8e974d3d3c7b310c96a6cee18d1110e9bf5b8b72717f150487c3db", - "size": 68080 + "mode": 416, + "sha256": "777bd6decfcd2a36c43c853f176db156fd650bf2d0f9b458b62a69525dd041cb", + "size": 1065352 }, "python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "7ec2fbb273c8baa91974ed4f1387b56578e17a89e166560fe276d4bd22b2834c", - "size": 203576 + "mode": 416, + "sha256": "6b1417618b09387feb90cebf2ad369d93529ba5a7cd566d9dbe14974217f5338", + "size": 1131000 + }, + "python/iqpilot_private/models/iqc.cpython-312-aarch64-linux-gnu.so": { + "mode": 416, + "sha256": "f54f3dfbdf3abb6629243bef3cd0da8c5374fa179040d2cf40ab10ba79210129", + "size": 1327704 }, "python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "391d2e774fd645d126011bbb4a036fa1f25647b2ef006faa421d042120cabc55", - "size": 339984 + "mode": 416, + "sha256": "a087f40591c917c8ed0341ea10a47f8adc8452f3b566084550175e667d6459f1", + "size": 1327512 }, "runtime": { "entries": { @@ -238,13 +244,15 @@ } }, "signatures": { - "python/iqpilot_private/models/__init__.cpython-312-aarch64-linux-gnu.so": "8Oas7JZZ7zmVIMRIENhqnWB9SbrCDlLbhouUn7EnLpCBiatVBCwNVa9z42al4oT32XllAxv3NxYKoCs7iVO3AQ==", - "python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so": "FbTHsGldom1DcQHjJ5a6V4tnbg1DSHpTVE6oYDQDuoeZX8hsp3FoNURK+4DW/jpDojZsTLU4LQW6PFOBwLtHAg==", - "python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so": "2saywleDwtIVnGvYySpHwLhUQqDaj27LU3fj6UPwoOWJSx+YGiksNZlG5xlSGe53j1M7qyNSMPUMB8AyYFG1Aw==", - "python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so": "PMnolJ5vV1lbrkOdnbvc+hXsaKaH4/Ocxv2jr4L+Uc5eZwqhX60yerFtS/JgMSZNowoaYvBYyod5NJCQl3ZXBg==", - "python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so": "vZvAPdHliUZ6TiI4Wa7yn5KwQWIzGlstyUlj35su+qLbkvFZuvpwJkKKpgcADI1AyUVXPCg1OFxyrUWEq3HvBQ==", - "python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so": "kDfRCaG/n6H+2Qi3iX2JKyTRzglN/uy72UzhOsQ4rtaLD/I8jJoL5UMdQuI6hh6bafFAH3Ory42bVye60ZCyBA==", - "python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so": "X1zNt2VT3wMGiAp/cgUi/glUNE44zIGSBOOYlO1wN1YL5OoPz/ad14zE3QXmyxOkvsIsC9w0+VNCKkr4m6nvBA==", - "python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so": "B1jVEO0cjOqMoG3Fbkrca4eMPyU5s2EcW+rtMMhS7dH/l1Jp5bcYWehFJcKTx0O4zmmeUOZzIR4XavMG2Qp1AQ==" + "python/iqpilot_private/models/_enroll.cpython-312-aarch64-linux-gnu.so": "okulq02oEg6c/AjfK6ldwGokYY5VjEWvLwM1b2nW4VT95M/nzfPBX7feWjLepB0vThiRa76Qf3bBmk7Zcv/2CQ==", + "python/iqpilot_private/models/_iqcore_binding.cpython-312-aarch64-linux-gnu.so": "eHe88UatX5GonlzYAjPEMZBTvuRduNZayvvKLpNfJmU2TtXBmpQTn0Nvwzw+UTzDrsy7vHkzA4mmVbTcFjRaBQ==", + "python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so": "5VQ4Tz0Twma+VsCUCfDEQ7dPIV1GUH3xzkNRstWgPeky2MbFWpnjRFk6MUxLj1a7sPSs1s+oSZjj9Jh6jXMbDw==", + "python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so": "Ih4+8VDbepxfCupUOktXYjQu1hkQxdBNZ5yp58LCv0pTwjQVBEDqGeVrOaPVZr5wcfc067sx6k6c391JIDmsBg==", + "python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so": "9S+eMQgb5p2TbXL2Msn+1oawSSoxKfzjm9n6GsCzrERFFKACpEjPeflBnnZFeTkVGogVgPe/YDXIExt9HU1yCw==", + "python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so": "H7qadyNEqpmq9ICZwDbBwirJkZZ8uNPr3qq4b6ppQWseoil7kXkXVaQEbgjbkFfs6fXgT8miz9TuV7MkCUFtCQ==", + "python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so": "DLbub0F37+i718kJS8o4utxkQlc0JnVLwLbixexRfWWayI7xpqlhyYYtjkU1OAJ4V7di0d9vT5rrrnNwcBCYDg==", + "python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so": "9WFTiHzwW4flxL996vGCem/NfcwrAvJ0iVY4PnhVkOmdUgZI5feazGl4px/5BqDFAsmzpv2X7GDddRfnje0BBA==", + "python/iqpilot_private/models/iqc.cpython-312-aarch64-linux-gnu.so": "Wy7zbJ3Uq/zKPYur4llisNV78dD1Br0k4RaGRE6LmYBZhP7E5UPh/PkiGN35a4KQw7coVTz7k6QNmhkBwV/QBA==", + "python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so": "T1upWFYQnUx6h50DChQYVvUe8cU0Y1SIGtTWqi9bBbRHJrUiyF5eTU/2tyCRAfhXUNmej4jDC3GXen3LbZ/aCA==" } } diff --git a/artifacts/iqpilot_model_selector_private/manifest.json.sig b/artifacts/iqpilot_model_selector_private/manifest.json.sig index 18c70554..7c82e5b7 100644 --- a/artifacts/iqpilot_model_selector_private/manifest.json.sig +++ b/artifacts/iqpilot_model_selector_private/manifest.json.sig @@ -1 +1 @@ -lMc1eIwvOZFGqSSv1vD2DViOTsO8E/B5QQOHOXeHimq1QAd0OBb+GvdEtAWIVurZJQD0UAJJAKdLoWX0xr5+DQ== +ygr/GU1zFptQyHq7psL+DQ3j+AeLjH+CDWNPoCgknpaFjg3RTwwxSwmt7HoTwHf2GIoLjO5IAAphORP4ARqwDg== diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/_vendored.json b/artifacts/iqpilot_model_selector_private/python/_iqclosure/_vendored.json index 7a4e1822..af09850a 100644 --- a/artifacts/iqpilot_model_selector_private/python/_iqclosure/_vendored.json +++ b/artifacts/iqpilot_model_selector_private/python/_iqclosure/_vendored.json @@ -1,5 +1,7 @@ [ + "iqpilot.common.api", "iqpilot.common.api.base", + "iqpilot.common.api.konn3kt", "iqpilot.common.basedir", "iqpilot.common.gpio", "iqpilot.common.i2c", @@ -8,7 +10,6 @@ "iqpilot.common.swaglog", "iqpilot.common.time_helpers", "iqpilot.common.utils", - "iqpilot.konn3kt.cloud_client", "iqpilot.selfdrive.iqmodeld.config", "iqpilot.selfdrive.iqmodeld.model_cache", "iqpilot.selfdrive.iqmodeld.models.fetcher", diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/__init__.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/__init__.py index e69de29b..b7e90dac 100644 --- a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/__init__.py +++ b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/__init__.py @@ -0,0 +1,26 @@ +import iqpilot.common.api.konn3kt + + +class Api: + def __init__(self, dongle_id): + self.service = iqpilot.common.api.konn3kt.Konn3ktApi(dongle_id) + + def request(self, method, endpoint, **params): + return self.service.request(method, endpoint, **params) + + def get(self, *args, **kwargs): + return self.service.get(*args, **kwargs) + + def post(self, *args, **kwargs): + return self.service.post(*args, **kwargs) + + def get_token(self, payload_extra=None, expiry_hours=1): + return self.service.get_token(payload_extra, expiry_hours) + + +def api_get(endpoint, method='GET', timeout=None, access_token=None, session=None, **params): + return iqpilot.common.api.konn3kt.Konn3ktApi(None).api_get(endpoint, method, timeout, access_token, session, **params) + + +def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: + return iqpilot.common.api.konn3kt.Konn3ktApi(None).get_key_pair() diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/base.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/base.py index 371a8ab4..0aef9c35 100644 --- a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/base.py +++ b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/base.py @@ -7,10 +7,44 @@ from functools import lru_cache from iqpilot.system.hardware.hw import Paths from iqpilot.system.version import get_version -# name: jwt signature algorithm KEYS = {"id_rsa": "RS256", "id_ecdsa": "ES256"} +_CUSTODIAN_KEY = object() +_IQKEEPER_SOCKET = "/run/iqkeeper/iqkeeper.sock" + + +def _custodian_available() -> bool: + import stat + try: + return stat.S_ISSOCK(os.stat(_IQKEEPER_SOCKET).st_mode) + except OSError: + return False + + +def _custodian_sign_jwt(dongle_id: str, payload: dict, alg: str) -> str: + import base64 + import json + import socket + nonce = base64.b64encode(os.urandom(16)).decode() + packet = json.dumps({ + "v": 1, + "op": "sign", + "dongle_id": dongle_id, + "payload": payload, + "alg": alg, + "nonce": nonce, + }).encode() + with socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET) as sock: + sock.settimeout(25.0) + sock.connect(_IQKEEPER_SOCKET) + sock.send(packet) + reply_data = sock.recv(8192) + reply = json.loads(reply_data) if reply_data else {} + if reply.get("nonce") != nonce or not reply.get("ok"): + raise PermissionError(f"custodian sign failed: {reply.get('error', 'unknown')}") + return reply["token"] + @lru_cache(maxsize=4) def load_signing_key(private_key: str): @@ -60,6 +94,9 @@ class BaseApi: } if payload_extra is not None: payload.update(payload_extra) + if self.private_key is _CUSTODIAN_KEY: + serializable = {k: v.isoformat() if isinstance(v, datetime) else v for k, v in payload.items()} + return _custodian_sign_jwt(self.dongle_id, serializable, self.jwt_algorithm) key = load_signing_key(self.private_key) if self.private_key else self.private_key token = jwt.encode(payload, key, algorithm=self.jwt_algorithm) if isinstance(token, bytes): @@ -73,9 +110,25 @@ class BaseApi: return self.request(method, endpoint, timeout, access_token, session=session, json=json, **params) @staticmethod - def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: + def get_key_pair() -> tuple[str, str | object, str] | tuple[None, None, None]: for key in KEYS: - if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'): - with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public: - return KEYS[key], private.read(), public.read() + priv_path = Paths.persist_root() + f'/comma/{key}' + pub_path = priv_path + '.pub' + if not os.path.isfile(priv_path) or not os.path.isfile(pub_path): + continue + try: + with open(pub_path) as public: + pub = public.read() + except OSError: + continue + try: + with open(priv_path) as private: + return KEYS[key], private.read(), pub + except PermissionError: + import time + for _attempt in range(10): + if _custodian_available(): + return KEYS[key], _CUSTODIAN_KEY, pub + time.sleep(0.5) + continue return None, None, None diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/konn3kt.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/konn3kt.py new file mode 100644 index 00000000..cae7ea46 --- /dev/null +++ b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/api/konn3kt.py @@ -0,0 +1,12 @@ +# Copyright (c) 2026 IQ.Lvbs. All rights reserved. +import os + +from iqpilot.common.api.base import BaseApi + +API_HOST = os.getenv('API_HOST', 'https://api-iqlabs.konn3kt.com') + + +class Konn3ktApi(BaseApi): + def __init__(self, dongle_id): + super().__init__(dongle_id, API_HOST) + self.user_agent = "openpilot-" diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/realtime.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/realtime.py index 72578964..ca79bbab 100644 --- a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/realtime.py +++ b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/realtime.py @@ -62,10 +62,23 @@ def config_background_thread() -> None: set_core_affinity(list(range(os.cpu_count() or 1))) +def _swap_active() -> bool: + try: + with open("/proc/swaps") as f: + return len(f.read().splitlines()) > 1 + except Exception: + return False + + def lock_memory() -> None: - """mlockall this process so memory reclaim/compaction can't stall it. RT control - procs only (locking ui/modeld would worsen pressure). Best-effort.""" - if sys.platform != 'linux' or PC: + """mlockall this process so swap can't evict its pages and stall the RT loop. + RT control procs only (locking ui/modeld would worsen pressure). Best-effort. + + No-op without active swap: with none, anonymous pages (thread stacks, heap) + are already unreclaimable, so mlockall buys nothing — it only pins every 8 MB + thread stack and MCL_FUTURE allocation in full (~400 MB/proc here) and, being + non-migratable, starves CMA. The device runs swapless by design (iqos-zram).""" + if sys.platform != 'linux' or PC or not _swap_active(): return try: import ctypes diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/utils.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/utils.py index a80270bb..005d75b8 100644 --- a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/utils.py +++ b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/common/utils.py @@ -31,24 +31,44 @@ class Timer: realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" +_sudo_available: bool | None = None + + def sudo_write(val: str, path: str) -> None: + global _sudo_available try: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - os.system(f"sudo chmod a+w {path}") - try: + return + except OSError: + pass + # AGNOS 4.9.9.2 removes owner root; a privileged boot step opens these nodes to the stack so a direct write normally works. Only probe a non-interactive escalation once (interactive sudo blocks with no tty, and sudo -n itself can stall), and never let its failure or timeout propagate. + if _sudo_available is False: + return + try: + _sudo_available = subprocess.run(["sudo", "-n", "chmod", "a+w", path], timeout=2, capture_output=True).returncode == 0 + if _sudo_available: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - # fallback for debugfs files - os.system(f"sudo su -c 'echo {val} > {path}'") + except Exception: + _sudo_available = False def sudo_read(path: str) -> str: + global _sudo_available try: - return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + with open(path, encoding='utf8') as f: + return f.read().strip() + except OSError: + pass + if _sudo_available is False: + return "" + try: + result = subprocess.run(["sudo", "-n", "cat", path], timeout=2, capture_output=True, text=True) + _sudo_available = result.returncode == 0 + return result.stdout.strip() except Exception: + _sudo_available = False return "" diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/konn3kt/__init__.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/konn3kt/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py deleted file mode 100644 index 2897609e..00000000 --- a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos -""" - - -import os -from iqpilot.common.api.base import BaseApi -API_HOST = os.getenv('KONN3KT_API_HOST', 'https://api-iqlabs.konn3kt.com') - -class Konn3ktApi(BaseApi): - - def __init__(self, dongle_id): - super().__init__(dongle_id, API_HOST) - self.user_agent = "konn3kt-device-" - - def get_token(self, expiry_hours=1): - return super()._get_token(expiry_hours=expiry_hours) diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_cache.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_cache.py index 5c104245..0b1cba59 100644 --- a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_cache.py +++ b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_cache.py @@ -4,7 +4,7 @@ from pathlib import Path _RUNTIME_ARTIFACT_PATTERNS = ( - re.compile(r"egpu_.+_amd_(?:tinygrad|policy|policy_oob|model_oob)\.pkl$"), + re.compile(r"egpu_.+_amd_(?:tinygrad|policy|policy_oob|model_oob2)\.pkl$"), re.compile(r"emac_warp_\d+x\d+_tinygrad\.pkl$"), re.compile(r".+_usbemac\.pkl$"), re.compile(r".+\.onnx$"), diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py index 05cce298..ee2eb77f 100644 --- a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py +++ b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py @@ -305,7 +305,40 @@ def get_runtime_bundle_upgrade(bundle, params: Params = None, available_bundles= return _find_runtime_upgrade(bundle, params, available_bundles) +_forced_bundle = None + + +def load_default_model_session(): + """Stage the shipped default model files and return a decoded default bundle + WITHOUT persisting it as the active selection, so the next boot retries the + user's own model instead of staying on the default forever.""" + bundle_dict = _load_default_bundle_dict() + ensure_default_model_files(bundle_dict) + return ModelBundle(**bundle_dict) + + +class forced_active_bundle: + """Make get_active_bundle return `bundle` for the duration of the block, used to + build a runner for a session-only fallback bundle without touching Params.""" + def __init__(self, bundle): + self._bundle = bundle + + def __enter__(self): + global _forced_bundle + self._prev = _forced_bundle + _forced_bundle = self._bundle + return self._bundle + + def __exit__(self, *exc): + global _forced_bundle + _forced_bundle = self._prev + return False + + def get_active_bundle(params: Params = None): + if _forced_bundle is not None: + return _forced_bundle + params = Params() if params is None else params try: diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py index 48a34ea4..c38e33b9 100644 --- a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py +++ b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py @@ -125,16 +125,32 @@ class Amplifier: print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") def set_configs(self, configs: list[AmpConfig]) -> bool: + import threading + tries = 15 backoff = 0. for i in range(tries): - try: - self._set_configs(configs) - return True - except OSError: + result = [None] + error = [None] + def _worker(): + try: + self._set_configs(configs) + result[0] = True + except OSError as e: + error[0] = e + t = threading.Thread(target=_worker, daemon=True) + t.start() + t.join(timeout=2) + if t.is_alive(): backoff += 0.1 time.sleep(backoff) - print(f"Failed to set amp config, {tries - i - 1} retries left") + print(f"Failed to set amp config (timeout), {tries - i - 1} retries left") + continue + if result[0]: + return True + backoff += 0.1 + time.sleep(backoff) + print(f"Failed to set amp config, {tries - i - 1} retries left") return False def set_global_shutdown(self, amp_disabled: bool) -> bool: diff --git a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py index 2f71ed63..2506fb9e 100644 --- a/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py +++ b/artifacts/iqpilot_model_selector_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py @@ -469,8 +469,10 @@ class Tici(HardwareBase): if self.amplifier is not None: self.amplifier.initialize_configuration(self.get_device_type()) - # Allow hardwared to write engagement status to kmsg - os.system("sudo chmod a+w /dev/kmsg") + try: + subprocess.run(["sudo", "-n", "chmod", "a+w", "/dev/kmsg"], timeout=2, capture_output=True, check=False) + except Exception: + pass # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL gpio_init(GPIO.SOM_ST_IO, True) @@ -514,10 +516,10 @@ class Tici(HardwareBase): affine_irq(3, "xhci-hcd:usb1") try: pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() - subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) - subprocess.call(["sudo", "taskset", "-pc", "3", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessException as e: - print(str(e)) + subprocess.run(["sudo", "-n", "chrt", "-f", "-p", "1", pid], timeout=2, capture_output=True, check=False) + subprocess.run(["sudo", "-n", "taskset", "-pc", "3", pid], timeout=2, capture_output=True, check=False) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + pass def configure_modem(self): from iqpilot.common.params import Params diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/__init__.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/__init__.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index b3c2f010..00000000 Binary files a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/__init__.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/_enroll.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/_enroll.cpython-312-aarch64-linux-gnu.so new file mode 100644 index 00000000..f78cd01f Binary files /dev/null and b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/_enroll.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/_iqcore_binding.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/_iqcore_binding.cpython-312-aarch64-linux-gnu.so new file mode 100644 index 00000000..81c379f3 Binary files /dev/null and b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/_iqcore_binding.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index ebe494fb..057b0cd5 Binary files a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 5ab33e1c..c7ccd7ff Binary files a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index c6d235ef..8d4377ab Binary files a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index ab1ef9a8..3c455c92 Binary files a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 1049ed09..a965699b Binary files a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index d33fdc5d..d2a0ceaa Binary files a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/iqc.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/iqc.cpython-312-aarch64-linux-gnu.so new file mode 100644 index 00000000..d5652741 Binary files /dev/null and b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/iqc.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 7cac5421..b6c094ac Binary files a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navassist_private/manifest.json b/artifacts/iqpilot_navassist_private/manifest.json index 8d538a0b..bc1b02d4 100644 --- a/artifacts/iqpilot_navassist_private/manifest.json +++ b/artifacts/iqpilot_navassist_private/manifest.json @@ -1,158 +1,54 @@ { + "bundle_version": 1792268600, "python/_iqclosure/_vendored.json": { "mode": 420, - "sha256": "6435d4e0f3a9354a6a2e9421f503ccfc8156432a4be70c9b6703515d10169c9b", - "size": 577 - }, - "python/_iqclosure/iqpilot/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/common/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/common/gpio.py": { - "mode": 420, - "sha256": "01ba8cb32a644a2df545e764ba19bf1c7a320a217f983f8bac73b45d3a6891cf", - "size": 2408 - }, - "python/_iqclosure/iqpilot/common/i2c.py": { - "mode": 420, - "sha256": "4af8f9da7504a548d15f18b91d71c1379cb7c05c84be031448764cc49920ce68", - "size": 2449 - }, - "python/_iqclosure/iqpilot/common/logging_extra.py": { - "mode": 420, - "sha256": "ce9b3b3d8498d76e1c4c0352a3e98e72759bb7f367ff8629c1329c46040595c3", - "size": 6628 - }, - "python/_iqclosure/iqpilot/common/realtime.py": { - "mode": 420, - "sha256": "f137ef62c1602a1f7c329731883dc518c62288bd25596891858d460ae3209ae2", - "size": 3966 - }, - "python/_iqclosure/iqpilot/common/swaglog.py": { - "mode": 420, - "sha256": "0a133b4ef49f80b8890f115e81273f3f3e21351ae79f92dc02abc033e6d24027", - "size": 5274 - }, - "python/_iqclosure/iqpilot/common/time_helpers.py": { - "mode": 420, - "sha256": "c14cda1f84149d5ffa63a2adec5ca24126f77c1412a78412e5e3e68a6fd12bcf", - "size": 474 - }, - "python/_iqclosure/iqpilot/common/utils.py": { - "mode": 420, - "sha256": "6475ad94bc89252398c41e9eabf7929ba5eb8fa23a08ad7545e7a67b400af9f3", - "size": 8090 - }, - "python/_iqclosure/iqpilot/system/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/system/hardware/__init__.py": { - "mode": 420, - "sha256": "889391bffca493e74bfc108cae2976a53b644caca13ab600061fd29e9622c627", - "size": 695 - }, - "python/_iqclosure/iqpilot/system/hardware/base.py": { - "mode": 420, - "sha256": "a56293f7c23e503ff62d0d3ed39f1831b5544a65b37378165934361a5ddcd304", - "size": 4966 - }, - "python/_iqclosure/iqpilot/system/hardware/hw.py": { - "mode": 420, - "sha256": "af504d08f49754fa49aa3040c6bfcc3d122c2948e50d4d481625ae8f00aa5256", - "size": 3398 - }, - "python/_iqclosure/iqpilot/system/hardware/pc/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/system/hardware/pc/hardware.py": { - "mode": 420, - "sha256": "2520895597c375ae570bd04d5f189e874c89d642f3f07bdbeb20388cb6691433", - "size": 258 - }, - "python/_iqclosure/iqpilot/system/hardware/tici/__init__.py": { - "mode": 420, - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "size": 0 - }, - "python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py": { - "mode": 420, - "sha256": "98f46f8705259b00efae7274186b4e68201aad467a19de4f16d5f850d10ab0b5", - "size": 7585 - }, - "python/_iqclosure/iqpilot/system/hardware/tici/hardware.py": { - "mode": 420, - "sha256": "f1ddffa9bdb5071a49fd65eb4b49958b60b1873e1ec70e6f3f2e2b2501abce2f", - "size": 25584 - }, - "python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py": { - "mode": 420, - "sha256": "d22858982705531732d5a20a45a73795496e5bbd9e23028c24609a7e58ac7aed", - "size": 890 - }, - "python/_iqclosure/iqpilot/system/hardware/tici/lpa.py": { - "mode": 420, - "sha256": "2bffb84aef35892c4788e80b6d937bf2eebd15e226b72a82c4f39217667c1ca0", - "size": 53692 - }, - "python/_iqclosure/iqpilot/system/hardware/tici/pins.py": { - "mode": 420, - "sha256": "b57ba4262e6a6a79f2db15c27356acf3e54f579a7edd13b483b2e16d140aca6d", - "size": 678 + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "size": 3 }, "python/iqpilot_private/__init__.py": { "mode": 420, "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0 }, - "python/iqpilot_private/navassist/__init__.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "2aaa1930828be7ea5e108c9fd723bcb8cf29a9b8e40a11f2544579d022e57fec", - "size": 67640 - }, "python/iqpilot_private/navassist/__init__.py": { "mode": 420, "sha256": "ee4237375e199e74a35b5c1851f32eec3d0799966b7f1211c86ce00a0f756088", "size": 112 }, + "python/iqpilot_private/navassist/_cipher.cpython-312-aarch64-linux-gnu.so": { + "mode": 416, + "sha256": "e6b8cdc774506ce0f3b416111fd9522a22f488286b5449d121936ccf682412c7", + "size": 1064536 + }, "python/iqpilot_private/navassist/appsetup.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "aa0ac6242e25d57f16ea5b668c935805e9f62f40450844c67ce49c3d13537874", - "size": 135392 + "mode": 416, + "sha256": "7483113a98a51a567f1d5487f8fd6d9fa3f0652f1fc8b5b8be2d2a0f3f17d1eb", + "size": 1064568 }, "python/iqpilot_private/navassist/assets.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "528c0bc888f65784c123cfcf07fd677a0cc61f8931eaa033486a60c592ed6412", - "size": 201704 + "mode": 416, + "sha256": "2168c0e7d825a4c9b5fde5b353c5097f95ac8ece2b42d632527849e66c1e0cd0", + "size": 1064616 }, "python/iqpilot_private/navassist/container.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "6f8999f04cfdc4c264743d537b59904a5c874c5503dd82c936f4b7f80dcb6e6e", - "size": 134016 + "mode": 416, + "sha256": "519d14e719328449cf673607b790a88a356d025fac01d162cb0b53398ba02d49", + "size": 1130072 }, "python/iqpilot_private/navassist/daemon.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "d78fbdbb8773f39f19508ec20c0c55114ab15dddbbbf563873453e89fbf7c8ca", - "size": 203288 + "mode": 416, + "sha256": "594cb881a71598e8d96e0152df4c71be09ceb037a37a6498723853293c5ab5a3", + "size": 1130168 }, "python/iqpilot_private/navassist/guard.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "4e58188a2a984431825c8071cb7cfbd49dc87b83887020c5883baf119a6afce7", - "size": 68240 + "mode": 416, + "sha256": "4d2c0fa7dcf9a77dc8998b508683098c48c786a276957e07e296510ff7df9d5c", + "size": 1130232 }, "python/iqpilot_private/navassist/paths.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "6d5c4dffbc09cc2fcab1114750b6e3905c98baee24f8df2bbd5891d6764740fc", - "size": 69656 + "mode": 416, + "sha256": "422b9bb95559bf6732e31d82a8b3bf435a9bd7bd917788ec0e2654b865205310", + "size": 999096 }, "runtime": { "entries": { @@ -163,12 +59,12 @@ } }, "signatures": { - "python/iqpilot_private/navassist/__init__.cpython-312-aarch64-linux-gnu.so": "+TJsarV/gTM+PeWfwHOteKjOmhgp9W1f9adooEzjEtofVRVTB3mLZMY36kjRKgk1cMxMSdezyQi6r0ES4zdVAQ==", - "python/iqpilot_private/navassist/appsetup.cpython-312-aarch64-linux-gnu.so": "Ra8I1sD/cUTsohgsfY54zCi2Kh4xh2JR4DqgHuhlJUyrIETd4XsQoxuD5AWOkhshh75ZsN0PWpjEJ9TAZaGwAw==", - "python/iqpilot_private/navassist/assets.cpython-312-aarch64-linux-gnu.so": "GpsdQhg4y2Eo6tr6MLFNPJ0NkKz47rqdGArLQTJypz8QK1RMv7wX64AeWol9M7eUnymuYOvfTbn5Wsn6iJfoBA==", - "python/iqpilot_private/navassist/container.cpython-312-aarch64-linux-gnu.so": "i7TCM/c4Igl6hT1jcW00mzCnSnt3vCeaYgfluN4eHD1QUCGdswQZU1lZohA3stywXNzmvZriGuv7kpeT10k6Cg==", - "python/iqpilot_private/navassist/daemon.cpython-312-aarch64-linux-gnu.so": "befiXfF18Kz/KIDXc5XzIlO+B6YnOc6E3XGbGDVKzAphlIkhko+bHBSz0D0tid5bVWtzR8I3xhA9X9CJdaQTBA==", - "python/iqpilot_private/navassist/guard.cpython-312-aarch64-linux-gnu.so": "JE88scCtxeGCkLiAtZInFwkv3WzVj/JStHn7jKXoHc/Zx9/TxOPfnS57tLlEb4i+lAOpbOY8bQlOnKeCyjQeAw==", - "python/iqpilot_private/navassist/paths.cpython-312-aarch64-linux-gnu.so": "JiqxEb7qnDP+w7KEK1Aapc6l65gJwndMGVbUaJCrqK7FBTBFb4gw4EDo/VsbYC6b6py6ScTs9v3iQqOztIXSAw==" + "python/iqpilot_private/navassist/_cipher.cpython-312-aarch64-linux-gnu.so": "z/ruMHSCKDVJTNgbP41sdxyIEu/SZSJYnAziOqY93F/SeO9q48/pN27QgCvEd6WusKuPmpmFqfWgkwY9zTqfCw==", + "python/iqpilot_private/navassist/appsetup.cpython-312-aarch64-linux-gnu.so": "n2ZFnpKhJd5hz0XE1j4SjuTHcloy5ypK3Q5XHyhvFAtcqgzgywInzv+mem+saG12zIvX4tqh3s1Tks+uhMy/CA==", + "python/iqpilot_private/navassist/assets.cpython-312-aarch64-linux-gnu.so": "XMYEFgSlBmBJtNkBRKJ4BsoIHpnGeRRjjsPIAHd/+8rcwrhlx9waFZ3LSBy44PfA28HlR/c3WWtIr9fkzfGNCw==", + "python/iqpilot_private/navassist/container.cpython-312-aarch64-linux-gnu.so": "5owuYV1oSSdF4PNIWHmcWAzdg84TY8jXuxqNYAlROYGnA9VUS7g3dSot8x5vanjYUg2ZmKsjl1/AZAWpbZMjDA==", + "python/iqpilot_private/navassist/daemon.cpython-312-aarch64-linux-gnu.so": "YEM143l5lVV6gQQaM2n9r8FN2ex6ycgXrcrugtmh1hvfZTYcZcWqN8zUID0+czrSmW1w61RMlgbRVRKv8OfFAA==", + "python/iqpilot_private/navassist/guard.cpython-312-aarch64-linux-gnu.so": "g6nnT1AgmvvZ7FrJ8jq0mpORaGwE4SursrT5D26I/C/vgIen1iXg4REKNAlAoZ/4T6O6eeLV7bqbAMKMl3X2AA==", + "python/iqpilot_private/navassist/paths.cpython-312-aarch64-linux-gnu.so": "64eeb2HJCNvxBD3Ii2BtDKX2TuUPWUjcXxNq3Yy7zJri6KseSINCKxUupVCiBNn1aN5LZgDJ5tSHWsKm8PGGAA==" } } diff --git a/artifacts/iqpilot_navassist_private/manifest.json.sig b/artifacts/iqpilot_navassist_private/manifest.json.sig index 8909e544..e956c41f 100644 --- a/artifacts/iqpilot_navassist_private/manifest.json.sig +++ b/artifacts/iqpilot_navassist_private/manifest.json.sig @@ -1 +1 @@ -tcQKs1d9JKU5JaobF9ma1hEtVlT+lXR5cnE9oa8aaSpd6/E3WLjwB4K7BIdDcPbZdiGUuSOyYPrj3KuSkeX4AA== +COO530ld0nPfZgPLWyKO8N2OWHm9TfpmtUz5xtAO+oKFPvJqv5a7peQ3nTJXGGUrNnhWAsG7KfxbXdEB27ImDA== diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/_vendored.json b/artifacts/iqpilot_navassist_private/python/_iqclosure/_vendored.json index f535c129..fe51488c 100644 --- a/artifacts/iqpilot_navassist_private/python/_iqclosure/_vendored.json +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/_vendored.json @@ -1,19 +1 @@ -[ - "iqpilot.common.gpio", - "iqpilot.common.i2c", - "iqpilot.common.logging_extra", - "iqpilot.common.realtime", - "iqpilot.common.swaglog", - "iqpilot.common.time_helpers", - "iqpilot.common.utils", - "iqpilot.system.hardware", - "iqpilot.system.hardware.base", - "iqpilot.system.hardware.hw", - "iqpilot.system.hardware.pc.hardware", - "iqpilot.system.hardware.tici", - "iqpilot.system.hardware.tici.amplifier", - "iqpilot.system.hardware.tici.hardware", - "iqpilot.system.hardware.tici.iwlist", - "iqpilot.system.hardware.tici.lpa", - "iqpilot.system.hardware.tici.pins" -] +[] diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/__init__.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/__init__.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/logging_extra.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/logging_extra.py deleted file mode 100644 index ceaf083c..00000000 --- a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/logging_extra.py +++ /dev/null @@ -1,249 +0,0 @@ -import io -import os -import sys -import copy -import json -import time -import uuid -import socket -import logging -import traceback -import numpy as np -from threading import local -from collections import OrderedDict -from contextlib import contextmanager - -LOG_TIMESTAMPS = "LOG_TIMESTAMPS" in os.environ - -def json_handler(obj): - if isinstance(obj, np.bool_): - return bool(obj) - # if isinstance(obj, (datetime.date, datetime.time)): - # return obj.isoformat() - return repr(obj) - -def json_robust_dumps(obj): - return json.dumps(obj, default=json_handler) - -class NiceOrderedDict(OrderedDict): - def __str__(self): - return json_robust_dumps(self) - -class SwagFormatter(logging.Formatter): - def __init__(self, swaglogger): - logging.Formatter.__init__(self, None, '%a %b %d %H:%M:%S %Z %Y') - - self.swaglogger = swaglogger - self.host = socket.gethostname() - - def format_dict(self, record): - record_dict = NiceOrderedDict() - - if isinstance(record.msg, dict): - record_dict['msg'] = record.msg - else: - try: - record_dict['msg'] = record.getMessage() - except (ValueError, TypeError): - record_dict['msg'] = [record.msg]+record.args - - record_dict['ctx'] = self.swaglogger.get_ctx() - - if record.exc_info: - record_dict['exc_info'] = self.formatException(record.exc_info) - - record_dict['level'] = record.levelname - record_dict['levelnum'] = record.levelno - record_dict['name'] = record.name - record_dict['filename'] = record.filename - record_dict['lineno'] = record.lineno - record_dict['pathname'] = record.pathname - record_dict['module'] = record.module - record_dict['funcName'] = record.funcName - record_dict['host'] = self.host - record_dict['process'] = record.process - record_dict['thread'] = record.thread - record_dict['threadName'] = record.threadName - record_dict['created'] = record.created - - return record_dict - - def format(self, record): - if self.swaglogger is None: - raise Exception("must set swaglogger before calling format()") - return json_robust_dumps(self.format_dict(record)) - -class SwagLogFileFormatter(SwagFormatter): - def fix_kv(self, k, v): - # append type to names to preserve legacy naming in logs - # avoids overlapping key namespaces with different types - # e.g. log.info() creates 'msg' -> 'msg$s' - # log.event() creates 'msg.health.logMonoTime' -> 'msg.health.logMonoTime$i' - # because overlapping namespace 'msg' caused problems - if isinstance(v, (str, bytes)): - k += "$s" - elif isinstance(v, float): - k += "$f" - elif isinstance(v, bool): - k += "$b" - elif isinstance(v, int): - k += "$i" - elif isinstance(v, dict): - nv = {} - for ik, iv in v.items(): - ik, iv = self.fix_kv(ik, iv) - nv[ik] = iv - v = nv - elif isinstance(v, list): - k += "$a" - return k, v - - def format(self, record): - if isinstance(record, str): - v = json.loads(record) - else: - v = self.format_dict(record) - - mk, mv = self.fix_kv('msg', v['msg']) - del v['msg'] - v[mk] = mv - v['id'] = uuid.uuid4().hex - - return json_robust_dumps(v) - -class SwagErrorFilter(logging.Filter): - def filter(self, record): - return record.levelno < logging.ERROR - -def _tmpfunc(): - return 0 - -def _srcfile(): - return os.path.normcase(_tmpfunc.__code__.co_filename) - -class SwagLogger(logging.Logger): - def __init__(self): - logging.Logger.__init__(self, "swaglog") - - self.global_ctx = {} - - self.log_local = local() - self.log_local.ctx = {} - - def local_ctx(self): - try: - return self.log_local.ctx - except AttributeError: - self.log_local.ctx = {} - return self.log_local.ctx - - def get_ctx(self): - return dict(self.local_ctx(), **self.global_ctx) - - @contextmanager - def ctx(self, **kwargs): - old_ctx = self.local_ctx() - self.log_local.ctx = copy.copy(old_ctx) or {} - self.log_local.ctx.update(kwargs) - try: - yield - finally: - self.log_local.ctx = old_ctx - - def bind(self, **kwargs): - self.local_ctx().update(kwargs) - - def bind_global(self, **kwargs): - self.global_ctx.update(kwargs) - - def event(self, event, *args, **kwargs): - evt = NiceOrderedDict() - evt['event'] = event - if args: - evt['args'] = args - evt.update(kwargs) - if 'error' in kwargs: - self.error(evt) - elif 'debug' in kwargs: - self.debug(evt) - else: - self.info(evt) - - def timestamp(self, event_name): - if LOG_TIMESTAMPS: - t = time.monotonic() - tstp = NiceOrderedDict() - tstp['timestamp'] = NiceOrderedDict() - tstp['timestamp']["event"] = event_name - tstp['timestamp']["time"] = t*1e9 - self.debug(tstp) - - def findCaller(self, stack_info=False, stacklevel=1): - """ - Find the stack frame of the caller so that we can note the source - file name, line number and function name. - """ - f = sys._getframe(3) - #On some versions of IronPython, currentframe() returns None if - #IronPython isn't run with -X:Frames. - if f is not None: - f = f.f_back - orig_f = f - while f and stacklevel > 1: - f = f.f_back - stacklevel -= 1 - if not f: - f = orig_f - rv = "(unknown file)", 0, "(unknown function)", None - while hasattr(f, "f_code"): - co = f.f_code - filename = os.path.normcase(co.co_filename) - - if filename == _srcfile: - f = f.f_back - continue - sinfo = None - if stack_info: - sio = io.StringIO() - sio.write('Stack (most recent call last):\n') - traceback.print_stack(f, file=sio) - sinfo = sio.getvalue() - if sinfo[-1] == '\n': - sinfo = sinfo[:-1] - sio.close() - rv = (co.co_filename, f.f_lineno, co.co_name, sinfo) - break - return rv - -if __name__ == "__main__": - log = SwagLogger() - - stdout_handler = logging.StreamHandler(sys.stdout) - stdout_handler.setLevel(logging.INFO) - stdout_handler.addFilter(SwagErrorFilter()) - log.addHandler(stdout_handler) - - stderr_handler = logging.StreamHandler(sys.stderr) - stderr_handler.setLevel(logging.ERROR) - log.addHandler(stderr_handler) - - log.info("asdasd %s", "a") - log.info({'wut': 1}) - log.warning("warning") - log.error("error") - log.critical("critical") - log.event("test", x="y") - - with log.ctx(): - stdout_handler.setFormatter(SwagFormatter(log)) - stderr_handler.setFormatter(SwagFormatter(log)) - log.bind(user="some user") - log.info("in req") - print("") - log.warning("warning") - print("") - log.error("error") - print("") - log.critical("critical") - print("") - log.event("do_req", a=1, b="c") diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/realtime.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/realtime.py deleted file mode 100644 index 9fd2f695..00000000 --- a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/realtime.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Utilities for reading real time clocks and keeping soft real time constraints.""" -import gc -import os -import sys -import time - -from setproctitle import getproctitle - -from iqpilot.common.utils import MovingAverage -from iqpilot.system.hardware import PC - - -# time step for each process -DT_CTRL = 0.01 # controlsd -DT_MDL = 0.05 # model -DT_HW = 0.5 # hardwared and manager -DT_DMON = 0.05 # driver monitoring - - -class Priority: - # CORE 2 - # - modeld = 55 - # - camerad = 54 - CTRL_LOW = 51 # plannerd & radard - - # CORE 3 - # - pandad = 55 - CTRL_HIGH = 53 - - -def set_core_affinity(cores: list[int]) -> None: - if sys.platform == 'linux' and not PC: - os.sched_setaffinity(0, cores) - - -def config_realtime_process(cores: int | list[int], priority: int) -> None: - gc.disable() - if sys.platform == 'linux' and not PC: - os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(priority)) - c = cores if isinstance(cores, list) else [cores, ] - set_core_affinity(c) - - -def config_background_thread() -> None: - if sys.platform == 'linux' and not PC: - os.sched_setscheduler(0, os.SCHED_OTHER, os.sched_param(0)) - set_core_affinity(list(range(os.cpu_count() or 1))) - - -def lock_memory() -> None: - """mlockall this process so memory reclaim/compaction can't stall it. RT control - procs only (locking ui/modeld would worsen pressure). Best-effort.""" - if sys.platform != 'linux' or PC: - return - try: - import ctypes - import resource - resource.setrlimit(resource.RLIMIT_MEMLOCK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)) - MCL_CURRENT, MCL_FUTURE = 0x1, 0x2 - libc = ctypes.CDLL("libc.so.6", use_errno=True) - if libc.mlockall(MCL_CURRENT | MCL_FUTURE) != 0: - raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno())) - except Exception as e: - try: - from iqpilot.common.swaglog import cloudlog - cloudlog.warning(f"lock_memory (mlockall) failed: {e}") - except Exception: - pass - - -class Ratekeeper: - def __init__(self, rate: float, print_delay_threshold: float | None = 0.0) -> None: - """Rate in Hz for ratekeeping. print_delay_threshold must be nonnegative.""" - self._interval = 1. / rate - self._print_delay_threshold = print_delay_threshold - self._frame = 0 - self._remaining = 0.0 - self._process_name = getproctitle() - self._last_monitor_time = -1. - self._next_frame_time = -1. - - self.avg_dt = MovingAverage(100) - self.avg_dt.add_value(self._interval) - - def reset(self) -> None: - self._remaining = 0.0 - self._last_monitor_time = -1. - self._next_frame_time = -1. - self.avg_dt = MovingAverage(100) - self.avg_dt.add_value(self._interval) - - @property - def frame(self) -> int: - return self._frame - - @property - def remaining(self) -> float: - return self._remaining - - @property - def lag(self) -> float: - return max(0., -self._remaining) - - @property - def lagging(self) -> bool: - expected_dt = self._interval * (1 / 0.9) - return self.avg_dt.get_average() > expected_dt - - # Maintain loop rate by calling this at the end of each loop - def keep_time(self) -> bool: - lagged = self.monitor_time() - if self._remaining > 0: - time.sleep(self._remaining) - return lagged - - # Monitors the cumulative lag, but does not enforce a rate - def monitor_time(self) -> bool: - if self._last_monitor_time < 0: - self._next_frame_time = time.monotonic() + self._interval - self._last_monitor_time = time.monotonic() - - prev = self._last_monitor_time - self._last_monitor_time = time.monotonic() - self.avg_dt.add_value(self._last_monitor_time - prev) - - lagged = False - remaining = self._next_frame_time - time.monotonic() - self._next_frame_time += self._interval - if self._print_delay_threshold is not None and remaining < -self._print_delay_threshold: - print(f"{self._process_name} lagging by {-remaining * 1000:.2f} ms") - lagged = True - self._frame += 1 - self._remaining = remaining - return lagged diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/swaglog.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/swaglog.py deleted file mode 100644 index f8fd9d39..00000000 --- a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/swaglog.py +++ /dev/null @@ -1,165 +0,0 @@ -import logging -import os -import sys -import time -import warnings -from pathlib import Path -from logging.handlers import BaseRotatingHandler - -import zmq - -from iqpilot.common.logging_extra import SwagLogger, SwagFormatter, SwagLogFileFormatter -from iqpilot.system.hardware.hw import Paths - - -def get_file_handler(): - Path(Paths.swaglog_root()).mkdir(parents=True, exist_ok=True) - base_filename = os.path.join(Paths.swaglog_root(), "swaglog") - handler = SwaglogRotatingFileHandler(base_filename) - return handler - -class SwaglogRotatingFileHandler(BaseRotatingHandler): - def __init__(self, base_filename, interval=60, max_bytes=1024*256, backup_count=2500, encoding=None): - super().__init__(base_filename, mode="a", encoding=encoding, delay=True) - self.base_filename = base_filename - self.interval = interval # seconds - self.max_bytes = max_bytes - self.backup_count = backup_count - self.log_files = self.get_existing_logfiles() - log_indexes = [f.split(".")[-1] for f in self.log_files] - self.last_file_idx = max([int(i) for i in log_indexes if i.isdigit()] or [-1]) - self.last_rollover = None - self.doRollover() - - def _open(self): - self.last_rollover = time.monotonic() - self.last_file_idx += 1 - next_filename = f"{self.base_filename}.{self.last_file_idx:010}" - stream = open(next_filename, self.mode, encoding=self.encoding) - self.log_files.insert(0, next_filename) - return stream - - def get_existing_logfiles(self): - log_files = list() - base_dir = os.path.dirname(self.base_filename) - for fn in os.listdir(base_dir): - fp = os.path.join(base_dir, fn) - if fp.startswith(self.base_filename) and os.path.isfile(fp): - log_files.append(fp) - return sorted(log_files) - - def shouldRollover(self, record): - size_exceeded = self.max_bytes > 0 and self.stream.tell() >= self.max_bytes - time_exceeded = self.interval > 0 and self.last_rollover + self.interval <= time.monotonic() - return size_exceeded or time_exceeded - - def doRollover(self): - if self.stream: - self.stream.close() - self.stream = self._open() - - if self.backup_count > 0: - while len(self.log_files) > self.backup_count: - to_delete = self.log_files.pop() - if os.path.exists(to_delete): # just being safe, should always exist - os.remove(to_delete) - -class UnixDomainSocketHandler(logging.Handler): - def __init__(self, formatter): - logging.Handler.__init__(self) - self.setFormatter(formatter) - self.pid = None - - self.zctx = None - self.sock = None - - def __del__(self): - self.close() - - def close(self): - if self.sock is not None: - self.sock.close() - if self.zctx is not None: - self.zctx.term() - - def connect(self): - self.zctx = zmq.Context() - self.sock = self.zctx.socket(zmq.PUSH) - self.sock.setsockopt(zmq.LINGER, 10) - self.sock.connect(Paths.swaglog_ipc()) - self.pid = os.getpid() - - def emit(self, record): - if os.getpid() != self.pid: - # TODO suppresses warning about forking proc with zmq socket, fix root cause - warnings.filterwarnings("ignore", category=ResourceWarning, message="unclosed.*") - self.connect() - - msg = self.format(record).rstrip('\n') - # print("SEND".format(repr(msg))) - try: - s = chr(record.levelno)+msg - self.sock.send(s.encode('utf8'), zmq.NOBLOCK) - except zmq.error.Again: - # drop :/ - pass - - -class ForwardingHandler(logging.Handler): - def __init__(self, target_logger): - super().__init__() - self.target_logger = target_logger - - def emit(self, record): - self.target_logger.handle(record) - - -def add_file_handler(log): - """ - Function to add the file log handler to swaglog. - This can be used to store logs when logmessaged is not running. - """ - handler = get_file_handler() - handler.setFormatter(SwagLogFileFormatter(log)) - log.addHandler(handler) - - -cloudlog = log = SwagLogger() -log.setLevel(logging.DEBUG) - - -class PrettyConsoleFormatter(logging.Formatter): - # StreamHandler writes to stderr, so tty-gate on that - _COLOR = sys.stderr.isatty() and os.environ.get('NO_COLOR') is None - - def format(self, record): - msg = record.getMessage() - if not self._COLOR: - return f"{record.filename}: {msg}" - lvl = record.levelno - if lvl >= 50: lc, ln = "\033[1;38;5;196m", "CRIT" - elif lvl >= 40: lc, ln = "\033[1;38;5;203m", " ERR" - elif lvl >= 30: lc, ln = "\033[38;5;214m", "WARN" - elif lvl >= 20: lc, ln = "\033[38;5;110m", "info" - else: lc, ln = "\033[38;5;244m", " dbg" - body = f"\033[1;38;5;210m{msg}\033[0m" if lvl >= 40 else msg - src = "" if record.filename == "(unknown file)" else f"\033[2m{record.filename}\033[0m " - return f"{lc}{ln:>4}\033[0m {src}{body}" - - -outhandler = logging.StreamHandler() -outhandler.setFormatter(PrettyConsoleFormatter()) - -print_level = os.environ.get('LOGPRINT', 'warning') -if print_level == 'debug': - outhandler.setLevel(logging.DEBUG) -elif print_level == 'info': - outhandler.setLevel(logging.INFO) -elif print_level == 'warning': - outhandler.setLevel(logging.WARNING) - -ipchandler = UnixDomainSocketHandler(SwagFormatter(log)) - -log.addHandler(outhandler) -# logs are sent through IPC before writing to disk to prevent disk I/O blocking -log.addHandler(ipchandler) diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/__init__.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/hw.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/hw.py deleted file mode 100644 index 42409a97..00000000 --- a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/hw.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -import platform -from pathlib import Path - -from iqpilot.system.hardware import PC - -DEFAULT_DOWNLOAD_CACHE_ROOT = "/tmp/comma_download_cache" - -class Paths: - _persist_root_cache: str | None = None - - @staticmethod - def _is_writable_persist_root(path: str) -> bool: - try: - os.makedirs(path, exist_ok=True) - comma_dir = os.path.join(path, "comma") - os.makedirs(comma_dir, exist_ok=True) - - probe_path = os.path.join(comma_dir, ".rw_probe") - with open(probe_path, "w") as f: - f.write("1") - os.remove(probe_path) - return True - except OSError: - return False - - @staticmethod - def comma_home() -> str: - return os.path.join(str(Path.home()), ".comma" + os.environ.get("OPENPILOT_PREFIX", "")) - - @staticmethod - def params() -> str: - if os.environ.get("PARAMS_ROOT"): - return os.environ["PARAMS_ROOT"] - return os.path.join(Paths.comma_home(), "params") if PC else "/data/params" - - @staticmethod - def log_root() -> str: - if os.environ.get('LOG_ROOT', False): - return os.environ['LOG_ROOT'] - elif PC: - return str(Path(Paths.comma_home()) / "media" / "0" / "realdata") - else: - return '/data/media/0/realdata/' - - @staticmethod - def swaglog_root() -> str: - if PC: - return os.path.join(Paths.comma_home(), "log") - else: - return "/data/log/" - - @staticmethod - def swaglog_ipc() -> str: - return "ipc:///tmp/logmessage" + os.environ.get("OPENPILOT_PREFIX", "") - - @staticmethod - def download_cache_root() -> str: - if os.environ.get('COMMA_CACHE', False): - return os.environ['COMMA_CACHE'] + "/" - return DEFAULT_DOWNLOAD_CACHE_ROOT + os.environ.get("OPENPILOT_PREFIX", "") + "/" - - @staticmethod - def persist_root() -> str: - if PC: - return os.path.join(Paths.comma_home(), "persist") - - if Paths._persist_root_cache is not None: - return Paths._persist_root_cache - - for candidate in ("/persist", "/data/persist"): - if Paths._is_writable_persist_root(candidate): - Paths._persist_root_cache = candidate - return candidate - - # Keep previous behavior as a last resort. - Paths._persist_root_cache = "/persist" - return Paths._persist_root_cache - - @staticmethod - def stats_root() -> str: - if PC: - return str(Path(Paths.comma_home()) / "stats") - else: - return "/data/stats/" - - @staticmethod - def config_root() -> str: - if PC: - return Paths.comma_home() - else: - return "/tmp/.comma" - - @staticmethod - def shm_path() -> str: - if PC and platform.system() == "Darwin": - return "/tmp" # This is not really shared memory on macOS, but it's the closest we can get - return "/dev/shm" - - @staticmethod - def model_root() -> str: - if PC: - return str(Path(Paths.comma_home()) / "media" / "0" / "models") - else: - return "/data/media/0/models" - - @staticmethod - def crash_log_root() -> str: - if PC: - return str(Path(Paths.comma_home()) / "community" / "crashes") - else: - return "/data/community/crashes" - - @staticmethod - def mapd_root() -> str: - if PC: - return str(Path(Paths.comma_home()) / "media" / "0" / "osm") - else: - return "/data/media/0/osm" - - @staticmethod - def screen_recordings_root() -> str: - if PC: - return str(Path(Paths.comma_home()) / "media" / "0" / "screen_recordings") - else: - return "/data/media/0/screen_recordings" diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/pc/__init__.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/pc/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/__init__.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/__init__.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/__init__.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index 051c26fc..00000000 Binary files a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/__init__.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/_cipher.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/_cipher.cpython-312-aarch64-linux-gnu.so new file mode 100644 index 00000000..1d30756b Binary files /dev/null and b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/_cipher.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/appsetup.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/appsetup.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 4c39c93d..972af2f4 Binary files a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/appsetup.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/appsetup.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/assets.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/assets.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index e5991bea..34f16ae1 Binary files a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/assets.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/assets.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/container.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/container.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 9bd8c6a3..d72636b0 Binary files a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/container.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/container.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/daemon.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/daemon.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 63896dc2..5a8dfb1c Binary files a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/daemon.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/daemon.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/guard.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/guard.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index ecf1679d..630bce6f Binary files a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/guard.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/guard.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/paths.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/paths.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 8dd31365..bbfa5dce Binary files a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/paths.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/paths.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/manifest.json b/artifacts/iqpilot_navd_private/manifest.json index 6e29720e..c4d1e880 100644 --- a/artifacts/iqpilot_navd_private/manifest.json +++ b/artifacts/iqpilot_navd_private/manifest.json @@ -1,8 +1,9 @@ { + "bundle_version": 1792551877, "python/_iqclosure/_vendored.json": { "mode": 420, - "sha256": "f66c056f33c5e8bd3c5add8e8ec397f3015278e2bb254c0c8e7a7645eb642d9e", - "size": 955 + "sha256": "47b758362d75bf5ad7db835875d9d49d9cbac1769f767123065189fa57c41cf8", + "size": 1128 }, "python/_iqclosure/iqpilot/__init__.py": { "mode": 420, @@ -51,8 +52,8 @@ }, "python/_iqclosure/iqpilot/common/realtime.py": { "mode": 420, - "sha256": "016be524079ef32df03b2c135eab8f83da60a46fc7f143620b836c65f33e39a3", - "size": 4423 + "sha256": "0190cf1f239f8a2bda573524a72a4b6a74e7ed70aa4c1e5f72d1f1159d59cd60", + "size": 4926 }, "python/_iqclosure/iqpilot/common/slc_utilities.py": { "mode": 420, @@ -81,8 +82,8 @@ }, "python/_iqclosure/iqpilot/common/utils.py": { "mode": 420, - "sha256": "103fa6c24f8b963576c482a34d7ecb020168fd3e85156493ae4432ee3b7b2c06", - "size": 5159 + "sha256": "c39b89a1ab34511dfa5f9efac783fde0e6d616963a416cf472bd1f604b57f84d", + "size": 5892 }, "python/_iqclosure/iqpilot/iq_maps/__init__.py": { "mode": 420, @@ -109,6 +110,36 @@ "sha256": "1ae07960497ecb76e79c56978d3d2df0d0b11f71dcdb2e7dd10fc446514ef95d", "size": 1242 }, + "python/_iqclosure/iqpilot/selfdrive/controls/__init__.py": { + "mode": 420, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0 + }, + "python/_iqclosure/iqpilot/selfdrive/controls/lib/__init__.py": { + "mode": 420, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0 + }, + "python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/__init__.py": { + "mode": 420, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0 + }, + "python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_change.py": { + "mode": 420, + "sha256": "0e4b4c54856d651d89e1c8896ebf8bb4f0a2efc0b8788a585094e06ecd6616d7", + "size": 8461 + }, + "python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py": { + "mode": 420, + "sha256": "6dd380acb469c52b16516c245f0409fc0288d187fb71c9320cdf68d46edab619", + "size": 4343 + }, + "python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lateral_edge_guard.py": { + "mode": 420, + "sha256": "6bb6bcb8e5323480bf1b992549279a7d4c17a1bd1bd260863cf7b61b887fa735", + "size": 10227 + }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/__init__.py": { "mode": 420, "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", @@ -156,13 +187,13 @@ }, "python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py": { "mode": 420, - "sha256": "98f46f8705259b00efae7274186b4e68201aad467a19de4f16d5f850d10ab0b5", - "size": 7585 + "sha256": "38e521356c56ac85f35543b9457d36639e55ed2ac916f30ea3b86948e806541f", + "size": 8016 }, "python/_iqclosure/iqpilot/system/hardware/tici/hardware.py": { "mode": 420, - "sha256": "e8eae4e8a75e1751525471cf54a8555b7bfd7f08ad84bf2e75642b5745f222db", - "size": 25601 + "sha256": "da5ecb280f7af14169903260dc941e3bfac033c93b184cb0abea0f1494c34582", + "size": 25731 }, "python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py": { "mode": 420, @@ -186,153 +217,158 @@ }, "python/iqpilot_private/navd/.source_sha": { "mode": 420, - "sha256": "835566c5efad9c2b4206c6969a534e10bceb77609737f687a3a96f88a85724d4", + "sha256": "cc19b30f292809d81a61e0141ea5f3c2529c577cc00912e21ad685696e7c1cec", "size": 41 }, - "python/iqpilot_private/navd/__init__.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "cbccf68e4dbfda59ba84584a470c31c18e0822fb218b5946764526f441f88a82", - "size": 67640 - }, "python/iqpilot_private/navd/__init__.py": { "mode": 420, "sha256": "3faeaa3363b2f50fa135ad4c1f2466853d684d562120425f77a58039837fd8fb", "size": 69 }, + "python/iqpilot_private/navd/action_engine.cpython-312-aarch64-linux-gnu.so": { + "mode": 416, + "sha256": "f6e3fb2d21f01109565d5c60bfdc9ca2dd0039a2d50a133c8ab7b1e2bf84a929", + "size": 1130856 + }, "python/iqpilot_private/navd/alpr_matcher.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "d22bfd9057cff7ca07acc8fec86d34cbfbed7fc05138ee68f05dd064950646b6", - "size": 269896 + "mode": 416, + "sha256": "44fa3ed9c2cebb5de4c101631ed48eec18d5eab022ec99068d309ba4a83ce887", + "size": 1261944 }, "python/iqpilot_private/navd/amap_client.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "534df527471f2166f331ae895ea3570d86e7c8743f852f15bb5e05f0908ab82d", - "size": 202104 + "mode": 416, + "sha256": "337962c364a5a14b06fe0c67c8e6472bee0e7c1e89b89c5b59d4c8d6a047c7cf", + "size": 1130792 }, "python/iqpilot_private/navd/event_builder.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "4d17f2ae1a59ad729b6b8e18b570269b67442220e9afd01f883f434628b1e852", - "size": 136952 + "mode": 416, + "sha256": "85db538d9ed70f76f0baa4ba767e0acc2e109be23518490630ab3df85bec8c8b", + "size": 1065192 }, "python/iqpilot_private/navd/helpers.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "945779adb944d2a04ee8bd81ffd67915e87bd8229f75dba4a25be36521fb45a4", - "size": 135896 + "mode": 416, + "sha256": "55a38939124fec29f39eb60397e855aac57793260522c7197d540edeb91aa9a5", + "size": 1130728 }, "python/iqpilot_private/navd/incident_feed.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "fe651ff34a3d4d31113aec6304444095832098e02709d1a9ae6fc3a290491ead", - "size": 135464 + "mode": 416, + "sha256": "c5e48e55287bef7351daed6632b6a6558a877827039fccbb0592e6f2ccd8cbe0", + "size": 1065240 }, "python/iqpilot_private/navd/iqmapd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "3a671a256a65c9931861a79b58d6003ed1491d9d275186b51821f5222c7b0596", - "size": 135560 + "mode": 416, + "sha256": "e1c1e39349f8a2e7e2448ea7ab5cbe31407cd96fb7c17bb01f80b2be411a0738", + "size": 1130792 }, "python/iqpilot_private/navd/long_decel.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "d53b2c8e61ed8783049871cd14f82213d4d17a3d2f2ee32a54ce4700368dd9e3", - "size": 69704 + "mode": 416, + "sha256": "ada6dca804de71fbb35eb31dfea2b1dfe7b922eb028baec3023cd9bb60442327", + "size": 1065208 }, "python/iqpilot_private/navd/mapbox_client.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "4723241d05218af09d00f870f075a4b6107b5b0a4ebcb521236ad30ec3197748", - "size": 68144 + "mode": 416, + "sha256": "014092a1ea036ddff81616d3cc073e304a15711eb4a9db85a202166b3e2ebb96", + "size": 1065256 }, "python/iqpilot_private/navd/model_map_raster_v3.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "a8804639cc1e7303a8fb3bd1902df2441ac8f6290b069516413d210ac3666766", - "size": 335360 + "mode": 416, + "sha256": "8cf1d67439aa68aa73651acbb319f9e17850e757de71149aabe2ff0f30690c66", + "size": 1261976 }, "python/iqpilot_private/navd/model_map_tile.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "27000992b85df0915171bd60f0b99bd955ea41df9a341521652d2bf4d19f18fc", - "size": 268840 + "mode": 416, + "sha256": "71c78d9cf3c2061e62fa3a7857ae6ddaf4a5df8b01e27ba47e38724aaf6c34b7", + "size": 1196440 }, "python/iqpilot_private/navd/nav_cameras.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "37a465ecb6bd44a663cc270b8a56d2ccff58146bbff0881d9974a6835af469cb", - "size": 203032 + "mode": 416, + "sha256": "260ed46054104fa512f5984c67ed0bf78f96fcfcd393c9b2d27fd9249a9cb658", + "size": 1065336 + }, + "python/iqpilot_private/navd/nav_model_input.cpython-312-aarch64-linux-gnu.so": { + "mode": 416, + "sha256": "b56cbd876dbf6efea8e41d7e4ac71cb493c46ea1f29c857c60b7f356641fc6b6", + "size": 2114264 }, "python/iqpilot_private/navd/nav_turn_signals.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "565774a388e7ff053cc37a6938bf54c2b580515a420595fb1074ff555dac9d83", - "size": 135512 + "mode": 416, + "sha256": "13c0d407851b8ce6ed440e47b185a7bbe644576a47c5f795c4cfd68ac4a790bd", + "size": 1130712 }, "python/iqpilot_private/navd/navbridge_source.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "0386767a5ac5dfbf7d99aea5cc3ea87e14839c9c4913c7a64acd554331937f02", - "size": 202288 + "mode": 416, + "sha256": "3fc18b853d11b3a64206b1971c88b3dbc521b6adfdbc2dbaf900dc21bd3be8f4", + "size": 1065240 }, "python/iqpilot_private/navd/navd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "d4e1cc13a1fad7c620df105b8e9b789b5aaf2315a14abefeda80449f37a48b81", - "size": 928696 + "mode": 416, + "sha256": "be61f91e3393d7221c8254fdead520e692a612faa593fc5c2fecccf825010fb3", + "size": 1917656 }, "python/iqpilot_private/navd/navincidentd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "9a4f05ed613b665acf3828699f0b917b8358e1619226c3cd410ebf278fb4d1e3", - "size": 135232 + "mode": 416, + "sha256": "551ce0787afc94a9f78ce9048dbfa12c07bc7da50fd19f243a40b1abad90c66b", + "size": 1065192 }, "python/iqpilot_private/navd/navrenderd.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "864a31c370a9fc3fcc950dfefccc3281ca55fb9eec691c4946009bc5f64c9082", - "size": 135520 + "mode": 416, + "sha256": "b314184d8bd4e618615e50aa99dd70a492037134fadd5a97366870258547375c", + "size": 999912 }, "python/iqpilot_private/navd/osm_cameras.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "c86dd543303e7a53428a3d8d94dc99e4c25cfec2aba8dc1d81236066bba5063b", - "size": 267848 + "mode": 416, + "sha256": "002def8baa4a55884b5bf9256304e1d4bc848270fd61d6d7b7ccfa6a62012321", + "size": 1130904 }, "python/iqpilot_private/navd/reroute.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "a98b7628ee0080dba75a69d72e4ff2d7be28699f965a16215fde480295bb7bc1", - "size": 136120 + "mode": 416, + "sha256": "4adbc74a85e2c652ec900be6106bc36b70efb9cd650d73b7480f6b1908c36d4f", + "size": 1065224 }, "python/iqpilot_private/navd/road_incidents.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "2501e3ed0ede544726bd1cd8ff8a957d7d3482913976882cd1da13dafdf50375", - "size": 203096 + "mode": 416, + "sha256": "1b19a6b4184d2a03069647b5e862e09f8efee2bd642240291aafe7e784e668a4", + "size": 1065352 }, "python/iqpilot_private/navd/route_manager.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "1f3c62a505946d73ac3b8aa6596ab774db185047a6792417e33ada76955f7fd5", - "size": 1211416 + "mode": 416, + "sha256": "3bbba164bdcd92f3a6abeb646405f26d23ee5693393c96be479b723f971246a1", + "size": 2114392 }, "python/iqpilot_private/navd/runtime_common.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "af2ca23713905a10763e354b1345adbf50c8ddd48694ce263410248351461b2a", - "size": 135792 + "mode": 416, + "sha256": "d809748b9a573d8ab3623f94f5dbb1ab7387904337af7de88b6cbf5220e8201f", + "size": 1130824 }, "python/iqpilot_private/navd/traffic.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "ba9dfae7e7c44b6123a2d0fd4c354570a97f7ae42c2b458adac20c76cf06cf82", - "size": 69664 + "mode": 416, + "sha256": "fa02a76f7e33007f0091509583bb540242eb8843ab499895eee124374e29870c", + "size": 999688 }, "python/iqpilot_private/navd/traffic_sources.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "84a5fbbe909c3e72ad6265ea1059eea56b9e36cc0ba7372851a0eb9c348f67d1", - "size": 135896 + "mode": 416, + "sha256": "a3b65415ad2516c5478bfd3789a259b2584e3016644a86b63f4931407199b992", + "size": 1065192 }, "python/iqpilot_private/navd/traffic_writer.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "4649ee7042cf35802d015c73e4a94889a66363c1f85d7b21655b1b8ce25f8f13", - "size": 136912 + "mode": 416, + "sha256": "a989e86db4f1bacf33016be0409b514254c1459d70380155122b581425d9faf5", + "size": 1130824 }, "python/iqpilot_private/navd/turn_desire.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "af9519fc7b9d768a6fdd980478feda5fa79765d0dd67433ce5308743d91a4b1e", - "size": 135464 + "mode": 416, + "sha256": "39e1aa4d9df8b61c3631522dcaee29ce05275ec92994efea7700fda8455ada65", + "size": 1130888 }, "python/iqpilot_private/navd/valhalla_process.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "ad17fd15c08ce235db8e2ccbbfb99ecc646a5b1a6505180e2de70d75b6c10f61", - "size": 204048 + "mode": 416, + "sha256": "50479c5b4967705c8149fcfa9474eaa1ee982de256b0fe7d7744782bf6254908", + "size": 1196440 }, "python/iqpilot_private/navd/web_server.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "6d3ffce1ac886cbface7e3fa5b1e5961fc80d6d7a38e2f4843c13933b0c508db", - "size": 266880 + "mode": 416, + "sha256": "dc3f401bfc363d4e8bf49d5aed3cd77235f2ddba1239df85a579c7d76552acca", + "size": 1196408 }, "runtime": { "entries": { @@ -371,33 +407,34 @@ } }, "signatures": { - "python/iqpilot_private/navd/__init__.cpython-312-aarch64-linux-gnu.so": "yX9sd4NqftVEIwGAF39dVoM6VnzqbqlwZtRWWjyfrEADTRhYpjtevLk8IoKUJpV37o+9GlU3+PFw4cBeW651AA==", - "python/iqpilot_private/navd/alpr_matcher.cpython-312-aarch64-linux-gnu.so": "7l2QNuXu52aKUDdiDmpnVIHi/4SLoikAgypmE5xcojd7ZLtEqMSHpKO73xzHujd2NU8FqhXvXAhBHLbRrCKJCw==", - "python/iqpilot_private/navd/amap_client.cpython-312-aarch64-linux-gnu.so": "4QxYI2YF6IaWbDAimEQNSfU7NX/0cmZKWy2FJcB+yZ/lPPefRXhPAZ6wtAUGp4nKyRIzn9OZXBtdFDTlgMyTBQ==", - "python/iqpilot_private/navd/event_builder.cpython-312-aarch64-linux-gnu.so": "qxPjAup8c18smc/fHOKf//aXjKwvNbHblXzZM58Ltq/cHOcU0b0/lzCgnB+PvhwTC63Ldz0Se2l19BP0HLd+BQ==", - "python/iqpilot_private/navd/helpers.cpython-312-aarch64-linux-gnu.so": "P7VLBQnUntVhX5od5L547jVbRXLiuL6u2Q+qjh73Q0dEkBdRGe7cBgxsB46ffJqPj9p6xJrCn5bDiDYqHsaeAA==", - "python/iqpilot_private/navd/incident_feed.cpython-312-aarch64-linux-gnu.so": "EKExNNbK33T19cWYXEZKrs2PGj+YBv7lso5B8pWxFca3P42iAzinxeOctQS0TjSjhNQWsb+rYtJLCblyko0kBQ==", - "python/iqpilot_private/navd/iqmapd.cpython-312-aarch64-linux-gnu.so": "GPnWEDRYUblywR3FcrCnyTOkaeWAL/ftUDiHKjetIFYlkrKDSyognQVfeNzZ6HKfIFnpOTqjuR4zkiRoycEYBQ==", - "python/iqpilot_private/navd/long_decel.cpython-312-aarch64-linux-gnu.so": "qvynUnnwxo/1/5TnboG6voiSkwZ00YUIidcckTm8beR7VmE1Eg3RW00ntqbW7MC91Czs9JQR1ayewVSbGC7KDQ==", - "python/iqpilot_private/navd/mapbox_client.cpython-312-aarch64-linux-gnu.so": "aKptFZ+DALBXiYSrO9EikwY62WT9seEMfmrUAo9sdR1CoSdGScGhykYpYlUhFDb0zf3+WlI5KcwVbV32SwmkBw==", - "python/iqpilot_private/navd/model_map_raster_v3.cpython-312-aarch64-linux-gnu.so": "DjSk2azxFA7ZAa2O1+g3WzDELbOQZ+0MlxqsSUx6DyqXTVf7CBeoOF/B/LrY9Sql5XeXIz3EFxxLsbVaVCdXBQ==", - "python/iqpilot_private/navd/model_map_tile.cpython-312-aarch64-linux-gnu.so": "MiQzkKQfwOevWoXY+3SiyZg7Dej9bM14n8kBpGnJxwz1NPDLTKnZJm/WZZRz4u32CAR5Hn4Epsg+Jc2yYKCgCg==", - "python/iqpilot_private/navd/nav_cameras.cpython-312-aarch64-linux-gnu.so": "AfjKysW4o67PwDzBsJr53ASxZTyjmzFsaydy32f9kn0ZC5vxb1wPZTXP2aDfl3nexAx29+nuPK1zHW1JxknhBA==", - "python/iqpilot_private/navd/nav_turn_signals.cpython-312-aarch64-linux-gnu.so": "nSmQGWc+aKKbPr2ZW5lhyxV0P4H+lrdWJ5D5nKxTX2uStaDHbz4w4jddNxUuCU2fyuRWeNstXPagCMHQLUteAg==", - "python/iqpilot_private/navd/navbridge_source.cpython-312-aarch64-linux-gnu.so": "DJvMIUqtDXa1a7HVlJS+7yYwtwI46cRMPneFdXkIDR2LfPEgLmi1VysUeRS5Da5Ym1RxmipsrVktEz/iKYceDg==", - "python/iqpilot_private/navd/navd.cpython-312-aarch64-linux-gnu.so": "f9HGBUc7TxkOEDwSYPyDoGrKbNRuTtrZLStq6VTRsXok0Xg1HXc2fkCMoqYBHpVRWTallQ3mC58AfRUQxia4Dg==", - "python/iqpilot_private/navd/navincidentd.cpython-312-aarch64-linux-gnu.so": "pw4tv47FUqcGiNSSxBgEyyWn6FCWNUefUL0BJa9Zji8iuQx72VPIlrg967p1jWbAGleNzBPfP7M2fefzwyLiDg==", - "python/iqpilot_private/navd/navrenderd.cpython-312-aarch64-linux-gnu.so": "1+eSagkgdKZcQX3LPgwfsikdVn0NQOzR6OqsTMvAAODavqHoeeGwOvYxZJrF/WwkaLna8PzAOfIDg1j++yGlCQ==", - "python/iqpilot_private/navd/osm_cameras.cpython-312-aarch64-linux-gnu.so": "RhkyJ0iqvSaj9B0MwiEFIa3VgvZbF/F3uJ/8kucXymwoC0+ceszFmqAQRK47ZZal3De4F7xY2eDHYI7pzUTbCw==", - "python/iqpilot_private/navd/reroute.cpython-312-aarch64-linux-gnu.so": "13dDmGrDfxtSkWRCTyNigelgQhthLHJ0VoISNCUY6OcLua84xXxA6tNyK4Hi2ltu3diPyvvkw4n5JXM9x6kLCg==", - "python/iqpilot_private/navd/road_incidents.cpython-312-aarch64-linux-gnu.so": "+zYcT09rCxpOuU06PfyVQu3A0Vq3LXDYX7TjrJSfhx3oWJ/AroilWe8cPYe35+81r4RR8Ck/hG5KJnglS5G8AA==", - "python/iqpilot_private/navd/route_manager.cpython-312-aarch64-linux-gnu.so": "J3LzxN/QzuaYF0Cj8ZJmHnAA2CPsrumbZddyqumJGLIyfA2/69RmMzwtTkHMk+Lfly655rZPK9qMrfyv0xt+Bg==", - "python/iqpilot_private/navd/runtime_common.cpython-312-aarch64-linux-gnu.so": "0J8G7awbYfdOP4reUCU2gzWwV3hn+xL7Bl9VWmHQM73QjOG6RDpZtLIbswqNHSG4BT3UPfLuEj4pmTN7pJlKBQ==", - "python/iqpilot_private/navd/traffic.cpython-312-aarch64-linux-gnu.so": "PhHUekKd65AbwvgJsPjIbeJ4LuK5UoK+og+spL+8Z7V5bSuyDD93riSSTJT+FhF+wOxZMJynX3+wwBynlTu9DQ==", - "python/iqpilot_private/navd/traffic_sources.cpython-312-aarch64-linux-gnu.so": "13j7/ucICrQFB8qJbhrI1kV2aglF9qutVK/pZrosV9rgVYfO4iAnIJ4LVd8lQ/XK7jqL8k6TYsC2/NB1ED2JBw==", - "python/iqpilot_private/navd/traffic_writer.cpython-312-aarch64-linux-gnu.so": "bp9ef/Pnp+QijE9kp+Af8px1szN8XVkbn5oXJ4GoO2pI9+ADvD+vUOblEx8R9k1F0t9RtJ6REbhCfBIfX1uwAA==", - "python/iqpilot_private/navd/turn_desire.cpython-312-aarch64-linux-gnu.so": "u2RdJ8NBPrJdt3lT/a235y3RyYP2VlmeQbPpfep6/pkoiOlgGUz/WMW+c9XQdxjirZzJmzkH8QM+RO/HltUFCQ==", - "python/iqpilot_private/navd/valhalla_process.cpython-312-aarch64-linux-gnu.so": "FHv6YD7eNg3jn9FU2bkWJ3VYdGXT8UWNnHgouIDinnsr9frKHvbRN5n5drB0pqwSzAiJGhTcB9vlMlWDzewNDw==", - "python/iqpilot_private/navd/web_server.cpython-312-aarch64-linux-gnu.so": "pX7b/E+cHfdPVuYCBB8kG8DVW1t+Wz/CPf8r+nxg5BX0PWZyvxk66Eu+oGpdzDAQf3a6hjBoh5fg4qdbDMm9DA==" + "python/iqpilot_private/navd/action_engine.cpython-312-aarch64-linux-gnu.so": "F25IiQTQJ+1SY2eGZJ5VBId72iU68SJtUpwiV1jangm7hw2vE9t7bQe0YCvok8G0xhVW8vOFDstIooN5Dat9DQ==", + "python/iqpilot_private/navd/alpr_matcher.cpython-312-aarch64-linux-gnu.so": "bZE0Y0HJ5XNADty9I4Vty5eE9NOP6YQHvG7+PPrvnOKxMRqAiUV6QJuFFwUbR1oKNwWXb5hpCFXq9WP3UgViBQ==", + "python/iqpilot_private/navd/amap_client.cpython-312-aarch64-linux-gnu.so": "u5yZ3JamaJ3VtmzAjAuzjuFRocb37Jje9ev9JlEmEqN0qb5Jtj1FHGp60/MIKQMUsM9ZuOWWHu92FbEcv55YAQ==", + "python/iqpilot_private/navd/event_builder.cpython-312-aarch64-linux-gnu.so": "a4ATJSFgAxYIT3wU1rT1aMShpOvYtleb4cl6jtjoo6snZsk9DntHPh5WWEHYv4xVuxLkuyS1ZCucTmvb7K/VAg==", + "python/iqpilot_private/navd/helpers.cpython-312-aarch64-linux-gnu.so": "TPo/qs1I8m1nzCaJIJJBFWBaXjDohg7wwiKmkQuc9hUVLa0DR/5ZV7amH0kr9zsp94oIFSnw4IU8Mdvd5DvcDw==", + "python/iqpilot_private/navd/incident_feed.cpython-312-aarch64-linux-gnu.so": "h5rbPaJEyyRVxcK1sQc8VnDLSY5JW4Hbc3USHMlOGbhdF+pSzSDcxfAtDvLNpmkX2uFJ9FMH9+fK9I9Yv3HoCQ==", + "python/iqpilot_private/navd/iqmapd.cpython-312-aarch64-linux-gnu.so": "FBWkcmDky7gY5I1ryD+HU4YU2k8kPNyWMxT+N0aP0lYCNIqP5eiNDZxuUHgW6wwOSI7RPvGtxvYD+JKZg+o2Cw==", + "python/iqpilot_private/navd/long_decel.cpython-312-aarch64-linux-gnu.so": "7p1ughocaGojOFcvk2Ha+rw6UQZQdi+krlAisXUJTqZN8kiTv9zLuAvk9aHwUBoXm8jkjupZAwlm/UsdONxXBQ==", + "python/iqpilot_private/navd/mapbox_client.cpython-312-aarch64-linux-gnu.so": "MJ61UvRbZegK1KTZkTzsmRRqVjuf+T87t/S9Cp9kO1zNVylimLXkgROHwhbRZT1oqVTb7Nk55D178Bqkv9UdDw==", + "python/iqpilot_private/navd/model_map_raster_v3.cpython-312-aarch64-linux-gnu.so": "8NDLi9SR9qK0BEgSq70qanx4Kmx18bxl7U4A7tUOx3fcSxCn9Etxu0WjCW3GdeM2bo3Q8+fhgcGJa39etVOQDw==", + "python/iqpilot_private/navd/model_map_tile.cpython-312-aarch64-linux-gnu.so": "F9U6kNARHQOQqvBHlg4ZRfL0Mq/x4kfecYZvKzuCR6+8eALW+01d53f4h0jxcm28GtH7nYR70P8QSFTz8IWMAg==", + "python/iqpilot_private/navd/nav_cameras.cpython-312-aarch64-linux-gnu.so": "zuUaBeiys3yYItmHQFlvU+rF3PzutPaPgICCBDFhYQLo6BBLkbDAFCUrovIgWKB0PXUR7IjrNQT/8fOYpwTzAg==", + "python/iqpilot_private/navd/nav_model_input.cpython-312-aarch64-linux-gnu.so": "vttzuUiF4Y0gx51NOriCHBVyQUaPw04CVhswPVB0SNcM4nC97fW+ndFlG7+JxydIRm2Q3MOrLKjIBnSvcLqGAA==", + "python/iqpilot_private/navd/nav_turn_signals.cpython-312-aarch64-linux-gnu.so": "Qd6LBQrIYM0vgUaT942pMobqZVW128Wt4VCpEvWUkv/u+DusG5bYQ/avShBKFQZt0nPvtDdPAn2QPtnoyx69Dg==", + "python/iqpilot_private/navd/navbridge_source.cpython-312-aarch64-linux-gnu.so": "xERABVRg2pZeXx3LT3bVnBMhVhdnrdicnpVsgHs3mtGfjcCWFld1WJ3IelRlhHbwkZQczpvM6JaeguaKEGlVAQ==", + "python/iqpilot_private/navd/navd.cpython-312-aarch64-linux-gnu.so": "Ktnh+A4Z79a21si4cRSDK34KvrWzmOtw9D3UBV0nScfc8l74oiIltHYE2AID1vLWRb6V6G7NASN6hh9AmgirDA==", + "python/iqpilot_private/navd/navincidentd.cpython-312-aarch64-linux-gnu.so": "G+ZThPhlEb12Tyr0iV8wn66SLHXG9zOjqv+cuuFEzrPQDBjo7mYZOlVlYJYHxSqG6hI893b0DlhOLLFt+CN5AA==", + "python/iqpilot_private/navd/navrenderd.cpython-312-aarch64-linux-gnu.so": "e1wxGgHryVbHl4eHCEyuxbCrMD79yPo5BL44f/WJqi2SuStJuM6SCdww3spPGACtjJdgPah1A+6jy8SIpu/cBg==", + "python/iqpilot_private/navd/osm_cameras.cpython-312-aarch64-linux-gnu.so": "8LXmLX7Ut3OwB9xEspgneyqI3H0TuIxlARYmgyll9lT+HQ/O6nDBi+/sAn9DR9uIQjefe5BXWEopfLbk0oZSBA==", + "python/iqpilot_private/navd/reroute.cpython-312-aarch64-linux-gnu.so": "tO1fXQJX7MiXmtkzxy9sIiX1vvYQkxmUSdsaU7UmHG+p43U70h1IDo0bbZEL1eqOy2pA+YC3Eo8XKuRipmQfCg==", + "python/iqpilot_private/navd/road_incidents.cpython-312-aarch64-linux-gnu.so": "FfHZRP5sG1ujkbwNO/B7gzIKYLHzc7SUP0gYocahsEketuU2hoBMoUSInOY0rYqgSth/w9NyLKxo61ITYQagCA==", + "python/iqpilot_private/navd/route_manager.cpython-312-aarch64-linux-gnu.so": "fTHOaZdAPhvE0KDFLphOsYKpwY+FmeGX3B1yNT/X+v5RAZEk4jLALmZQyW1N/C0YF0wXNFSnfRfdVLD6mK8YDA==", + "python/iqpilot_private/navd/runtime_common.cpython-312-aarch64-linux-gnu.so": "wbveJfZ1Ils6rOwJj6QL2AVWrGh3mgq8nAG9l+S3fXY25b4auler6B3YvMgaEiH+1f1zTFEGqg1IuzMyX/bQAA==", + "python/iqpilot_private/navd/traffic.cpython-312-aarch64-linux-gnu.so": "5ZA6DOEtJLDtdIiltra49wsj1QyHDigPsgBHe1dHlseIk6hMy2pKP7ExbG7xmKmqoix3H5Z5F0G/fX+i4kFvDg==", + "python/iqpilot_private/navd/traffic_sources.cpython-312-aarch64-linux-gnu.so": "BEyGPH6vF9MMMGTxOCkNEaXcFRrqvM1iiXiUdcK9pZZrIqx1PQivC3eCNnITsscKqOpdvLiYtiL4XCZJIJL0Dg==", + "python/iqpilot_private/navd/traffic_writer.cpython-312-aarch64-linux-gnu.so": "ELbFb6mMCSwm28q/MnqU9bRZKT9KAnalxL8oj/jCGAGNEmCvTgg+tocj/aSI6sn0i7oiZstmtomUwdeG1zpbAQ==", + "python/iqpilot_private/navd/turn_desire.cpython-312-aarch64-linux-gnu.so": "P5pI2GdvQObiLF9s319H2OvHmAq+cMgs9zdljhdb2HUkBQfK2sMbstgbrJ5omMYjp6ZcDFhydrXFhUsZGbGyCQ==", + "python/iqpilot_private/navd/valhalla_process.cpython-312-aarch64-linux-gnu.so": "oNdyumR4l7seqk3PcSzAt2/w9f3fHxTml4TGeiIXBhev98DACg1lnWmqBQPVaa7nW533J6SCDWHPkDNIz9KpCg==", + "python/iqpilot_private/navd/web_server.cpython-312-aarch64-linux-gnu.so": "VOldQ5K+jWexchjOLGty4l8goCHgYMRuSbdKs/oDNhtfhYPO9HHmcw1Agjkl1Rn8MiNJRYpaLf2+MopyHDIjCw==" } } diff --git a/artifacts/iqpilot_navd_private/manifest.json.sig b/artifacts/iqpilot_navd_private/manifest.json.sig index f36a0b5f..168b2295 100644 --- a/artifacts/iqpilot_navd_private/manifest.json.sig +++ b/artifacts/iqpilot_navd_private/manifest.json.sig @@ -1 +1 @@ -jF2L6FQ5zgdd+okjdZFb89Lb7SNzdYU7CfXxMotSFMQu4zqJvKQ8Y+fkeqY1tyLLRDn2Je4Thmyoi/qdN7K8Aw== +AD5fLVMfNodHrJG6Q9EouN2kU8XSDsY2D/XdSA0Z4+6tWeeUwTCCxH9VadyHvlUifg4MmPBDedSdzB7+2YBcDg== diff --git a/artifacts/iqpilot_navd_private/python/_iqclosure/_vendored.json b/artifacts/iqpilot_navd_private/python/_iqclosure/_vendored.json index 6cef27bc..bfc65682 100644 --- a/artifacts/iqpilot_navd_private/python/_iqclosure/_vendored.json +++ b/artifacts/iqpilot_navd_private/python/_iqclosure/_vendored.json @@ -16,6 +16,9 @@ "iqpilot.iq_maps", "iqpilot.selfdrive.car.cruise", "iqpilot.selfdrive.car.long_increments", + "iqpilot.selfdrive.controls.lib.helpers.lane_change", + "iqpilot.selfdrive.controls.lib.helpers.lane_turn", + "iqpilot.selfdrive.controls.lib.helpers.lateral_edge_guard", "iqpilot.selfdrive.iqmodeld.config", "iqpilot.system", "iqpilot.system.hardware", diff --git a/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/common/realtime.py b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/common/realtime.py index 72578964..ca79bbab 100644 --- a/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/common/realtime.py +++ b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/common/realtime.py @@ -62,10 +62,23 @@ def config_background_thread() -> None: set_core_affinity(list(range(os.cpu_count() or 1))) +def _swap_active() -> bool: + try: + with open("/proc/swaps") as f: + return len(f.read().splitlines()) > 1 + except Exception: + return False + + def lock_memory() -> None: - """mlockall this process so memory reclaim/compaction can't stall it. RT control - procs only (locking ui/modeld would worsen pressure). Best-effort.""" - if sys.platform != 'linux' or PC: + """mlockall this process so swap can't evict its pages and stall the RT loop. + RT control procs only (locking ui/modeld would worsen pressure). Best-effort. + + No-op without active swap: with none, anonymous pages (thread stacks, heap) + are already unreclaimable, so mlockall buys nothing — it only pins every 8 MB + thread stack and MCL_FUTURE allocation in full (~400 MB/proc here) and, being + non-migratable, starves CMA. The device runs swapless by design (iqos-zram).""" + if sys.platform != 'linux' or PC or not _swap_active(): return try: import ctypes diff --git a/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/common/utils.py b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/common/utils.py index a80270bb..005d75b8 100644 --- a/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/common/utils.py +++ b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/common/utils.py @@ -31,24 +31,44 @@ class Timer: realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" +_sudo_available: bool | None = None + + def sudo_write(val: str, path: str) -> None: + global _sudo_available try: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - os.system(f"sudo chmod a+w {path}") - try: + return + except OSError: + pass + # AGNOS 4.9.9.2 removes owner root; a privileged boot step opens these nodes to the stack so a direct write normally works. Only probe a non-interactive escalation once (interactive sudo blocks with no tty, and sudo -n itself can stall), and never let its failure or timeout propagate. + if _sudo_available is False: + return + try: + _sudo_available = subprocess.run(["sudo", "-n", "chmod", "a+w", path], timeout=2, capture_output=True).returncode == 0 + if _sudo_available: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - # fallback for debugfs files - os.system(f"sudo su -c 'echo {val} > {path}'") + except Exception: + _sudo_available = False def sudo_read(path: str) -> str: + global _sudo_available try: - return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + with open(path, encoding='utf8') as f: + return f.read().strip() + except OSError: + pass + if _sudo_available is False: + return "" + try: + result = subprocess.run(["sudo", "-n", "cat", path], timeout=2, capture_output=True, text=True) + _sudo_available = result.returncode == 0 + return result.stdout.strip() except Exception: + _sudo_available = False return "" diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/__init__.py b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/__init__.py similarity index 100% rename from artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/__init__.py rename to artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/__init__.py diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/ui/__init__.py b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/__init__.py similarity index 100% rename from artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/ui/__init__.py rename to artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/__init__.py diff --git a/artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/__init__.py b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/__init__.py similarity index 100% rename from artifacts/iqpilot_hephaestusd_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/__init__.py rename to artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/__init__.py diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_change.py b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_change.py similarity index 100% rename from artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_change.py rename to artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_change.py diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py similarity index 91% rename from artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py rename to artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py index c2cf85e5..6dfb043d 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py +++ b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py @@ -16,7 +16,6 @@ TurnDirection = custom.IQTurnSignalDirection TURN_TRIGGER_MPS = 20 * CV.MPH_TO_MS TURN_SPEED_GATE_MPS = TURN_TRIGGER_MPS LANE_CHANGE_SPEED_MIN = TURN_SPEED_GATE_MPS -TURN_SPEED_CEILING_MPS = 35 * CV.MPH_TO_MS @dataclass @@ -27,13 +26,6 @@ class _TurnGateState: refresh_tick: int = 0 -def _mph_param_to_mps(raw_value) -> float: - try: - return float(raw_value) * CV.MPH_TO_MS - except (TypeError, ValueError): - return TURN_TRIGGER_MPS - - def _resolve_signal_choice(speed_mps: float, speed_limit_mps: float, left_signal: bool, @@ -59,9 +51,8 @@ class TurnSignalPlanner: self.reload_setup() def _refresh_from_params(self) -> None: - requested_gate = _mph_param_to_mps(self._params.get("IQLaneTurnValue", return_default=True)) self._state.active = self._params.get_bool("IQLaneTurnDesire") - self._state.speed_limit_mps = min(TURN_SPEED_CEILING_MPS, requested_gate) + self._state.speed_limit_mps = TURN_TRIGGER_MPS def _consume_legacy_kwargs(self, **legacy) -> tuple[bool, bool, bool, bool, float]: return ( diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lateral_edge_guard.py b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lateral_edge_guard.py similarity index 100% rename from artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lateral_edge_guard.py rename to artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lateral_edge_guard.py diff --git a/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py index 48a34ea4..c38e33b9 100644 --- a/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py +++ b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py @@ -125,16 +125,32 @@ class Amplifier: print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") def set_configs(self, configs: list[AmpConfig]) -> bool: + import threading + tries = 15 backoff = 0. for i in range(tries): - try: - self._set_configs(configs) - return True - except OSError: + result = [None] + error = [None] + def _worker(): + try: + self._set_configs(configs) + result[0] = True + except OSError as e: + error[0] = e + t = threading.Thread(target=_worker, daemon=True) + t.start() + t.join(timeout=2) + if t.is_alive(): backoff += 0.1 time.sleep(backoff) - print(f"Failed to set amp config, {tries - i - 1} retries left") + print(f"Failed to set amp config (timeout), {tries - i - 1} retries left") + continue + if result[0]: + return True + backoff += 0.1 + time.sleep(backoff) + print(f"Failed to set amp config, {tries - i - 1} retries left") return False def set_global_shutdown(self, amp_disabled: bool) -> bool: diff --git a/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py index 2f71ed63..2506fb9e 100644 --- a/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py +++ b/artifacts/iqpilot_navd_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py @@ -469,8 +469,10 @@ class Tici(HardwareBase): if self.amplifier is not None: self.amplifier.initialize_configuration(self.get_device_type()) - # Allow hardwared to write engagement status to kmsg - os.system("sudo chmod a+w /dev/kmsg") + try: + subprocess.run(["sudo", "-n", "chmod", "a+w", "/dev/kmsg"], timeout=2, capture_output=True, check=False) + except Exception: + pass # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL gpio_init(GPIO.SOM_ST_IO, True) @@ -514,10 +516,10 @@ class Tici(HardwareBase): affine_irq(3, "xhci-hcd:usb1") try: pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() - subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) - subprocess.call(["sudo", "taskset", "-pc", "3", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessException as e: - print(str(e)) + subprocess.run(["sudo", "-n", "chrt", "-f", "-p", "1", pid], timeout=2, capture_output=True, check=False) + subprocess.run(["sudo", "-n", "taskset", "-pc", "3", pid], timeout=2, capture_output=True, check=False) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + pass def configure_modem(self): from iqpilot.common.params import Params diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/.source_sha b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/.source_sha index e4a64c95..64c809df 100644 --- a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/.source_sha +++ b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/.source_sha @@ -1 +1 @@ -e704fff8c426fe807222f8997f5b3b510777eaa0 +22cda719a4b809517acf792ee41d968a605ffb9f diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/__init__.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/__init__.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index 5294c73c..00000000 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/__init__.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/action_engine.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/action_engine.cpython-312-aarch64-linux-gnu.so new file mode 100644 index 00000000..4544bc73 Binary files /dev/null and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/action_engine.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/alpr_matcher.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/alpr_matcher.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 768ec72d..3220d6d8 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/alpr_matcher.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/alpr_matcher.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/amap_client.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/amap_client.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 863dab23..57eb3e4b Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/amap_client.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/amap_client.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/event_builder.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/event_builder.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 91062cbe..65cc7ae7 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/event_builder.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/event_builder.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/helpers.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/helpers.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 5f9fd02c..58e42097 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/helpers.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/helpers.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/incident_feed.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/incident_feed.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 304bb8a0..55df8881 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/incident_feed.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/incident_feed.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/iqmapd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/iqmapd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index e4643213..627e6dc7 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/iqmapd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/iqmapd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/long_decel.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/long_decel.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 1b77c6f7..b3f7e315 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/long_decel.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/long_decel.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/mapbox_client.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/mapbox_client.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 83692ea1..5e3aeabd Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/mapbox_client.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/mapbox_client.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/model_map_raster_v3.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/model_map_raster_v3.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 9a68fc5d..48caa850 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/model_map_raster_v3.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/model_map_raster_v3.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/model_map_tile.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/model_map_tile.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index ad0610a1..535cfd66 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/model_map_tile.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/model_map_tile.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_cameras.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_cameras.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index c3b7d88a..81f2e423 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_cameras.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_cameras.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_model_input.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_model_input.cpython-312-aarch64-linux-gnu.so new file mode 100644 index 00000000..54831579 Binary files /dev/null and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_model_input.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_turn_signals.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_turn_signals.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 6c787101..af71aa80 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_turn_signals.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/nav_turn_signals.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navbridge_source.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navbridge_source.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 5e20f984..0c029f54 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navbridge_source.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navbridge_source.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 3cc9ce65..1d298ea8 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navincidentd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navincidentd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index f258f541..89372d4d Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navincidentd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navincidentd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navrenderd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navrenderd.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 1be26efd..7e846e70 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navrenderd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/navrenderd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/osm_cameras.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/osm_cameras.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index f387dbd0..18dbbabc Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/osm_cameras.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/osm_cameras.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/reroute.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/reroute.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index cb3d10a0..1df0d416 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/reroute.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/reroute.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/road_incidents.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/road_incidents.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 2fa70060..03f2f1d9 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/road_incidents.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/road_incidents.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/route_manager.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/route_manager.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index aa681d5a..3b281b6b Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/route_manager.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/route_manager.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/runtime_common.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/runtime_common.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 126e28cd..08567ebf Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/runtime_common.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/runtime_common.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 06038f91..cacbfad0 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic_sources.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic_sources.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 026dc50a..271a8af0 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic_sources.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic_sources.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic_writer.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic_writer.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 42795a37..b0169501 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic_writer.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/traffic_writer.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/turn_desire.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/turn_desire.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 65018ac3..6a2aba0d Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/turn_desire.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/turn_desire.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/valhalla_process.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/valhalla_process.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 70e3b147..4b96a203 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/valhalla_process.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/valhalla_process.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/web_server.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/web_server.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index e7b0e7af..5d449693 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/web_server.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/web_server.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_updater_private/manifest.json b/artifacts/iqpilot_updater_private/manifest.json index d484b464..e0af9ceb 100644 --- a/artifacts/iqpilot_updater_private/manifest.json +++ b/artifacts/iqpilot_updater_private/manifest.json @@ -1,4 +1,5 @@ { + "bundle_version": 1792366342, "python/_iqclosure/_vendored.json": { "mode": 420, "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", @@ -14,25 +15,10 @@ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0 }, - "python/iqpilot_private/updater/_hf_maps_blob.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "69b858142aeb82cf730f042f6bc354dabb19579654872eaed3f73d482a856802", - "size": 67616 - }, - "python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "d8d9ccc3059b6239f2be41437e97b160b96278e419d84f9c0c243a2ba3ebf94c", - "size": 67616 - }, - "python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "cea69f651768732bcbbd1a7edff668876dcf6787c332e859f6f01c945d74cbe0", - "size": 67616 - }, "python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so": { - "mode": 493, - "sha256": "384782451d53a5ca89809f05bf5c68577596dfbaca3f2f82bb0a972663897491", - "size": 136608 + "mode": 416, + "sha256": "4747080855bf483d044614d3676a2241d2decff1f285aa4592c48d8156ce8ed5", + "size": 1065176 }, "runtime": { "entries": { @@ -43,9 +29,6 @@ } }, "signatures": { - "python/iqpilot_private/updater/_hf_maps_blob.cpython-312-aarch64-linux-gnu.so": "2hlUdv1cuGpzNrKoSx3VqRGdaVsgkkGhiHKBirfYkEwJmQJizQ966d6iZQoIzh8LEmRdhi+77iLFXQiEWApSDQ==", - "python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so": "EHoKO6/esRwuP7oxR9a5FXiQVPjh+3CmtXS1jNQor7yOpra/LijNPHvvu423y+vlOn1KkVCbhZgaHrsB/3oJDA==", - "python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so": "oqWDyKXImMpVEHwBohGW1nL7oZaNMu12aP/zMfowkjDAsHZJOM+TOicUNketIi990nJMbhApcJk3zpoM+1nGCw==", - "python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so": "xp2MN37LX95Rbt3zGC/ZysyTCv8wUi6xpzbHoTLMwRfq4cDpWafItWgFiypcn5ANsAQA7weqMbihmtq6EbV3Bg==" + "python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so": "QVSnJ2ij0v+VkQ557zcAGKhCM9k2k3F+idMIWlzOpgahoMi8w7yKAtmRx/hNUbcZabHDoCHDvYEMd2oM7piMCw==" } } diff --git a/artifacts/iqpilot_updater_private/manifest.json.sig b/artifacts/iqpilot_updater_private/manifest.json.sig index 3c650df7..9920230f 100644 --- a/artifacts/iqpilot_updater_private/manifest.json.sig +++ b/artifacts/iqpilot_updater_private/manifest.json.sig @@ -1 +1 @@ -Q62ipKl3jmGXHvKYVnhQY7sk4ZSS7AmhXf23DY4sYqoTP7hn9WDDlQD82bMhQJdcY+Obcr9fRn1EGmGTuMUjDw== +geWGPMi5tiN+dzvastdSeQphlF5Yrf7P+YZQlxTuoEGtgE7T58YpG/1Y4Y4NzVOtwXl2hNhCmBTrQUANRB8GBA== diff --git a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_hf_maps_blob.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_hf_maps_blob.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index aabbbf8e..00000000 Binary files a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_hf_maps_blob.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index 168c4883..00000000 Binary files a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so deleted file mode 100755 index b4bc7150..00000000 Binary files a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so and /dev/null differ diff --git a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so old mode 100755 new mode 100644 index 372c9b9e..e37a7811 Binary files a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/package_sources/iqdbc/iqdbc/can/parser.py b/artifacts/package_sources/iqdbc/iqdbc/can/parser.py index 0db42b18..e1d4e5a3 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/can/parser.py +++ b/artifacts/package_sources/iqdbc/iqdbc/can/parser.py @@ -236,7 +236,8 @@ class CANParser: if src != self.bus: continue bus_empty = False - if self.enable_capture: + # frames before controls are ready predate the ECU knockout and safety-mode forwarding change + if self.controls_ready and self.enable_capture: self.seen_addresses.add(address) state = self.message_states.get(address) if state is None or len(dat) > 64: diff --git a/artifacts/package_sources/iqdbc/iqdbc/can/tests/test_packer_parser.py b/artifacts/package_sources/iqdbc/iqdbc/can/tests/test_packer_parser.py index f6ea3922..fb83c97b 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/can/tests/test_packer_parser.py +++ b/artifacts/package_sources/iqdbc/iqdbc/can/tests/test_packer_parser.py @@ -10,10 +10,13 @@ MAX_BAD_COUNTER = 5 class TestCanParserPacker: def test_seen_addresses(self): parser = CANParser(TEST_DBC, [], 0) - parser.update([0, [(0x123, b'\x00', 0), (0x124, b'\x00', 1)]]) + parser.update([0, [(0x122, b'\x00', 0)]]) + assert parser.seen_addresses == set() + parser.controls_ready = True + parser.update([1, [(0x123, b'\x00', 0), (0x124, b'\x00', 1)]]) assert parser.seen_addresses == {0x123} parser.enable_capture = False - parser.update([1, [(0x125, b'\x00', 0)]]) + parser.update([2, [(0x125, b'\x00', 0)]]) assert parser.seen_addresses == {0x123} def test_packer(self): diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/__init__.py b/artifacts/package_sources/iqdbc/iqdbc/car/__init__.py index a77c8212..bcffa309 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/__init__.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/__init__.py @@ -91,6 +91,7 @@ class Bus(StrEnum): main = auto() party = auto() ap_party = auto() + ap_pt = auto() def rate_limit(new_value, last_value, dw_step, up_step): diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/car.capnp b/artifacts/package_sources/iqdbc/iqdbc/car/car.capnp index 48a71b75..feb54b6c 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/car.capnp +++ b/artifacts/package_sources/iqdbc/iqdbc/car/car.capnp @@ -727,6 +727,11 @@ struct CarParams { fcaGiorgio @32; rivian @33; volkswagenMeb @34; + byd @35; + volvo @36; + bmw @37; + mg @38; + teslaLegacy @39; } enum SteerControlType { diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/docs_definitions.py b/artifacts/package_sources/iqdbc/iqdbc/car/docs_definitions.py index 8950e687..5e3a153b 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/docs_definitions.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/docs_definitions.py @@ -143,6 +143,10 @@ class CarHarness(EnumBase): tesla_a = BaseCarHarness("Tesla A connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) tesla_b = BaseCarHarness("Tesla B connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) tesla_c = BaseCarHarness("Tesla C connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) + tesla_model_s_hw1 = BaseCarHarness("Tesla Model S HW1 connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) + tesla_model_x_hw1 = BaseCarHarness("Tesla Model X HW1 connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) + tesla_model_sx_hw2 = BaseCarHarness("Tesla Model S/X HW2 connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) + tesla_model_sx_hw3 = BaseCarHarness("Tesla Model S/X HW3 connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) psa_a = BaseCarHarness("PSA A connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) # custom harness diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carcontroller.py b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carcontroller.py index 773084f3..77cc9d8b 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carcontroller.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carcontroller.py @@ -482,7 +482,8 @@ class CarController(CarControllerBase): # LFA and HDA icons if self.frame % 5 == 0 and (not hda2 or hda2_long or camera_scc): - can_sends.extend(hyundaicanfd.create_lfahda_cluster(self.packer, CS, self.CAN, CC.longActive, CC.latActive)) + can_sends.extend(hyundaicanfd.create_lfahda_cluster(self.packer, CS, self.CAN, CC.longActive, CC.latActive, + synthesize=hda2_long and not camera_scc)) if not camera_scc: can_sends.extend(hyundaicanfd.create_lfa_icon_non_camera_scc(self.packer, CS, self.CAN, CC)) diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carstate.py b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carstate.py index 3a18f887..bf245406 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carstate.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carstate.py @@ -217,6 +217,8 @@ class CarState(CarStateBase): parser._add_message(name, ignore_counter = ignore_counter) # ← 이름으로 등록 def add_and_cache(parser, name: str, attr: str, ignore_counter: bool = False): + if parser is None: + return False add_if_seen(parser, name, ignore_counter) if name in parser.vl: # 등록 성공했을 때만 setattr(self, attr, parser.vl[name]) @@ -227,7 +229,7 @@ class CarState(CarStateBase): self.cp.controls_ready = self.cp_cam.controls_ready = True if self.cp_alt is not None: self.cp_alt.controls_ready = True - elif self.controls_ready_count == 100: + elif self.controls_ready_count == 127: self.cp.enable_capture = self.cp_cam.enable_capture = False if self.cp_alt is not None: self.cp_alt.enable_capture = False @@ -278,8 +280,11 @@ class CarState(CarStateBase): add_and_cache(self.cp, self.cruise_btns_msg_canfd, "cruise_buttons_msg") if not add_and_cache(self.cp_cam, "CAM_0x362", "cam_0x362") and self.cp_alt is not None: add_and_cache(self.cp_alt, "CAM_0x362", "cam_0x362") - if not add_and_cache(self.cp_alt, "CAM_0x2a4", "cam_0x2a4", ignore_counter=True) and self.cp_cam is not None: - add_and_cache(self.cp_cam, "CAM_0x2a4", "cam_0x2a4", ignore_counter=True) + # Standard HDA2 A-CAN loses its startup copy when Panda enables safety. + camera_first = self.CP.flags & HyundaiFlags.CANFD_HDA2 and not self.CP.flags & HyundaiFlags.CAMERA_SCC + primary, fallback = (self.cp_cam, self.cp_alt) if camera_first else (self.cp_alt, self.cp_cam) + if not add_and_cache(primary, "CAM_0x2a4", "cam_0x2a4", ignore_counter=True): + add_and_cache(fallback, "CAM_0x2a4", "cam_0x2a4", ignore_counter=True) elif self.controls_ready_count == 125: add_and_cache(self.cp, "MANUAL_SPEED_LIMIT_ASSIST", "manual_speed_limit_assist", ignore_counter = True) if self.gear_msg_canfd == "ACCELERATOR": diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/hyundaicanfd.py b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/hyundaicanfd.py index cd462ede..9f81fca9 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/hyundaicanfd.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/hyundaicanfd.py @@ -4,7 +4,7 @@ import numpy as np from iqdbc.car import CanBusBase from iqdbc.car.carlog import carlog from iqdbc.car.crc import CRC16_XMODEM -from iqdbc.car.hyundai.values import HyundaiFlags, HyundaiExtFlags +from iqdbc.car.hyundai.values import Buttons, HyundaiFlags, HyundaiExtFlags from iqpilot.common.params import Params from iqdbc.car.common.conversions import Conversions as CV from iqpilot.cereal import log @@ -334,18 +334,15 @@ def create_acc_cancel(packer, CP, CAN, cruise_info_copy): }) return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values) -def create_lfahda_cluster(packer, CS, CAN, long_active, lat_active): - - +def create_lfahda_cluster(packer, CS, CAN, long_active, lat_active, synthesize=False): if CS.lfahda_cluster is not None: values = copy.copy(CS.lfahda_cluster) rx_counter = values.pop("COUNTER", None) + elif synthesize: + values = {"HDA_OptUsmSta": 2} + rx_counter = None else: return [] - values = {} - rx_counter = None - values["LFA_OptUsmSta"] = 2 - values["HDA_OptUsmSta"] = 2 values["HDA_CntrlModSta"] = 2 if long_active else 0 values["HDA_LFA_SymSta"] = 2 if lat_active else 0 return [packer.make_can_msg("LFAHDA_CLUSTER", CAN.ECAN, values, rx_counter=rx_counter)] @@ -588,7 +585,7 @@ def create_adrv_messages(CP, packer, CAN, frame): ret = [] if not CP.flags & HyundaiFlags.CAMERA_SCC.value: - values = {} + ret.append(packer.make_can_msg("ADRV_0x51", CAN.ACAN, {})) ret.extend(create_fca_warning_light(CP, packer, CAN, frame)) if frame % 5 == 0: @@ -770,6 +767,9 @@ def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control, values = copy.copy(CS.cruise_buttons_msg) # Keep the physical long press on ECAN for CarState, but don't forward it to CAM. values["NORMAL_CRUISE_MAIN_BTN"] = 0 + # the camera's own SCC cancels when SET- is held ~2 s; openpilot sets speed from the ECAN copy + if CC.enabled and values["CRUISE_BUTTONS"] in (Buttons.RES_ACCEL, Buttons.SET_DECEL): + values["CRUISE_BUTTONS"] = Buttons.NONE if HDA_LFA_SymSta == 0 and 0 < frame % 200 < 12: values["LFA_BTN"] = 1 diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_camera_frame_source.py b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_camera_frame_source.py new file mode 100644 index 00000000..ec01b13e --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_camera_frame_source.py @@ -0,0 +1,115 @@ +import pytest + +from iqdbc.can import CANPacker +from iqdbc.car import Bus, gen_empty_fingerprint, get_safety_config, structs +from iqdbc.car.hyundai.interface import CarInterface +from iqdbc.car.hyundai.values import CAR, CAMERA_SCC_CAR, DBC, HyundaiFlags +from iqpilot.common.params import Params + +CAMERA_FRAME = 0x2A4 +CANFD_CARS = tuple(sorted(CAR.with_flags(HyundaiFlags.CANFD))) + + +def build(candidate, hda2, camera_scc, alpha_long, monkeypatch, tmp_path): + monkeypatch.setenv("PARAMS_ROOT", str(tmp_path)) + params = Params() + params.put_bool("ControlsReady", True) + params.put_int("HyundaiCameraSCC", camera_scc) + fingerprint = gen_empty_fingerprint() + if hda2: + fingerprint[2 if camera_scc == 0 else 1][0x50] = 16 + fingerprint[2][CAMERA_FRAME] = 24 + cp = CarInterface.get_params(candidate, fingerprint, [], alpha_long, False, False) + cp_iq = CarInterface.get_params_iq(cp, candidate, fingerprint, [], alpha_long, False, False) + return CarInterface(cp, cp_iq) + + +def register(interface, seen=(Bus.cam, Bus.alt)): + for bus in seen: + interface.can_parsers[bus].seen_addresses.add(CAMERA_FRAME) + interface.CS.controls_ready_count = 123 + interface.CS.monitor_fingerprint(interface.can_parsers, True) + + +@pytest.mark.parametrize("candidate", CANFD_CARS, ids=str) +@pytest.mark.parametrize("alpha_long", (False, True)) +@pytest.mark.parametrize("hda2,camera_scc,expected", ( + (True, 0, Bus.cam), (True, 1, Bus.alt), (True, 2, Bus.alt), + (False, 0, Bus.alt), (False, 1, Bus.alt), +)) +def test_source_preference_across_canfd_platforms(candidate, alpha_long, hda2, camera_scc, expected, monkeypatch, tmp_path): + interface = build(candidate, hda2, camera_scc, alpha_long, monkeypatch, tmp_path) + register(interface) + # Factory camera-SCC platforms retain their original A-CAN preference too. + if candidate in CAMERA_SCC_CAR: + expected = Bus.alt + for bus in (Bus.cam, Bus.alt): + assert (CAMERA_FRAME in interface.can_parsers[bus].addresses) == (bus == expected) + parser = interface.can_parsers[expected] + assert interface.CS.cam_0x2a4 is parser.vl["CAM_0x2a4"] + state = parser.message_states[CAMERA_FRAME] + assert not state.ignore_alive + assert not state.ignore_checksum + + +@pytest.mark.parametrize("hda2,camera_scc", ((True, 0), (True, 1), (False, 0))) +@pytest.mark.parametrize("seen", ((Bus.cam,), (Bus.alt,), ())) +def test_single_source_and_absent_frame(hda2, camera_scc, seen, monkeypatch, tmp_path): + interface = build(CAR.KIA_EV6, hda2, camera_scc, False, monkeypatch, tmp_path) + register(interface, seen) + for bus in (Bus.cam, Bus.alt): + assert (CAMERA_FRAME in interface.can_parsers[bus].addresses) == (bus in seen) + + +def test_startup_copy_disappears_but_camera_stays_valid(monkeypatch, tmp_path): + interface = build(CAR.KIA_EV6, True, 0, False, monkeypatch, tmp_path) + register(interface) + camera = interface.can_parsers[Bus.cam] + alternate = interface.can_parsers[Bus.alt] + packer = CANPacker(DBC[CAR.KIA_EV6][Bus.pt]) + for frame in range(200): + nanos = (frame + 1) * 50_000_000 + camera_msg = packer.make_can_msg("CAM_0x2a4", camera.bus, {"COUNTER": frame % 256}) + frames = [camera_msg] + if frame < 20: + frames.append((camera_msg[0], camera_msg[1], alternate.bus)) + for parser in (camera, alternate): + parser.update([(nanos, frames)]) + assert parser.can_valid + # A real loss on the selected source must still fail validation. + for frame in range(50): + camera.update([(nanos + (frame + 1) * 50_000_000, [])]) + valid = camera.can_valid + assert not valid + + +def test_missing_alternate_parser_uses_camera(monkeypatch, tmp_path): + interface = build(CAR.KIA_EV6, True, 0, False, monkeypatch, tmp_path) + del interface.can_parsers[Bus.alt] + register(interface, (Bus.cam,)) + assert CAMERA_FRAME in interface.can_parsers[Bus.cam].addresses + + +@pytest.mark.parametrize("camera_scc", (0, 1)) +def test_alternate_steering_camera_frame_is_unchanged(camera_scc, monkeypatch, tmp_path): + interface = build(CAR.KIA_EV6, True, camera_scc, False, monkeypatch, tmp_path) + interface.CP.flags |= HyundaiFlags.CANFD_HDA2_ALT_STEERING.value + for bus in (Bus.cam, Bus.alt): + interface.can_parsers[bus].seen_addresses.add(0x362) + register(interface, ()) + assert 0x362 in interface.can_parsers[Bus.cam].addresses + assert 0x362 not in interface.can_parsers[Bus.alt].addresses + assert interface.CS.cam_0x362 is interface.can_parsers[Bus.cam].vl["CAM_0x362"] + + +def test_standard_hda2_with_second_panda(monkeypatch, tmp_path): + interface = build(CAR.KIA_EV6, True, 0, False, monkeypatch, tmp_path) + safety = interface.CP.safetyConfigs[0] + interface.CP.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput), + get_safety_config(safety.safetyModel, safety.safetyParam)] + interface = CarInterface(interface.CP, interface.CP_IQ) + assert interface.can_parsers[Bus.cam].bus == 6 + assert interface.can_parsers[Bus.alt].bus == 4 + register(interface) + assert CAMERA_FRAME in interface.can_parsers[Bus.cam].addresses + assert CAMERA_FRAME not in interface.can_parsers[Bus.alt].addresses diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_camera_scc_button_forwarding.py b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_camera_scc_button_forwarding.py new file mode 100644 index 00000000..96146228 --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_camera_scc_button_forwarding.py @@ -0,0 +1,72 @@ +import pytest + +from iqdbc.can.parser import get_raw_value +from iqdbc.car import Bus, gen_empty_fingerprint, structs +from iqdbc.car.hyundai.hyundaicanfd import CanBus +from iqdbc.car.hyundai.interface import CarInterface +from iqdbc.car.hyundai.values import CAR, Buttons, HyundaiFlags +from iqpilot.common.params import Params + +CRUISE_BUTTONS = 0x1CF +CAMERA_SCC_EV6 = HyundaiFlags.CANFD | HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.EV + + +def camera_scc_ev6(monkeypatch, tmp_path): + monkeypatch.setenv("PARAMS_ROOT", str(tmp_path)) + Params().put_bool("ControlsReady", True) + fingerprint = gen_empty_fingerprint() + cp = CarInterface.get_params(CAR.KIA_EV6, fingerprint, [], True, False, False) + cp.flags = int(CAMERA_SCC_EV6) + cp.openpilotLongitudinalControl = True + cp_iq = CarInterface.get_params_iq(cp, CAR.KIA_EV6, fingerprint, [], True, False, False) + interface = CarInterface(cp, cp_iq) + interface.update([(10_000_000, [])]) + buttons = {sig: 0 for sig in interface.can_parsers[Bus.pt].dbc.name_to_msg["CRUISE_BUTTONS"].sigs} + interface.CS.cruise_buttons_msg = buttons + interface.CS.cruise_btns_msg_canfd = "CRUISE_BUTTONS" + interface.CS.MainMode_ACC = True + interface.CS.ACCMode = 1 + return interface + + +def buttons_sent_to_camera(interface, enabled, driver_button, frames=40): + cc = structs.CarControl() + cc.enabled = enabled + cc = cc.as_reader() + cc_iq = structs.IQCarControl() + interface.CS.cruise_buttons_msg["CRUISE_BUTTONS"] = driver_button + signal = interface.can_parsers[Bus.pt].dbc.name_to_msg["CRUISE_BUTTONS"].sigs["CRUISE_BUTTONS"] + camera_bus = CanBus(interface.CP).CAM + seen = [] + t = 10_000_000 + for _ in range(frames): + t += 10_000_000 + _, sends = interface.apply(cc, cc_iq, t) + seen += [get_raw_value(dat, signal) for addr, dat, bus in sends if addr == CRUISE_BUTTONS and bus == camera_bus] + return seen + + +@pytest.mark.parametrize("held", [Buttons.SET_DECEL, Buttons.RES_ACCEL]) +def test_engaged_camera_scc_never_sees_a_held_set_speed_button(held, monkeypatch, tmp_path): + interface = camera_scc_ev6(monkeypatch, tmp_path) + seen = buttons_sent_to_camera(interface, True, held) + assert seen + assert held not in seen + + +@pytest.mark.parametrize("button", [Buttons.CANCEL, Buttons.GAP_DIST]) +def test_engaged_camera_scc_still_gets_cancel_and_gap(button, monkeypatch, tmp_path): + interface = camera_scc_ev6(monkeypatch, tmp_path) + assert set(buttons_sent_to_camera(interface, True, button)) == {button} + + +def test_disengaged_camera_scc_gets_the_driver_buttons(monkeypatch, tmp_path): + interface = camera_scc_ev6(monkeypatch, tmp_path) + assert Buttons.SET_DECEL in buttons_sent_to_camera(interface, False, Buttons.SET_DECEL) + + +def test_engaged_camera_scc_keeps_openpilots_own_rearm_press(monkeypatch, tmp_path): + interface = camera_scc_ev6(monkeypatch, tmp_path) + interface.CS.ACCMode = 4 + interface.CS.out.vEgo = 20.0 + assert Buttons.SET_DECEL in buttons_sent_to_camera(interface, True, Buttons.NONE, frames=220) diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_hda2_long_adas_replacement.py b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_hda2_long_adas_replacement.py new file mode 100644 index 00000000..8597b2c7 --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_hda2_long_adas_replacement.py @@ -0,0 +1,77 @@ +import pytest + +from iqdbc.car import gen_empty_fingerprint, structs +from iqdbc.car.hyundai.hyundaicanfd import CanBus +from iqdbc.car.hyundai.interface import CarInterface +from iqdbc.car.hyundai.values import CAR, HyundaiFlags, HyundaiSafetyFlags +from iqdbc.safety.tests.libsafety import libsafety_py +from iqpilot.common.params import Params + +ADRV_0x51 = 0x51 +LFAHDA_CLUSTER = 0x1E0 +HDA2_EV = HyundaiFlags.CANFD | HyundaiFlags.CANFD_HDA2 | HyundaiFlags.EV + + +def ev6(monkeypatch, tmp_path, flags=HDA2_EV, openpilot_long=True): + monkeypatch.setenv("PARAMS_ROOT", str(tmp_path)) + Params().put_bool("ControlsReady", True) + fingerprint = gen_empty_fingerprint() + cp = CarInterface.get_params(CAR.KIA_EV6, fingerprint, [], openpilot_long, False, False) + cp.flags = int(flags) + cp.openpilotLongitudinalControl = openpilot_long + cp_iq = CarInterface.get_params_iq(cp, CAR.KIA_EV6, fingerprint, [], openpilot_long, False, False) + return CarInterface(cp, cp_iq) + + +def sent_frames(interface, frames=100): + cc = structs.CarControl().as_reader() + cc_iq = structs.IQCarControl() + out = [] + t = 0 + for _ in range(frames): + t += 10_000_000 + interface.update([(t, [])]) + _, sends = interface.apply(cc, cc_iq, t) + out.extend(sends) + return out + + +def test_hda2_long_replaces_the_silenced_adas_heartbeat_and_cluster_status(monkeypatch, tmp_path): + interface = ev6(monkeypatch, tmp_path) + can = CanBus(interface.CP) + sends = sent_frames(interface) + + heartbeat = [s for s in sends if s[0] == ADRV_0x51] + assert len(heartbeat) == 100 + assert {s[2] for s in heartbeat} == {can.ACAN} + + cluster = [s for s in sends if s[0] == LFAHDA_CLUSTER] + assert len(cluster) == 20 + assert {s[2] for s in cluster} == {can.ECAN} + + +@pytest.mark.parametrize("flags,openpilot_long", [ + (HDA2_EV, False), + (HDA2_EV | HyundaiFlags.CAMERA_SCC, True), +]) +def test_cars_that_keep_their_adas_ecu_send_neither(flags, openpilot_long, monkeypatch, tmp_path): + interface = ev6(monkeypatch, tmp_path, flags, openpilot_long) + addresses = {s[0] for s in sent_frames(interface)} + + assert ADRV_0x51 not in addresses + if not openpilot_long: + assert LFAHDA_CLUSTER not in addresses + + +def test_panda_safety_transmits_the_replacements_for_this_ev6(monkeypatch, tmp_path): + interface = ev6(monkeypatch, tmp_path) + sends = [s for s in sent_frames(interface) if s[0] in (ADRV_0x51, LFAHDA_CLUSTER)] + safety = libsafety_py.libsafety + param = HyundaiSafetyFlags.EV_GAS | HyundaiSafetyFlags.LONG | HyundaiSafetyFlags.CANFD_LKA_STEERING + assert param == 21 + assert safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, param) == 0 + safety.init_tests() + + assert sends + for addr, dat, bus in sends: + assert safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat)), (hex(addr), bus) diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_pre_ready_frames.py b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_pre_ready_frames.py new file mode 100644 index 00000000..0d045185 --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/tests/test_pre_ready_frames.py @@ -0,0 +1,58 @@ +import pytest + +from iqdbc.can import CANPacker +from iqdbc.car import Bus, gen_empty_fingerprint +from iqdbc.car.hyundai.hyundaicanfd import CanBus +from iqdbc.car.hyundai.interface import CarInterface +from iqdbc.car.hyundai.values import CAR, DBC, HyundaiFlags +from iqpilot.common.params import Params + +HDA2_EV = HyundaiFlags.CANFD | HyundaiFlags.CANFD_HDA2 | HyundaiFlags.EV +DT_NS = 10_000_000 + + +def hda2_ev6(openpilot_long, monkeypatch, tmp_path): + monkeypatch.setenv("PARAMS_ROOT", str(tmp_path)) + Params().put_bool("ControlsReady", False) + fingerprint = gen_empty_fingerprint() + cp = CarInterface.get_params(CAR.KIA_EV6, fingerprint, [], openpilot_long, False, False) + cp.flags = int(HDA2_EV) + cp.openpilotLongitudinalControl = openpilot_long + cp_iq = CarInterface.get_params_iq(cp, CAR.KIA_EV6, fingerprint, [], openpilot_long, False, False) + return CarInterface(cp, cp_iq) + + +def drive(interface, frames_before_ready, frames_after_ready, steps_before=60, steps_after=200): + t = 0 + for _ in range(steps_before): + t += DT_NS + interface.update([(t, frames_before_ready)]) + Params().put_bool("ControlsReady", True) + for _ in range(steps_after): + t += DT_NS + interface.update([(t, frames_after_ready)]) + + +def frame(packer, name, bus): + addr, dat, _ = packer.make_can_msg(name, bus, {}) + return addr, dat, bus + + +@pytest.mark.parametrize("openpilot_long", [True, False]) +def test_frames_that_stop_at_controls_ready_are_never_registered(openpilot_long, monkeypatch, tmp_path): + interface = hda2_ev6(openpilot_long, monkeypatch, tmp_path) + can = CanBus(interface.CP) + packer = CANPacker(DBC[CAR.KIA_EV6][Bus.pt]) + scc_on_ecan = frame(packer, "SCC_CONTROL", can.ECAN) + lane_on_acan = frame(packer, "CAM_0x2a4", can.ACAN) + lane_on_cam = frame(packer, "CAM_0x2a4", can.CAM) + + before = [scc_on_ecan, lane_on_acan, lane_on_cam] + after = [lane_on_cam] if openpilot_long else [scc_on_ecan, lane_on_cam] + drive(interface, before, after) + + parsers = interface.can_parsers + assert lane_on_cam[0] in parsers[Bus.cam].addresses + assert lane_on_acan[0] not in parsers[Bus.alt].addresses + assert (scc_on_ecan[0] in parsers[Bus.pt].addresses) == (not openpilot_long) + assert interface.CS.cam_0x2a4 is not None diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/carcontroller.py b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/carcontroller.py index 5b91e937..99a94871 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/carcontroller.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/carcontroller.py @@ -5,7 +5,8 @@ from iqdbc.car.lateral import apply_steer_angle_limits_vm from iqdbc.car.interfaces import CarControllerBase from iqdbc.car.tesla import TESLA_BLINKERS from iqdbc.car.tesla.teslacan import TeslaCAN -from iqdbc.car.tesla.values import CarControllerParams +from iqdbc.car.tesla.teslacan_legacy import TeslaCANLegacy +from iqdbc.car.tesla.values import CAR, CarControllerParams, LEGACY_CARS, LEGACY_HW1_CARS, get_legacy_canbus from iqdbc.car.vehicle_model import VehicleModel from iqpilot.selfdrive.car.enhanced_stock_longitudinal_control import get_set_speed_kph_from_params from iqdbc.lvbs.car.tesla.torque_blend import TorqueBlendController @@ -36,7 +37,46 @@ class CarController(CarControllerBase): self.blinker_request_prev = False self.blinker_cancel_frame = 0 + self.legacy = CP.carFingerprint in LEGACY_CARS + if self.legacy: + from iqdbc.car.tesla.interface import CarInterface + canbus = get_legacy_canbus(CP.carFingerprint) + packers = {canbus.party: CANPacker(dbc_names[Bus.party]), canbus.powertrain: CANPacker(dbc_names[Bus.pt])} + self.tesla_can_legacy = TeslaCANLegacy(packers, canbus) + self.VM = VehicleModel(CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_HW3)) + + def update_legacy(self, CC, CS): + actuators = CC.actuators + can_sends = [] + + lat_active = CC.latActive and CS.hands_on_level < 3 + + if self.frame % CarControllerParams.STEER_STEP == 0: + self.apply_angle_last = apply_steer_angle_limits_vm(actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg, + lat_active, CarControllerParams, self.VM) + counter = (self.frame // CarControllerParams.STEER_STEP) % 16 + can_sends.append(self.tesla_can_legacy.create_steering_control(counter, self.apply_angle_last, lat_active)) + + if self.frame % 10 == 0 and self.CP.carFingerprint not in LEGACY_HW1_CARS: + counter = (self.frame // 10) % 16 + can_sends.append(self.tesla_can_legacy.create_steering_allowed(counter)) + + if self.frame % 4 == 0: + state = 13 if CC.cruiseControl.cancel else 4 + accel = float(np.clip(actuators.accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX)) + counter = (self.frame // 4) % 8 + can_sends.append(self.tesla_can_legacy.create_longitudinal_command(state, accel, counter, CS.out.vEgo, CC.longActive, CS.out.gasPressed)) + + new_actuators = actuators.as_builder() + new_actuators.steeringAngleDeg = self.apply_angle_last + + self.frame += 1 + return new_actuators, can_sends + def update(self, CC, CC_IQ, CS, now_nanos): + if self.legacy: + return self.update_legacy(CC, CS) + actuators = CC.actuators can_sends = [] diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/carstate.py b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/carstate.py index 172836ed..a1622bad 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/carstate.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/carstate.py @@ -4,7 +4,8 @@ from iqdbc.car import Bus, structs from iqdbc.car.common.conversions import Conversions as CV from iqdbc.car.interfaces import CarStateBase from iqdbc.car.tesla import TESLA_BLINKERS -from iqdbc.car.tesla.values import DBC, CANBUS, GEAR_MAP, STEER_THRESHOLD, TeslaFlags +from iqdbc.car.tesla.values import (DBC, CANBUS, CAR, GEAR_MAP, LEGACY_CARS, LEGACY_HW3_CARS, STEER_THRESHOLD, TeslaFlags, + get_legacy_canbus) from iqdbc.lvbs.car.tesla.iq_carstate import IQCarState from iqdbc.lvbs.car.tesla.values import TeslaFlagsIQ, TeslaSafetyFlagsIQ @@ -24,12 +25,10 @@ def stock_autosteer_invalid(CP, CP_IQ, autopilot_state: int, autosteer_enabled: class CarState(CarStateBase, IQCarState): def __init__(self, CP, CP_IQ): - from iqpilot.system.proprietary_runtime._verified_import import import_verified_module - iq_lvbs_alc = import_verified_module("iqpilot_alc_private", "iqpilot_private.konn3kt.iqlvbs.alc") CarStateBase.__init__(self, CP, CP_IQ) IQCarState.__init__(self, CP, CP_IQ) + self.legacy = CP.carFingerprint in LEGACY_CARS self.can_define = CANDefine(DBC[CP.carFingerprint][Bus.party]) - self.shifter_values = self.can_define.dv["DI_systemStatus"]["DI_gear"] self.summon = False self.summon_prev = False @@ -38,9 +37,25 @@ class CarState(CarStateBase, IQCarState): self.hands_on_level = 0 self.das_control = None self.das_body_controls_dat = b"" - self._odometer_store = iq_lvbs_alc.create_vehicle_odometer_store(CP, Params()) self.cruise_override = False + if self.legacy: + self.can_defines = { + **self.can_define.dv, + **CANDefine(DBC[CP.carFingerprint][Bus.pt]).dv, + **CANDefine(DBC[CP.carFingerprint][Bus.chassis]).dv, + } + self.shifter_values = self.can_defines["DI_torque2"]["DI_gear"] + self._odometer_store = None + else: + self.shifter_values = self.can_define.dv["DI_systemStatus"]["DI_gear"] + try: + from iqpilot.system.proprietary_runtime._verified_import import import_verified_module + iq_lvbs_alc = import_verified_module("iqpilot_alc_private", "iqpilot_private.konn3kt.iqlvbs.alc") + self._odometer_store = iq_lvbs_alc.create_vehicle_odometer_store(CP, Params()) + except Exception: + self._odometer_store = None + def update_summon_state(self, summon_state: str, cruise_enabled: bool): summon_now = summon_state in ("ACTIVE", "COMPLETE", "SELFPARK_STARTED") if summon_now and not self.summon_prev and not self.cruise_enabled_prev: @@ -50,7 +65,85 @@ class CarState(CarStateBase, IQCarState): self.summon_prev = summon_now self.cruise_enabled_prev = cruise_enabled + def update_legacy(self, can_parsers) -> tuple[structs.CarState, structs.IQCarState]: + cp_party = can_parsers[Bus.party] + cp_ap_party = can_parsers[Bus.ap_party] + cp_pt = can_parsers[Bus.pt] + cp_ap_pt = can_parsers[Bus.ap_pt] + cp_chassis = can_parsers[Bus.chassis] + ret = structs.CarState() + ret_iq = structs.IQCarState() + + ret.vEgoRaw = cp_chassis.vl["ESP_B"]["ESP_vehicleSpeed"] * CV.KPH_TO_MS + ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw) + + ret.gasPressed = cp_pt.vl["DI_torque1"]["DI_pedalPos"] > 0 + + ret.brakePressed = cp_chassis.vl["BrakeMessage"]["driverBrakeStatus"] == 2 + + if self.CP.carFingerprint in LEGACY_HW3_CARS: + epas_status = cp_party.vl["EPAS_sysStatus"] + else: + epas_status = cp_chassis.vl["EPAS_sysStatus"] + self.hands_on_level = epas_status["EPAS_handsOnLevel"] + ret.steeringAngleDeg = -epas_status["EPAS_internalSAS"] + ret.steeringRateDeg = -cp_chassis.vl["STW_ANGLHP_STAT"]["StW_AnglHP_Spd"] + ret.steeringTorque = -epas_status["EPAS_torsionBarTorque"] + + ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > STEER_THRESHOLD, 5) + + eac_status = self.can_defines["EPAS_sysStatus"]["EPAS_eacStatus"].get(int(epas_status["EPAS_eacStatus"]), None) + ret.steerFaultPermanent = eac_status == "EAC_FAULT" + ret.steerFaultTemporary = eac_status == "EAC_INHIBITED" + + eac_error_code = self.can_defines["EPAS_sysStatus"]["EPAS_eacErrorCode"].get(int(epas_status["EPAS_eacErrorCode"]), None) + ret.steeringDisengage = self.hands_on_level >= 3 or (eac_status == "EAC_INHIBITED" and + eac_error_code == "EAC_ERROR_HIGH_ANGLE_RATE_SAFETY") + + cruise_state = self.can_defines["DI_state"]["DI_cruiseState"].get(int(cp_chassis.vl["DI_state"]["DI_cruiseState"]), None) + speed_units = self.can_defines["DI_state"]["DI_speedUnits"].get(int(cp_chassis.vl["DI_state"]["DI_speedUnits"]), None) + + cruise_enabled = cruise_state in ("ENABLED", "STANDSTILL", "OVERRIDE", "PRE_FAULT", "PRE_CANCEL") + + ret.cruiseState.enabled = cruise_enabled + if speed_units == "KPH": + ret.cruiseState.speed = max(cp_chassis.vl["DI_state"]["DI_digitalSpeed"] * CV.KPH_TO_MS, 1e-3) + elif speed_units == "MPH": + ret.cruiseState.speed = max(cp_chassis.vl["DI_state"]["DI_digitalSpeed"] * CV.MPH_TO_MS, 1e-3) + ret.cruiseState.available = cruise_state == "STANDBY" or ret.cruiseState.enabled + ret.cruiseState.standstill = False + ret.standstill = cruise_state == "STANDSTILL" + ret.accFaulted = cruise_state == "FAULT" + + ret.gearShifter = GEAR_MAP[self.can_defines["DI_torque2"]["DI_gear"].get(int(cp_chassis.vl["DI_torque2"]["DI_gear"]), "DI_GEAR_INVALID")] + + doors = ["DOOR_STATE_FL", "DOOR_STATE_FR", "DOOR_STATE_RL", "DOOR_STATE_RR", "DOOR_STATE_FrontTrunk", "BOOT_STATE"] + ret.doorOpen = any(self.can_defines["GTW_carState"][door].get(int(cp_chassis.vl["GTW_carState"][door]), "OPEN") == "OPEN" for door in doors) + + if self.CP.carFingerprint == CAR.TESLA_MODEL_X_HW1: + ret.leftBlinker = cp_chassis.vl["STW_ACTN_RQ"]["TurnIndLvr_Stat"] == 1 + ret.rightBlinker = cp_chassis.vl["STW_ACTN_RQ"]["TurnIndLvr_Stat"] == 2 + else: + ret.leftBlinker = cp_chassis.vl["GTW_carState"]["BC_indicatorLStatus"] == 1 + ret.rightBlinker = cp_chassis.vl["GTW_carState"]["BC_indicatorRStatus"] == 1 + + if self.CP.flags & TeslaFlags.NO_SDM1: + ret.seatbeltUnlatched = cp_chassis.vl["RCM_status"]["RCM_buckleDriverStatus"] != 1 + else: + ret.seatbeltUnlatched = cp_chassis.vl["SDM1"]["SDM_bcklDrivStatus"] != 1 + + ret.stockAeb = cp_ap_pt.vl["DAS_control"]["DAS_aebEvent"] == 1 + + ret.stockLkas = cp_ap_party.vl["DAS_steeringControl"]["DAS_steeringControlType"] == 2 + + self.das_control = copy.copy(cp_ap_pt.vl["DAS_control"]) + + return ret, ret_iq + def update(self, can_parsers) -> tuple[structs.CarState, structs.IQCarState]: + if self.legacy: + return self.update_legacy(can_parsers) + cp_party = can_parsers[Bus.party] cp_ap_party = can_parsers[Bus.ap_party] ret = structs.CarState() @@ -188,6 +281,16 @@ class CarState(CarStateBase, IQCarState): @staticmethod def get_can_parsers(CP, CP_IQ): + if CP.carFingerprint in LEGACY_CARS: + canbus = get_legacy_canbus(CP.carFingerprint) + return { + Bus.party: CANParser(DBC[CP.carFingerprint][Bus.party], [], canbus.party), + Bus.ap_party: CANParser(DBC[CP.carFingerprint][Bus.party], [], canbus.autopilot_party), + Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], canbus.powertrain), + Bus.ap_pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], canbus.autopilot_powertrain), + Bus.chassis: CANParser(DBC[CP.carFingerprint][Bus.chassis], [], canbus.chassis), + } + parsers = { Bus.party: CANParser(DBC[CP.carFingerprint][Bus.party], [], CANBUS.party), Bus.ap_party: CANParser(DBC[CP.carFingerprint][Bus.party], [], CANBUS.autopilot_party), diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/fingerprints.py b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/fingerprints.py index 39373440..2a0c8888 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/fingerprints.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/fingerprints.py @@ -55,4 +55,31 @@ FW_VERSIONS = { b'TeM3_SP_XP002p2_0.0.0 (36),XPR003.10.0', ], }, + CAR.TESLA_MODEL_X_HW1: { + (Ecu.eps, 0x730, None): [ + b'1057658-00-00B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + }, + CAR.TESLA_MODEL_X_HW2: { + (Ecu.eps, 0x730, None): [ + b'\x10#\x81', + ], + }, + CAR.TESLA_MODEL_S_HW1: { + (Ecu.eps, 0x730, None): [ + b'1016704-00-HAA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'\x10\x00A', + ], + }, + CAR.TESLA_MODEL_S_HW2: { + (Ecu.eps, 0x730, None): [ + b'\x10#\x01', + ], + }, + CAR.TESLA_MODEL_S_HW3: { + (Ecu.eps, 0x730, None): [ + b'SX_0.0.0 (99),S013.7', + b'SX_0.0.0 (99),SR013.7', + ], + }, } diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/interface.py b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/interface.py index a25089aa..cf8a40c8 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/interface.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/interface.py @@ -2,7 +2,8 @@ from iqdbc.car import Bus, get_safety_config, structs from iqdbc.car.interfaces import CarInterfaceBase from iqdbc.car.tesla.carcontroller import CarController from iqdbc.car.tesla.carstate import CarState -from iqdbc.car.tesla.values import TeslaSafetyFlags, TeslaFlags, CANBUS, CAR, DBC, Ecu, is_legacy_das_steering +from iqdbc.car.tesla.values import (TeslaSafetyFlags, TeslaFlags, TeslaLegacySafetyFlags, CANBUS, CAR, DBC, Ecu, LEGACY_CARS, + LEGACY_HW1_CARS, LEGACY_HW2_CARS, is_legacy_das_steering) from iqdbc.car.tesla.radar_interface import RadarInterface, RADAR_START_ADDR from iqdbc.lvbs.car.tesla.values import TeslaFlagsIQ, TeslaSafetyFlagsIQ @@ -15,6 +16,9 @@ class CarInterface(CarInterfaceBase): @staticmethod def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + if candidate in LEGACY_CARS: + return CarInterface._get_params_legacy(ret, candidate, fingerprint) + ret.brand = "tesla" ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.tesla)] @@ -63,10 +67,45 @@ class CarInterface(CarInterfaceBase): return ret + @staticmethod + def _get_params_legacy(ret: structs.CarParams, candidate, fingerprint) -> structs.CarParams: + ret.brand = "tesla" + + if not any(0x201 in messages for messages in fingerprint.values()): + ret.flags |= TeslaFlags.NO_SDM1.value + + safety_model = structs.CarParams.SafetyModel.teslaLegacy + if candidate in LEGACY_HW1_CARS: + ret.safetyConfigs = [ + get_safety_config(safety_model, int(TeslaLegacySafetyFlags.HW1)), + ] + else: + hw = TeslaLegacySafetyFlags.HW2 if candidate in LEGACY_HW2_CARS else TeslaLegacySafetyFlags.HW3 + ret.safetyConfigs = [ + get_safety_config(safety_model, int(hw)), + get_safety_config(safety_model, int(hw | TeslaLegacySafetyFlags.EXTERNAL_PANDA)), + ] + + ret.steerLimitTimer = 0.4 + ret.steerActuatorDelay = 0.1 + ret.steerAtStandstill = True + + ret.steerControlType = structs.CarParams.SteerControlType.angle + ret.radarUnavailable = candidate == CAR.TESLA_MODEL_S_HW2 + + ret.alphaLongitudinalAvailable = True + ret.openpilotLongitudinalControl = True + ret.safetyConfigs[0].safetyParam |= TeslaLegacySafetyFlags.LONG_CONTROL.value + + return ret + @staticmethod def _get_params_iq(stock_cp: structs.CarParams, ret: structs.IQCarParams, candidate, fingerprint: dict[int, dict[int, int]], car_fw: list[structs.CarParams.CarFw], alpha_long: bool, is_release_iq: bool, docs: bool) -> structs.IQCarParams: + if candidate in LEGACY_CARS: + return ret + stock_cp.enableBsm = True if candidate == CAR.TESLA_MODEL_X: @@ -74,7 +113,7 @@ class CarInterface(CarInterfaceBase): # Vehicle-bus messages can be slow enough to miss the initial capture window. # Accept either the established 0x3DF marker or the absolute odometer frame. - vehicle_bus_seen = any(0x3DF in bus or 0x3B6 in bus for bus in fingerprint.values()) + vehicle_bus_seen = any(address in fingerprint[CANBUS.vehicle] for address in (0x3DF, 0x3B6)) if vehicle_bus_seen: ret.flags |= TeslaFlagsIQ.HAS_VEHICLE_BUS.value ret.iqSafetyFlags |= TeslaSafetyFlagsIQ.HAS_VEHICLE_BUS diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/radar_interface.py b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/radar_interface.py index d21d0228..b1dad859 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/radar_interface.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/radar_interface.py @@ -1,35 +1,67 @@ from iqdbc.can import CANParser from iqdbc.car import Bus, structs from iqdbc.car.interfaces import RadarInterfaceBase -from iqdbc.car.tesla.values import DBC +from iqdbc.car.tesla.values import DBC, LEGACY_BOSCH_RADAR_CARS, LEGACY_CARS, get_legacy_canbus RADAR_START_ADDR = 0x410 RADAR_MSG_COUNT = 80 # 40 points * 2 messages each +BOSCH_NUM_POINTS = 32 +BOSCH_RADAR_POINT_FREQ = 8 +BOSCH_TRIGGER_MSG = 878 +BOSCH_MAX_RANGE = 250.0 +BOSCH_MIN_PROB_EXIST = 50.0 + + +def _is_bosch_radar(CP): + return CP.carFingerprint in LEGACY_BOSCH_RADAR_CARS + + +def _radar_bus(CP): + if CP.carFingerprint in LEGACY_CARS: + return get_legacy_canbus(CP.carFingerprint).radar + return 1 + def get_radar_can_parser(CP): if Bus.radar not in DBC[CP.carFingerprint]: return None - messages = [('RadarStatus', 16)] - for i in range(RADAR_MSG_COUNT // 2): + if _is_bosch_radar(CP): + messages = [('TeslaRadarSguInfo', 8)] + num_points = BOSCH_NUM_POINTS + freq = BOSCH_RADAR_POINT_FREQ + else: + messages = [('RadarStatus', 16)] + num_points = RADAR_MSG_COUNT // 2 + freq = 16 + + for i in range(num_points): messages.extend([ - (f'RadarPoint{i}_A', 16), - (f'RadarPoint{i}_B', 16), + (f'RadarPoint{i}_A', freq), + (f'RadarPoint{i}_B', freq), ]) - return CANParser(DBC[CP.carFingerprint][Bus.radar], messages, 1) + return CANParser(DBC[CP.carFingerprint][Bus.radar], messages, _radar_bus(CP)) class RadarInterface(RadarInterfaceBase): def __init__(self, CP, CP_IQ): super().__init__(CP, CP_IQ) self.updated_messages = set() - self.trigger_msg = RADAR_START_ADDR + RADAR_MSG_COUNT - 1 self.track_id = 0 + self.bosch_radar = _is_bosch_radar(CP) + if self.bosch_radar: + self.trigger_msg = BOSCH_TRIGGER_MSG + self.num_points = BOSCH_NUM_POINTS + else: + self.trigger_msg = RADAR_START_ADDR + RADAR_MSG_COUNT - 1 + self.num_points = RADAR_MSG_COUNT // 2 + self.radar_off_can = CP.radarUnavailable self.rcp = get_radar_can_parser(CP) + self.last_radar_data: structs.RadarData | None = None def update(self, can_strings): if self.radar_off_can or self.rcp is None: @@ -39,10 +71,13 @@ class RadarInterface(RadarInterfaceBase): self.updated_messages.update(vls) if self.trigger_msg not in self.updated_messages: + if self.bosch_radar: + return self.last_radar_data return None rr = self._update(self.updated_messages) self.updated_messages.clear() + self.last_radar_data = rr return rr @@ -54,13 +89,17 @@ class RadarInterface(RadarInterfaceBase): if not self.rcp.can_valid: ret.errors.canError = True - radar_status = self.rcp.vl['RadarStatus'] - if radar_status['shortTermUnavailable']: - ret.errors.radarUnavailableTemporary = True - if radar_status['sensorBlocked'] or radar_status['vehDynamicsError']: - ret.errors.radarFault = True + if self.bosch_radar: + if self.rcp.vl['TeslaRadarSguInfo']['RADC_HWFail']: + ret.errors.radarFault = True + else: + radar_status = self.rcp.vl['RadarStatus'] + if radar_status['shortTermUnavailable']: + ret.errors.radarUnavailableTemporary = True + if radar_status['sensorBlocked'] or radar_status['vehDynamicsError']: + ret.errors.radarFault = True - for i in range(RADAR_MSG_COUNT // 2): + for i in range(self.num_points): msg_a = self.rcp.vl[f'RadarPoint{i}_A'] msg_b = self.rcp.vl[f'RadarPoint{i}_B'] @@ -73,6 +112,11 @@ class RadarInterface(RadarInterfaceBase): del self.pts[i] continue + if self.bosch_radar and (msg_a['LongDist'] > BOSCH_MAX_RANGE or msg_a['LongDist'] <= 0 or msg_a['ProbExist'] < BOSCH_MIN_PROB_EXIST): + if i in self.pts: + del self.pts[i] + continue + if i not in self.pts: self.pts[i] = structs.RadarData.RadarPoint() self.pts[i].trackId = self.track_id diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/teslacan_legacy.py b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/teslacan_legacy.py new file mode 100644 index 00000000..590da5bf --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/teslacan_legacy.py @@ -0,0 +1,72 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" +from iqdbc.car.common.conversions import Conversions as CV +from iqdbc.car.interfaces import V_CRUISE_MAX +from iqdbc.car.tesla.values import CarControllerParams + + +class TeslaCANLegacy: + def __init__(self, packers, canbus): + self.packers = packers + self.canbus = canbus + self.CCP = CarControllerParams + self.jerk_upper = self.CCP.JERK_LIMIT_MAX + self.jerk_lower = self.CCP.JERK_LIMIT_MIN + + @staticmethod + def checksum(msg_id, dat): + ret = (msg_id & 0xFF) + ((msg_id >> 8) & 0xFF) + ret += sum(dat) + return ret & 0xFF + + def create_steering_control(self, counter, angle, enabled): + values = { + "DAS_steeringControlCounter": counter, + "DAS_steeringAngleRequest": -angle, + "DAS_steeringHapticRequest": 0, + "DAS_steeringControlType": 1 if enabled else 0, + } + + packer = self.packers[self.canbus.party] + data = packer.make_can_msg("DAS_steeringControl", self.canbus.party, values)[1] + values["DAS_steeringControlChecksum"] = self.checksum(0x488, data[:3]) + return packer.make_can_msg("DAS_steeringControl", self.canbus.party, values) + + def create_longitudinal_command(self, acc_state, accel, counter, v_ego, active, gas_pressed): + set_speed = max(v_ego * CV.MS_TO_KPH, 0) + if active: + set_speed = 0 if accel < 0 else V_CRUISE_MAX + + if gas_pressed: + self.jerk_upper = self.jerk_lower = 0.0 + else: + self.jerk_lower = max(self.jerk_lower - self.CCP.JERK_RAMP_RATE, self.CCP.JERK_LIMIT_MIN) + self.jerk_upper = min(self.jerk_upper + self.CCP.JERK_RAMP_RATE, self.CCP.JERK_LIMIT_MAX) + + values = { + "DAS_setSpeed": set_speed, + "DAS_accState": acc_state, + "DAS_aebEvent": 0, + "DAS_jerkMin": self.jerk_lower, + "DAS_jerkMax": self.jerk_upper, + "DAS_accelMin": accel, + "DAS_accelMax": max(accel, 0), + "DAS_controlCounter": counter, + } + + packer = self.packers[self.canbus.powertrain] + data = packer.make_can_msg("DAS_control", self.canbus.powertrain, values)[1] + values["DAS_controlChecksum"] = self.checksum(0x2b9, data[:7]) + return packer.make_can_msg("DAS_control", self.canbus.powertrain, values) + + def create_steering_allowed(self, counter): + values = { + "APS_eacMonitorCounter": counter, + "APS_eacAllow": 1, + } + + packer = self.packers[self.canbus.party] + data = packer.make_can_msg("APS_eacMonitor", self.canbus.party, values)[1] + values["APS_eacMonitorChecksum"] = self.checksum(0x27d, data[:2]) + return packer.make_can_msg("APS_eacMonitor", self.canbus.party, values) diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/tests/test_tesla.py b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/tests/test_tesla.py index 6a0d64ca..3eac82da 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/tests/test_tesla.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/tests/test_tesla.py @@ -13,7 +13,7 @@ from iqdbc.car.tesla.teslacan import TeslaCAN from iqdbc.car.tesla.radar_interface import RADAR_START_ADDR from iqdbc.car.tesla.carcontroller import CarController from iqdbc.car.tesla.carstate import CarState, stock_autosteer_invalid -from iqdbc.car.tesla.values import (CANBUS, CAR, FW_PATTERN, LEGACY_DAS_STEERING_FW, TeslaFlags, TeslaSafetyFlags, +from iqdbc.car.tesla.values import (CANBUS, CAR, FW_PATTERN, LEGACY_CARS, LEGACY_DAS_STEERING_FW, TeslaFlags, TeslaSafetyFlags, get_platform_codes, is_ascii_version_fw, is_hw4_gen2_fw, is_legacy_das_steering) from iqdbc.lvbs.car.interfaces import _apply_tesla_options from iqdbc.lvbs.car.tesla.values import TeslaFlagsIQ, TeslaSafetyFlagsIQ @@ -44,6 +44,8 @@ def fw_match(fw: bytes): class TestTeslaFwPattern: def test_all_known_fw_parses(self): for car, ecus in FW_VERSIONS.items(): + if car in LEGACY_CARS: + continue for fws in ecus.values(): for fw in fws: if not is_ascii_version_fw(fw): @@ -53,6 +55,8 @@ class TestTeslaFwPattern: def test_only_hw4_gen2_ships_a_binary_fw(self): # a binary FW can only ever match exactly, so it must not creep into other platforms for car, ecus in FW_VERSIONS.items(): + if car in LEGACY_CARS: + continue for fws in ecus.values(): for fw in fws: if is_ascii_version_fw(fw): @@ -217,6 +221,31 @@ class TestStockAutosteerInvalid: class TestTeslaFingerprint: + @pytest.mark.parametrize("candidate", [CAR.TESLA_MODEL_3, CAR.TESLA_MODEL_Y, CAR.TESLA_MODEL_X]) + @pytest.mark.parametrize("bus", [CANBUS.party, CANBUS.vehicle, CANBUS.autopilot_party]) + @pytest.mark.parametrize("marker", [0x3DF, 0x3B6]) + def test_vehicle_bus_addon_requires_marker_on_vehicle_bus(self, candidate, bus, marker): + fingerprint = gen_empty_fingerprint() + fingerprint[bus][marker] = 8 + CP = CarInterface.get_params(candidate, fingerprint, [], False, False, False) + CP_IQ = CarInterface.get_params_iq(CP, candidate, fingerprint, [], False, False, False) + addon_present = bus == CANBUS.vehicle + + assert bool(CP_IQ.flags & TeslaFlagsIQ.HAS_VEHICLE_BUS) == addon_present + assert bool(CP_IQ.iqSafetyFlags & TeslaSafetyFlagsIQ.HAS_VEHICLE_BUS) == addon_present + assert (Bus.adas in CarState.get_can_parsers(CP, CP_IQ)) == addon_present + + def test_standard_harness_with_mirrored_odometer_has_no_vehicle_bus_parser(self): + fingerprint = gen_empty_fingerprint() + for bus in (CANBUS.party, CANBUS.autopilot_party): + fingerprint[bus][0x3B6] = 6 + CP = CarInterface.get_params(CAR.TESLA_MODEL_3, fingerprint, [], False, False, False) + CP_IQ = CarInterface.get_params_iq(CP, CAR.TESLA_MODEL_3, fingerprint, [], False, False, False) + + assert not CP_IQ.flags & TeslaFlagsIQ.HAS_VEHICLE_BUS + assert not CP_IQ.iqSafetyFlags & TeslaSafetyFlagsIQ.HAS_VEHICLE_BUS + assert Bus.adas not in CarState.get_can_parsers(CP, CP_IQ) + def test_radar_detection(self): # Test radar availability detection for cars with radar DBC defined for radar in (True, False): diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/tests/test_tesla_legacy.py b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/tests/test_tesla_legacy.py new file mode 100644 index 00000000..bd9eb9c9 --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/tests/test_tesla_legacy.py @@ -0,0 +1,564 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" +import pytest + +from iqdbc.can import CANPacker, CANParser +from iqdbc.car import Bus, gen_empty_fingerprint, structs +from iqdbc.car.fw_versions import match_fw_to_car +from iqdbc.car.structs import CarParams +from iqdbc.car.tesla.carcontroller import CarController +from iqdbc.car.tesla.carstate import CarState +from iqdbc.car.tesla.fingerprints import FW_VERSIONS +from iqdbc.car.tesla.interface import CarInterface +from iqdbc.car.tesla.radar_interface import BOSCH_TRIGGER_MSG, RadarInterface +from iqdbc.car.tesla.teslacan_legacy import TeslaCANLegacy +from iqdbc.car.tesla.values import (CAR, DBC, LEGACY_CARS, LEGACY_HW1_CARS, LEGACY_HW2_CARS, TeslaFlags, + TeslaLegacySafetyFlags, get_legacy_canbus) + +Ecu = CarParams.Ecu +NANOS = 1_000_000_000 + + +def make_params(candidate, fingerprint=None): + fingerprint = fingerprint or gen_empty_fingerprint() + CP = CarInterface.get_params(candidate, fingerprint, [], False, False, False) + CP_IQ = CarInterface.get_params_iq(CP, candidate, fingerprint, [], False, False, False) + return CP, CP_IQ + + +def checksum(addr, dat): + return ((addr & 0xFF) + ((addr >> 8) & 0xFF) + sum(dat)) & 0xFF + + +class TestLegacyParams: + @pytest.mark.parametrize("candidate", LEGACY_CARS) + def test_safety_configs(self, candidate): + CP, CP_IQ = make_params(candidate) + + assert CP.brand == "tesla" + assert CP.steerControlType == structs.CarParams.SteerControlType.angle + assert CP.openpilotLongitudinalControl + assert all(cfg.safetyModel == CarParams.SafetyModel.teslaLegacy for cfg in CP.safetyConfigs) + assert CP.safetyConfigs[0].safetyParam & TeslaLegacySafetyFlags.LONG_CONTROL + assert CP_IQ.flags == 0 + assert not CP.enableBsm + + if candidate in LEGACY_HW1_CARS: + assert len(CP.safetyConfigs) == 1 + assert CP.safetyConfigs[0].safetyParam & TeslaLegacySafetyFlags.HW1 + assert not CP.safetyConfigs[0].safetyParam & TeslaLegacySafetyFlags.EXTERNAL_PANDA + else: + hw = TeslaLegacySafetyFlags.HW2 if candidate in LEGACY_HW2_CARS else TeslaLegacySafetyFlags.HW3 + assert len(CP.safetyConfigs) == 2 + assert CP.safetyConfigs[0].safetyParam & hw + assert not CP.safetyConfigs[0].safetyParam & TeslaLegacySafetyFlags.EXTERNAL_PANDA + assert CP.safetyConfigs[1].safetyParam & hw + assert CP.safetyConfigs[1].safetyParam & TeslaLegacySafetyFlags.EXTERNAL_PANDA + + def test_hw3_safety_param_values_match_safety_header(self): + CP, _ = make_params(CAR.TESLA_MODEL_S_HW3) + assert [cfg.safetyParam for cfg in CP.safetyConfigs] == [33, 36] + + def test_no_sdm1_flag(self): + fingerprint = gen_empty_fingerprint() + CP, _ = make_params(CAR.TESLA_MODEL_S_HW3, fingerprint) + assert CP.flags & TeslaFlags.NO_SDM1 + + fingerprint[0][0x201] = 5 + CP, _ = make_params(CAR.TESLA_MODEL_S_HW3, fingerprint) + assert not CP.flags & TeslaFlags.NO_SDM1 + + def test_radar_unavailable_matches_xnor(self): + unavailable = {c for c in LEGACY_CARS if make_params(c)[0].radarUnavailable} + assert unavailable == {CAR.TESLA_MODEL_S_HW2} + + def test_modern_platforms_unchanged(self): + CP, _ = make_params(CAR.TESLA_MODEL_Y) + assert CP.safetyConfigs[0].safetyModel == CarParams.SafetyModel.tesla + assert len(CP.safetyConfigs) == 1 + + +class TestLegacyBuses: + @pytest.mark.parametrize("candidate, expected", [ + (CAR.TESLA_MODEL_S_HW1, {Bus.party: 0, Bus.ap_party: 2, Bus.pt: 0, Bus.ap_pt: 2, Bus.chassis: 0}), + (CAR.TESLA_MODEL_X_HW1, {Bus.party: 0, Bus.ap_party: 2, Bus.pt: 0, Bus.ap_pt: 2, Bus.chassis: 0}), + (CAR.TESLA_MODEL_S_HW2, {Bus.party: 0, Bus.ap_party: 2, Bus.pt: 4, Bus.ap_pt: 6, Bus.chassis: 0}), + (CAR.TESLA_MODEL_X_HW2, {Bus.party: 0, Bus.ap_party: 2, Bus.pt: 4, Bus.ap_pt: 6, Bus.chassis: 0}), + (CAR.TESLA_MODEL_S_HW3, {Bus.party: 0, Bus.ap_party: 2, Bus.pt: 4, Bus.ap_pt: 6, Bus.chassis: 1}), + (CAR.TESLA_MODEL_X_HW3, {Bus.party: 0, Bus.ap_party: 2, Bus.pt: 4, Bus.ap_pt: 6, Bus.chassis: 1}), + ]) + def test_parser_buses(self, candidate, expected): + CP, CP_IQ = make_params(candidate) + parsers = CarState.get_can_parsers(CP, CP_IQ) + assert {bus: parser.bus for bus, parser in parsers.items()} == expected + + def test_hw3_radar_is_on_the_external_panda(self): + assert get_legacy_canbus(CAR.TESLA_MODEL_S_HW3).radar == 5 + assert get_legacy_canbus(CAR.TESLA_MODEL_X_HW3).radar == 5 + assert get_legacy_canbus(CAR.TESLA_MODEL_S_HW2).radar == 1 + + def test_dbc_map(self): + assert DBC[CAR.TESLA_MODEL_S_HW3][Bus.party] == 'tesla_raven_party' + assert DBC[CAR.TESLA_MODEL_S_HW3][Bus.pt] == 'tesla_powertrain' + assert DBC[CAR.TESLA_MODEL_S_HW3][Bus.chassis] == 'tesla_can' + assert DBC[CAR.TESLA_MODEL_S_HW3][Bus.radar] == 'tesla_radar_continental_generated' + assert DBC[CAR.TESLA_MODEL_S_HW1][Bus.pt] == 'tesla_can' + assert DBC[CAR.TESLA_MODEL_S_HW2][Bus.pt] == 'tesla_powertrain' + assert DBC[CAR.TESLA_MODEL_S_HW1][Bus.radar] == 'tesla_radar_bosch_generated' + + +class TestLegacyFingerprint: + @pytest.mark.parametrize("candidate", [c for c in LEGACY_CARS if c in FW_VERSIONS]) + def test_exact_match(self, candidate): + for fw in FW_VERSIONS[candidate][(Ecu.eps, 0x730, None)]: + car_fw = [CarParams.CarFw(ecu=Ecu.eps, fwVersion=fw, address=0x730, subAddress=0, brand='tesla')] + exact, matches = match_fw_to_car(car_fw, '0' * 17, log=False) + assert exact + assert matches == {candidate} + + def test_hw3_fw_is_distinct_from_modern_platforms(self): + modern = set() + for car, ecus in FW_VERSIONS.items(): + if car not in LEGACY_CARS: + modern |= {fw for fws in ecus.values() for fw in fws} + for fw in FW_VERSIONS[CAR.TESLA_MODEL_S_HW3][(Ecu.eps, 0x730, None)]: + assert fw not in modern + + +def feed(parsers, *groups): + messages = [msg for group in groups for msg in group] + for parser in parsers.values(): + parser.update([(NANOS, messages)]) + + +def pack(dbc, bus, **messages): + packer = CANPacker(dbc) + return [packer.make_can_msg(name, bus, values) for name, values in messages.items()] + + +class TestLegacyCarState: + @staticmethod + def car_state(candidate, fingerprint=None): + CP, CP_IQ = make_params(candidate, fingerprint) + cs = CarState(CP, CP_IQ) + parsers = CarState.get_can_parsers(CP, CP_IQ) + cs.update(parsers) + return cs, parsers + + def test_hw3_decodes_every_source(self): + cs, parsers = self.car_state(CAR.TESLA_MODEL_S_HW3) + + feed( + parsers, + pack('tesla_raven_party', 0, EPAS_sysStatus={ + "EPAS_internalSAS": 10.0, "EPAS_handsOnLevel": 1, "EPAS_torsionBarTorque": 0.5, "EPAS_eacStatus": 2}), + pack('tesla_can', 1, + ESP_B={"ESP_vehicleSpeed": 72.0}, + BrakeMessage={"driverBrakeStatus": 2}, + DI_state={"DI_cruiseState": 2, "DI_speedUnits": 1, "DI_digitalSpeed": 90}, + DI_torque2={"DI_gear": 4}, + GTW_carState={"BC_indicatorLStatus": 1, "DOOR_STATE_FL": 0}, + STW_ANGLHP_STAT={"StW_AnglHP_Spd": 12.0}, + RCM_status={"RCM_buckleDriverStatus": 1}), + pack('tesla_powertrain', 4, DI_torque1={"DI_pedalPos": 40}), + pack('tesla_powertrain', 6, DAS_control={"DAS_aebEvent": 1}), + pack('tesla_raven_party', 2, DAS_steeringControl={"DAS_steeringControlType": 2}), + ) + ret, ret_iq = cs.update(parsers) + + assert ret.vEgoRaw == pytest.approx(20.0, abs=0.01) + assert ret.brakePressed + assert ret.gasPressed + assert ret.steeringAngleDeg == pytest.approx(-10.0, abs=0.1) + assert ret.steeringTorque == pytest.approx(-0.5, abs=0.01) + assert ret.steeringRateDeg == pytest.approx(-12.0, abs=0.5) + assert ret.cruiseState.enabled + assert ret.cruiseState.available + assert ret.cruiseState.speed == pytest.approx(25.0, abs=0.01) + assert ret.gearShifter == structs.CarState.GearShifter.drive + assert ret.leftBlinker + assert not ret.rightBlinker + assert not ret.doorOpen + assert not ret.seatbeltUnlatched + assert ret.stockAeb + assert ret.stockLkas + assert cs.hands_on_level == 1 + assert cs.das_control["DAS_aebEvent"] == 1 + + def test_model_x_hw3_reads_the_same_sources_as_model_s_hw3(self): + cs, parsers = self.car_state(CAR.TESLA_MODEL_X_HW3) + feed( + parsers, + pack('tesla_raven_party', 0, EPAS_sysStatus={"EPAS_internalSAS": 4.0, "EPAS_handsOnLevel": 0, "EPAS_eacStatus": 2}), + pack('tesla_can', 1, ESP_B={"ESP_vehicleSpeed": 36.0}, DI_state={"DI_cruiseState": 2, "DI_speedUnits": 1, "DI_digitalSpeed": 40}), + pack('tesla_powertrain', 4, DI_torque1={"DI_pedalPos": 0}), + ) + ret, _ = cs.update(parsers) + assert ret.vEgoRaw == pytest.approx(10.0, abs=0.01) + assert ret.steeringAngleDeg == pytest.approx(-4.0, abs=0.1) + assert ret.cruiseState.enabled + + def test_hw3_door_open(self): + cs, parsers = self.car_state(CAR.TESLA_MODEL_S_HW3) + for door in ("DOOR_STATE_FL", "DOOR_STATE_FR", "DOOR_STATE_RL", "DOOR_STATE_RR", "DOOR_STATE_FrontTrunk", "BOOT_STATE"): + feed(parsers, pack('tesla_can', 1, GTW_carState={door: 1})) + ret, _ = cs.update(parsers) + assert ret.doorOpen, door + + feed(parsers, pack('tesla_can', 1, GTW_carState={"DOOR_STATE_FL": 0})) + ret, _ = cs.update(parsers) + assert not ret.doorOpen + + def test_hw3_cruise_states(self): + cs, parsers = self.car_state(CAR.TESLA_MODEL_S_HW3) + expectations = {1: (False, True, False), 2: (True, True, False), 3: (True, True, True), 5: (False, False, False)} + for state, (enabled, available, standstill) in expectations.items(): + feed(parsers, pack('tesla_can', 1, DI_state={"DI_cruiseState": state, "DI_speedUnits": 0, "DI_digitalSpeed": 50})) + ret, _ = cs.update(parsers) + assert ret.cruiseState.enabled == enabled, state + assert ret.cruiseState.available == available, state + assert ret.standstill == standstill, state + assert ret.accFaulted == (state == 5) + assert ret.cruiseState.speed == pytest.approx(50 * 0.44704, abs=0.01) + + def test_hw3_steer_faults(self): + cs, parsers = self.car_state(CAR.TESLA_MODEL_S_HW3) + feed(parsers, pack('tesla_raven_party', 0, EPAS_sysStatus={"EPAS_eacStatus": 3, "EPAS_handsOnLevel": 0})) + ret, _ = cs.update(parsers) + assert ret.steerFaultPermanent and not ret.steerFaultTemporary + + feed(parsers, pack('tesla_raven_party', 0, EPAS_sysStatus={"EPAS_eacStatus": 0, "EPAS_eacErrorCode": 9})) + ret, _ = cs.update(parsers) + assert ret.steerFaultTemporary + assert ret.steeringDisengage + + feed(parsers, pack('tesla_raven_party', 0, EPAS_sysStatus={"EPAS_eacStatus": 2, "EPAS_handsOnLevel": 3})) + ret, _ = cs.update(parsers) + assert ret.steeringDisengage + + def test_hw3_seatbelt_with_sdm1(self): + fingerprint = gen_empty_fingerprint() + fingerprint[1][0x201] = 5 + cs, parsers = self.car_state(CAR.TESLA_MODEL_S_HW3, fingerprint) + feed(parsers, pack('tesla_can', 1, SDM1={"SDM_bcklDrivStatus": 0})) + ret, _ = cs.update(parsers) + assert ret.seatbeltUnlatched + + feed(parsers, pack('tesla_can', 1, SDM1={"SDM_bcklDrivStatus": 1})) + ret, _ = cs.update(parsers) + assert not ret.seatbeltUnlatched + + def test_hw3_seatbelt_without_sdm1(self): + cs, parsers = self.car_state(CAR.TESLA_MODEL_S_HW3) + feed(parsers, pack('tesla_can', 1, RCM_status={"RCM_buckleDriverStatus": 0})) + ret, _ = cs.update(parsers) + assert ret.seatbeltUnlatched + + feed(parsers, pack('tesla_can', 1, RCM_status={"RCM_buckleDriverStatus": 1})) + ret, _ = cs.update(parsers) + assert not ret.seatbeltUnlatched + + def test_hw1_reads_everything_from_party_bus(self): + cs, parsers = self.car_state(CAR.TESLA_MODEL_S_HW1) + feed( + parsers, + pack('tesla_can', 0, + EPAS_sysStatus={"EPAS_internalSAS": -5.0, "EPAS_handsOnLevel": 0, "EPAS_eacStatus": 2}, + ESP_B={"ESP_vehicleSpeed": 36.0}, + BrakeMessage={"driverBrakeStatus": 1}, + DI_state={"DI_cruiseState": 2, "DI_speedUnits": 1, "DI_digitalSpeed": 40}, + DI_torque1={"DI_pedalPos": 0}, + DI_torque2={"DI_gear": 1}, + GTW_carState={"BC_indicatorRStatus": 1}, + SDM1={"SDM_bcklDrivStatus": 1}, + STW_ANGLHP_STAT={"StW_AnglHP_Spd": 0}), + pack('tesla_can', 2, + DAS_control={"DAS_aebEvent": 0}, + DAS_steeringControl={"DAS_steeringControlType": 0}), + ) + ret, _ = cs.update(parsers) + + assert ret.vEgoRaw == pytest.approx(10.0, abs=0.01) + assert not ret.brakePressed + assert not ret.gasPressed + assert ret.steeringAngleDeg == pytest.approx(5.0, abs=0.1) + assert ret.gearShifter == structs.CarState.GearShifter.park + assert ret.rightBlinker and not ret.leftBlinker + assert not ret.stockAeb and not ret.stockLkas + + def test_model_x_hw1_blinkers_come_from_stalk(self): + cs, parsers = self.car_state(CAR.TESLA_MODEL_X_HW1) + feed(parsers, pack('tesla_can', 0, STW_ACTN_RQ={"TurnIndLvr_Stat": 2})) + ret, _ = cs.update(parsers) + assert ret.rightBlinker and not ret.leftBlinker + + feed(parsers, pack('tesla_can', 0, STW_ACTN_RQ={"TurnIndLvr_Stat": 1})) + ret, _ = cs.update(parsers) + assert ret.leftBlinker and not ret.rightBlinker + + def test_hw2_gas_comes_from_powertrain_bus(self): + cs, parsers = self.car_state(CAR.TESLA_MODEL_S_HW2) + feed(parsers, pack('tesla_powertrain', 4, DI_torque1={"DI_pedalPos": 20})) + ret, _ = cs.update(parsers) + assert ret.gasPressed + + feed(parsers, pack('tesla_powertrain', 0, DI_torque1={"DI_pedalPos": 0})) + ret, _ = cs.update(parsers) + assert ret.gasPressed + + +class FakeActuators: + def __init__(self, angle=0.0, accel=0.0): + self.steeringAngleDeg = angle + self.accel = accel + + def as_builder(self): + return self + + +class FakeCarControl: + def __init__(self, lat_active=True, long_active=True, cancel=False, angle=0.0, accel=0.0): + self.actuators = FakeActuators(angle, accel) + self.latActive = lat_active + self.longActive = long_active + self.cruiseControl = type("Cruise", (), {"cancel": cancel})() + + +class FakeCarState: + hands_on_level = 0 + das_control = {"DAS_controlCounter": 0} + out = type("Out", (), {"vEgoRaw": 20.0, "steeringAngleDeg": 0.0, "vEgo": 20.0, "gasPressed": False})() + + +def make_controller(candidate): + CP, CP_IQ = make_params(candidate) + parsers = CarState.get_can_parsers(CP, CP_IQ) + dbc_names = {bus: parser.dbc_name for bus, parser in parsers.items()} + return CarController(dbc_names, CP, CP_IQ) + + +class TestLegacyCarController: + @staticmethod + def run_frames(controller, frames, cc): + sent = {} + for frame in range(frames): + _, can_sends = controller.update(cc, structs.IQCarControl(), FakeCarState(), 0) + sent[frame] = can_sends + return sent + + def test_hw3_message_addresses_and_buses(self): + controller = make_controller(CAR.TESLA_MODEL_S_HW3) + sent = self.run_frames(controller, 20, FakeCarControl()) + + steering = [m for f in sent.values() for m in f if m[0] == 0x488] + eac = [m for f in sent.values() for m in f if m[0] == 0x27d] + long = [m for f in sent.values() for m in f if m[0] == 0x2bf] + + assert len(steering) == 10 + assert len(eac) == 2 + assert len(long) == 5 + assert {m[2] for m in steering} == {0} + assert {m[2] for m in eac} == {0} + assert {m[2] for m in long} == {4} + assert not [m for f in sent.values() for m in f if m[0] == 0x2b9] + + def test_hw1_sends_long_on_party_bus_without_eac_monitor(self): + controller = make_controller(CAR.TESLA_MODEL_S_HW1) + sent = self.run_frames(controller, 20, FakeCarControl()) + + addrs = {m[0] for f in sent.values() for m in f} + assert addrs == {0x488, 0x2b9} + assert {m[2] for f in sent.values() for m in f} == {0} + + def test_hw2_message_addresses_and_buses(self): + controller = make_controller(CAR.TESLA_MODEL_X_HW2) + sent = self.run_frames(controller, 20, FakeCarControl()) + + by_addr = {} + for f in sent.values(): + for m in f: + by_addr.setdefault(m[0], set()).add(m[2]) + assert by_addr == {0x488: {0}, 0x27d: {0}, 0x2bf: {4}} + + def test_steering_checksum_and_counter(self): + controller = make_controller(CAR.TESLA_MODEL_S_HW3) + sent = self.run_frames(controller, 8, FakeCarControl(angle=5.0)) + steering = [m for f in sent.values() for m in f if m[0] == 0x488] + + parser = CANParser('tesla_raven_party', [("DAS_steeringControl", 50)], 0) + counters = [] + for i, (addr, dat, bus) in enumerate(steering): + assert dat[3] == checksum(addr, dat[:3]) + parser.update([(NANOS + i * 20_000_000, [(addr, dat, bus)])]) + counters.append(int(parser.vl["DAS_steeringControl"]["DAS_steeringControlCounter"])) + assert counters == [0, 1, 2, 3] + + def test_steering_is_idle_when_lateral_inactive(self): + controller = make_controller(CAR.TESLA_MODEL_S_HW3) + sent = self.run_frames(controller, 4, FakeCarControl(lat_active=False, angle=5.0)) + steering = [m for f in sent.values() for m in f if m[0] == 0x488] + + parser = CANParser('tesla_raven_party', [("DAS_steeringControl", 50)], 0) + for i, (addr, dat, bus) in enumerate(steering): + parser.update([(NANOS + i * 20_000_000, [(addr, dat, bus)])]) + assert parser.vl["DAS_steeringControl"]["DAS_steeringControlType"] == 0 + + def test_steering_angle_is_negated_and_rate_limited(self): + controller = make_controller(CAR.TESLA_MODEL_S_HW3) + angles = [] + parser = CANParser('tesla_raven_party', [("DAS_steeringControl", 50)], 0) + for frame in range(20): + _, can_sends = controller.update(FakeCarControl(angle=30.0), structs.IQCarControl(), FakeCarState(), 0) + for addr, dat, bus in can_sends: + if addr == 0x488: + parser.update([(NANOS + frame * 10_000_000, [(addr, dat, bus)])]) + angles.append(parser.vl["DAS_steeringControl"]["DAS_steeringAngleRequest"]) + + assert angles[0] <= 0.0 + assert angles[-1] < angles[0] + assert all(abs(b - a) <= 5.0 + 0.1 for a, b in zip(angles, angles[1:], strict=False)) + + def test_longitudinal_checksum_and_state(self): + controller = make_controller(CAR.TESLA_MODEL_S_HW3) + _, can_sends = controller.update(FakeCarControl(accel=1.0), structs.IQCarControl(), FakeCarState(), 0) + long = [m for m in can_sends if m[0] == 0x2bf] + assert len(long) == 1 + + addr, dat, bus = long[0] + assert dat[7] == checksum(0x2b9, dat[:7]) + + parser = CANParser('tesla_powertrain', [("DAS_control", 25)], 4) + parser.update([(NANOS, [(addr, dat, bus)])]) + values = parser.vl["DAS_control"] + assert values["DAS_accState"] == 4 + assert values["DAS_accelMax"] == pytest.approx(1.0, abs=0.05) + assert values["DAS_accelMin"] == pytest.approx(1.0, abs=0.05) + assert values["DAS_aebEvent"] == 0 + + def test_cancel_sends_silent_cancel_state(self): + controller = make_controller(CAR.TESLA_MODEL_S_HW3) + _, can_sends = controller.update(FakeCarControl(cancel=True, long_active=False), structs.IQCarControl(), FakeCarState(), 0) + addr, dat, bus = next(m for m in can_sends if m[0] == 0x2bf) + + parser = CANParser('tesla_powertrain', [("DAS_control", 25)], 4) + parser.update([(NANOS, [(addr, dat, bus)])]) + assert parser.vl["DAS_control"]["DAS_accState"] == 13 + + def test_accel_is_clipped_to_safety_limits(self): + controller = make_controller(CAR.TESLA_MODEL_S_HW3) + _, can_sends = controller.update(FakeCarControl(accel=10.0), structs.IQCarControl(), FakeCarState(), 0) + addr, dat, bus = next(m for m in can_sends if m[0] == 0x2bf) + + parser = CANParser('tesla_powertrain', [("DAS_control", 25)], 4) + parser.update([(NANOS, [(addr, dat, bus)])]) + assert parser.vl["DAS_control"]["DAS_accelMax"] <= 2.0 + 0.05 + + +class TestTeslaCanLegacy: + @staticmethod + def make(candidate=CAR.TESLA_MODEL_S_HW3): + canbus = get_legacy_canbus(candidate) + packers = {canbus.party: CANPacker(DBC[candidate][Bus.party]), canbus.powertrain: CANPacker(DBC[candidate][Bus.pt])} + return TeslaCANLegacy(packers, canbus) + + def test_jerk_limits_collapse_on_gas_and_ramp_back(self): + can = self.make() + can.create_longitudinal_command(4, 0.5, 0, 20.0, True, True) + assert can.jerk_upper == 0.0 + assert can.jerk_lower == 0.0 + + for i in range(1, 6): + can.create_longitudinal_command(4, 0.5, i, 20.0, True, False) + assert 0.0 < can.jerk_upper < 4.9 + assert -4.9 < can.jerk_lower < 0.0 + assert can.jerk_upper == pytest.approx(-can.jerk_lower) + + for i in range(1000): + can.create_longitudinal_command(4, 0.5, i % 8, 20.0, True, False) + assert can.jerk_upper == pytest.approx(4.9) + assert can.jerk_lower == pytest.approx(-4.9) + + def test_set_speed_follows_accel_sign_when_active(self): + can = self.make() + parser = CANParser('tesla_powertrain', [("DAS_control", 25)], 4) + + for accel, expect_zero in ((-0.5, True), (0.5, False)): + addr, dat, bus = can.create_longitudinal_command(4, accel, 0, 20.0, True, False) + parser.update([(NANOS, [(addr, dat, bus)])]) + set_speed = parser.vl["DAS_control"]["DAS_setSpeed"] + assert (set_speed == 0) == expect_zero + + def test_idle_set_speed_tracks_ego(self): + can = self.make() + parser = CANParser('tesla_powertrain', [("DAS_control", 25)], 4) + addr, dat, bus = can.create_longitudinal_command(4, 0.0, 0, 20.0, False, False) + parser.update([(NANOS, [(addr, dat, bus)])]) + assert parser.vl["DAS_control"]["DAS_setSpeed"] == pytest.approx(72.0, abs=0.5) + + +class TestLegacyRadar: + def test_hw3_uses_continental_radar_on_external_panda_bus(self): + CP, CP_IQ = make_params(CAR.TESLA_MODEL_S_HW3) + ri = RadarInterface(CP, CP_IQ) + assert not ri.bosch_radar + assert ri.rcp.bus == 5 + assert ri.trigger_msg == 0x410 + 80 - 1 + + @pytest.mark.parametrize("candidate", [CAR.TESLA_MODEL_S_HW1, CAR.TESLA_MODEL_X_HW1, CAR.TESLA_MODEL_X_HW2]) + def test_bosch_radar_parser(self, candidate): + CP, CP_IQ = make_params(candidate) + ri = RadarInterface(CP, CP_IQ) + assert ri.bosch_radar + assert ri.rcp.bus == 1 + assert ri.trigger_msg == BOSCH_TRIGGER_MSG + assert ri.num_points == 32 + + def test_s_hw2_radar_is_off(self): + CP, CP_IQ = make_params(CAR.TESLA_MODEL_S_HW2) + ri = RadarInterface(CP, CP_IQ) + assert ri.radar_off_can + + def test_bosch_points_are_filtered_and_held_between_frames(self): + CP, CP_IQ = make_params(CAR.TESLA_MODEL_S_HW1) + ri = RadarInterface(CP, CP_IQ) + dbc = DBC[CAR.TESLA_MODEL_S_HW1][Bus.radar] + packer = CANPacker(dbc) + + def frame(good_dist, exist, nanos): + msgs = [packer.make_can_msg("TeslaRadarSguInfo", 1, {"RADC_HWFail": 0})] + for i in range(32): + a = {"Index": i % 2, "Tracked": 0} + b = {"Index2": i % 2} + if i == 0: + a = {"LongDist": good_dist, "LatDist": 1.0, "LongSpeed": -2.0, "ProbExist": exist, "Tracked": 1, + "Meas": 1, "Index": 1, "LongAccel": 0.5} + b = {"LatSpeed": 0.25, "Index2": 1} + msgs.append(packer.make_can_msg(f"RadarPoint{i}_A", 1, a)) + msgs.append(packer.make_can_msg(f"RadarPoint{i}_B", 1, b)) + return [(nanos, msgs)] + + ri.update(frame(40.0, 90.0, NANOS)) + ri.update(frame(40.0, 90.0, NANOS + 125_000_000)) + rr = ri.update(frame(40.0, 90.0, NANOS + 250_000_000)) + assert rr is not None + assert len(rr.points) == 1 + point = rr.points[0] + assert point.dRel == pytest.approx(40.0, abs=0.1) + assert point.yRel == pytest.approx(1.0, abs=0.2) + assert point.vRel == pytest.approx(-2.0, abs=0.1) + + rr = ri.update(frame(0.0, 90.0, NANOS + 375_000_000)) + assert rr is not None and len(rr.points) == 0 + + rr = ri.update(frame(40.0, 10.0, NANOS + 500_000_000)) + assert rr is not None and len(rr.points) == 0 + + +@pytest.mark.parametrize("candidate", LEGACY_CARS) +def test_modern_tesla_options_are_not_applied(candidate): + from iqdbc.lvbs.car.interfaces import _apply_tesla_options + CP, CP_IQ = make_params(candidate) + _apply_tesla_options(CP, CP_IQ, {"IQTeslaTorqueBlend": "1", "IQTeslaFsdVisualization": "1"}) + assert CP_IQ.flags == 0 + assert CP_IQ.iqSafetyFlags == 0 diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/values.py b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/values.py index 2367da75..b3b6c84a 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/tesla/values.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tesla/values.py @@ -78,6 +78,74 @@ class CAR(Platforms): [TeslaCarHW4ModelSXDocs("Tesla Model X (with HW4) 2024")], CarSpecs(mass=2495., wheelbase=2.960, steerRatio=12.0), ) + TESLA_MODEL_X_HW1 = TeslaPlatformConfig( + [CarDocs("Tesla Model X (with HW1) 2014-16", "All", car_parts=CarParts.common([CarHarness.tesla_model_x_hw1]))], + CarSpecs(mass=2447., wheelbase=2.960, steerRatio=15.0), + { + Bus.chassis: 'tesla_can', + Bus.party: 'tesla_can', + Bus.pt: 'tesla_can', + Bus.radar: 'tesla_radar_bosch_generated', + }, + ) + TESLA_MODEL_X_HW2 = TeslaPlatformConfig( + [CarDocs("Tesla Model X (with HW2) 2016-19", "All", car_parts=CarParts.common([CarHarness.tesla_model_sx_hw2]))], + CarSpecs(mass=2100., wheelbase=2.960, steerRatio=15.0), + { + Bus.chassis: 'tesla_can', + Bus.party: 'tesla_can', + Bus.pt: 'tesla_powertrain', + Bus.radar: 'tesla_radar_bosch_generated', + }, + ) + TESLA_MODEL_S_HW1 = TeslaPlatformConfig( + [CarDocs("Tesla Model S (with HW1) 2014-16", "All", car_parts=CarParts.common([CarHarness.tesla_model_s_hw1]))], + CarSpecs(mass=2100., wheelbase=2.960, steerRatio=15.0), + { + Bus.chassis: 'tesla_can', + Bus.party: 'tesla_can', + Bus.pt: 'tesla_can', + Bus.radar: 'tesla_radar_bosch_generated', + }, + ) + TESLA_MODEL_S_HW2 = TeslaPlatformConfig( + [CarDocs("Tesla Model S (with HW2) 2017-19", "All", car_parts=CarParts.common([CarHarness.tesla_model_sx_hw2]))], + CarSpecs(mass=2100., wheelbase=2.960, steerRatio=15.0), + { + Bus.chassis: 'tesla_can', + Bus.party: 'tesla_can', + Bus.pt: 'tesla_powertrain', + Bus.radar: 'tesla_radar_bosch_generated', + }, + ) + TESLA_MODEL_X_HW3 = TeslaPlatformConfig( + [CarDocs("Tesla Model X (with HW3) 2021-23", "All", car_parts=CarParts.common([CarHarness.tesla_model_sx_hw3]))], + CarSpecs(mass=2468., wheelbase=2.960, steerRatio=15.0), + { + Bus.chassis: 'tesla_can', + Bus.party: 'tesla_raven_party', + Bus.pt: 'tesla_powertrain', + Bus.radar: 'tesla_radar_continental_generated', + }, + ) + TESLA_MODEL_S_HW3 = TeslaPlatformConfig( + [CarDocs("Tesla Model S (with HW3) 2020-23", "All", car_parts=CarParts.common([CarHarness.tesla_model_sx_hw3]))], + CarSpecs(mass=2100., wheelbase=2.960, steerRatio=15.0), + { + Bus.chassis: 'tesla_can', + Bus.party: 'tesla_raven_party', + Bus.pt: 'tesla_powertrain', + Bus.radar: 'tesla_radar_continental_generated', + }, + ) + + +LEGACY_CARS = (CAR.TESLA_MODEL_S_HW1, CAR.TESLA_MODEL_S_HW2, CAR.TESLA_MODEL_S_HW3, CAR.TESLA_MODEL_X_HW1, CAR.TESLA_MODEL_X_HW2, + CAR.TESLA_MODEL_X_HW3) +LEGACY_HW1_CARS = (CAR.TESLA_MODEL_S_HW1, CAR.TESLA_MODEL_X_HW1) +LEGACY_HW2_CARS = (CAR.TESLA_MODEL_S_HW2, CAR.TESLA_MODEL_X_HW2) +LEGACY_HW3_CARS = (CAR.TESLA_MODEL_S_HW3, CAR.TESLA_MODEL_X_HW3) +LEGACY_BOSCH_RADAR_CARS = LEGACY_HW1_CARS + LEGACY_HW2_CARS # Cars with this EPS FW have a 2-bit DAS_steeringControlType and use TeslaFlags.LEGACY_DAS_STEERING @@ -240,6 +308,11 @@ FW_QUERY_CONFIG = FwQueryConfig( [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.UDS_VERSION_RESPONSE], bus=0, ), + Request( + [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST], + [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE], + bus=0, + ), ], match_fw_to_car_fuzzy=match_fw_to_car_fuzzy, ) @@ -251,6 +324,24 @@ class CANBUS: autopilot_party = 2 +@dataclass(frozen=True) +class LegacyCanBus: + party: int + chassis: int + powertrain: int + radar: int + autopilot_party: int + autopilot_powertrain: int + + +def get_legacy_canbus(candidate: str) -> LegacyCanBus: + if candidate in LEGACY_HW1_CARS: + return LegacyCanBus(party=0, chassis=0, powertrain=0, radar=1, autopilot_party=2, autopilot_powertrain=2) + if candidate in LEGACY_HW3_CARS: + return LegacyCanBus(party=0, chassis=1, powertrain=4, radar=5, autopilot_party=2, autopilot_powertrain=6) + return LegacyCanBus(party=0, chassis=0, powertrain=4, radar=1, autopilot_party=2, autopilot_powertrain=6) + + GEAR_MAP = { "DI_GEAR_INVALID": CarState.GearShifter.unknown, "DI_GEAR_P": CarState.GearShifter.park, @@ -288,6 +379,7 @@ class CarControllerParams: JERK_LIMIT_MAX = 4.9 # m/s^3, ACC faults at 5.0 JERK_LIMIT_MIN = -4.9 # m/s^3, ACC faults at 5.0 JERK_UP = 1.0 # m/s^3 + JERK_RAMP_RATE = JERK_LIMIT_MAX * 0.002 class TeslaSafetyFlags(IntFlag): @@ -296,6 +388,14 @@ class TeslaSafetyFlags(IntFlag): HW4_GEN2 = 4 +class TeslaLegacySafetyFlags(IntFlag): + LONG_CONTROL = 1 + EXTERNAL_PANDA = 4 + HW1 = 8 + HW2 = 16 + HW3 = 32 + + class TeslaFlags(IntFlag): LONG_CONTROL = 1 LEGACY_DAS_STEERING = 2 @@ -304,6 +404,7 @@ class TeslaFlags(IntFlag): HW4_GEN2 = 8 # blinkers and the seatbelt buckle are only on the VEHICLE bus, which needs its CAN lines tapped HW4_GEN2_VEHICLE_BUS = 16 + NO_SDM1 = 32 DBC = CAR.create_dbc_map() diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tests/routes.py b/artifacts/package_sources/iqdbc/iqdbc/car/tests/routes.py index 6ad0f6af..4a4e4778 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/tests/routes.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tests/routes.py @@ -37,6 +37,13 @@ route_exempt_cars = [ TOYOTA.TOYOTA_COROLLA, TOYOTA.TOYOTA_RAV4H, + TESLA.TESLA_MODEL_S_HW1, + TESLA.TESLA_MODEL_S_HW2, + TESLA.TESLA_MODEL_S_HW3, + TESLA.TESLA_MODEL_X_HW1, + TESLA.TESLA_MODEL_X_HW2, + TESLA.TESLA_MODEL_X_HW3, + HONDA.HONDA_CLARITY, GM.CHEVROLET_BOLT_NON_ACC, GM.CHEVROLET_BOLT_NON_ACC_1ST_GEN, diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tests/test_fw_fingerprint.py b/artifacts/package_sources/iqdbc/iqdbc/car/tests/test_fw_fingerprint.py index 7d36179d..2a2b2f4c 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/tests/test_fw_fingerprint.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tests/test_fw_fingerprint.py @@ -287,7 +287,7 @@ class TestFwFingerprintTiming: print(f'get_vin {name} case, query time={self.total_time / self.N} seconds') def test_fw_query_timing(self, subtests, mocker): - total_ref_time = {1: 7.8, 2: 8.4} + total_ref_time = {1: 7.9, 2: 8.5} brand_ref_times = { 1: { 'gm': 1.0, @@ -299,7 +299,7 @@ class TestFwFingerprintTiming: 'mazda': 0.1, 'nissan': 0.8, 'subaru': 0.65, - 'tesla': 0.2, + 'tesla': 0.3, 'toyota': 0.7, 'volkswagen': 0.95, 'rivian': 0.3, diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/torque_data/override.toml b/artifacts/package_sources/iqdbc/iqdbc/car/torque_data/override.toml index e7b2cfea..1be990a4 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/torque_data/override.toml +++ b/artifacts/package_sources/iqdbc/iqdbc/car/torque_data/override.toml @@ -18,6 +18,12 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] # Tesla angle based controllers "TESLA_MODEL_3" = [nan, 2.5, nan] "TESLA_MODEL_Y" = [nan, 2.5, nan] +"TESLA_MODEL_X_HW1" = [nan, 2.5, nan] +"TESLA_MODEL_X_HW2" = [nan, 2.5, nan] +"TESLA_MODEL_S_HW1" = [nan, 2.5, nan] +"TESLA_MODEL_S_HW2" = [nan, 2.5, nan] +"TESLA_MODEL_S_HW3" = [nan, 2.5, nan] +"TESLA_MODEL_X_HW3" = [nan, 2.5, nan] # Guess "FORD_BRONCO_SPORT_MK1" = [nan, 1.5, nan] diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py index 4bd61e9e..9623b639 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py @@ -18,7 +18,7 @@ from iqdbc.car.volkswagen.pq_radar_handler import PQRadarHandler from iqdbc.car.volkswagen.values import ( CanBus, CarControllerParams, MQB_A0_CARS, VolkswagenFlags, VolkswagenFlagsIQ, apply_pq_stopping_accel, ) -from iqdbc.car.volkswagen.mebutils import LongControlJerk, LongControlLimit, LatControlCurvature, LongStopHold +from iqdbc.car.volkswagen.mebutils import CurvatureSmoother, LongControlJerk, LongControlLimit, LatControlCurvature, LongStopHold from iqdbc.car.vehicle_model import VehicleModel iqpilot_path = os.path.join(os.path.dirname(__file__), '..', '..', '..') @@ -42,6 +42,29 @@ def dVisual(CCS, CS): decelV = False return decelV +# The MQB ACC_02.ACC_Abstandsindex and PQ ACC_GRA_Anzeige.ACA_gemZeitl ("measured time gap") lead marker is +# a time headway on a log scale, not a distance. Fitted to the stock radar's own output against lead distance +# and speed on 8 stock-ACC cars (Golf, Jetta, Atlas, Taos, Arteon and Kodiaq digital clusters, Octavia analog, +# Sharan PQ): position = 0.17 + 0.465 * ln(headway), from about 0.7 s (closest) to 6 s (farthest). Stopped, +# the radar still reports a finite headway, as if the car were rolling at about 3.5 m/s. +LEAD_HEADWAY_POSITION_OFFSET = 0.17 +LEAD_HEADWAY_POSITION_PER_LOG_S = 0.465 +LEAD_HEADWAY_MIN_SPEED = 3.5 # m/s +# Raw code at the closest and farthest positions: 4-bit analog MQB and PQ clusters use 1-15, digital MQB +# clusters (Kombi_03.KBI_Variante) the 10-bit 34-972 the stock radar sends. 0 means no lead on both. +LEAD_INDEX_RANGE_ANALOG = (1, 15) +LEAD_INDEX_RANGE_DIGITAL = (34, 972) + + +def lead_headway_index(lead_distance: float, v_ego: float, digital_cluster: bool) -> int: + if lead_distance <= 0: + return 0 + headway = lead_distance / max(v_ego, LEAD_HEADWAY_MIN_SPEED) + position = clip(LEAD_HEADWAY_POSITION_OFFSET + LEAD_HEADWAY_POSITION_PER_LOG_S * np.log(headway), 0.0, 1.0) + closest, farthest = LEAD_INDEX_RANGE_DIGITAL if digital_cluster else LEAD_INDEX_RANGE_ANALOG + return int(round(closest + position * (farthest - closest))) + + def accel_during_driver_override(accel: float, gas_pressed: bool, keep_long_active: bool) -> float: return 0.0 if gas_pressed and keep_long_active else accel @@ -64,8 +87,11 @@ def ea_blinker_command(left_request, right_request, left_active, right_active): class CarController(CarControllerBase): def __init__(self, dbc_names, CP, CP_IQ): - from iqpilot.system.proprietary_runtime._verified_import import import_verified_module - self._iq_lvbs_alc = import_verified_module("iqpilot_alc_private", "iqpilot_private.konn3kt.iqlvbs.alc") + try: + from iqpilot.system.proprietary_runtime._verified_import import import_verified_module + self._iq_lvbs_alc = import_verified_module("iqpilot_alc_private", "iqpilot_private.konn3kt.iqlvbs.alc") + except Exception: + self._iq_lvbs_alc = None super().__init__(dbc_names, CP, CP_IQ) self._params = Params() self.CCP = CarControllerParams(CP) @@ -102,6 +128,10 @@ class CarController(CarControllerBase): self.steering_power_last = 0 self.long_jerk_control = LongControlJerk(dt=(DT_CTRL * self.CCP.ACC_CONTROL_STEP)) if self.CP.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO) else None self.long_limit_control = LongControlLimit(dt=(DT_CTRL * self.CCP.ACC_CONTROL_STEP)) if self.CP.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO) else None + self.curvature_smoother = ( + CurvatureSmoother(self.CCP.CURVATURE_SMOOTH_SPEED_BP, self.CCP.CURVATURE_SMOOTH_TAU_V, DT_CTRL * self.CCP.STEER_STEP) + if self.CP.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO) else None + ) self.long_stop_hold = LongStopHold(dt=(DT_CTRL * self.CCP.ACC_CONTROL_STEP)) if self.CP.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO) else None self.gra_acc_counter_last = None self.gra_cancel_ticks = 0 @@ -215,7 +245,7 @@ class CarController(CarControllerBase): self._pq_patch_checked = True self.eps_timer_workaround = not self._params.get_bool("VwPqEpsPatched") blend_active = bool(self.blend_stock_radar and self.radar_handler is not None and self.CP.openpilotLongitudinalControl) - AngleLateralControl = self._iq_lvbs_alc.angle_lateral_control_enabled(self, CS) + AngleLateralControl = self._iq_lvbs_alc.angle_lateral_control_enabled(self, CS) if self._iq_lvbs_alc else False self.entering = CS.vw_iq_lvbs_alc_entering self.active = CS.vw_iq_lvbs_alc_active @@ -239,6 +269,7 @@ class CarController(CarControllerBase): apply_curvature = apply_curvature + (CS.out.steeringCurvature - (CC.currentCurvature - CC.rollCompensation)) else: apply_curvature = actuators.curvature + (CS.out.steeringCurvature - CC.currentCurvature) + apply_curvature = self.curvature_smoother.update(apply_curvature, CS.out.vEgo) apply_curvature = apply_std_curvature_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw, CS.out.steeringCurvature, CS.out.steeringPressed, self.CCP.STEER_STEP, CC.latActive, self.CCP.CURVATURE_LIMITS) @@ -261,12 +292,13 @@ class CarController(CarControllerBase): hca_enabled = False apply_curvature = 0. steering_power = 0 + self.curvature_smoother.reset(apply_curvature) can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature, hca_enabled, steering_power)) self.apply_curvature_last = apply_curvature self.steering_power_last = steering_power else: - if CC.latActive and not AngleLateralControl: + if CC.latActive and CC.enabled and not AngleLateralControl: torque_scale = self._get_mqb_steering_torque_scale(CS.out.vEgo, iq_mqb_steering_lockout) new_torque = int(round(actuators.torque * self.CCP.STEER_MAX * torque_scale)) apply_torque = apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.CCP) @@ -322,8 +354,9 @@ class CarController(CarControllerBase): ea_simulated_torque = CS.out.steeringTorque can_sends.append(self.CCS.create_eps_update(self.packer_pt, self.CAN.cam, CS.eps_stock_values, ea_simulated_torque)) - self._iq_lvbs_alc.update_vw_alc(self, CC, CS, actuators, can_sends, apply_torque) - if self.frame % self.CCP.STEER_STEP == 0: + if self._iq_lvbs_alc: + self._iq_lvbs_alc.update_vw_alc(self, CC, CS, actuators, can_sends, apply_torque) + if self._iq_lvbs_alc and self.frame % self.CCP.STEER_STEP == 0: self._iq_lvbs_alc.append_private_apd(self, CC_IQ, can_sends) if self.CP.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO) and self.CP.flags & VolkswagenFlags.STOCK_KLR_PRESENT: @@ -494,9 +527,9 @@ class CarController(CarControllerBase): hud_control.leadVisible, hud_control.leadDistanceBars + 1, show_distance_bars, CS.esp_hold_confirmation, distance, gap, fcw_alert, acc_hud_event, speed_limit)) else: - # MLB scales the raw lead distance against the set follow gap in the packer, the others clamp to a bar count + # MLB scales the raw lead distance against the set follow gap in the packer, MQB and PQ send a headway index leadDistance = hud_control.leadDistance if self.CCS is mlbcan else \ - (min(15, hud_control.leadDistance) if hud_control.leadDistance != 0 else 0) + lead_headway_index(hud_control.leadDistance, CS.out.vEgo, CS.upscale_lead_car_signal) self.leadDistanceBars = min(3, hud_control.leadDistanceBars) acc_hud_status = self.CCS.acc_hud_status_value(CS.out.cruiseState.available, CS.out.accFaulted, CC.longActive and hud_engaged, hud_override) @@ -509,7 +542,7 @@ class CarController(CarControllerBase): self.leadDistanceBars, fcw_alert, lead_visible or fcw_alert, self.unavailable, decel, d_unresponsive, **hud_kwargs)) - if self.CP.flags & VolkswagenFlags.PQ: + if self._iq_lvbs_alc and self.CP.flags & VolkswagenFlags.PQ: self._iq_lvbs_alc.update_turn_signals(self, CC, CS, can_sends) if self.CP.openpilotLongitudinalControl and (self.CP.flags & VolkswagenFlags.PQ): diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carstate.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carstate.py index 06777838..2d70977f 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carstate.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carstate.py @@ -32,8 +32,11 @@ class CarState(CarStateBase): DRIVER_TORQUE_TAU = 0.10 def __init__(self, CP, CP_IQ): - from iqpilot.system.proprietary_runtime._verified_import import import_verified_module - self._iq_lvbs_alc = import_verified_module("iqpilot_alc_private", "iqpilot_private.konn3kt.iqlvbs.alc") + try: + from iqpilot.system.proprietary_runtime._verified_import import import_verified_module + self._iq_lvbs_alc = import_verified_module("iqpilot_alc_private", "iqpilot_private.konn3kt.iqlvbs.alc") + except Exception: + self._iq_lvbs_alc = None super().__init__(CP, CP_IQ) self._params = Params() self.frame = 0 @@ -102,10 +105,11 @@ class CarState(CarStateBase): self.motor1_stock = {} self.motor3_frame = 0 self.motor1_frame = 0 - self._odometer_store = self._iq_lvbs_alc.create_vehicle_odometer_store(CP, self._params) + self._odometer_store = self._iq_lvbs_alc.create_vehicle_odometer_store(CP, self._params) if self._iq_lvbs_alc else None def _update_odometer(self, ret: structs.CarState, raw_km: float) -> None: - """Publish the cluster value while proprietary Konn3kt code owns persistence.""" + if self._odometer_store is None: + return odometer_km = self._odometer_store.record(raw_km) if odometer_km is not None: ret.odometer = odometer_km @@ -138,13 +142,16 @@ class CarState(CarStateBase): return button_events def _update_mqb_iq_alc_state(self, pt_cp): - self._iq_lvbs_alc.update_mqb_carstate_alc_state(self, pt_cp) + if self._iq_lvbs_alc: + self._iq_lvbs_alc.update_mqb_carstate_alc_state(self, pt_cp) def _update_mlb_iq_alc_state(self, pt_cp): - self._iq_lvbs_alc.update_mlb_carstate_alc_state(self, pt_cp) + if self._iq_lvbs_alc: + self._iq_lvbs_alc.update_mlb_carstate_alc_state(self, pt_cp) def _update_pq_iq_alc_state(self, pt_cp): - self._iq_lvbs_alc.update_pq_carstate_alc_state(self, pt_cp) + if self._iq_lvbs_alc: + self._iq_lvbs_alc.update_pq_carstate_alc_state(self, pt_cp) def update(self, can_parsers) -> tuple[structs.CarState, structs.IQCarState]: pt_cp = can_parsers[Bus.pt] @@ -317,7 +324,7 @@ class CarState(CarStateBase): ret.steeringAngleDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradwinkel"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradwinkel"])] ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])] ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])] - driver_override_threshold = self._iq_lvbs_alc.vw_driver_override_threshold_cnm(self, "mqb", self.CCP.STEER_DRIVER_ALLOWANCE) + driver_override_threshold = self._iq_lvbs_alc.vw_driver_override_threshold_cnm(self, "mqb", self.CCP.STEER_DRIVER_ALLOWANCE) if self._iq_lvbs_alc else self.CCP.STEER_DRIVER_ALLOWANCE ret.steeringPressed = abs(self.driver_torque_filter.update(ret.steeringTorque)) > driver_override_threshold self.curvature = -pt_cp.vl["QFK_01"]["Curvature"] * (1, -1)[int(pt_cp.vl["QFK_01"]["Curvature_VZ"])] @@ -528,7 +535,7 @@ class CarState(CarStateBase): self.LH_3_Sign = pt_cp.vl["Lenkhilfe_3"]["LH3_BLWSign"] self.LH2_steeringState = pt_cp.vl["Lenkhilfe_2"]["LH2_aktLenkeingriff"] self._update_pq_iq_alc_state(pt_cp) - driver_override_threshold = self._iq_lvbs_alc.vw_driver_override_threshold_cnm(self, "pq", self.CCP.STEER_DRIVER_ALLOWANCE) + driver_override_threshold = self._iq_lvbs_alc.vw_driver_override_threshold_cnm(self, "pq", self.CCP.STEER_DRIVER_ALLOWANCE) if self._iq_lvbs_alc else self.CCP.STEER_DRIVER_ALLOWANCE ret.steeringPressed = abs(ret.steeringTorque) > driver_override_threshold # Consume blind-spot monitoring info/warning LED states, if available. diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/mqbcan.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/mqbcan.py index e75525b7..fc49a095 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/mqbcan.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/mqbcan.py @@ -342,9 +342,12 @@ def create_acc_accel_control(packer, bus, acc_type, accel, acc_control, stopping return commands -def create_acc_hud_control(packer, bus, acc_hud_status, set_speed, leadDistance, distanceBars, fcw_alert, leadVisible, unavailable, decel, d_unresponsive, +def create_acc_hud_control(packer, bus, acc_hud_status, set_speed, lead_index, distanceBars, fcw_alert, leadVisible, unavailable, decel, d_unresponsive, *, priority_boost): priodisp = 0 if fcw_alert else 1 if priority_boost else 2 if acc_hud_status in (3, 4) else 3 + # The cluster draws the stock 1.0, 1.3, 1.8, 2.4 and 3.6 s gaps as notches 1-5. Aggressive, standard and relaxed + # (1.25, 1.45 and 1.75 s plus 3 m, about 1.37, 1.57 and 1.87 s at highway speed) take notches 2, 3 and 4: + # of the assignments that keep each personality distinct, it reads closest to how they actually follow. leadDistanceBars = distanceBars + 1 if distanceBars in (1, 2, 3) else 2 values = { "ACC_Status_Anzeige": acc_hud_status, @@ -353,7 +356,7 @@ def create_acc_hud_control(packer, bus, acc_hud_status, set_speed, leadDistance, "ACC_Optischer_Fahrerhinweis": 1 if fcw_alert else 0, "ACC_Display_Prio": priodisp, "ACC_Relevantes_Objekt": leadDistanceBars, - "ACC_Abstandsindex": leadDistance if leadVisible else 0, + "ACC_Abstandsindex": lead_index if leadVisible else 0, "ACC_Akustik_02": fcw_alert, } diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/pqcan.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/pqcan.py index d6fe3fed..6c61f5ae 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/pqcan.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/pqcan.py @@ -138,16 +138,18 @@ def create_blinker_control(packer, bus, leftBlinker, rightBlinker): return packer.make_can_msg("Blinkmodi_02", bus, values) -def create_acc_hud_control(packer, bus, acc_hud_status, set_speed, leadDistance, distanceBars, fcw_alert, leadVisible, unavailable, decel, d_unresponsive, +def create_acc_hud_control(packer, bus, acc_hud_status, set_speed, lead_index, distanceBars, fcw_alert, leadVisible, unavailable, decel, d_unresponsive, *, priority_boost=False): priodisp = 0 if (fcw_alert or decel or d_unresponsive or acc_hud_status in (6, 7)) else 1 if priority_boost else 2 if acc_hud_status in (3, 4) else 3 + # The same gap notches as MQB, see mqbcan.create_acc_hud_control. The field is 4 bits but the stock radar + # uses the low notch numbers (a stock Sharan sends 1 and 2) leadDistanceBars = distanceBars + 1 if distanceBars in (1, 2, 3) else 2 values = { "ACA_StaACC": acc_hud_status, "ACA_AnzDisplay": 1 if acc_hud_status in (3, 4) else 0, "ACA_Zeitluecke": leadDistanceBars, "ACA_V_Wunsch": set_speed, - "ACA_gemZeitl": min(15, max(1, int(round(leadDistance)))) if leadVisible else 0, + "ACA_gemZeitl": lead_index if leadVisible else 0, "ACA_PrioDisp": priodisp, "ACA_Akustik1": d_unresponsive, "ACA_Akustik2": fcw_alert, diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_lead_headway_display.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_lead_headway_display.py new file mode 100644 index 00000000..b6d9853c --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_lead_headway_display.py @@ -0,0 +1,110 @@ +"""The MQB/PQ cluster's lead marker and gap notches, as encoded on the wire.""" +import pytest + +from iqdbc.can import CANParser +from iqdbc.car import structs +from iqdbc.car.common.conversions import Conversions as CV +from iqdbc.car.volkswagen.carcontroller import ( + LEAD_HEADWAY_MIN_SPEED, LEAD_INDEX_RANGE_ANALOG, LEAD_INDEX_RANGE_DIGITAL, lead_headway_index, +) +from iqdbc.car.volkswagen.interface import CarInterface +from iqdbc.car.volkswagen.values import CAR + +SECOND_NOTCH = 1.3 # the stock gap settings run 1.0, 1.3, 1.8, 2.4, 3.6 s +FOLLOW_TIMES = {1: 1.25, 2: 1.45, 3: 1.75} # leadDistanceBars: T_FOLLOW, aggressive to relaxed +STOP_DISTANCE = 3.0 # the planner follows at T_FOLLOW * v + STOP_DISTANCE + + +@pytest.mark.parametrize("digital", [False, True]) +@pytest.mark.parametrize("speed", [0.0, 10.0, 30.0]) +def test_grows_with_distance_and_saturates_at_the_ends(digital, speed): + codes = [lead_headway_index(d / 2, speed, digital) for d in range(1, 600)] + closest, farthest = LEAD_INDEX_RANGE_DIGITAL if digital else LEAD_INDEX_RANGE_ANALOG + assert codes == sorted(codes) + assert (codes[0], codes[-1]) == (closest, farthest) + + +def test_reads_headway_not_distance(): + assert lead_headway_index(1.8 * 10.0, 10.0, False) == lead_headway_index(1.8 * 30.0, 30.0, False) + assert lead_headway_index(30.0, 10.0, False) > lead_headway_index(30.0, 25.0, False) + + +def test_stopped_reads_distance_alone(): + assert lead_headway_index(5.0, 0.0, False) == lead_headway_index(5.0, LEAD_HEADWAY_MIN_SPEED, False) + + +def make_car(candidate): + fingerprint = {bus: {} for bus in range(8)} + cp = CarInterface.get_params(candidate, fingerprint, [], alpha_long=True, is_release=False, docs=False) + cp_iq = CarInterface.get_params_iq(cp, candidate, fingerprint, [], alpha_long=True, is_release_iq=False, docs=False) + car = CarInterface(cp, cp_iq) + car.update([(0, [])]) + car.CS.out = structs.CarState() + car.CS.out.cruiseState.available = True + return car + + +def send_hud(car, *, lead_distance, speed, bars=2, lead_visible=True): + car.CS.out.vEgo = speed + cc = structs.CarControl() + cc.enabled = cc.longActive = True + cc.hudControl.leadVisible = lead_visible + cc.hudControl.leadDistance = lead_distance + cc.hudControl.leadDistanceBars = bars + cc.hudControl.leadFollowTime = FOLLOW_TIMES[bars] + car.CC.frame += -car.CC.frame % car.CC.CCP.ACC_HUD_STEP + now_nanos = (car.CC.frame + 1) * 10_000_000 + _, messages = car.apply(cc.as_reader(), structs.IQCarControl(), now_nanos) + return now_nanos, messages + + +def mqb_hud(car, **kwargs): + now_nanos, messages = send_hud(car, **kwargs) + parser = CANParser("vw_mqb", [("ACC_02", 0)], car.CC.CAN.pt) + parser.update([(now_nanos, messages)]) + return parser.vl["ACC_02"]["ACC_Gesetzte_Zeitluecke"], parser.vl["ACC_02"]["ACC_Abstandsindex"] + + +def pq_hud(car, **kwargs): + now_nanos, messages = send_hud(car, **kwargs) + parser = CANParser("vw_pq", [("ACC_GRA_Anzeige", 0)], car.CC.CAN.pt) + parser.update([(now_nanos, messages)]) + return parser.vl["ACC_GRA_Anzeige"]["ACA_Zeitluecke"], parser.vl["ACC_GRA_Anzeige"]["ACA_gemZeitl"] + + +PLATFORMS = [ + pytest.param(CAR.VOLKSWAGEN_GOLF_MK7, mqb_hud, False, id="mqb_analog"), + pytest.param(CAR.VOLKSWAGEN_GOLF_MK7, mqb_hud, True, id="mqb_digital"), + pytest.param(CAR.VOLKSWAGEN_PASSAT_NMS, pq_hud, False, id="pq"), +] + + +@pytest.mark.parametrize("candidate, hud, digital", PLATFORMS) +def test_reported_cases(candidate, hud, digital): + car = make_car(candidate) + car.CS.upscale_lead_car_signal = digital + closest = (LEAD_INDEX_RANGE_DIGITAL if digital else LEAD_INDEX_RANGE_ANALOG)[0] + speed = 45 * CV.MPH_TO_MS + # About one second behind at 45 mph read the far end; it belongs at the closest notch, short of the second + assert closest < hud(car, lead_distance=20.0, speed=speed)[1] < lead_headway_index(SECOND_NOTCH * speed, speed, digital) + # Stopped 1.5 m behind a car read second-closest + assert hud(car, lead_distance=1.5, speed=0.0)[1] == closest + + +@pytest.mark.parametrize("candidate, hud, digital", PLATFORMS) +def test_no_lead_sends_no_object(candidate, hud, digital): + car = make_car(candidate) + car.CS.upscale_lead_car_signal = digital + assert hud(car, lead_distance=30.0, speed=20.0, lead_visible=False)[1] == 0 + assert hud(car, lead_distance=0.0, speed=20.0)[1] == 0 + + +@pytest.mark.parametrize("candidate, hud, digital", PLATFORMS) +def test_personalities_get_distinct_notches_and_ordered_markers(candidate, hud, digital): + car = make_car(candidate) + car.CS.upscale_lead_car_signal = digital + speed = 25.0 + notches, markers = zip(*(hud(car, bars=bars, lead_distance=FOLLOW_TIMES[bars] * speed + STOP_DISTANCE, speed=speed) + for bars in FOLLOW_TIMES), strict=True) + assert notches == (2, 3, 4) + assert markers[0] < markers[1] < markers[2] diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py index fb5821f2..e0e0d3e8 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py @@ -258,6 +258,34 @@ def test_mlb_no_ecan_transmits_on_the_powertrain_bus(): assert {bus for _, _, bus in sent} == {1} +def test_hca_torque_stops_the_moment_openpilot_disengages(): + a4 = _a4_mk4_car() + packer = CANPacker("vw_mlb") + nanos = 0 + + def run(enabled, lat_active, steps=20): + nonlocal nanos + CC = structs.CarControl() + CC.enabled = enabled + CC.latActive = lat_active + CC.actuators.torque = 1.0 + CC = CC.as_reader() + CC_IQ = structs.IQCarControl() + for _ in range(steps): + nanos += int(DT_CTRL * 1e9) + a4.update([(nanos, _a4_mk4_frames(packer, eps_torque=0))]) + a4.apply(CC, CC_IQ, nanos) + return a4.CC.HCA_Status, a4.CC.apply_torque_last + + engaged_status, engaged_torque = run(enabled=True, lat_active=True) + assert engaged_status in (5, 7) + assert engaged_torque != 0 + + disengaged_status, disengaged_torque = run(enabled=False, lat_active=True) + assert disengaged_status == 3 + assert disengaged_torque == 0 + + def test_mlb_no_hca_eps_reports_a_steer_fault_without_a_can_fault(): a4 = _a4_mk4_car() assert a4.CP.flags & VolkswagenFlagsIQ.IQ_MLB_NO_HCA_EPS diff --git a/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/tesla/tesla_radar_bosch.py b/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/tesla/tesla_radar_bosch.py index adfd2d84..a6985d5d 100755 --- a/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/tesla/tesla_radar_bosch.py +++ b/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/tesla/tesla_radar_bosch.py @@ -45,7 +45,7 @@ BS_: BU_: Autopilot Radar Diag -BO_ 769 TeslaRadarSguInfo: 8 Radar +BO_ 769 TeslaRadarSguInfo: 6 Radar SG_ RADC_VerticalMisalignment : 0|8@1+ (1,0) [0|255] "" Autopilot SG_ RADC_SCUTemperature : 8|8@1+ (1,-128) [-128|127] "" Autopilot SG_ RADC_VMA_Plaus : 16|8@1+ (1,0) [0|255] "" Autopilot diff --git a/artifacts/package_sources/iqdbc/iqdbc/dbc/tesla_can.dbc b/artifacts/package_sources/iqdbc/iqdbc/dbc/tesla_can.dbc index 56624c3e..447231d5 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/dbc/tesla_can.dbc +++ b/artifacts/package_sources/iqdbc/iqdbc/dbc/tesla_can.dbc @@ -63,6 +63,10 @@ VAL_TABLE_ DI_speedUnits 1 "DI_SPEED_KPH" 0 "DI_SPEED_MPH" ; VAL_TABLE_ DI_state 4 "DI_STATE_ENABLE" 3 "DI_STATE_FAULT" 2 "DI_STATE_CLEAR_FAULT" 1 "DI_STATE_STANDBY" 0 "DI_STATE_PREAUTH" ; VAL_TABLE_ DI_velocityEstimatorState 4 "VE_STATE_BACKUP_MOTOR" 3 "VE_STATE_BACKUP_WHEELS_B" 2 "VE_STATE_BACKUP_WHEELS_A" 1 "VE_STATE_WHEELS_NORMAL" 0 "VE_STATE_NOT_INITIALIZED" ; +BO_ 637 APS_eacMonitor: 3 APS + SG_ APS_eacAllow : 0|2@1+ (1,0) [0|0] "" EPAS3P + SG_ APS_eacMonitorCounter : 8|4@1+ (1,0) [0|0] "" EPAS3P + SG_ APS_eacMonitorChecksum : 16|8@1+ (1,0) [0|0] "" EPAS3P BO_ 1160 DAS_steeringControl: 4 NEO SG_ DAS_steeringControlType : 23|2@0+ (1,0) [0|0] "" EPAS @@ -206,7 +210,7 @@ BO_ 309 ESP_135h: 5 ESP BO_ 341 ESP_B: 8 ESP SG_ ESP_BChecksum : 39|8@0+ (1,0) [0|255] "" NEO,EPAS SG_ ESP_BCounter : 62|4@0+ (1,0) [1|15] "" NEO,EPAS - SG_ ESP_vehicleSpeed : 47|16@0+ (0.00999999978,0) [0|0] "kph" NEO,EPAS + SG_ ESP_vehicleSpeed : 47|16@0+ (0.01,0) [0|0] "kph" NEO,EPAS SG_ ESP_vehicleSpeedQF : 57|2@0+ (1,0) [1|2] "" NEO,EPAS SG_ ESP_wheelPulseCountFrL : 7|8@0+ (1,0) [0|254] "" NEO,EPAS SG_ ESP_wheelPulseCountFrR : 15|8@0+ (1,0) [0|254] "" NEO,EPAS @@ -663,6 +667,31 @@ BO_ 1064 UI_telemetryControl: 8 GTW BO_ 522 BrakeMessage: 8 XXX SG_ driverBrakeStatus : 2|2@1+ (1,0) [0|3] "" XXX +BO_ 529 RCM_status: 8 RCM + SG_ RCM_driverOrientationInitStatus : 2|3@0+ (1,0) [0|7] "" GTW,OCS + SG_ RCM_armStatus : 5|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_warningIndicatorState : 7|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_tellTalePassengerState : 9|2@0+ (1,0) [0|3] "" GTW,OCS,DAS + SG_ RCM_bucklePassengerStatus : 11|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_tellTaleDriverState : 13|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_buckleDriverStatus : 15|2@0+ (1,0) [0|3] "" GTW,OCS,DAS + SG_ RCM_seatTrackPositionDrvr : 19|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_passAirbagRequest : 21|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_chimeState1stRow : 23|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_vinLearnFlag : 25|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_hwConfigPin58 : 27|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_hwConfigPin57 : 29|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_hwConfigPin56 : 31|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_passOccupantStatus : 33|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_airbagCutoffSwStatus : 35|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_privateCANStatus : 37|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_publicCANStatus : 39|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_edr2State : 45|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_edr1State : 47|2@0+ (1,0) [0|3] "" GTW,OCS + SG_ RCM_buckle2ndRowStatus : 50|3@0+ (1,0) [0|3] "" GTW,OCS,DAS + SG_ RCM_statusCounter : 55|4@0+ (1,0) [0|15] "" GTW,OCS,DAS + SG_ RCM_statusChecksum : 63|8@0+ (1,0) [0|255] "" GTW,OCS,DAS + BO_ 921 AutopilotStatus: 8 XXX SG_ autopilotStatus : 0|4@1+ (1,0) [0|0] "" XXX SG_ DAS_blindSpotRearLeft : 4|2@1+ (1,0) [0|0] "" XXX @@ -794,6 +823,7 @@ VAL_ 521 DAS_locSpeed 2047 "SNA" ; VAL_ 521 DAS_locAccelMin 511 "SNA" ; VAL_ 521 DAS_locAccelMax 511 "SNA" ; VAL_ 522 driverBrakeStatus 2 "APPLIED" 1 "NOT_APPLIED" ; +VAL_ 637 APS_eacAllow 3 "APS_EAC_SNA" 2 "APS_EAC_RESERVED" 1 "APS_EAC_ALLOW" 0 "APS_EAC_INHIBIT" ; VAL_ 760 UI_mapSpeedLimitUnits 1 "KPH" 0 "MPH" ; VAL_ 760 UI_userSpeedOffsetUnits 1 "KPH" 0 "MPH" ; VAL_ 643 AirTemp_Insd 255 "SNA" ; @@ -895,7 +925,7 @@ VAL_ 1001 DAS_highLowBeamDecision 3 "DAS_HIGH_BEAM_SNA" 2 "DAS_HIGH_BEAM_ON" 1 " VAL_ 1001 DAS_highLowBeamOffReason 5 "HIGH_BEAM_OFF_REASON_SNA" 4 "HIGH_BEAM_OFF_REASON_HEAD_LIGHT" 3 "HIGH_BEAM_OFF_REASON_AMBIENT_LIGHT" 2 "HIGH_BEAM_OFF_REASON_MOVING_RADAR_TARGET" 1 "HIGH_BEAM_OFF_REASON_MOVING_VISION_TARGET" 0 "HIGH_BEAM_ON" ; VAL_ 1001 DAS_turnIndicatorRequestReason 6 "DAS_ACTIVE_COMMANDED_LANE_CHANGE" 5 "DAS_CANCEL_FORK" 4 "DAS_CANCEL_LANE_CHANGE" 3 "DAS_ACTIVE_FORK" 2 "DAS_ACTIVE_SPEED_LANE_CHANGE" 1 "DAS_ACTIVE_NAV_LANE_CHANGE" 0 "DAS_NONE" ; VAL_ 1160 DAS_steeringAngleRequest 16384 "ZERO_ANGLE" ; -VAL_ 1160 DAS_steeringControlType 1 "ANGLE_CONTROL" 3 "DISABLED" 0 "NONE" 2 "RESERVED" ; +VAL_ 1160 DAS_steeringControlType 2 "LANE_KEEP_ASSIST" 0 "NONE" 1 "ANGLE_CONTROL" 3 "EMERGENCY_LANE_KEEP" ; VAL_ 1160 DAS_steeringHapticRequest 1 "ACTIVE" 0 "IDLE" ; diff --git a/artifacts/package_sources/iqdbc/iqdbc/dbc/tesla_raven_party.dbc b/artifacts/package_sources/iqdbc/iqdbc/dbc/tesla_raven_party.dbc new file mode 100644 index 00000000..40ac48cc --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/dbc/tesla_raven_party.dbc @@ -0,0 +1,115 @@ +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_: + +BU_: IBST ESP EPAS EPAS3P APS APP DAS + +BO_ 14 STW_ANGLHP_STAT: 8 APS + SG_ StW_AnglHP : 5|14@0+ (0.1,-819.2) [-819.2|819] "deg" EPAS3P + SG_ StW_AnglHP_Spd : 21|14@0+ (0.5,-4096) [-4096|4095.5] "deg/s" EPAS3P + SG_ StW_AnglHP_Sens_Stat : 33|2@0+ (1,0) [0|0] "" EPAS3P + SG_ StW_AnglHP_Sens_Id : 35|2@0+ (1,0) [0|0] "" EPAS3P + SG_ MC_STW_ANGLHP_STAT : 55|4@0+ (1,0) [0|15] "" EPAS3P + SG_ CRC_STW_ANGLHP_STAT : 63|8@0+ (1,0) [0|0] "" EPAS3P + +BO_ 637 APS_eacMonitor: 3 APS + SG_ APS_eacAllow : 0|2@1+ (1,0) [0|0] "" EPAS3P + SG_ APS_eacMonitorCounter : 8|4@1+ (1,0) [0|0] "" EPAS3P + SG_ APS_eacMonitorChecksum : 16|8@1+ (1,0) [0|0] "" EPAS3P + +BO_ 880 EPAS_sysStatus: 8 EPAS + SG_ EPAS_steeringRackForce : 1|10@0+ (50,-25575) [-25575|25575] "N" Vector__XXX + SG_ EPAS_steeringFault : 2|1@0+ (1,0) [0|1] "" APS,APP + SG_ EPAS_steeringReduced : 3|1@0+ (1,0) [0|1] "" APS,APP + SG_ EPAS_currentTuneMode : 7|4@0+ (1,0) [0|0] "" APS,APP + SG_ EPAS_torsionBarTorque : 19|12@0+ (0.01,-20.5) [-20.5|20.45] "Nm" APS,APP + SG_ EPAS_eacErrorCode : 23|4@0+ (1,0) [0|15] "" APS,APP + SG_ EPAS_internalSAS : 37|14@0+ (0.1,-819.2) [-819.2|819] "deg" APS,APP + SG_ EPAS_handsOnLevel : 39|2@0+ (1,0) [0|3] "" APS,APP + SG_ EPAS_sysStatusCounter : 51|4@0+ (1,0) [0|15] "" APS,APP + SG_ EPAS_eacStatus : 55|3@0+ (1,0) [0|7] "" APS,APP + SG_ EPAS_sysStatusChecksum : 63|8@0+ (1,0) [0|255] "" APS,APP + +BO_ 907 ESP_private1: 8 ESP + SG_ ESP_private1Checksum : 0|8@1+ (1,0) [0|255] "" IBST,EPAS + SG_ ESP_private1Counter : 8|4@1+ (1,0) [0|15] "" IBST,EPAS + SG_ ESP_pEstMax : 16|8@1+ (1,0) [0|250] "bar" IBST + SG_ ESP_pEstMaxQF : 24|2@1+ (1,0) [0|2] "" IBST + SG_ ESP_vehicleSpeed : 26|14@1+ (0.05625,0) [0|921.54375] "km/h" IBST,EPAS + SG_ ESP_vehicleSpeedQF : 40|3@1+ (1,0) [0|6] "" IBST,EPAS + SG_ ESP_ignitionOn : 43|1@1+ (1,0) [0|1] "" IBST + +BO_ 911 IBST_private2: 8 IBST + SG_ IBST_private2Checksum : 0|8@1+ (1,0) [0|255] "" ESP + SG_ IBST_private2Counter : 8|4@1+ (1,0) [0|15] "" ESP + SG_ IBST_pForceBlendingPotentialQF : 12|2@1+ (1,0) [0|2] "" ESP + SG_ IBST_brakePedalApplied : 16|1@1+ (1,0) [0|1] "1" ESP + SG_ IBST_brakePedalAppliedQF : 17|2@1+ (1,0) [0|3] "" ESP + SG_ IBST_status : 19|3@1+ (1,0) [0|7] "1" ESP + SG_ IBST_pRunout : 22|8@1+ (1,0) [0|250] "bar" ESP + SG_ IBST_pRunoutQF : 30|2@1+ (1,0) [0|3] "" ESP + SG_ IBST_compatibilityIndex : 32|4@1+ (1,0) [0|15] "" ESP + SG_ IBST_pForceBlendingPotential : 40|8@1+ (0.5,0) [0|125] "bar" ESP + +BO_ 1160 DAS_steeringControl: 4 DAS + SG_ DAS_steeringAngleRequest : 6|15@0+ (0.1,-1638.35) [-1638.35|1638.35] "deg" EPAS3P,APS + SG_ DAS_steeringHapticRequest : 7|1@0+ (1,0) [0|0] "" EPAS3P,APS + SG_ DAS_steeringControlCounter : 19|4@0+ (1,0) [0|0] "" EPAS3P,APS + SG_ DAS_steeringControlType : 23|2@0+ (1,0) [0|0] "" EPAS3P,APS + SG_ DAS_steeringControlChecksum : 31|8@0+ (1,0) [0|0] "" EPAS3P,APS + +VAL_ 14 StW_AnglHP 16383 "SNA" ; +VAL_ 14 StW_AnglHP_Spd 16383 "SNA" ; +VAL_ 14 StW_AnglHP_Sens_Stat 3 "SNA" 2 "ERR" 1 "INI" 0 "OK" ; +VAL_ 14 StW_AnglHP_Sens_Id 3 "SNA" 2 "KOSTAL" 1 "DELPHI" 0 "TEST" ; +VAL_ 637 APS_eacAllow 3 "APS_EAC_SNA" 2 "APS_EAC_RESERVED" 1 "APS_EAC_ALLOW" 0 "APS_EAC_INHIBIT" ; +VAL_ 880 EPAS_steeringRackForce 1023 "SNA" 1022 "NOT_IN_SPEC" ; +VAL_ 880 EPAS_steeringFault 1 "FAULT" 0 "NO_FAULT" ; +VAL_ 880 EPAS_steeringReduced 1 "REDUCED_ASSIST" 0 "NORMAL_ASSIST" ; +VAL_ 880 EPAS_currentTuneMode 7 "UNAVAILABLE" 6 "RWD_SPORT" 5 "RWD_STANDARD" 4 "RWD_COMFORT" 3 "DM_SPORT" 2 "DM_STANDARD" 1 "DM_COMFORT" 0 "FAIL_SAFE_DEFAULT" ; +VAL_ 880 EPAS_torsionBarTorque 4095 "SNA" 4094 "UNDEFINABLE_DATA" ; +VAL_ 880 EPAS_eacErrorCode 15 "SNA" 14 "EAC_EXTERNAL_MONITOR_INHIBIT" 13 "EAC_ERROR_PINION_VEL_DIFF" 12 "EAC_ERROR_LOW_ASSIST" 11 "EAC_ERROR_HIGH_TORSION_SAFETY" 10 "EAC_ERROR_HIGH_MMOT_SAFETY" 9 "EAC_ERROR_HIGH_ANGLE_RATE_SAFETY" 8 "EAC_ERROR_HIGH_ANGLE_SAFETY" 7 "EAC_ERROR_HIGH_ANGLE_RATE_REQ" 6 "EAC_ERROR_HIGH_ANGLE_REQ" 5 "EAR_ERROR_MAX_STEER_DELTA" 4 "EAC_ERROR_TMP_FAULT" 3 "EAC_ERROR_HANDS_ON" 2 "EAC_ERROR_MAX_SPEED" 1 "EAC_ERROR_MIN_SPEED" 0 "EAC_ERROR_IDLE" ; +VAL_ 880 EPAS_handsOnLevel 3 "LEVEL_3" 2 "LEVEL_2" 1 "LEVEL_1" 0 "LEVEL_0" ; +VAL_ 880 EPAS_eacStatus 7 "EAC_ELK_ACTIVE" 6 "EAC_ELK_AVAILABLE" 5 "EAC_LKA_ACTIVE" 4 "EAC_LKA_ELK_AVAILABLE" 3 "EAC_FAULT" 2 "EAC_ACTIVE" 1 "EAC_AVAILABLE" 0 "EAC_INHIBITED" ; +VAL_ 907 ESP_pEstMaxQF 2 "PESTMAX_FAULT" 1 "PESTMAX_NORMAL" 0 "PESTMAX_NOT_INITIALIZED" ; +VAL_ 907 ESP_vehicleSpeedQF 6 "SPEED_INV_UNDER_V" 5 "SPEED_NOT_CALC" 4 "SPEED_REDUCED_MON" 3 "SPEED_DIFF_CALC" 2 "SPEED_FAULT" 1 "SPEED_NORMAL" 0 "SPEED_NOT_INITIALIZED" ; +VAL_ 907 ESP_ignitionOn 1 "ESP_IGNITION_ON" 0 "ESP_IGNITION_OFF" ; +VAL_ 911 IBST_pForceBlendingPotentialQF 2 "BLENDING_POTENTIAL_FAULT" 1 "BLENDING_POTENTIAL_NORMAL" 0 "BLENDING_POTENTIAL_NOT_INIT" ; +VAL_ 911 IBST_brakePedalApplied 1 "BRAKE_PEDAL_APPLIED" 0 "BRAKE_PEDAL_NOT_APPLIED" ; +VAL_ 911 IBST_brakePedalAppliedQF 2 "BRAKE_PEDAL_APPLIED_FAULT" 1 "BRAKE_PEDAL_APPLIED_NORMAL" 0 "BRAKE_PEDAL_APPLIED_OFF" ; +VAL_ 911 IBST_status 7 "IBOOSTER_READY" 6 "IBOOSTER_POST_RUN_REDUCED" 5 "IBOOSTER_REDUCED" 4 "IBOOSTER_STANDALONE" 3 "IBOOSTER_UNDEFINED_1" 2 "IBOOSTER_DIAGNOSTIC" 1 "IBOOSTER_FAULT" 0 "IBOOSTER_NOT_INITIALIZED" ; +VAL_ 911 IBST_pRunoutQF 2 "RUNNOUT_FAULT" 1 "RUNNOUT_NORMAL" 0 "RUNNOUT_NOT_INITIALIZED" ; +VAL_ 1160 DAS_steeringAngleRequest 16384 "ZERO_ANGLE" ; +VAL_ 1160 DAS_steeringHapticRequest 1 "HAPTIC_ACTIVE" 0 "HAPTIC_IDLE" ; +VAL_ 1160 DAS_steeringControlType 3 "EMERGENCY_LANE_KEEP" 2 "LANE_KEEP_ASSIST" 1 "ANGLE_CONTROL" 0 "NONE" ; \ No newline at end of file diff --git a/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/interfaces.py b/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/interfaces.py index dd118170..5ca575ad 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/interfaces.py +++ b/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/interfaces.py @@ -9,6 +9,7 @@ from collections.abc import Callable from iqdbc.car import structs from iqdbc.car.can_definitions import CanRecvCallable, CanSendCallable from iqdbc.car.subaru.values import SubaruFlags +from iqdbc.car.tesla.values import LEGACY_CARS from iqdbc.lvbs.car.subaru.iq_values import SubaruFlagsIQ, SubaruSafetyFlagsIQ from iqdbc.lvbs.car.tesla.values import TeslaFlagsIQ, TeslaSafetyFlagsIQ from iqdbc.lvbs.car.toyota.values import ToyotaFlagsIQ @@ -89,7 +90,7 @@ def _apply_long_tuning(CI, CP: structs.CarParams, CP_IQ: structs.IQCarParams, def _apply_tesla_options(CP: structs.CarParams, CP_IQ: structs.IQCarParams, params_dict: dict[str, str]) -> None: - if CP.brand == 'tesla': + if CP.brand == 'tesla' and CP.carFingerprint not in LEGACY_CARS: torque_blend = int(params_dict.get("IQTeslaTorqueBlend", 0)) == 1 if torque_blend: CP_IQ.flags |= TeslaFlagsIQ.COOP_STEERING.value diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/declarations.h b/artifacts/package_sources/iqdbc/iqdbc/safety/declarations.h index 69868b7a..990de372 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/declarations.h +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/declarations.h @@ -34,6 +34,7 @@ #define SAFETY_PSA 31U #define SAFETY_RIVIAN 33U #define SAFETY_VOLKSWAGEN_MEB 34U +#define SAFETY_TESLA_LEGACY 39U #define GET_BIT(msg, b) ((bool)!!(((msg)->data[((b) / 8U)] >> ((b) % 8U)) & 0x1U)) #define GET_FLAG(value, mask) (((value) & (mask)) == (mask)) @@ -353,6 +354,7 @@ extern const safety_hooks nissan_hooks; extern const safety_hooks subaru_hooks; extern const safety_hooks subaru_preglobal_hooks; extern const safety_hooks tesla_hooks; +extern const safety_hooks tesla_legacy_hooks; extern const safety_hooks toyota_hooks; extern const safety_hooks volkswagen_mlb_hooks; extern const safety_hooks volkswagen_mqb_hooks; diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/modes/tesla_legacy.h b/artifacts/package_sources/iqdbc/iqdbc/safety/modes/tesla_legacy.h new file mode 100644 index 00000000..9bb94478 --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/modes/tesla_legacy.h @@ -0,0 +1,272 @@ +/* + * Copyright © IQ.Lvbs, a part of Project Teal Lvbs. + * All Rights Reserved. + * Licensed under: https://konn3kt.com/tos + */ + +#pragma once + +#include "iqdbc/safety/declarations.h" + +#define TESLA_LEGACY_FLAG_EXTERNAL_PANDA 4U +#define TESLA_LEGACY_FLAG_HW1 8U +#define TESLA_LEGACY_FLAG_HW2 16U +#define TESLA_LEGACY_FLAG_HW3 32U + +#define TESLA_LEGACY_ADDR_ESP_B 0x155U +#define TESLA_LEGACY_ADDR_BRAKE_MESSAGE 0x20aU +#define TESLA_LEGACY_ADDR_BRAKE_MESSAGE_PT 0x1f8U +#define TESLA_LEGACY_ADDR_DI_STATE 0x368U +#define TESLA_LEGACY_ADDR_DI_STATE_PT 0x256U +#define TESLA_LEGACY_ADDR_EPAS_SYS_STATUS 0x370U +#define TESLA_LEGACY_ADDR_DAS_STEERING_CONTROL 0x488U +#define TESLA_LEGACY_ADDR_APS_EAC_MONITOR 0x27dU +#define TESLA_LEGACY_ADDR_DAS_CONTROL 0x2b9U +#define TESLA_LEGACY_ADDR_DAS_CONTROL_PT 0x2bfU +#define TESLA_LEGACY_ADDR_DI_TORQUE1 0x106U +#define TESLA_LEGACY_ADDR_DI_TORQUE1_HW1 0x108U + +static bool tesla_legacy_external_panda = false; +static bool tesla_legacy_hw1 = false; + +static uint8_t tesla_legacy_chassis_bus = 0U; +static unsigned int tesla_legacy_das_control_addr = TESLA_LEGACY_ADDR_DAS_CONTROL_PT; +static unsigned int tesla_legacy_di_torque1_addr = TESLA_LEGACY_ADDR_DI_TORQUE1; + +static bool tesla_legacy_stock_aeb = false; +static bool tesla_legacy_stock_lkas = false; +static bool tesla_legacy_stock_lkas_prev = false; + +static void tesla_legacy_rx_hook(const CANPacket_t *msg) { + if (!tesla_legacy_external_panda && (msg->bus == 0U) && (msg->addr == TESLA_LEGACY_ADDR_EPAS_SYS_STATUS)) { + const int angle_meas_new = (((msg->data[4] & 0x3FU) << 8) | msg->data[5]) - 8192U; + update_sample(&angle_meas, angle_meas_new); + + const int hands_on_level = msg->data[4] >> 6; + const int eac_status = msg->data[6] >> 5; + const int eac_error_code = msg->data[2] >> 4; + + steering_disengage = (hands_on_level >= 3) || ((eac_status == 0) && (eac_error_code == 9)); + } + + if (!tesla_legacy_external_panda && (msg->bus == tesla_legacy_chassis_bus) && (msg->addr == TESLA_LEGACY_ADDR_ESP_B)) { + float speed = ((msg->data[6] | (msg->data[5] << 8)) * 0.01) * KPH_TO_MS; + UPDATE_VEHICLE_SPEED(speed); + } + + if ((tesla_legacy_external_panda || tesla_legacy_hw1) && (msg->bus == 0U) && (msg->addr == tesla_legacy_di_torque1_addr)) { + gas_pressed = msg->data[6] != 0U; + } + + if ((tesla_legacy_external_panda && (msg->bus == 0U) && (msg->addr == TESLA_LEGACY_ADDR_BRAKE_MESSAGE_PT)) || + (!tesla_legacy_external_panda && (msg->bus == tesla_legacy_chassis_bus) && (msg->addr == TESLA_LEGACY_ADDR_BRAKE_MESSAGE))) { + brake_pressed = (((msg->data[0] & 0x0CU) >> 2) != 1U); + } + + if ((tesla_legacy_external_panda && (msg->bus == 0U) && (msg->addr == TESLA_LEGACY_ADDR_DI_STATE_PT)) || + (!tesla_legacy_external_panda && (msg->bus == tesla_legacy_chassis_bus) && (msg->addr == TESLA_LEGACY_ADDR_DI_STATE))) { + int cruise_state = (msg->data[1] >> 4) & 0x07U; + bool cruise_engaged = (cruise_state == 2) || + (cruise_state == 3) || + (cruise_state == 4) || + (cruise_state == 6) || + (cruise_state == 7); + vehicle_moving = cruise_state != 3; + pcm_cruise_check(cruise_engaged); + } + + if (msg->bus == 2U) { + if ((tesla_legacy_external_panda || tesla_legacy_hw1) && (msg->addr == tesla_legacy_das_control_addr)) { + tesla_legacy_stock_aeb = (msg->data[2] & 0x03U) == 1U; + } + + if (!tesla_legacy_external_panda && (msg->addr == TESLA_LEGACY_ADDR_DAS_STEERING_CONTROL)) { + int steering_control_type = msg->data[2] >> 6; + bool tesla_legacy_stock_lkas_now = steering_control_type == 2; + + if (tesla_legacy_stock_lkas_now && !tesla_legacy_stock_lkas_prev && !is_lat_active()) { + tesla_legacy_stock_lkas = true; + } + if (!tesla_legacy_stock_lkas_now) { + tesla_legacy_stock_lkas = false; + } + tesla_legacy_stock_lkas_prev = tesla_legacy_stock_lkas_now; + } + } +} + +static bool tesla_legacy_tx_hook(const CANPacket_t *msg) { + const AngleSteeringLimits TESLA_LEGACY_STEERING_LIMITS = { + .max_angle = 3600, + .angle_deg_to_can = 10, + .frequency = 50U, + }; + + // NOTE: based off TESLA_MODEL_S_HW3 to match the car controller + const AngleSteeringParams TESLA_LEGACY_STEERING_PARAMS = { + .slip_factor = -0.0005666493436310427, + .steer_ratio = 15., + .wheelbase = 2.96, + }; + + const LongitudinalLimits TESLA_LEGACY_LONG_LIMITS = { + .max_accel = 425, + .min_accel = 288, + .inactive_accel = 375, + .zero_accel = 375, + }; + + bool tx = true; + bool violation = false; + + if (!tesla_legacy_external_panda && (msg->addr == TESLA_LEGACY_ADDR_DAS_STEERING_CONTROL)) { + int raw_angle_can = ((msg->data[0] & 0x7FU) << 8) | msg->data[1]; + int desired_angle = raw_angle_can - 16384; + int steer_control_type = msg->data[2] >> 6; + bool steer_control_enabled = steer_control_type == 1; + + if (steer_angle_cmd_checks_vm(desired_angle, steer_control_enabled, TESLA_LEGACY_STEERING_LIMITS, TESLA_LEGACY_STEERING_PARAMS)) { + violation = true; + } + + bool valid_steer_control_type = (steer_control_type == 0) || + (steer_control_type == 1); + if (!valid_steer_control_type) { + violation = true; + } + + if (tesla_legacy_stock_lkas) { + violation = true; + } + } + + if ((tesla_legacy_external_panda || tesla_legacy_hw1) && (msg->addr == tesla_legacy_das_control_addr)) { + int aeb_event = msg->data[2] & 0x03U; + if (aeb_event != 0) { + violation = true; + } + + if (tesla_legacy_stock_aeb) { + violation = true; + } + + int raw_accel_max = ((msg->data[6] & 0x1FU) << 4) | (msg->data[5] >> 4); + int raw_accel_min = ((msg->data[5] & 0x0FU) << 5) | (msg->data[4] >> 3); + + // Both limits below zero could let the car reverse after coming to a standstill + if ((raw_accel_max < TESLA_LEGACY_LONG_LIMITS.inactive_accel) && (raw_accel_min < TESLA_LEGACY_LONG_LIMITS.inactive_accel)) { + violation = true; + } + + violation |= longitudinal_accel_checks(raw_accel_max, TESLA_LEGACY_LONG_LIMITS); + violation |= longitudinal_accel_checks(raw_accel_min, TESLA_LEGACY_LONG_LIMITS); + } + + if (violation) { + tx = false; + } + + return tx; +} + +static bool tesla_legacy_fwd_hook(int bus_num, int addr) { + bool block_msg = false; + + if (bus_num == 2) { + if (!tesla_legacy_external_panda && !tesla_legacy_hw1 && (addr == (int)TESLA_LEGACY_ADDR_APS_EAC_MONITOR)) { + block_msg = true; + } + + if (!tesla_legacy_external_panda && (addr == (int)TESLA_LEGACY_ADDR_DAS_STEERING_CONTROL) && !tesla_legacy_stock_lkas) { + block_msg = true; + } + + if ((tesla_legacy_external_panda || tesla_legacy_hw1) && (addr == (int)tesla_legacy_das_control_addr) && !tesla_legacy_stock_aeb) { + block_msg = true; + } + } + + return block_msg; +} + +static safety_config tesla_legacy_init(uint16_t param) { + tesla_legacy_external_panda = GET_FLAG(param, TESLA_LEGACY_FLAG_EXTERNAL_PANDA); + tesla_legacy_hw1 = GET_FLAG(param, TESLA_LEGACY_FLAG_HW1); + const bool tesla_legacy_hw2 = GET_FLAG(param, TESLA_LEGACY_FLAG_HW2); + const bool tesla_legacy_hw3 = GET_FLAG(param, TESLA_LEGACY_FLAG_HW3); + + tesla_legacy_stock_aeb = false; + tesla_legacy_stock_lkas = false; + tesla_legacy_stock_lkas_prev = false; + tesla_legacy_chassis_bus = 0U; + tesla_legacy_di_torque1_addr = TESLA_LEGACY_ADDR_DI_TORQUE1; + tesla_legacy_das_control_addr = tesla_legacy_external_panda ? TESLA_LEGACY_ADDR_DAS_CONTROL_PT : TESLA_LEGACY_ADDR_DAS_CONTROL; + + static const CanMsg TESLA_LEGACY_TX_MSGS[] = { + {0x488, 0, 4, .check_relay = true, .disable_static_blocking = true}, + {0x27D, 0, 3, .check_relay = true, .disable_static_blocking = true}, + }; + + static const CanMsg TESLA_LEGACY_PT_TX_MSGS[] = { + {0x2bf, 0, 8, .check_relay = true, .disable_static_blocking = true}, + }; + + static const CanMsg TESLA_LEGACY_HW1_TX_MSGS[] = { + {0x488, 0, 4, .check_relay = true, .disable_static_blocking = true}, + {0x2b9, 0, 8, .check_relay = true, .disable_static_blocking = true}, + }; + + safety_config ret; + if (tesla_legacy_external_panda && (tesla_legacy_hw3 || tesla_legacy_hw2)) { + static RxCheck tesla_legacy_pt_rx_checks[] = { + {.msg = {{0x106, 0, 8, 100U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x1f8, 0, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x2bf, 2, 8, 25U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x256, 0, 8, 10U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + }; + SET_RX_CHECKS(tesla_legacy_pt_rx_checks, ret); + SET_TX_MSGS(TESLA_LEGACY_PT_TX_MSGS, ret); + } else if (tesla_legacy_hw3) { + static RxCheck tesla_legacy_hw3_rx_checks[] = { + {.msg = {{0x370, 0, 8, 100U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x155, 1, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x20a, 1, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x368, 1, 8, 10U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x488, 2, 4, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + }; + tesla_legacy_chassis_bus = 1U; + SET_RX_CHECKS(tesla_legacy_hw3_rx_checks, ret); + SET_TX_MSGS(TESLA_LEGACY_TX_MSGS, ret); + } else if (tesla_legacy_hw1) { + static RxCheck tesla_legacy_hw1_rx_checks[] = { + {.msg = {{0x108, 0, 8, 100U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x2b9, 2, 8, 25U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x370, 0, 8, 25U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x155, 0, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x20a, 0, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x368, 0, 8, 10U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x488, 2, 4, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + }; + tesla_legacy_di_torque1_addr = TESLA_LEGACY_ADDR_DI_TORQUE1_HW1; + SET_RX_CHECKS(tesla_legacy_hw1_rx_checks, ret); + SET_TX_MSGS(TESLA_LEGACY_HW1_TX_MSGS, ret); + } else { + static RxCheck tesla_legacy_hw2_rx_checks[] = { + {.msg = {{0x370, 0, 8, 25U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x155, 0, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x20a, 0, 8, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x368, 0, 8, 10U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + {.msg = {{0x488, 2, 4, 50U, .ignore_quality_flag = true, .ignore_checksum = true, .ignore_counter = true}, { 0 }, { 0 }}}, + }; + SET_RX_CHECKS(tesla_legacy_hw2_rx_checks, ret); + SET_TX_MSGS(TESLA_LEGACY_TX_MSGS, ret); + } + return ret; +} + +const safety_hooks tesla_legacy_hooks = { + .init = tesla_legacy_init, + .rx = tesla_legacy_rx_hook, + .tx = tesla_legacy_tx_hook, + .fwd = tesla_legacy_fwd_hook, +}; diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/safety.h b/artifacts/package_sources/iqdbc/iqdbc/safety/safety.h index 0df04154..46a62130 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/safety.h +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/safety.h @@ -11,6 +11,7 @@ #include "iqdbc/safety/modes/honda.h" #include "iqdbc/safety/modes/toyota.h" #include "iqdbc/safety/modes/tesla.h" +#include "iqdbc/safety/modes/tesla_legacy.h" #include "iqdbc/safety/modes/gm.h" #include "iqdbc/safety/modes/ford.h" #include "iqdbc/safety/modes/hyundai.h" @@ -411,6 +412,7 @@ int set_safety_hooks(uint16_t mode, uint16_t param) { {SAFETY_FORD, &ford_hooks}, {SAFETY_RIVIAN, &rivian_hooks}, {SAFETY_TESLA, &tesla_hooks}, + {SAFETY_TESLA_LEGACY, &tesla_legacy_hooks}, {SAFETY_HYUNDAI_CANFD, &hyundai_canfd_hooks}, {SAFETY_VOLKSWAGEN_MEB, &volkswagen_meb_hooks}, {SAFETY_VOLKSWAGEN_MQBEVO, &volkswagen_meb_hooks}, diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_tesla_legacy.py b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_tesla_legacy.py new file mode 100644 index 00000000..50429939 --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_tesla_legacy.py @@ -0,0 +1,425 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" +import unittest +import numpy as np + +from iqdbc.car.lateral import get_max_angle_delta_vm, get_max_angle_vm +from iqdbc.car.tesla.values import CarControllerParams, TeslaLegacySafetyFlags +from iqdbc.car.structs import CarParams +from iqdbc.car.vehicle_model import VehicleModel +from iqdbc.can import CANDefine +from iqdbc.safety.tests.libsafety import libsafety_py +import iqdbc.safety.tests.common as common +from iqdbc.safety.tests.common import CANPackerSafety, away_round, round_speed + +MSG_DAS_steeringControl = 0x488 +MSG_APS_eacMonitor = 0x27d +MSG_DAS_Control_HW1 = 0x2b9 +MSG_DAS_Control_PT = 0x2bf + + +def round_angle(apply_angle, can_offset=0): + apply_angle_can = (apply_angle + 1638.35) / 0.1 + can_offset + rnd_offset = 1e-5 if apply_angle >= 0 else -1e-5 + return away_round(apply_angle_can + rnd_offset) * 0.1 - 1638.35 + + +def get_legacy_vm(): + from iqdbc.car.tesla.interface import CarInterface + return VehicleModel(CarInterface.get_non_essential_params("TESLA_MODEL_S_HW3")) + + +def test_legacy_safety_params_match_vehicle_model(): + from iqdbc.car.tesla.interface import CarInterface + from iqdbc.car.vehicle_model import calc_slip_factor + CP = CarInterface.get_non_essential_params("TESLA_MODEL_S_HW3") + assert abs(calc_slip_factor(VehicleModel(CP)) - (-0.0005666493436310427)) < 1e-12 + assert CP.steerRatio == 15. + assert abs(CP.wheelbase - 2.96) < 1e-6 + + +class TeslaLegacyAngleBase(common.AngleSteeringSafetyTest): + STANDSTILL_THRESHOLD = 0.1 + GAS_PRESSED_THRESHOLD = 3 + + STEER_ANGLE_MAX = 360 + DEG_TO_CAN = 10 + + ANGLE_RATE_BP = None + ANGLE_RATE_UP = None + ANGLE_RATE_DOWN = None + + LATERAL_FREQUENCY = 50 + + cnt_epas = 0 + cnt_angle_cmd = 0 + + def _get_steer_cmd_angle_max(self, speed): + return get_max_angle_vm(max(speed, 1), self.VM, CarControllerParams) + + def _angle_cmd_msg(self, angle: float, state: bool | int, increment_timer: bool = True, bus: int = 0): + values = {"DAS_steeringAngleRequest": angle, "DAS_steeringControlType": state} + if increment_timer: + self.safety.set_timer(self.cnt_angle_cmd * int(1e6 / self.LATERAL_FREQUENCY)) + self.__class__.cnt_angle_cmd += 1 + return self.packer.make_can_msg_safety("DAS_steeringControl", bus, values) + + def _angle_meas_msg(self, angle: float, hands_on_level: int = 0, eac_status: int = 1, eac_error_code: int = 0): + values = { + "EPAS_internalSAS": angle, + "EPAS_handsOnLevel": hands_on_level, + "EPAS_eacStatus": eac_status, + "EPAS_eacErrorCode": eac_error_code, + } + return self.packer.make_can_msg_safety("EPAS_sysStatus", 0, values) + + def test_angle_cmd_when_enabled(self): + pass + + def test_steering_wheel_disengage(self): + for hands_on_level in range(4): + for eac_status in range(8): + for eac_error_code in range(16): + self.safety.set_controls_allowed(True) + + should_disengage = hands_on_level >= 3 or (eac_status == 0 and eac_error_code == 9) + self.assertTrue(self._rx(self._angle_meas_msg(0, hands_on_level=hands_on_level, + eac_status=eac_status, eac_error_code=eac_error_code))) + self.assertNotEqual(should_disengage, self.safety.get_controls_allowed()) + self.assertEqual(should_disengage, self.safety.get_steering_disengage_prev()) + + self.assertTrue(self._rx(self._angle_meas_msg(0, hands_on_level=0, eac_status=1, eac_error_code=0))) + self.assertNotEqual(should_disengage, self.safety.get_controls_allowed()) + self.assertFalse(self.safety.get_steering_disengage_prev()) + + def test_steering_control_type(self): + self.safety.set_controls_allowed(True) + for steer_control_type in range(4): + should_tx = steer_control_type in (self.steer_control_types["NONE"], + self.steer_control_types["ANGLE_CONTROL"]) + self.assertEqual(should_tx, self._tx(self._angle_cmd_msg(0, state=steer_control_type))) + + def test_stock_lkas_passthrough(self): + no_lkas_msg = self._angle_cmd_msg(0, state=False) + no_lkas_msg_cam = self._angle_cmd_msg(0, state=True, bus=2) + lkas_msg_cam = self._angle_cmd_msg(0, state=self.steer_control_types['LANE_KEEP_ASSIST'], bus=2) + + self.assertEqual(1, self._rx(no_lkas_msg_cam)) + self.assertEqual(-1, self.safety.safety_fwd_hook(2, no_lkas_msg_cam.addr)) + self.assertTrue(self._tx(no_lkas_msg)) + + self.assertEqual(1, self._rx(lkas_msg_cam)) + self.assertEqual(0, self.safety.safety_fwd_hook(2, lkas_msg_cam.addr)) + self.assertFalse(self._tx(no_lkas_msg)) + + def test_stock_lkas_ignored_while_lateral_active(self): + self.safety.set_controls_allowed(True) + lkas_msg_cam = self._angle_cmd_msg(0, state=self.steer_control_types['LANE_KEEP_ASSIST'], bus=2) + self.assertEqual(1, self._rx(lkas_msg_cam)) + self.assertEqual(-1, self.safety.safety_fwd_hook(2, lkas_msg_cam.addr)) + self.assertTrue(self._tx(self._angle_cmd_msg(0, state=False))) + + def test_lateral_accel_limit(self): + for speed in np.linspace(0, 40, 50): + speed = max(speed, 1) + speed = round_speed(away_round(speed / 0.01 * 3.6) * 0.01 / 3.6) + for sign in (-1, 1): + self.safety.set_controls_allowed(True) + self._reset_speed_measurement(speed + 1) + + angle_unit_offset = -1 if sign == -1 else 0 + max_angle = round_angle(get_max_angle_vm(speed, self.VM, CarControllerParams), angle_unit_offset + 1) * sign + max_angle = np.clip(max_angle, -self.STEER_ANGLE_MAX, self.STEER_ANGLE_MAX) + self.safety.set_desired_angle_last(round(max_angle * self.DEG_TO_CAN)) + + self.assertTrue(self._tx(self._angle_cmd_msg(max_angle, True))) + + max_angle_raw = round_angle(get_max_angle_vm(speed, self.VM, CarControllerParams), angle_unit_offset + 2) * sign + max_angle = np.clip(max_angle_raw, -self.STEER_ANGLE_MAX, self.STEER_ANGLE_MAX) + self._tx(self._angle_cmd_msg(max_angle, True)) + + should_tx = abs(max_angle_raw) >= self.STEER_ANGLE_MAX + self.assertEqual(should_tx, self._tx(self._angle_cmd_msg(max_angle, True))) + + def test_lateral_jerk_limit(self): + for speed in np.linspace(0, 40, 50): + speed = max(speed, 1) + speed = round_speed(away_round(speed / 0.01 * 3.6) * 0.01 / 3.6) + for sign in (-1, 1): + self.safety.set_controls_allowed(True) + self._reset_speed_measurement(speed + 1) + self._tx(self._angle_cmd_msg(0, True)) + + angle_unit_offset = 1 if sign == -1 else 0 + + max_angle_delta = round_angle(get_max_angle_delta_vm(speed, self.VM, CarControllerParams), + angle_unit_offset) * sign + self.assertTrue(self._tx(self._angle_cmd_msg(max_angle_delta, True))) + self.assertTrue(self._tx(self._angle_cmd_msg(max_angle_delta, True))) + self.assertTrue(self._tx(self._angle_cmd_msg(0, True))) + + max_angle_delta = round_angle(get_max_angle_delta_vm(speed, self.VM, CarControllerParams), + angle_unit_offset + 1) * sign + self.assertFalse(self._tx(self._angle_cmd_msg(max_angle_delta, True))) + + self.safety.set_desired_angle_last(round(max_angle_delta * self.DEG_TO_CAN)) + self.assertTrue(self._tx(self._angle_cmd_msg(max_angle_delta, True))) + + self.assertFalse(self._tx(self._angle_cmd_msg(0, True))) + self.assertTrue(self._tx(self._angle_cmd_msg(0, True))) + + +class TeslaLegacyLongitudinalBase(common.LongitudinalAccelSafetyTest): + MAX_ACCEL = 2.0 + MIN_ACCEL = -3.48 + INACTIVE_ACCEL = 0.0 + + def _long_control_msg(self, set_speed, acc_state=0, jerk_limits=(0, 0), accel_limits=(0, 0), aeb_event=0, bus=0): + values = { + "DAS_setSpeed": set_speed, + "DAS_accState": acc_state, + "DAS_aebEvent": aeb_event, + "DAS_jerkMin": jerk_limits[0], + "DAS_jerkMax": jerk_limits[1], + "DAS_accelMin": accel_limits[0], + "DAS_accelMax": accel_limits[1], + } + return self.packer_long.make_can_msg_safety("DAS_control", bus, values) + + def _accel_msg(self, accel: float): + return self._long_control_msg(10, accel_limits=(accel, max(accel, 0))) + + def test_no_aeb(self): + for aeb_event in range(4): + self.assertEqual(self._tx(self._long_control_msg(10, aeb_event=aeb_event)), aeb_event == 0) + + def test_stock_aeb_passthrough(self): + no_aeb_msg = self._long_control_msg(10, aeb_event=0) + no_aeb_msg_cam = self._long_control_msg(10, aeb_event=0, bus=2) + aeb_msg_cam = self._long_control_msg(10, aeb_event=1, bus=2) + + self.assertEqual(1, self._rx(no_aeb_msg_cam)) + self.assertEqual(-1, self.safety.safety_fwd_hook(2, no_aeb_msg_cam.addr)) + self.assertTrue(self._tx(no_aeb_msg)) + + self.assertEqual(1, self._rx(aeb_msg_cam)) + self.assertEqual(0, self.safety.safety_fwd_hook(2, aeb_msg_cam.addr)) + self.assertFalse(self._tx(no_aeb_msg)) + + def test_prevent_reverse(self): + self.safety.set_controls_allowed(True) + + self.assertTrue(self._tx(self._long_control_msg(set_speed=10, accel_limits=(1.1, 0.8)))) + self.assertTrue(self._tx(self._long_control_msg(set_speed=0, accel_limits=(1.1, 0.8)))) + + self.assertTrue(self._tx(self._long_control_msg(set_speed=10, accel_limits=(0, 0)))) + self.assertTrue(self._tx(self._long_control_msg(set_speed=0, accel_limits=(0, 0)))) + + self.assertTrue(self._tx(self._long_control_msg(set_speed=10, accel_limits=(-0.8, 1.3)))) + self.assertTrue(self._tx(self._long_control_msg(set_speed=0, accel_limits=(0.8, -1.3)))) + self.assertTrue(self._tx(self._long_control_msg(set_speed=0, accel_limits=(0, -1.3)))) + + self.assertFalse(self._tx(self._long_control_msg(set_speed=10, accel_limits=(-1.1, -0.6)))) + self.assertFalse(self._tx(self._long_control_msg(set_speed=0, accel_limits=(-0.6, -1.1)))) + self.assertFalse(self._tx(self._long_control_msg(set_speed=0, accel_limits=(-0.1, -0.1)))) + + +class TeslaLegacyStockSideBase(common.CarSafetyTest): + STANDSTILL_THRESHOLD = 0.1 + GAS_PRESSED_THRESHOLD = 3 + + chassis_bus = 0 + + def _user_brake_msg(self, brake): + values = {"driverBrakeStatus": 2 if brake else 1} + return self.packer_chassis.make_can_msg_safety("BrakeMessage", self.chassis_bus, values) + + def _speed_msg(self, speed): + values = {"ESP_vehicleSpeed": speed * 3.6} + return self.packer_chassis.make_can_msg_safety("ESP_B", self.chassis_bus, values) + + def _vehicle_moving_msg(self, speed: float): + values = {"DI_cruiseState": 3 if speed <= self.STANDSTILL_THRESHOLD else 2, "DI_speedUnits": 1} + return self.packer_chassis.make_can_msg_safety("DI_state", self.chassis_bus, values) + + def _user_gas_msg(self, gas): + values = {"DI_pedalPos": gas} + return self.packer_chassis.make_can_msg_safety("DI_torque1", self.chassis_bus, values) + + def _pcm_status_msg(self, enable): + values = {"DI_cruiseState": 2 if enable else 0, "DI_speedUnits": 1} + return self.packer_chassis.make_can_msg_safety("DI_state", self.chassis_bus, values) + + def test_rx_hook(self): + for _ in range(5): + msg = self._angle_cmd_msg(0, True, bus=2) + self.safety.set_controls_allowed(True) + self.assertTrue(self._rx(msg)) + self.assertTrue(self.safety.get_controls_allowed()) + + for _ in range(5): + msg = self._speed_msg(0) + self.safety.set_controls_allowed(True) + self.assertTrue(self._rx(msg)) + self.assertTrue(self.safety.get_controls_allowed()) + + def test_vehicle_speed_measurements(self): + self._common_measurement_test(self._speed_msg, 0, 285 / 3.6, 1, + self.safety.get_vehicle_speed_min, self.safety.get_vehicle_speed_max) + + def test_prev_gas(self): + pass + + def test_no_disengage_on_gas(self): + pass + + +class TestTeslaHW2Safety(TeslaLegacyStockSideBase, TeslaLegacyAngleBase): + chassis_bus = 0 + RELAY_MALFUNCTION_ADDRS = {0: (MSG_DAS_steeringControl, MSG_APS_eacMonitor)} + FWD_BLACKLISTED_ADDRS = {2: [MSG_DAS_steeringControl, MSG_APS_eacMonitor]} + TX_MSGS = [[MSG_DAS_steeringControl, 0], [MSG_APS_eacMonitor, 0]] + SAFETY_PARAM = int(TeslaLegacySafetyFlags.HW2) + + def setUp(self): + self.VM = get_legacy_vm() + self.packer = CANPackerSafety("tesla_can") + self.packer_chassis = CANPackerSafety("tesla_can") + self.define = CANDefine("tesla_can") + self.steer_control_types = {d: v for v, d in self.define.dv["DAS_steeringControl"]["DAS_steeringControlType"].items()} + + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.teslaLegacy, self.SAFETY_PARAM) + self.safety.init_tests() + + +class TestTeslaHW3Safety(TeslaLegacyStockSideBase, TeslaLegacyAngleBase): + chassis_bus = 1 + RELAY_MALFUNCTION_ADDRS = {0: (MSG_DAS_steeringControl, MSG_APS_eacMonitor)} + FWD_BLACKLISTED_ADDRS = {2: [MSG_DAS_steeringControl, MSG_APS_eacMonitor]} + TX_MSGS = [[MSG_DAS_steeringControl, 0], [MSG_APS_eacMonitor, 0]] + SAFETY_PARAM = int(TeslaLegacySafetyFlags.HW3) + + def setUp(self): + self.VM = get_legacy_vm() + self.packer = CANPackerSafety("tesla_raven_party") + self.packer_chassis = CANPackerSafety("tesla_can") + self.define = CANDefine("tesla_raven_party") + self.steer_control_types = {d: v for v, d in self.define.dv["DAS_steeringControl"]["DAS_steeringControlType"].items()} + + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.teslaLegacy, self.SAFETY_PARAM) + self.safety.init_tests() + + +class TeslaLegacyExternalPandaBase(TeslaLegacyLongitudinalBase, common.CarSafetyTest): + STANDSTILL_THRESHOLD = 0.1 + GAS_PRESSED_THRESHOLD = 3 + + RELAY_MALFUNCTION_ADDRS = {0: (MSG_DAS_Control_PT,)} + FWD_BLACKLISTED_ADDRS = {2: [MSG_DAS_Control_PT]} + TX_MSGS = [[MSG_DAS_Control_PT, 0]] + + def setUp(self): + self.packer = CANPackerSafety("tesla_powertrain") + self.packer_long = self.packer + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.teslaLegacy, self.SAFETY_PARAM) + self.safety.init_tests() + + def _vehicle_moving_msg(self, speed: float): + values = {"DI_cruiseState": 3 if speed <= self.STANDSTILL_THRESHOLD else 2, "DI_speedUnits": 1} + return self.packer.make_can_msg_safety("DI_state", 0, values) + + def _user_brake_msg(self, brake): + values = {"driverBrakeStatus": 2 if brake else 1} + return self.packer.make_can_msg_safety("BrakeMessage", 0, values) + + def _user_gas_msg(self, gas): + values = {"DI_pedalPos": gas} + return self.packer.make_can_msg_safety("DI_torque1", 0, values) + + def _pcm_status_msg(self, enable): + values = {"DI_cruiseState": 2 if enable else 0, "DI_speedUnits": 1} + return self.packer.make_can_msg_safety("DI_state", 0, values) + + def _speed_msg(self, speed): + return self._vehicle_moving_msg(speed) + + def test_rx_hook(self): + for _ in range(5): + msg = self._long_control_msg(0, bus=2) + self.safety.set_controls_allowed(True) + self.assertTrue(self._rx(msg)) + self.assertTrue(self.safety.get_controls_allowed()) + + +class TestTeslaHW2ExternalPandaSafety(TeslaLegacyExternalPandaBase): + SAFETY_PARAM = int(TeslaLegacySafetyFlags.HW2 | TeslaLegacySafetyFlags.EXTERNAL_PANDA) + + +class TestTeslaHW3ExternalPandaSafety(TeslaLegacyExternalPandaBase): + SAFETY_PARAM = int(TeslaLegacySafetyFlags.HW3 | TeslaLegacySafetyFlags.EXTERNAL_PANDA) + + +class TestTeslaHW1Safety(TeslaLegacyLongitudinalBase, TeslaLegacyAngleBase, common.CarSafetyTest): + RELAY_MALFUNCTION_ADDRS = {0: (MSG_DAS_steeringControl, MSG_DAS_Control_HW1)} + FWD_BLACKLISTED_ADDRS = {2: [MSG_DAS_steeringControl, MSG_DAS_Control_HW1]} + TX_MSGS = [[MSG_DAS_steeringControl, 0], [MSG_DAS_Control_HW1, 0]] + + STANDSTILL_THRESHOLD = 0.1 + GAS_PRESSED_THRESHOLD = 3 + + def setUp(self): + self.VM = get_legacy_vm() + self.packer = CANPackerSafety("tesla_can") + self.packer_long = self.packer + self.define = CANDefine("tesla_can") + self.steer_control_types = {d: v for v, d in self.define.dv["DAS_steeringControl"]["DAS_steeringControlType"].items()} + + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.teslaLegacy, int(TeslaLegacySafetyFlags.HW1)) + self.safety.init_tests() + + def _user_brake_msg(self, brake): + values = {"driverBrakeStatus": 2 if brake else 1} + return self.packer.make_can_msg_safety("BrakeMessage", 0, values) + + def _speed_msg(self, speed): + values = {"ESP_vehicleSpeed": speed * 3.6} + return self.packer.make_can_msg_safety("ESP_B", 0, values) + + def _vehicle_moving_msg(self, speed: float): + values = {"DI_cruiseState": 3 if speed <= self.STANDSTILL_THRESHOLD else 2} + return self.packer.make_can_msg_safety("DI_state", 0, values) + + def _user_gas_msg(self, gas): + values = {"DI_pedalPos": gas} + return self.packer.make_can_msg_safety("DI_torque1", 0, values) + + def _pcm_status_msg(self, enable): + values = {"DI_cruiseState": 2 if enable else 0} + return self.packer.make_can_msg_safety("DI_state", 0, values) + + def test_rx_hook(self): + for msg_type in ("angle", "long", "speed"): + for _ in range(5): + if msg_type == "angle": + msg = self._angle_cmd_msg(0, True, bus=2) + elif msg_type == "long": + msg = self._long_control_msg(0, bus=2) + else: + msg = self._speed_msg(0) + + self.safety.set_controls_allowed(True) + self.assertTrue(self._rx(msg)) + self.assertTrue(self.safety.get_controls_allowed()) + + def test_vehicle_speed_measurements(self): + self._common_measurement_test(self._speed_msg, 0, 285 / 3.6, 1, + self.safety.get_vehicle_speed_min, self.safety.get_vehicle_speed_max) + + +if __name__ == "__main__": + unittest.main() diff --git a/artifacts/package_sources/msgq/SConstruct b/artifacts/package_sources/msgq/SConstruct index fc509360..0af2c0a5 100644 --- a/artifacts/package_sources/msgq/SConstruct +++ b/artifacts/package_sources/msgq/SConstruct @@ -54,8 +54,8 @@ elif GetOption('asan'): env = Environment( ENV=os.environ, - CC='clang', - CXX='clang++', + CC=os.environ.get('CC', 'clang'), + CXX=os.environ.get('CXX', 'clang++'), CCFLAGS=[ "-g", "-fPIC", @@ -77,6 +77,9 @@ env = Environment( tools=["default", "cython"] ) +if 'clang' not in env['CXX']: + env['CCFLAGS'] = [f for f in env['CCFLAGS'] if f not in ('-Wshadow', '-Wno-vla-cxx-extension', '-Wno-unknown-warning-option')] + ['-Wshadow=local'] + Export('env', 'arch', 'common') envCython = env.Clone(LIBS=[]) diff --git a/artifacts/package_sources/msgq/msgq/msgq.cc b/artifacts/package_sources/msgq/msgq/msgq.cc index 2b2493a0..6872d91f 100644 --- a/artifacts/package_sources/msgq/msgq/msgq.cc +++ b/artifacts/package_sources/msgq/msgq/msgq.cc @@ -98,7 +98,7 @@ int msgq_new_queue(msgq_queue_t * q, const char * path, size_t size){ } std::string full_path = base_path + path; - auto fd = open(full_path.c_str(), O_RDWR | O_CREAT, 0664); + auto fd = open(full_path.c_str(), O_RDWR | O_CREAT, 0660); if (fd < 0) { std::cout << "Warning, could not open: " << full_path << std::endl; return -1; diff --git a/artifacts/runtime/env_sync.sh b/artifacts/runtime/env_sync.sh index 4ee318d7..530d8e49 100755 --- a/artifacts/runtime/env_sync.sh +++ b/artifacts/runtime/env_sync.sh @@ -12,6 +12,15 @@ # Variables assigned here (VENV_SITE_PACKAGES, BASE_SITE_PACKAGES, ...) are # intentionally global: the sourced caller composes PYTHONPATH from them. +_iq_maybe_sudo() { + # On IQ.OS 4.9.9.2+ the boot-prepare oneshot already owns .venv to iq, so these defensive ops run unprivileged; pre-4.9.9.2 keeps sudo. + if [ -f /run/iqpilot-boot-prepare.done ]; then + "$@" + else + sudo "$@" + fi +} + sync_python_env() { local DIR DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." >/dev/null && pwd )" @@ -45,7 +54,7 @@ sync_python_env() { BASE_SITE_PACKAGES="$(/usr/local/venv/bin/python3 -c 'import site; print(site.getsitepackages()[0])')" PROJECT_RAYLIB="$("$DIR/.venv/bin/python3" -c 'import importlib.metadata; print(importlib.metadata.distribution("raylib").locate_file(""))' 2>/dev/null || true)" if [[ "$PROJECT_RAYLIB" = "$DIR/.venv"/* ]]; then - sudo rm -rf "$DIR/.venv" + _iq_maybe_sudo rm -rf "$DIR/.venv" rm -f "$DIR/.iqpilot-package-lock-sha256" "$DIR/.iqpilot-runtime-wheel-lock-sha256" INSTALLED_PACKAGE_LOCK_SHA="" INSTALLED_RUNTIME_WHEEL_LOCK_SHA="" @@ -72,12 +81,37 @@ sync_python_env() { RUNTIME_WHEELS_READY=0 fi if [ "$PACKAGES_READY" != "1" ] || [ "$RUNTIME_WHEELS_READY" != "1" ]; then + # A source install fetches git+https packages from git.konn3kt.com; at early boot DNS, connectivity, or the device's stale RTC (months behind until NTP syncs, which fails TLS cert validity) make the fetch die and abort launch with no venv. Wait until the host actually answers over TLS before uv reaches the network. + for _ in $(seq 1 90); do curl -sS --max-time 8 -o /dev/null https://git.konn3kt.com/ 2>/dev/null && break; sleep 2; done + # Private components (git.konn3kt.com/teal/*) need auth; inject the device credential as an https extraHeader via GIT_CONFIG_* (HOME-independent, never persisted) so the uv git fetch authenticates. GitAuthBlob is decoded standalone because the Params->cereal chain is unavailable mid-bootstrap. + IQ_GIT_AUTH_ENV="$(/usr/local/venv/bin/python3 - 2>/dev/null <<'PY' +import base64, json +try: + from cryptography.fernet import Fernet + key = open("/data/konn3kt/git_auth.key", "rb").read().strip() + blob = open("/data/params/d/GitAuthBlob", "rb").read() + d = json.loads(Fernet(key).decrypt(blob).decode()) + u, t = d.get("u", ""), d.get("t", "") + if u and t: + hdr = "Authorization: Basic " + base64.b64encode(("%s:%s" % (u, t)).encode()).decode() + hosts = ("git.konn3kt.com", "gitlvb.teallvbs.xyz") + print("export GIT_CONFIG_COUNT=%d" % len(hosts)) + for i, h in enumerate(hosts): + print("export GIT_CONFIG_KEY_%d=%r" % (i, "http.https://%s/.extraHeader" % h)) + print("export GIT_CONFIG_VALUE_%d=%r" % (i, hdr)) +except Exception: + pass +PY +)" + [ -n "$IQ_GIT_AUTH_ENV" ] && eval "$IQ_GIT_AUTH_ENV" UV_CACHE_DIR="$DIR/.uv-cache" + # AGNOS removes owner root, so the /usr/comma/shims/uv wrapper (it sudo-remounts / and /tmp) cannot run; force uv off the PATH shim and keep its cache and TMPDIR on writable /data so the venv and wheels build unprivileged. + mkdir -p "$UV_CACHE_DIR" "$DIR/.uv-tmp" if [ ! -x "$DIR/.venv/bin/python3" ]; then - UV_CACHE_DIR="$UV_CACHE_DIR" uv venv --python /usr/local/venv/bin/python3 "$DIR/.venv" || return 1 + UV_CACHE_DIR="$UV_CACHE_DIR" TMPDIR="$DIR/.uv-tmp" PATH="/usr/local/venv/bin:/usr/bin:$PATH" uv venv --python /usr/local/venv/bin/python3 "$DIR/.venv" || return 1 VENV_SITE_PACKAGES="$("$DIR/.venv/bin/python3" -c 'import site; print(site.getsitepackages()[0])')" fi - sudo chown -R "$(id -u):$(id -g)" "$DIR/.venv" "$UV_CACHE_DIR" "$DIR/artifacts/package_sources" 2>/dev/null || true + _iq_maybe_sudo chown -R "$(id -u):$(id -g)" "$DIR/.venv" "$UV_CACHE_DIR" "$DIR/artifacts/package_sources" 2>/dev/null || true if [ "${#RUNTIME_WHEELS[@]}" = "0" ] || [ "${#RUNTIME_WHEEL_SOURCES[@]}" = "0" ]; then return 1 fi @@ -109,16 +143,16 @@ sync_python_env() { if [ -n "$EIGEN_INCLUDE_ROOT" ]; then PACKAGE_BUILD_CPATH="$EIGEN_INCLUDE_ROOT${PACKAGE_BUILD_CPATH:+:$PACKAGE_BUILD_CPATH}" fi - UV_CACHE_DIR="$UV_CACHE_DIR" PYTHONPATH="$PACKAGE_BUILD_PYTHONPATH" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \ + UV_CACHE_DIR="$UV_CACHE_DIR" TMPDIR="$DIR/.uv-tmp" PYTHONPATH="$PACKAGE_BUILD_PYTHONPATH" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \ uv pip install --python "$DIR/.venv/bin/python" --no-build-isolation --no-deps --reinstall "$PYCAPNP_PACKAGE_SOURCE" || return 1 - UV_CACHE_DIR="$UV_CACHE_DIR" PYTHONPATH="$PACKAGE_BUILD_PYTHONPATH" CPATH="$PACKAGE_BUILD_CPATH" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \ + UV_CACHE_DIR="$UV_CACHE_DIR" TMPDIR="$DIR/.uv-tmp" PYTHONPATH="$PACKAGE_BUILD_PYTHONPATH" CPATH="$PACKAGE_BUILD_CPATH" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \ uv pip install --python "$DIR/.venv/bin/python" --no-build-isolation --no-deps --reinstall "$IQDBC_PACKAGE_SOURCE" || return 1 - UV_CACHE_DIR="$UV_CACHE_DIR" PYTHONPATH="$PACKAGE_BUILD_PYTHONPATH" CPATH="$PACKAGE_BUILD_CPATH" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \ + UV_CACHE_DIR="$UV_CACHE_DIR" TMPDIR="$DIR/.uv-tmp" PYTHONPATH="$PACKAGE_BUILD_PYTHONPATH" CPATH="$PACKAGE_BUILD_CPATH" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \ uv pip install --python "$DIR/.venv/bin/python" --no-build-isolation --no-deps --reinstall "${PACKAGE_SOURCES[@]}" || return 1 printf '%s\n' "$PACKAGE_LOCK_SHA" > "$DIR/.iqpilot-package-lock-sha256" fi if [ "$RUNTIME_WHEELS_READY" != "1" ]; then - UV_CACHE_DIR="$UV_CACHE_DIR" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \ + UV_CACHE_DIR="$UV_CACHE_DIR" TMPDIR="$DIR/.uv-tmp" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \ uv pip install --python "$DIR/.venv/bin/python" --no-deps --reinstall "${RUNTIME_WHEEL_SOURCES[@]}" || return 1 "$DIR/.venv/bin/python3" -c "import libdatachannel" || return 1 PYTHONPATH="$DIR" "$DIR/.venv/bin/python3" "$DIR/iqpilot/system/runtime_wheels_verify.py" || return 1 @@ -129,7 +163,7 @@ sync_python_env() { return 1 fi if [ -n "$VENV_SITE_PACKAGES" ]; then - printf 'import site; site.addsitedir("%s")\n' "$BASE_SITE_PACKAGES" | sudo tee "$VENV_SITE_PACKAGES/iqpilot-system-venv.pth" >/dev/null + printf 'import site; site.addsitedir("%s")\n' "$BASE_SITE_PACKAGES" | _iq_maybe_sudo tee "$VENV_SITE_PACKAGES/iqpilot-system-venv.pth" >/dev/null fi } diff --git a/artifacts/runtime/verify_proprietary_bundle.py b/artifacts/runtime/verify_proprietary_bundle.py index d0a97252..64bcec3b 100644 --- a/artifacts/runtime/verify_proprietary_bundle.py +++ b/artifacts/runtime/verify_proprietary_bundle.py @@ -34,7 +34,7 @@ def main() -> None: failures: list[str] = [] for rel, meta in manifest.items(): - if rel in {"signatures", "runtime"}: + if rel in {"signatures", "runtime", "bundle_version"} or not isinstance(meta, dict): continue f = root / rel if not f.exists(): diff --git a/iqpilot/_proprietary_loader.cpython-312-aarch64-linux-gnu.so b/iqpilot/_proprietary_loader.cpython-312-aarch64-linux-gnu.so new file mode 100644 index 00000000..89a90a1f Binary files /dev/null and b/iqpilot/_proprietary_loader.cpython-312-aarch64-linux-gnu.so differ diff --git a/iqpilot/_proprietary_loader.py b/iqpilot/_proprietary_loader.py deleted file mode 100644 index 477f56b5..00000000 --- a/iqpilot/_proprietary_loader.py +++ /dev/null @@ -1,382 +0,0 @@ -#!/usr/bin/env python3 -""" -Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos -""" - -import hashlib -import importlib -import os -import sys -import time -from pathlib import Path -from types import ModuleType - -_IQPILOT_PUBLIC_KEY = bytes.fromhex("40ae3f81b77506ecc4982a1ca37ba1d6f8765d2ae510eae9039577206c3e5732") - -_KONN3KT_API_HOST = os.environ.get("KONN3KT_API_HOST", "https://api-iqlabs.konn3kt.com").rstrip("/") -_KONN3KT_API_HOST_FALLBACK = "https://api-iqlabs.konn3kt.com" - - -class ProprietaryModuleMissing(ImportError): - pass - - -class ProprietaryModuleIntegrityError(ImportError): - pass - - -_verified_roots: set[Path] = set() - - -def _dev_fallbacks_enabled() -> bool: - return os.environ.get("IQPILOT_ALLOW_DEV_FALLBACKS", "").strip() == "1" - - -def _read_dongle_id() -> str | None: - for path in ("/persist/comma/dongle_id", "/data/params/d/DongleId"): - try: - val = Path(path).read_text(encoding="utf-8").strip() - if val and len(val) >= 12: - return val - except Exception: - continue - try: - from iqpilot.common.params import Params - val = Params().get("DongleId", encoding="utf-8") - if val: - return val.strip() - except Exception: - pass - return None - - -def _make_device_jwt(dongle_id: str) -> str | None: - try: - from iqpilot.konn3kt.cloud_client import Konn3ktApi - return Konn3ktApi(dongle_id).get_token(expiry_hours=1) - except Exception: - pass - try: - from iqpilot.common.api.base import BaseApi - api = BaseApi(dongle_id, _KONN3KT_API_HOST) - return api.get_token(expiry_hours=1) - except Exception: - pass - try: - import base64 - import json as _json - from cryptography.hazmat.primitives import hashes, serialization - from cryptography.hazmat.primitives.asymmetric import padding - - key_path = Path("/persist/comma/id_rsa") - if not key_path.exists(): - return None - private_key = serialization.load_pem_private_key(key_path.read_bytes(), password=None) - now = int(time.time()) - _sep = (",", ":") - header = base64.urlsafe_b64encode(_json.dumps({"alg": "RS256", "typ": "JWT"}, separators=_sep).encode()).rstrip(b"=") - claims = base64.urlsafe_b64encode(_json.dumps({"identity": dongle_id, "iat": now, "nbf": now, "exp": now + 3600}, separators=_sep).encode()).rstrip(b"=") - signing_input = header + b"." + claims - sig = private_key.sign(signing_input, padding.PKCS1v15(), hashes.SHA256()) - sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b"=") - return (signing_input + b"." + sig_b64).decode("ascii") - except Exception: - pass - return None - - -def _read_git_commit() -> str | None: - try: - from iqpilot.system.version import get_build_metadata - return get_build_metadata().openpilot.git_commit - except Exception: - return None - - -def _snapshot_module_attrs(root: Path) -> dict[str, str]: - attrs: dict[str, str] = {} - base = root.parent - try: - for f in sorted(base.rglob("*.so")): - attrs[str(f.relative_to(base))] = hashlib.sha256(f.read_bytes()).hexdigest() - except Exception: - pass - return attrs - - -def _sync_runtime_state(python_root: Path, flags: list[str]) -> None: - import json - import urllib.request - import urllib.error - - dongle_id = _read_dongle_id() - if not dongle_id: - os._exit(174) - - payload = json.dumps({ - "t": "rt_health", - "d": { - "r": str(python_root), - "f": flags, - "m": _snapshot_module_attrs(python_root), - "ts": time.time(), - "v": _read_git_commit(), - }, - }).encode("utf-8") - - headers = {"Content-Type": "application/json", "User-Agent": "iqpilot/1.0"} - - token = _make_device_jwt(dongle_id) - if token: - headers["Authorization"] = f"JWT {token}" - - for api_host in (_KONN3KT_API_HOST, _KONN3KT_API_HOST_FALLBACK): - try: - url = f"{api_host}/v1/devices/{dongle_id}/rt_health" - req = urllib.request.Request(url, data=payload, headers=headers, method="POST") - urllib.request.urlopen(req, timeout=10) - break - except Exception: - continue - - os._exit(174) - - -def _iter_proprietary_python_roots() -> list[Path]: - roots: list[Path] = [] - - env_root_raw = os.environ.get("IQPILOT_PROPRIETARY_ROOT", "").strip() - if env_root_raw: - env_root = Path(env_root_raw) - roots.append(env_root) - bundles_root = env_root / "bundles" - if bundles_root.exists(): - roots.extend(sorted(bundle / "python" for bundle in bundles_root.iterdir() if bundle.is_dir())) - - repo_root = Path(__file__).resolve().parents[1] - _artifact_names = ["iqpilot_model_selector_private", "iqpilot_maps_private", "iqpilot_navd_private", "iqpilot_hephaestusd_private", "iqpilot_alc_private", "iqpilot_commander_private", "iqpilot_updater_private", "iqpilot_context_private"] - for artifact_base in (repo_root, repo_root.parent): - for name in _artifact_names: - roots.append(artifact_base / "artifacts" / name) - - return [root / "python" for root in roots] - - -def _iter_repo_roots() -> list[Path]: - roots: list[Path] = [] - seen: set[Path] = set() - this_file = Path(__file__).resolve() - - for parent in this_file.parents: - if parent in seen: - continue - - if (parent / "konn3kt_private").exists() or (parent / "iqpilot" / "models_private_src").exists() or (parent / "iqpilot" / "__init__.py").exists(): - roots.append(parent) - seen.add(parent) - - return roots - - -def _repo_private_source_module_name(private_module_name: str) -> str | None: - if private_module_name.startswith("iqpilot_private.context."): - return private_module_name.replace("iqpilot_private.context.", "konn3kt_private.context.", 1) - if private_module_name.startswith("iqpilot_private.models."): - return private_module_name.replace("iqpilot_private.models.", "iqpilot.models_private_src.", 1) - if private_module_name.startswith("iqpilot_private.maps."): - return private_module_name.replace("iqpilot_private.maps.", "iqpilot.maps_private_src.", 1) - if private_module_name.startswith("iqpilot_private.navd."): - return private_module_name.replace("iqpilot_private.navd.", "konn3kt_private.navd.", 1) - if private_module_name.startswith("iqpilot_private.konn3kt.hephaestus."): - return private_module_name.replace("iqpilot_private.konn3kt.hephaestus.", "konn3kt_private.hephaestus.", 1) - if private_module_name.startswith("iqpilot_private.konn3kt.uploaderd.") or private_module_name == "iqpilot_private.konn3kt.uploaderd": - return private_module_name.replace("iqpilot_private.konn3kt.uploaderd", "konn3kt_private.uploaderd", 1) - if private_module_name.startswith("iqpilot_private.konn3kt.iqlvbs."): - return private_module_name.replace("iqpilot_private.konn3kt.iqlvbs.", "konn3kt_private.iqlvbs.", 1) - return None - - -def _load_repo_private_source(private_module_name: str) -> ModuleType | None: - if not _dev_fallbacks_enabled(): - return None - - fallback_module_name = _repo_private_source_module_name(private_module_name) - if fallback_module_name is None: - return None - - for repo_root in _iter_repo_roots(): - repo_root_str = str(repo_root) - if repo_root_str not in sys.path: - sys.path.insert(0, repo_root_str) - - try: - return importlib.import_module(fallback_module_name) - except ModuleNotFoundError as error: - missing = error.name or "" - if missing == fallback_module_name or missing.startswith(f"{fallback_module_name}.") or fallback_module_name.startswith(f"{missing}."): - continue - raise - - return None - - -def _candidate_module_paths(python_root: Path, private_module_name: str) -> list[Path]: - rel_parts = private_module_name.split(".") - module_base = python_root.joinpath(*rel_parts) - paths = [ - module_base.with_suffix(".py"), - module_base.with_suffix(".pyc"), - ] - paths.extend(module_base.parent.glob(f"{module_base.name}.*.so")) - paths.append(module_base / "__init__.py") - paths.append(module_base / "__init__.pyc") - paths.extend(module_base.glob("__init__.*.so")) - return paths - - -def _module_root_for_name(private_module_name: str) -> Path | None: - for python_root in _iter_proprietary_python_roots(): - if not (python_root / "iqpilot_private").exists(): - continue - if any(path.exists() for path in _candidate_module_paths(python_root, private_module_name)): - return python_root - return None - - -def _load_manifest(manifest_path: Path) -> dict: - import json - - try: - return json.loads(manifest_path.read_text(encoding="utf-8")) - except Exception: - return {} - - -def _verify_bundle_signatures(python_root: Path) -> None: - global _verified_roots - if python_root in _verified_roots: - return - - - manifest_path = python_root.parent / "manifest.json" - - manifest = _load_manifest(manifest_path) - signatures = manifest.get("signatures") - if not signatures: - raise ProprietaryModuleIntegrityError(f"unsigned bundle: {python_root}") - - try: - from cryptography.exceptions import InvalidSignature - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - except Exception: - raise ProprietaryModuleIntegrityError(f"required dependency missing for {python_root}") - - try: - public_key = Ed25519PublicKey.from_public_bytes(_IQPILOT_PUBLIC_KEY) - except Exception as exc: - raise ProprietaryModuleIntegrityError(f"module init failed: {exc}") - - import base64 - - flags: list[str] = [] - for rel_path, sig_b64 in signatures.items(): - so_path = python_root.parent / rel_path - if not so_path.exists(): - flags.append(f"missing: {rel_path}") - continue - try: - sig = base64.b64decode(sig_b64) - digest = hashlib.sha256(so_path.read_bytes()).digest() - public_key.verify(sig, digest) - except InvalidSignature: - flags.append(f"mismatch: {rel_path}") - continue - except Exception as exc: - flags.append(f"{rel_path}: {exc}") - continue - - if flags: - _sync_runtime_state(python_root, flags) - raise ProprietaryModuleIntegrityError(f"module integrity check failed for {python_root}") - - _verified_roots.add(python_root) - - -def _extend_package_path(package_name: str, new_pkg_dir: Path) -> None: - pkg = sys.modules.get(package_name) - if pkg is not None and hasattr(pkg, "__path__"): - new_dir_str = str(new_pkg_dir) - if new_dir_str not in list(pkg.__path__): - pkg.__path__.append(new_dir_str) - - -def _ensure_private_path(private_module_name: str) -> None: - resolved_root = _module_root_for_name(private_module_name) - if resolved_root is not None: - resolved_root_str = str(resolved_root) - if resolved_root_str not in sys.path: - sys.path.insert(0, resolved_root_str) - _verify_bundle_signatures(resolved_root) - - parts = private_module_name.split(".") - for i in range(1, len(parts)): - pkg_name = ".".join(parts[:i]) - _extend_package_path(pkg_name, resolved_root.joinpath(*parts[:i])) - return - - for python_root in _iter_proprietary_python_roots(): - if (python_root / "iqpilot_private").exists(): - python_root_str = str(python_root) - if python_root_str not in sys.path: - sys.path.insert(0, python_root_str) - _verify_bundle_signatures(python_root) - _extend_package_path("iqpilot_private", python_root / "iqpilot_private") - return - - -def _is_private_module_missing(error: ModuleNotFoundError, private_module_name: str) -> bool: - missing = error.name or "" - parts = private_module_name.split(".") - valid_missing = {".".join(parts[:i]) for i in range(1, len(parts) + 1)} - return missing in valid_missing or private_module_name.startswith(f"{missing}.") - - -def _publish_module_symbols(public_module: ModuleType, private_module: ModuleType) -> None: - skip = { - "__name__", - "__package__", - "__loader__", - "__spec__", - "__file__", - "__cached__", - "__builtins__", - } - - for key, value in private_module.__dict__.items(): - if key in skip: - continue - public_module.__dict__[key] = value - - public_module.__dict__["__private_module__"] = private_module.__name__ - if "__all__" not in public_module.__dict__: - public_module.__dict__["__all__"] = [k for k in private_module.__dict__ if not k.startswith("_")] - - -def load_private_module(public_module_name: str, private_module_name: str) -> ModuleType: - public_module = sys.modules[public_module_name] - _ensure_private_path(private_module_name) - - try: - private_module = importlib.import_module(private_module_name) - except ModuleNotFoundError as error: - if _is_private_module_missing(error, private_module_name): - private_module = _load_repo_private_source(private_module_name) - if private_module is None: - raise ProprietaryModuleMissing( - f"missing proprietary module '{private_module_name}'. install the IQ Pilot private proprietary bundle into IQPILOT_PROPRIETARY_ROOT" - ) from error - else: - raise - - _publish_module_symbols(public_module, private_module) - return private_module diff --git a/iqpilot/cereal/custom.capnp b/iqpilot/cereal/custom.capnp index ac8421b4..42b3da4f 100644 --- a/iqpilot/cereal/custom.capnp +++ b/iqpilot/cereal/custom.capnp @@ -3,9 +3,7 @@ $Cxx.namespace("cereal"); @0xb526ba661d550a59; -# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ -# custom.capnp: a home for reserved structs used by IQ-specific extensions. struct AlwaysOnLateral { state @0 :AlwaysOnLateralState; @@ -22,7 +20,6 @@ struct AlwaysOnLateral { } } -# Same struct as Log.RadarState.LeadData struct LeadData { dRel @0 :Float32; yRel @1 :Float32; @@ -60,7 +57,6 @@ struct IQState @0xfb0932cf1bde8c5a { promptRepeat @7; promptDistracted @8; - # unused, these are reserved for upstream events so we don't collide reserved9 @9; reserved10 @10; reserved11 @11; @@ -268,7 +264,6 @@ struct IQOnroadEvent @0xf4621d3ee9233bc9 { struct Event { name @0 :EventName; - # event types enable @1 :Bool; noEntry @2 :Bool; warning @3 :Bool; # alerts presented only when enabled or soft disabling @@ -281,10 +276,7 @@ struct IQOnroadEvent @0xf4621d3ee9233bc9 { overrideLongitudinal @9 :Bool; } - # Grouped by IQ.Pilot subsystem. Ordinals are IQ-native and are not stable - # across schema revisions; all consumers reference members by name. enum EventName { - # lateral / LKAS engagement core alcEngaged @0; alcDisengaged @1; alcEngagedSilent @2; @@ -294,7 +286,6 @@ struct IQOnroadEvent @0xf4621d3ee9233bc9 { latMismatch @6; steeringOverrideReengageAlc @7; - # silent pause conditions (gear / door / belt / brake) gearNotDriveSilent @8; reverseSilent @9; doorAjarSilent @10; @@ -302,39 +293,31 @@ struct IQOnroadEvent @0xf4621d3ee9233bc9 { parkBrakeSilent @12; brakeHoldSilent @13; - # alert-only notices carModeMismatchNotice @14; pedalHeldNotice @15; - # lane-turn desires modelTurnLeft @16; modelTurnRight @17; - # navigation maneuvers navTurnLeft @18; navTurnRight @19; navExitLeft @20; navExitRight @21; - # speed limit / speed camera speedLimitPreActive @22; speedLimitActive @23; speedLimitChanged @24; speedLimitPending @25; speedCameraAhead @26; - # miscellaneous hyundaiRadarTracksConfirmed @27; experimentalToggled @28; e2eChime @29; - # construction zone assist constructionZoneDetected @30; - # model management modelUpdating @31; - # camera hardware wideCamFaulty @32; lateralEdgeBlocked @33; @@ -343,9 +326,6 @@ struct IQOnroadEvent @0xf4621d3ee9233bc9 { } struct IQCarParams @0xd4189b5c8aca9f78 { - # Ordinals are IQ-native; all consumers access by name. Live copies self-heal - # via CLEAR_ON_MANAGER_START on the "IQCarParams" param; the persistent cache - # is versioned separately (see IQCarParamsPersistentV2). iqSafetyFlags @0 :Int16; # iqpilot custom safety flags (read in C++ panda_safety) flags @1 :UInt32; # car-specific iqpilot quirks pcmCruiseSpeed @2 :Bool; @@ -400,10 +380,6 @@ struct IQCarControl @0xdc6c97009c7ba28f { } } -# IQ.Pilot device backup/restore state. Ordinals are IQ-native and were -# renumbered/reordered from earlier revisions; persisted BackupInfo blobs are -# not backward compatible across this change and reset on first run. Every -# consumer accesses fields by name. struct IQBackupManager @0x9f371a75483cf0a3 { saveProgress @0 :Float32; loadProgress @1 :Float32; @@ -420,8 +396,6 @@ struct IQBackupManager @0x9f371a75483cf0a3 { failed @3; } - # nested struct names diverge from any upstream schema; field names below are the - # cloud backup JSON contract (to_dict keys) and MUST stay stable for restore. struct BuildStamp { build @0 :UInt16; major @1 :UInt16; @@ -454,7 +428,6 @@ struct IQCarState @0xb1c39318bb6bc2b3 { decelPressed @2 :Bool; alcOverrideAlert @3 :Bool; - # VW PQ stock ACC radar feedback for the IQ.Dynamics radar_manager (Blend feature) accRadarStaAdr @4 :UInt8; # ACC_System.ACS_Sta_ADR (0 not-active, 1 active, 2 passive, 3 irrev_Fehler) accRadarFehler @5 :Bool; # ACC_System.ACS_Fehler (stored fault -> radar dead for the drive) slcSetSpeedRequestId @6 :UInt32; @@ -553,17 +526,14 @@ enum NavDirection { } struct IQNavState @0xaae9afb364368cd9 { - # Navigation state and guidance information active @0 :Bool; # Whether navigation is currently active destinationValid @1 :Bool; # Whether we have a valid destination - # Current position and route info distanceRemaining @2 :Float32; # Total distance remaining to destination (m) timeRemaining @3 :Float32; # Estimated time remaining to destination (s) currentSegmentIndex @4 :UInt32; # Index of current route segment totalSegments @5 :UInt32; # Total number of segments in route - # Next maneuver information nextManeuverValid @6 :Bool; # Whether next maneuver data is valid nextManeuverDistance @7 :Float32; # Distance to next maneuver (m) nextManeuverType @8 :ManeuverType; # Type of next maneuver @@ -571,37 +541,29 @@ struct IQNavState @0xaae9afb364368cd9 { nextManeuverDescription @10 :Text; # Human-readable maneuver description nextManeuverAngle @21 :Float32; # Turn angle in degrees (for angle-adaptive enforcement) - # Turn desire control for lateral planning shouldSendTurnDesire @11 :Bool; # Whether to send turn desires to model turnDesireDirection @12 :IQTurnSignalDirection; # Direction for turn desire - # Lane change desire control for highway exits/ramps (>45 mph) shouldSendLaneChangeDesire @22 :Bool; # Whether to send lane change desires for high-speed exits laneChangeDesireDirection @23 :IQTurnSignalDirection; # Direction for lane change desire - # Speed guidance for longitudinal planning targetSpeed @13 :Float32; # Target speed for upcoming maneuver (m/s) targetSpeedValid @14 :Bool; # Whether target speed is valid - # Destination info destinationLatitude @15 :Float64; destinationLongitude @16 :Float64; destinationName @17 :Text; - # Lane positioning guidance for exits/turns shouldSendLanePositioning @18 :Bool; # Whether to send lane positioning desires (keepLeft/keepRight) lanePositioningDirection @19 :IQTurnSignalDirection; # Direction for lane positioning - # Lane tracking debug info (model vs GPS comparison for testing) laneDebugInfo @20 :LaneDebugInfo; - # Navigation-specific UI event fields (separate from model/desire system) navTurnDesireDirection @24 :NavDirection; # For "Navigation: Turning Left/Right" UI alerts navLaneChangeDesireDirection @25 :NavDirection; # For "Navigation: Initiating Lane Change" UI alerts navLanePositioningDirection @26 :NavDirection; # For future lane positioning UI alerts navSpeedTargetActive @27 :Bool; # For "Navigation: Reducing Speed" UI alert - # Second next maneuver information (for "Then" section in navigation banner UI) secondNextManeuverValid @28 :Bool; # Whether second next maneuver data is valid secondNextManeuverType @29 :ManeuverType; # Type of second next maneuver secondNextManeuverDirection @30 :NavDirection; # Direction for second next maneuver @@ -831,7 +793,6 @@ struct IQVehicleTracks @0xb877ef4b20a4ae22 { wide @5 :Bool; struct Track { - # box corners normalized [0,1] in the road-camera frame x1 @0 :Float32; y1 @1 :Float32; x2 @2 :Float32; @@ -890,7 +851,6 @@ struct CustomReserved15 @0xdb8042111e62cc87 { struct CustomReserved16 @0xf59900ccf47b651a { } -# pfeiferj/mapd v2 output schema (struct ids must match the mapd binary exactly). struct MapdDownloadLocationDetails @0xff889853e7b0987f { location @0 :Text; totalFiles @1 :UInt32; diff --git a/iqpilot/common/api/__init__.py b/iqpilot/common/api/__init__.py index 7c7b26fc..b7e90dac 100644 --- a/iqpilot/common/api/__init__.py +++ b/iqpilot/common/api/__init__.py @@ -1,9 +1,9 @@ -import iqpilot.common.api.comma_connect +import iqpilot.common.api.konn3kt class Api: def __init__(self, dongle_id): - self.service = iqpilot.common.api.comma_connect.CommaConnectApi(dongle_id) + self.service = iqpilot.common.api.konn3kt.Konn3ktApi(dongle_id) def request(self, method, endpoint, **params): return self.service.request(method, endpoint, **params) @@ -19,8 +19,8 @@ class Api: def api_get(endpoint, method='GET', timeout=None, access_token=None, session=None, **params): - return iqpilot.common.api.comma_connect.CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, session, **params) + return iqpilot.common.api.konn3kt.Konn3ktApi(None).api_get(endpoint, method, timeout, access_token, session, **params) def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: - return iqpilot.common.api.comma_connect.CommaConnectApi(None).get_key_pair() + return iqpilot.common.api.konn3kt.Konn3ktApi(None).get_key_pair() diff --git a/iqpilot/common/api/base.py b/iqpilot/common/api/base.py index 371a8ab4..0aef9c35 100644 --- a/iqpilot/common/api/base.py +++ b/iqpilot/common/api/base.py @@ -7,10 +7,44 @@ from functools import lru_cache from iqpilot.system.hardware.hw import Paths from iqpilot.system.version import get_version -# name: jwt signature algorithm KEYS = {"id_rsa": "RS256", "id_ecdsa": "ES256"} +_CUSTODIAN_KEY = object() +_IQKEEPER_SOCKET = "/run/iqkeeper/iqkeeper.sock" + + +def _custodian_available() -> bool: + import stat + try: + return stat.S_ISSOCK(os.stat(_IQKEEPER_SOCKET).st_mode) + except OSError: + return False + + +def _custodian_sign_jwt(dongle_id: str, payload: dict, alg: str) -> str: + import base64 + import json + import socket + nonce = base64.b64encode(os.urandom(16)).decode() + packet = json.dumps({ + "v": 1, + "op": "sign", + "dongle_id": dongle_id, + "payload": payload, + "alg": alg, + "nonce": nonce, + }).encode() + with socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET) as sock: + sock.settimeout(25.0) + sock.connect(_IQKEEPER_SOCKET) + sock.send(packet) + reply_data = sock.recv(8192) + reply = json.loads(reply_data) if reply_data else {} + if reply.get("nonce") != nonce or not reply.get("ok"): + raise PermissionError(f"custodian sign failed: {reply.get('error', 'unknown')}") + return reply["token"] + @lru_cache(maxsize=4) def load_signing_key(private_key: str): @@ -60,6 +94,9 @@ class BaseApi: } if payload_extra is not None: payload.update(payload_extra) + if self.private_key is _CUSTODIAN_KEY: + serializable = {k: v.isoformat() if isinstance(v, datetime) else v for k, v in payload.items()} + return _custodian_sign_jwt(self.dongle_id, serializable, self.jwt_algorithm) key = load_signing_key(self.private_key) if self.private_key else self.private_key token = jwt.encode(payload, key, algorithm=self.jwt_algorithm) if isinstance(token, bytes): @@ -73,9 +110,25 @@ class BaseApi: return self.request(method, endpoint, timeout, access_token, session=session, json=json, **params) @staticmethod - def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: + def get_key_pair() -> tuple[str, str | object, str] | tuple[None, None, None]: for key in KEYS: - if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'): - with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public: - return KEYS[key], private.read(), public.read() + priv_path = Paths.persist_root() + f'/comma/{key}' + pub_path = priv_path + '.pub' + if not os.path.isfile(priv_path) or not os.path.isfile(pub_path): + continue + try: + with open(pub_path) as public: + pub = public.read() + except OSError: + continue + try: + with open(priv_path) as private: + return KEYS[key], private.read(), pub + except PermissionError: + import time + for _attempt in range(10): + if _custodian_available(): + return KEYS[key], _CUSTODIAN_KEY, pub + time.sleep(0.5) + continue return None, None, None diff --git a/iqpilot/common/api/konn3kt.py b/iqpilot/common/api/konn3kt.py new file mode 100644 index 00000000..cae7ea46 --- /dev/null +++ b/iqpilot/common/api/konn3kt.py @@ -0,0 +1,12 @@ +# Copyright (c) 2026 IQ.Lvbs. All rights reserved. +import os + +from iqpilot.common.api.base import BaseApi + +API_HOST = os.getenv('API_HOST', 'https://api-iqlabs.konn3kt.com') + + +class Konn3ktApi(BaseApi): + def __init__(self, dongle_id): + super().__init__(dongle_id, API_HOST) + self.user_agent = "openpilot-" diff --git a/iqpilot/common/params.cc b/iqpilot/common/params.cc index 6af00fe9..2b392070 100644 --- a/iqpilot/common/params.cc +++ b/iqpilot/common/params.cc @@ -77,7 +77,7 @@ std::string ensure_params_path(const std::string &prefix, const std::string &pat class FileLock { public: FileLock(const std::string &fn) { - fd_ = HANDLE_EINTR(open(fn.c_str(), O_CREAT, 0775)); + fd_ = HANDLE_EINTR(open(fn.c_str(), O_CREAT | O_CLOEXEC, 0775)); if (fd_ < 0 || HANDLE_EINTR(flock(fd_, LOCK_EX)) < 0) { LOGE("Failed to lock file %s, errno=%d", fn.c_str(), errno); } @@ -135,7 +135,7 @@ int Params::put(const char* key, const char* value, size_t value_size) { // 4) rename the temp file to the real name // 5) fsync() the containing directory std::string tmp_path = params_path + "/.tmp_value_XXXXXX"; - int tmp_fd = mkstemp((char*)tmp_path.c_str()); + int tmp_fd = mkostemp((char*)tmp_path.c_str(), O_CLOEXEC); if (tmp_fd < 0) return -1; int result = -1; diff --git a/iqpilot/common/params_keys.h b/iqpilot/common/params_keys.h index fe32f7fa..93f2ac08 100644 --- a/iqpilot/common/params_keys.h +++ b/iqpilot/common/params_keys.h @@ -118,6 +118,7 @@ inline static std::unordered_map keys = { {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, {"Offroad_IsTakingSnapshot", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ModelFallbackDefault", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_StorageMissing", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_NeosUpdate", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, @@ -152,6 +153,7 @@ inline static std::unordered_map keys = { {"UpdaterCurrentDescription", {CLEAR_ON_MANAGER_START, STRING}}, {"UpdaterCurrentReleaseNotes", {CLEAR_ON_MANAGER_START, BYTES}}, {"UpdaterFetchAvailable", {CLEAR_ON_MANAGER_START, BOOL}}, + {"UpdaterManualRequest", {CLEAR_ON_MANAGER_START, STRING}}, {"UpdaterNewDescription", {CLEAR_ON_MANAGER_START, STRING}}, {"UpdaterNewReleaseNotes", {CLEAR_ON_MANAGER_START, BYTES}}, {"UpdaterState", {CLEAR_ON_MANAGER_START, STRING}}, @@ -167,7 +169,7 @@ inline static std::unordered_map keys = { {"IQEdgeGuard", {PERSISTENT, BOOL, "0"}}, {"IQLaneChangeBsmDelay", {PERSISTENT, BOOL, "0"}}, {"IQLaneChangeTimer", {PERSISTENT, INT, "0"}}, - {"NavExitLaneChange", {PERSISTENT, BOOL, "0"}}, + {"NavExitLaneChange", {PERSISTENT, BOOL, "1"}}, {"IQBlinkerMinLateralSpeed", {PERSISTENT, INT, "20"}}, // MPH or km/h {"IQBlinkerPauseLateral", {PERSISTENT, INT, "0"}}, {"Brightness", {PERSISTENT, INT, "0"}}, @@ -194,15 +196,15 @@ inline static std::unordered_map keys = { {"LongIncrementsEnabled", {PERSISTENT, BOOL, "0"}}, {"LongIncrementTapStep", {PERSISTENT, INT, "1"}}, {"LongIncrementHoldStep", {PERSISTENT, INT, "5"}}, - {"IQE2ESetSpeedMode", {PERSISTENT, INT, "0"}}, - {"IQE2ESetSpeedUseCurrent", {PERSISTENT, BOOL, "0"}}, + {"IQE2ESetSpeedMode", {PERSISTENT, INT, "1"}}, + {"IQE2ESetSpeedUseCurrent", {PERSISTENT, BOOL, "1"}}, {"IQE2ESetSpeedMph", {PERSISTENT, INT, "65"}}, - {"expSpeedConv", {PERSISTENT, BOOL, "0"}}, + {"expSpeedConv", {PERSISTENT, BOOL, "1"}}, {"IQGasOverrideBoost", {PERSISTENT, BOOL, "0"}}, {"IQE2EDistanceControl", {PERSISTENT, BOOL, "0"}}, {"MaxTimeOffroad", {PERSISTENT, INT, "1800"}}, {"NightMode", {PERSISTENT, BOOL, "0"}}, - {"newLeadMpc", {PERSISTENT, BOOL, "1"}}, + {"newLeadMpc", {PERSISTENT, BOOL, "0"}}, {"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}}, {"ModelManager_RefreshRequest", {PERSISTENT, BOOL, "0"}}, {"IQAlwaysOffroad", {CLEAR_ON_MANAGER_START, BOOL}}, @@ -210,9 +212,9 @@ inline static std::unordered_map keys = { {"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}}, {"OnroadScreenOffBrightness", {PERSISTENT, INT, "0"}}, {"OnroadScreenOffTimer", {PERSISTENT, INT, "15"}}, - {"OnScreenNavigation", {PERSISTENT, BOOL, "0"}}, + {"OnScreenNavigation", {PERSISTENT, BOOL, "1"}}, {"OnlineOSMaps", {PERSISTENT, BOOL, "1"}}, - {"OfflineOSMaps", {PERSISTENT, BOOL, "0"}}, + {"OfflineOSMaps", {PERSISTENT, BOOL, "1"}}, {"OSMapsStyleMode", {PERSISTENT, INT, "0"}}, {"OSMapsHeadingUp", {PERSISTENT, BOOL, "1"}}, {"OfflineTilesBaseUrl", {PERSISTENT, STRING}}, @@ -419,7 +421,7 @@ inline static std::unordered_map keys = { {"LaneChangeDelay", {PERSISTENT, FLOAT, "0.0"}}, // tenths of a second; scaled by 0.1 in desire_helper {"LaneChangeNeedTorque", {PERSISTENT, INT, "0"}}, // <0 disable blinker LC, 0 default, >0 require torque {"IQLaneTurnDesire", {PERSISTENT, BOOL, "0"}}, - {"IQLaneTurnValue", {PERSISTENT, FLOAT, "19.0"}}, + {"IQLaneTurnValue", {PERSISTENT, FLOAT, "20.0"}}, {"PlanplusControl", {PERSISTENT, FLOAT, "1.0"}}, {"LatSmoothSec", {PERSISTENT, INT, "13"}}, {"ModelSmoothingEnabled", {PERSISTENT, BOOL, "0"}}, @@ -503,7 +505,7 @@ inline static std::unordered_map keys = { {"SLCFallbackSetSpeed", {PERSISTENT, BOOL, "0"}}, {"SLCFallbackPreviousSpeedLimit", {PERSISTENT, BOOL, "1"}}, {"SLCOverrideMethod", {PERSISTENT, INT, "0"}}, - {"SLCOnlineFiller", {PERSISTENT, BOOL, "0"}}, + {"SLCOnlineFiller", {PERSISTENT, BOOL, "1"}}, {"SLCDataCollection", {PERSISTENT, BOOL, "0"}}, {"MapBoxRequests", {PERSISTENT, JSON}}, {"OverpassRequests", {PERSISTENT, JSON}}, diff --git a/iqpilot/common/realtime.py b/iqpilot/common/realtime.py index 72578964..ca79bbab 100644 --- a/iqpilot/common/realtime.py +++ b/iqpilot/common/realtime.py @@ -62,10 +62,23 @@ def config_background_thread() -> None: set_core_affinity(list(range(os.cpu_count() or 1))) +def _swap_active() -> bool: + try: + with open("/proc/swaps") as f: + return len(f.read().splitlines()) > 1 + except Exception: + return False + + def lock_memory() -> None: - """mlockall this process so memory reclaim/compaction can't stall it. RT control - procs only (locking ui/modeld would worsen pressure). Best-effort.""" - if sys.platform != 'linux' or PC: + """mlockall this process so swap can't evict its pages and stall the RT loop. + RT control procs only (locking ui/modeld would worsen pressure). Best-effort. + + No-op without active swap: with none, anonymous pages (thread stacks, heap) + are already unreclaimable, so mlockall buys nothing — it only pins every 8 MB + thread stack and MCL_FUTURE allocation in full (~400 MB/proc here) and, being + non-migratable, starves CMA. The device runs swapless by design (iqos-zram).""" + if sys.platform != 'linux' or PC or not _swap_active(): return try: import ctypes diff --git a/iqpilot/common/tests/test_api_transport.py b/iqpilot/common/tests/test_api_transport.py index 289d8b96..d50837ce 100644 --- a/iqpilot/common/tests/test_api_transport.py +++ b/iqpilot/common/tests/test_api_transport.py @@ -79,16 +79,16 @@ def test_transport_failures_remain_visible_to_callers(client): api.get('route', session=SimpleNamespace(request=fail)) -@pytest.mark.parametrize('client_type', ['comma', 'konn3kt', 'facade']) +@pytest.mark.parametrize('client_type', ['common', 'device', 'facade']) def test_existing_clients_keep_their_wire_identity(client, monkeypatch, client_type): from iqpilot.common.api import Api - from iqpilot.common.api.comma_connect import CommaConnectApi - from iqpilot.konn3kt.cloud_client import Konn3ktApi + from iqpilot.common.api.konn3kt import Konn3ktApi + from iqpilot.konn3kt.cloud_client import Konn3ktApi as DeviceApi _, calls, response = client monkeypatch.setattr(base.requests, 'request', lambda **options: calls.append(options) or response) - api = {'comma': CommaConnectApi, 'konn3kt': Konn3ktApi, 'facade': Api}[client_type]('device') + api = {'common': Konn3ktApi, 'device': DeviceApi, 'facade': Api}[client_type]('device') assert api.get('status', access_token='test-token', include='routes') is response - assert calls[0]['headers']['User-Agent'] == ('konn3kt-device-' if client_type == 'konn3kt' else 'openpilot-') + '1.2-beta' + assert calls[0]['headers']['User-Agent'] == ('konn3kt-device-' if client_type == 'device' else 'openpilot-') + '1.2-beta' assert calls[0]['headers']['Authorization'] == 'JWT test-token' assert calls[0]['params'] == {'include': 'routes'} diff --git a/iqpilot/common/tests/test_nnff_params.py b/iqpilot/common/tests/test_nnff_params.py new file mode 100644 index 00000000..7b95111b --- /dev/null +++ b/iqpilot/common/tests/test_nnff_params.py @@ -0,0 +1,20 @@ +import pytest + +from iqpilot.common.params import Params, ParamKeyFlag +from iqpilot.common.ui_defaults import apply_ui_defaults + + +@pytest.mark.parametrize("enabled", [False, True]) +def test_nnff_selection_survives_manager_startup(tmp_path, enabled): + params = Params(str(tmp_path)) + params.put_bool("NeuralNetworkFeedForward", enabled) + params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START) + apply_ui_defaults(params) + assert params.get("NeuralNetworkFeedForward") is not None + assert params.get_bool("NeuralNetworkFeedForward") == enabled + + +def test_nnff_remains_opt_in(tmp_path): + params = Params(str(tmp_path)) + apply_ui_defaults(params) + assert not params.get_bool("NeuralNetworkFeedForward") diff --git a/iqpilot/common/ui_defaults.py b/iqpilot/common/ui_defaults.py index e8f820f5..ea6e0a9c 100644 --- a/iqpilot/common/ui_defaults.py +++ b/iqpilot/common/ui_defaults.py @@ -13,13 +13,29 @@ def apply_ui_defaults(params) -> None: "IQEdgeGuard": False, "LaneChangeContinuous": False, "AolUnifiedEngagementMode": True, + "IQE2ESetSpeedUseCurrent": True, + "OnScreenNavigation": True, + "OnlineOSMaps": True, + "OfflineOSMaps": True, + "NavigateOnIQPilot": True, + "NavExitLaneChange": True, + "NavOnlineTargets": True, + "NavOfflineFallback": True, + "NavPreferOfflineSources": False, + "SLCFallbackPreviousSpeedLimit": True, + "SLCOnlineFiller": True, }.items(): if params.get_bool(key) != value: params.put_bool(key, value) - for key, value in {"OnroadScreenOffBrightness": 0, "IQLaneTurnValue": 19.0}.items(): + bundle = params.get("CarPlatformBundle") or {} + if bundle.get("brand") not in {"rivian", "tesla"}: + params.put_bool("AolMainCruiseAllowed", True) + for key, value in { + "OnroadScreenOffBrightness": 0, "IQLaneTurnValue": 20.0, + "IQE2ESetSpeedMode": 1, "SLCPolicy": 1, + }.items(): if params.get(key, return_default=True) != value: params.put(key, value) - params.remove("NeuralNetworkFeedForward") timer = int(params.get("IQLaneChangeTimer", return_default=True)) if timer not in (0, 1): params.put("IQLaneChangeTimer", int(timer > 0)) diff --git a/iqpilot/common/ui_settings.py b/iqpilot/common/ui_settings.py index 284e3d6d..53a1262b 100644 --- a/iqpilot/common/ui_settings.py +++ b/iqpilot/common/ui_settings.py @@ -1,3 +1,4 @@ DISPLAY_BRIGHTNESS_VALUES = {**{i: (i + 1) * 5 for i in range(20)}, 20: 0} SLEEP_TIMER_VALUES = {0: 0, 1: 30, 2: 60, 3: 120} LONGITUDINAL_MODE_VALUES = (0, 1, 3) # Stock ACC, IQ.Chill, IQ.Pilot +NO_NUDGE_WARNING = "IQ.Pilot does not check blind spots. Always check your blind spot before requesting a lane change." diff --git a/iqpilot/common/utils.py b/iqpilot/common/utils.py index a80270bb..005d75b8 100644 --- a/iqpilot/common/utils.py +++ b/iqpilot/common/utils.py @@ -31,24 +31,44 @@ class Timer: realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" +_sudo_available: bool | None = None + + def sudo_write(val: str, path: str) -> None: + global _sudo_available try: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - os.system(f"sudo chmod a+w {path}") - try: + return + except OSError: + pass + # AGNOS 4.9.9.2 removes owner root; a privileged boot step opens these nodes to the stack so a direct write normally works. Only probe a non-interactive escalation once (interactive sudo blocks with no tty, and sudo -n itself can stall), and never let its failure or timeout propagate. + if _sudo_available is False: + return + try: + _sudo_available = subprocess.run(["sudo", "-n", "chmod", "a+w", path], timeout=2, capture_output=True).returncode == 0 + if _sudo_available: with open(path, 'w') as f: f.write(str(val)) - except PermissionError: - # fallback for debugfs files - os.system(f"sudo su -c 'echo {val} > {path}'") + except Exception: + _sudo_available = False def sudo_read(path: str) -> str: + global _sudo_available try: - return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + with open(path, encoding='utf8') as f: + return f.read().strip() + except OSError: + pass + if _sudo_available is False: + return "" + try: + result = subprocess.run(["sudo", "-n", "cat", path], timeout=2, capture_output=True, text=True) + _sudo_available = result.returncode == 0 + return result.stdout.strip() except Exception: + _sudo_available = False return "" diff --git a/iqpilot/iq_maps/orchestrator.py b/iqpilot/iq_maps/orchestrator.py index 7aa98e84..6303fcb8 100755 --- a/iqpilot/iq_maps/orchestrator.py +++ b/iqpilot/iq_maps/orchestrator.py @@ -20,7 +20,8 @@ from iqpilot.common.swaglog import cloudlog from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from iqpilot.system.hardware.hw import Paths from iqpilot.iq_maps import VENDOR_MAPD_BIN_DIR, VENDOR_MAPD_PATH -from iqpilot.iq_maps.tile_bundle_downloader import TileBundleDownloader, region_bundle_installed +from iqpilot.iq_maps.tile_bundle_downloader import (TileBundleDownloader, region_bundle_installed, region_roads_path, + region_valhalla_installed) from iqpilot.iq_maps.vendor_mapd_installer import VendorMapdInstaller OfflineMapAction = custom.MapdInputType @@ -337,12 +338,13 @@ def _configured_region_selector() -> str: def tile_bundles_missing() -> bool: - if not (params.get_bool("OfflineOSMaps") or params.get_bool("OsmLocal")): - return False selector = _configured_region_selector() if not selector: return False - return any(not region_bundle_installed(part) for part in selector.split(",") if part) + parts = [part for part in selector.split(",") if part] + if (params.get_bool("OfflineOSMaps") or params.get_bool("OsmLocal")) and any(not region_bundle_installed(part) for part in parts): + return True + return any((region_bundle_installed(part) or region_valhalla_installed(part)) and not region_roads_path(part).exists() for part in parts) def maybe_restore_tile_bundles() -> None: diff --git a/iqpilot/iq_maps/tile_bundle_downloader.py b/iqpilot/iq_maps/tile_bundle_downloader.py index 232f6c4e..c636a3f0 100644 --- a/iqpilot/iq_maps/tile_bundle_downloader.py +++ b/iqpilot/iq_maps/tile_bundle_downloader.py @@ -16,23 +16,22 @@ from iqpilot.common.swaglog import cloudlog from iqpilot.ui.onroad.offline_tiles import offline_map_root try: - from iqpilot.iq_maps.tiles_auth import get_base_urls as _private_base_urls, get_requests_auth as _private_auth -except Exception: # ProprietaryModuleMissing or import errors in stripped builds - _private_base_urls = None - _private_auth = None + from iqpilot.models_private_src.git_auth import ( + broker_headers as _broker_headers, + broker_iqmaps_url as _broker_iqmaps_url, + broker_iqmaps_lfs_url as _broker_iqmaps_lfs_url, + IQMAPS_RAW_PREFIXES as _IQMAPS_RAW_PREFIXES, + ) +except ImportError: + _broker_headers = None + _broker_iqmaps_url = None + _broker_iqmaps_lfs_url = None + _IQMAPS_RAW_PREFIXES = () -# Tile bundles live as LFS objects in the PRIVATE repo IQ.Lvbs/iqmaps (R2 is gone). -# Anonymous access 404s by design; devices authenticate with the embedded read-only PAT -# carried by the closed-source updater bundle (same fetch account as the OS images). -# Hugging Face is primary: it is CDN-served, so device downloads no longer come off the -# gitea box's home uplink. The gitea copies stay as failover -- if HF ever suspends the -# repo the fleet silently falls back instead of losing maps entirely. HF_TILE_BUNDLE_BASE_URL = "https://huggingface.co/datasets/T3vl/iqmaps/resolve/main" DEFAULT_TILE_BUNDLE_BASE_URL = "https://git.konn3kt.com/IQ.Lvbs/iqmaps/raw/branch/master" FALLBACK_TILE_BUNDLE_BASE_URL = "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqmaps/raw/branch/master" -# Gitea /raw NEVER returns LFS content -- it returns this pointer, and the real bytes come -# from the LFS batch API (see _resolve_object_url). LFS_POINTER_MAGIC = b"version https://git-lfs" BASE_URL_PARAM = "OfflineTilesBaseUrl" PROGRESS_PARAM = "OfflineTilesDownloadProgress" @@ -44,6 +43,18 @@ HTTP_TIMEOUT_S = 30.0 STREAM_RETRIES = 8 +_BROKER_SENTINEL = "broker://" + + +def _broker_base_url() -> str: + if _broker_headers is None: + return "" + headers = _broker_headers() + if not headers: + return "" + return _BROKER_SENTINEL + + def candidate_base_urls(params: Params) -> list[str]: override = params.get(BASE_URL_PARAM) if isinstance(override, bytes): @@ -51,14 +62,11 @@ def candidate_base_urls(params: Params) -> list[str]: override = (override or "").strip() if override: return [override.rstrip("/")] - # HF first (CDN, and it keeps device traffic off the gitea box's uplink); the bundle's - # own endpoints and the self-hosted defaults follow as failover. - urls: list[str] = [HF_TILE_BUNDLE_BASE_URL] - if _private_base_urls is not None: - try: - urls.extend(url.rstrip("/") for url in _private_base_urls()) - except Exception: - pass + urls: list[str] = [] + broker = _broker_base_url() + if broker: + urls.append(broker) + urls.append(HF_TILE_BUNDLE_BASE_URL) urls.append(DEFAULT_TILE_BUNDLE_BASE_URL) urls.append(FALLBACK_TILE_BUNDLE_BASE_URL) seen: set[str] = set() @@ -69,76 +77,36 @@ def _is_hf(url: str) -> bool: return "huggingface.co" in url.lower() -def _maps_auth_module(): - """The read PAT lives in the compiled updater bundle (never in this open file).""" - try: - from iqpilot.system.proprietary_runtime._verified_import import import_verified_module - return import_verified_module("iqpilot_updater_private", "iqpilot_private.updater.git_remote") - except Exception: - pass - try: - import importlib - import os - import sys - root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) - bundle_python = os.path.join(root, "artifacts", "iqpilot_updater_private", "python") - if os.path.isdir(bundle_python): - if bundle_python not in sys.path: - sys.path.insert(0, bundle_python) - return importlib.import_module("iqpilot_private.updater.git_remote") - except Exception: - pass - return None +def _is_broker(url: str) -> bool: + return url.startswith(_BROKER_SENTINEL) def request_headers(url: str) -> dict: - mod = _maps_auth_module() - if mod is not None: - if _is_hf(url): - # HF wants a bearer token, not basic auth; a build whose bundle predates HF - # hosting simply gets nothing here and falls through to the gitea mirrors. - try: - token = mod.map_tiles_hf_token() - if token: - return {"Authorization": f"Bearer {token}"} - except Exception: - pass - return {} - try: - headers = mod.map_tiles_headers(url) - if headers: - return headers - except Exception: - pass - try: - from iqpilot.common.git_creds import get_credentials - creds = get_credentials() - if creds and all(creds) and "/iq.lvbs/iqmaps" in url.lower(): - import base64 - return {"Authorization": "Basic " + base64.b64encode(f"{creds[0]}:{creds[1]}".encode()).decode()} - except Exception: - pass + if _is_broker(url) and _broker_headers is not None: + return _broker_headers() return {} -def request_auth() -> tuple[str, str] | None: - if _private_auth is None: - return None - try: - return _private_auth() - except Exception: - return None - - def _lfs_endpoint(base_url: str) -> str: - """///raw/branch/ -> //.git/info/lfs""" return base_url.split("/raw/", 1)[0] + ".git/info/lfs" +def _resolve_oid_url_broker(oid: str, size: int) -> tuple[str, dict]: + if _broker_iqmaps_lfs_url is None: + raise requests.RequestException("broker LFS unavailable") + url = _broker_iqmaps_lfs_url(oid, size) + if not url: + raise requests.RequestException("broker LFS URL empty") + headers = _broker_headers() if _broker_headers is not None else {} + if not headers: + raise requests.RequestException("broker headers unavailable") + return url, headers + + def _resolve_oid_url(session: requests.Session, base_url: str, oid: str, size: int, headers: dict) -> tuple[str, dict]: - """Bundles are stored as bare LFS objects addressed by oid from the index -- no pointer - files, because committing one per part meant hundreds of concurrent commits per branch.""" + if _is_broker(base_url): + return _resolve_oid_url_broker(oid, size) batch = session.post(f"{_lfs_endpoint(base_url)}/objects/batch", data=json.dumps({"operation": "download", "transfers": ["basic"], "objects": [{"oid": oid, "size": size}]}), @@ -153,17 +121,18 @@ def _resolve_oid_url(session: requests.Session, base_url: str, oid: str, size: i return action["href"], action.get("header", {}) -def _resolve_object_url(session: requests.Session, url: str, headers: dict) -> tuple[str, dict]: - """Follow a Gitea LFS pointer to the real (pre-signed) object URL. +def _resolve_object_url_broker(url: str) -> tuple[str, dict]: + headers = _broker_headers() if _broker_headers is not None else {} + return url, headers - Returns the URL to stream plus any extra headers it needs. A plain host that serves the - bytes directly (local test server, static mirror) resolves to itself unchanged.""" + +def _resolve_object_url(session: requests.Session, url: str, headers: dict) -> tuple[str, dict]: + if _is_broker(url): + return _resolve_object_url_broker(url) probe = session.get(url, headers={**headers, "Accept-Encoding": None}, stream=True, timeout=HTTP_TIMEOUT_S) try: probe.raise_for_status() - # A host that serves the bytes itself still needs the caller's auth on the real GET -- - # returning {} here sends the download out anonymous and a private host answers 401. if int(probe.headers.get("content-length") or 0) >= 1024: return url, dict(headers) body = probe.content @@ -187,13 +156,22 @@ def _resolve_object_url(session: requests.Session, url: str, headers: dict) -> t return action["href"], action.get("header", {}) +def _broker_index_url() -> str: + if _broker_iqmaps_url is None: + return "" + return _broker_iqmaps_url(DEFAULT_TILE_BUNDLE_BASE_URL + "/index.json") + + def fetch_index(base_url: str, session: requests.Session) -> dict: - index_url = f"{base_url}/index.json" - headers = request_headers(index_url) - # requests' auth= rewrites the Authorization header, so only fall back to it when the - # closed-source bundle gave us nothing. - response = session.get(index_url, timeout=HTTP_TIMEOUT_S, headers=headers, - auth=None if headers else request_auth()) + if _is_broker(base_url): + index_url = _broker_index_url() + if not index_url: + raise ValueError("broker iqmaps URL unavailable") + headers = request_headers(base_url) + else: + index_url = f"{base_url}/index.json" + headers = request_headers(index_url) + response = session.get(index_url, timeout=HTTP_TIMEOUT_S, headers=headers) response.raise_for_status() index = response.json() regions = index.get("regions") @@ -215,6 +193,10 @@ def region_valhalla_path(selector: str) -> Path: return region_bundle_dir(selector) / "valhalla" / "tiles.tar" +def region_roads_path(selector: str) -> Path: + return region_bundle_dir(selector) / "tiles" / "roads.mbtiles" + + def region_valhalla_installed(selector: str) -> bool: return region_valhalla_path(selector).exists() @@ -279,6 +261,11 @@ def _write_manifest(selector: str, entry: dict) -> None: "bytes": entry.get("valhalla_bytes"), "sha256": entry.get("valhalla_sha256", ""), } + if entry.get("roads_path") and region_roads_path(selector).exists(): + manifest["roads"] = { + "bytes": entry.get("roads_bytes"), + "sha256": entry.get("roads_sha256", ""), + } manifest_path = region_bundle_dir(selector) / "manifest.json" manifest_path.parent.mkdir(parents=True, exist_ok=True) manifest_path.write_text(json.dumps(manifest, indent=2)) @@ -332,6 +319,7 @@ class TileBundleDownloader: progress_offset, progress_total, 1, entry.get("valhalla_objects"), ) if ok: + self._download_roads(selector, entry, base_url, progress_offset + int(entry.get("valhalla_bytes", 0)), progress_total) _write_manifest(selector, entry) return ok night_path = region_bundle_path(selector) @@ -368,10 +356,29 @@ class TileBundleDownloader: cloudlog.warning(f"iq_maps: routing extract errored for {selector}: {exc}") if not nav_ok: cloudlog.warning(f"iq_maps: routing extract failed for {selector}; map tiles installed") + self._download_roads(selector, entry, base_url, progress_offset + int(entry.get("bytes", 0)) + int(entry.get("day_bytes", 0)) + + int(entry.get("valhalla_bytes", 0)), progress_total) _write_manifest(selector, entry) cloudlog.info(f"iq_maps: installed tile bundle {selector}") return True + def _download_roads(self, selector: str, entry: dict, base_url: str, progress_offset: int, progress_total: int) -> bool: + if not entry.get("roads_path"): + return False + # roads are additive like routing: a missing or corrupt archive must never cost the region its map + try: + ok = self._download_file( + selector, base_url, entry["roads_path"], int(entry.get("roads_bytes", 0)), + str(entry.get("roads_sha256", "")).strip().lower(), region_roads_path(selector), + progress_offset, progress_total, 1, entry.get("roads_objects"), + ) + except Exception as exc: + ok = False + cloudlog.warning(f"iq_maps: roads archive errored for {selector}: {exc}") + if not ok: + cloudlog.warning(f"iq_maps: roads archive failed for {selector}") + return ok + def _download_file(self, selector: str, base_url: str, remote_path: str, expected_bytes: int, expected_sha: str, final_path: Path, progress_offset: int, progress_total: int, parts: int = 1, @@ -379,12 +386,15 @@ class TileBundleDownloader: # Bundles are published as .pNN because Cloudflare caps proxied bodies at ~100MB. # They stream back-to-back into ONE .part file: concatenating afterwards would need # double the free space, which devices do not have. - base = f"{base_url}/{remote_path.lstrip('/')}" + if _is_broker(base_url) and _broker_iqmaps_url is not None: + broker_raw = _broker_iqmaps_url(DEFAULT_TILE_BUNDLE_BASE_URL + "/" + remote_path.lstrip('/')) + base = broker_raw or f"{base_url}/{remote_path.lstrip('/')}" + else: + base = f"{base_url}/{remote_path.lstrip('/')}" count = len(objects) if objects else parts if objects and not _is_hf(base_url): - urls = [None] * count # gitea: resolved per-attempt from the oid + urls = [None] * count else: - # HF (and plain mirrors) serve the same chunks as ordinary .pNN files urls = [base] if count <= 1 else [f"{base}.p{i:02d}" for i in range(count)] part_path = final_path.with_name(final_path.name + ".part") part_path.parent.mkdir(parents=True, exist_ok=True) @@ -417,22 +427,19 @@ class TileBundleDownloader: for index in range(first_part, len(urls)): if objects and not _is_hf(base_url): url_headers = request_headers(base_url) - auth = None if url_headers else request_auth() object_url, object_headers = _resolve_oid_url( self.session, base_url, objects[index]["oid"], int(objects[index]["size"]), url_headers) else: url = urls[index] - url_headers = request_headers(url) - auth = None if url_headers else request_auth() - # Re-resolve per part: a pre-signed LFS object URL can expire mid-download. + url_headers = request_headers(base_url) if _is_broker(base_url) else request_headers(url) object_url, object_headers = _resolve_object_url(self.session, url, url_headers) headers = dict(object_headers) offset = skip_in_part if index == first_part else 0 if offset: headers["Range"] = f"bytes={offset}-" with self.session.get(object_url, headers=headers, stream=True, - timeout=HTTP_TIMEOUT_S, auth=auth) as response: + timeout=HTTP_TIMEOUT_S) as response: if offset and response.status_code != 206: # server ignored the range: restart this whole file cleanly f.close() @@ -486,19 +493,23 @@ class TileBundleDownloader: return False wanted: list[tuple[str, dict]] = [] + roads_only: list[tuple[str, dict]] = [] for selector in selectors: entry = regions.get(selector) if entry is None: cloudlog.warning(f"iq_maps: no tile bundle published for {selector}") ok = False continue - if self.include_display and region_bundle_installed(selector) and self._installed_matches(selector, entry): - continue - if not self.include_display and self._routing_matches(selector, entry): + current = ((self.include_display and region_bundle_installed(selector) and self._installed_matches(selector, entry)) + or (not self.include_display and self._routing_matches(selector, entry))) + if current: + if not self._roads_matches(selector, entry): + roads_only.append((selector, entry)) continue wanted.append((selector, entry)) - progress_total = sum(self._download_bytes(entry) for _, entry in wanted) + progress_total = (sum(self._download_bytes(entry) for _, entry in wanted) + + sum(int(entry.get("roads_bytes", 0)) for _, entry in roads_only)) progress_offset = 0 for selector, entry in wanted: if self._should_abort(): @@ -510,6 +521,12 @@ class TileBundleDownloader: cloudlog.exception(f"iq_maps: tile bundle download failed for {selector}") ok = False progress_offset += self._download_bytes(entry) + for selector, entry in roads_only: + if self._should_abort(): + return False + if self._download_roads(selector, entry, base_url, progress_offset, progress_total): + _write_manifest(selector, entry) + progress_offset += int(entry.get("roads_bytes", 0)) return ok finally: self._publish_progress("", 0, 0, active=False) @@ -531,9 +548,22 @@ class TileBundleDownloader: except (OSError, ValueError): return False + @staticmethod + def _roads_matches(selector: str, entry: dict) -> bool: + if not entry.get("roads_path"): + return True + try: + path = region_roads_path(selector) + expected = str(entry.get("roads_sha256", "")).strip().lower() + if not expected or path.stat().st_size != int(entry.get("roads_bytes", 0)): + return False + return _hash_existing(path)[0].hexdigest() == expected + except (OSError, ValueError): + return False + def _download_bytes(self, entry: dict) -> int: display_bytes = int(entry.get("bytes", 0)) + int(entry.get("day_bytes", 0)) if self.include_display else 0 - return display_bytes + int(entry.get("valhalla_bytes", 0)) + return display_bytes + int(entry.get("valhalla_bytes", 0)) + int(entry.get("roads_bytes", 0)) @staticmethod def _installed_matches(selector: str, entry: dict) -> bool: diff --git a/iqpilot/konn3kt/registration.py b/iqpilot/konn3kt/registration.py index 11e4fc30..a3720bfa 100755 --- a/iqpilot/konn3kt/registration.py +++ b/iqpilot/konn3kt/registration.py @@ -1,18 +1,12 @@ #!/usr/bin/env python3 -import os -import time -import json -import jwt +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" import re import secrets -from typing import cast from pathlib import Path -from datetime import datetime, timedelta, UTC -from iqpilot.common.api import api_get, get_key_pair from iqpilot.common.params import Params -from iqpilot.common.spinner import Spinner -from iqpilot.system.hardware import HARDWARE from iqpilot.system.hardware.hw import Paths from iqpilot.common.swaglog import cloudlog @@ -35,6 +29,10 @@ def _read_persist_dongle_id() -> str | None: return None +def is_valid_dongle_id(dongle_id: str | None) -> bool: + return bool(dongle_id and _DONGLE_ID_RE.fullmatch(dongle_id)) + + def get_cached_dongle_id(params: Params | None = None, prefer_readonly: bool = True) -> str | None: ro = _read_persist_dongle_id() if is_valid_dongle_id(ro): @@ -46,8 +44,8 @@ def get_cached_dongle_id(params: Params | None = None, prefer_readonly: bool = T if v and v != UNREGISTERED_DONGLE_ID: return v.lower() if is_valid_dongle_id(v) else v return ro or None -def is_valid_dongle_id(dongle_id: str | None) -> bool: - return bool(dongle_id and _DONGLE_ID_RE.fullmatch(dongle_id)) + + def get_or_create_dongle_id(params: Params | None = None, prefer_readonly: bool = True) -> str: p = Params() if params is None else params dongle_id = get_cached_dongle_id(p, prefer_readonly=prefer_readonly) @@ -57,164 +55,32 @@ def get_or_create_dongle_id(params: Params | None = None, prefer_readonly: bool p.put("DongleId", dongle_id) cloudlog.warning(f"generated new DongleId={dongle_id} (no readonly dongle_id found)") return dongle_id + + +_impl = None + + +def _registration_impl(): + global _impl + if _impl is None: + from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + try: + _impl = load_private_module(__name__, "iqpilot_private.konn3kt.registration.reg_core") + except ProprietaryModuleMissing: + from konn3kt_private.registration import reg_core as _impl + return _impl + + def ensure_dev_pairing_identity(params: Params | None = None, force_reset: bool = False) -> dict[str, str]: - p = Params() if params is None else params - - persist_dir = Path(Paths.persist_root()) / "comma" - persist_dir.mkdir(parents=True, exist_ok=True) - - dongle_path = persist_dir / "dongle_id" - priv_path = persist_dir / "id_rsa" - pub_path = persist_dir / "id_rsa.pub" - - if force_reset: - for fp in (dongle_path, priv_path, pub_path): - try: - fp.unlink(missing_ok=True) - except Exception: - cloudlog.exception(f"failed to remove {fp}") - try: - (persist_dir / "konn3kt_prime_type").unlink(missing_ok=True) - except Exception: - pass - try: - p.remove("PrimeType") - except Exception: - pass - forced_dongle = os.getenv("KONN3KT_DEV_DONGLE_ID") - dongle_id = forced_dongle.strip().lower() if forced_dongle else None - if dongle_id and not is_valid_dongle_id(dongle_id): - cloudlog.error("KONN3KT_DEV_DONGLE_ID must be 16 hex chars") - dongle_id = None - if dongle_id is None: - existing = None - try: - existing = dongle_path.read_text().strip().lower() if dongle_path.is_file() else None - except Exception: - cloudlog.exception("failed reading existing dev dongle_id") - dongle_id = existing if is_valid_dongle_id(existing) else secrets.token_hex(8) - try: - dongle_path.write_text(dongle_id) - except Exception: - cloudlog.exception("failed writing dev dongle_id") - p.put("DongleId", dongle_id) - p.put("HardwareSerial", p.get("HardwareSerial") or f"DEV-{dongle_id}") - if force_reset or (not priv_path.is_file()) or (not pub_path.is_file()): - try: - from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import rsa - key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - priv_bytes = key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - pub_bytes = key.public_key().public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - priv_path.write_bytes(priv_bytes) - pub_path.write_bytes(pub_bytes) - except Exception: - cloudlog.exception("failed generating dev RSA keys") - raise - return { - "dongle_id": dongle_id, - "serial": p.get("HardwareSerial") or f"DEV-{dongle_id}", - "persist_dir": str(persist_dir), - } -def _normalize_imei(value: str | None) -> str: - return value or "" + return _registration_impl().ensure_dev_pairing_identity(params, force_reset) def get_registration_identifiers(wait_timeout: float = IMEI_WAIT_TIMEOUT, show_spinner: bool = False) -> tuple[str, str, str]: - serial = HARDWARE.get_serial() - spinner = Spinner() if show_spinner else None - start_time = time.monotonic() - imei1: str | None = None - imei2: str | None = None - - while time.monotonic() - start_time < wait_timeout: - try: - imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) - if imei1 or imei2: - break - except RuntimeError as e: - if "no modems" in str(e).lower(): - cloudlog.warning("No cellular modem available, proceeding without IMEI") - break - cloudlog.exception("Error getting imei, trying again...") - except Exception: - cloudlog.exception("Error getting imei, trying again...") - time.sleep(1) - - imei1 = _normalize_imei(imei1) - imei2 = _normalize_imei(imei2) - - if not imei1 and not imei2: - cloudlog.warning(f"proceeding with serial-only registration for serial={serial}") - if spinner is not None: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1 or None}, {imei2 or None})") - spinner.close() - - return serial, imei1, imei2 + return _registration_impl().get_registration_identifiers(wait_timeout, show_spinner) def register(show_spinner=False) -> str | None: - params = Params() - - dongle_id: str | None = get_cached_dongle_id(params, prefer_readonly=True) - if dongle_id in ("", UNREGISTERED_DONGLE_ID): - dongle_id = None - - jwt_algo, private_key, public_key = get_key_pair() - - if not public_key: - dongle_id = UNREGISTERED_DONGLE_ID - cloudlog.warning("missing public key") - elif dongle_id is None: - if show_spinner: - spinner = Spinner() - spinner.update("registering device") - - serial, imei1, imei2 = get_registration_identifiers(wait_timeout=IMEI_WAIT_TIMEOUT, show_spinner=False) - - backoff = 0 - start_time = time.monotonic() - while True: - try: - register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, - cast(str, private_key), algorithm=jwt_algo) - cloudlog.info("getting pilotauth") - cloudlog.info("getting pilotauth") - resp = api_get("v2/pilotauth/", method='POST', timeout=15, - imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token) - - if resp.status_code in (400, 402, 403): - cloudlog.info(f"Unable to register device, got {resp.status_code}") - dongle_id = UNREGISTERED_DONGLE_ID - else: - dongleauth = json.loads(resp.text) - dongle_id = dongleauth["dongle_id"] - break - except Exception: - cloudlog.exception("failed to authenticate") - backoff = min(backoff + 1, 15) - time.sleep(backoff) - - if time.monotonic() - start_time > 60 and show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") - return UNREGISTERED_DONGLE_ID - - if show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1 or None}, {imei2 or None})") - spinner.close() - - if dongle_id: - params.put("DongleId", dongle_id) - from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert - set_offroad_alert("Offroad_UnregisteredHardware", False) - return dongle_id + return _registration_impl().register(show_spinner) if __name__ == "__main__": diff --git a/iqpilot/selfdrive/car/vehicle_catalog.json b/iqpilot/selfdrive/car/vehicle_catalog.json index e7048978..79367200 100644 --- a/iqpilot/selfdrive/car/vehicle_catalog.json +++ b/iqpilot/selfdrive/car/vehicle_catalog.json @@ -3941,6 +3941,86 @@ ], "req": "All" }, + "TESLA_MODEL_S_HW1|2014-2016": { + "label": "Tesla Model S (with HW1) 2014-16", + "id": "TESLA_MODEL_S_HW1", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model S (with HW1)", + "yrs": [ + "2014", + "2015", + "2016" + ], + "req": "All" + }, + "TESLA_MODEL_S_HW2|2017-2019": { + "label": "Tesla Model S (with HW2) 2017-19", + "id": "TESLA_MODEL_S_HW2", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model S (with HW2)", + "yrs": [ + "2017", + "2018", + "2019" + ], + "req": "All" + }, + "TESLA_MODEL_S_HW3|2020-2023": { + "label": "Tesla Model S (with HW3) 2020-23", + "id": "TESLA_MODEL_S_HW3", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model S (with HW3)", + "yrs": [ + "2020", + "2021", + "2022", + "2023" + ], + "req": "All" + }, + "TESLA_MODEL_X_HW1|2014-2016": { + "label": "Tesla Model X (with HW1) 2014-16", + "id": "TESLA_MODEL_X_HW1", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model X (with HW1)", + "yrs": [ + "2014", + "2015", + "2016" + ], + "req": "All" + }, + "TESLA_MODEL_X_HW2|2016-2019": { + "label": "Tesla Model X (with HW2) 2016-19", + "id": "TESLA_MODEL_X_HW2", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model X (with HW2)", + "yrs": [ + "2016", + "2017", + "2018", + "2019" + ], + "req": "All" + }, + "TESLA_MODEL_X_HW3|2021-2023": { + "label": "Tesla Model X (with HW3) 2021-23", + "id": "TESLA_MODEL_X_HW3", + "mk": "Tesla", + "grp": "tesla", + "mdl": "Model X (with HW3)", + "yrs": [ + "2021", + "2022", + "2023" + ], + "req": "All" + }, "TESLA_MODEL_X|2024": { "label": "Tesla Model X (with HW4) 2024", "id": "TESLA_MODEL_X", diff --git a/iqpilot/selfdrive/controls/controlsd.py b/iqpilot/selfdrive/controls/controlsd.py index 7022a30b..aa63eaca 100755 --- a/iqpilot/selfdrive/controls/controlsd.py +++ b/iqpilot/selfdrive/controls/controlsd.py @@ -184,22 +184,18 @@ class Controls(IQControlsLayer): self.curvature = -self.VM.calc_curvature(steer_angle_without_offset, CS.vEgo, lp.roll) self.roll_compensation = -self.VM.roll_compensation(lp.roll, CS.vEgo) - lac_ext = getattr(self.LaC, "extension", None) + nnff_assist = getattr(self.LaC, "nnff_assist", None) + if nnff_assist is not None: + nnff_assist.update_model_v2(self.sm['modelV2'] if self.sm.all_checks(['modelV2']) else None) + if self.CP.lateralTuning.which() == 'torque' and hasattr(self.LaC, "update_live_torque_params"): torque_params = self.sm['lateralTorqueParameters'] if self.sm.all_checks(['lateralTorqueParameters']) and torque_params.useParams: + if hasattr(self.LaC, "set_live_torque_valid"): + self.LaC.set_live_torque_valid(torque_params.valid) self.LaC.update_live_torque_params(torque_params.latAccelFactorFiltered, torque_params.latAccelOffsetFiltered, torque_params.frictionCoefficientFiltered) - if lac_ext is not None: - lac_ext.update_limits() - - if lac_ext is not None: - lac_ext.update_model_v2(self.sm['modelV2']) - - if lac_ext is not None: - lac_ext.update_lateral_lag(self.CP.steerActuatorDelay) - long_plan = self.sm['longitudinalPlan'] model_v2 = self.sm['modelV2'] @@ -269,6 +265,8 @@ class Controls(IQControlsLayer): lat_accel_override = bool(CS.gasPressed) or bool(self.sm['iqState'].aol.active) self.desired_curvature, curvature_limited = clip_curvature(CS.vEgo, self.desired_curvature, new_desired_curvature, lp.roll, lat_accel_override) lat_delay = lateral_action_delay(self.params, self.CP, self.sm["lateralDelay"].lateralDelay) + LAT_SMOOTH_SECONDS + if nnff_assist is not None: + nnff_assist.update_lateral_lag(lat_delay) actuators.curvature = self.desired_curvature steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp, @@ -430,8 +428,8 @@ class Controls(IQControlsLayer): severity = "warning" detail = ( - f"update_us={update_us} state_control_us={state_control_us} publish_us={publish_us} " - f"tail_work_us={tail_work_us} rk_remaining_us={rk_remaining_us}" + f"update_us={update_us} state_control_us={state_control_us} publish_us={publish_us} " + + f"tail_work_us={tail_work_us} rk_remaining_us={rk_remaining_us}" ) self._perf.emit( "controlsd_slow_loop", diff --git a/iqpilot/selfdrive/controls/lib/accel_boost.py b/iqpilot/selfdrive/controls/lib/accel_boost.py index 61b6cd08..92de6e2a 100644 --- a/iqpilot/selfdrive/controls/lib/accel_boost.py +++ b/iqpilot/selfdrive/controls/lib/accel_boost.py @@ -1,10 +1,14 @@ """ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ """ +import numpy as np + from iqpilot.common.constants import CV -ACCEL_BOOST_MAX = 1.0 -ACCEL_BOOST_RATE = 0.1 +ACCEL_BOOST_MAX = 0.2 +ACCEL_BOOST_RATE = 0.025 +ACCEL_BOOST_DECAY_RATE = 0.1 +ACCEL_BOOST_PER_OVERRIDE = 0.05 ACCEL_BOOST_MIN_SPEED = 10 * CV.MPH_TO_MS @@ -13,12 +17,24 @@ class AccelBoost: self.enabled = enabled self.dt = dt self.value = 0.0 + self.override_boost = 0.0 + self.model_limited = False - def update(self, engaged: bool, v_ego: float, gas_pressed: bool) -> float: - if not self.enabled or not engaged: + def update(self, engaged: bool, v_ego: float, gas_pressed: bool, model_limited: bool) -> float: + enabled = self.enabled and engaged + if not enabled or not gas_pressed: + self.override_boost = 0.0 + self.model_limited = enabled and model_limited + + if not enabled: self.value = 0.0 elif v_ego < ACCEL_BOOST_MIN_SPEED: - self.value = max(0.0, self.value - ACCEL_BOOST_RATE * self.dt) - elif gas_pressed: - self.value = min(ACCEL_BOOST_MAX, self.value + ACCEL_BOOST_RATE * self.dt) + self.value = max(0.0, self.value - ACCEL_BOOST_DECAY_RATE * self.dt) + elif gas_pressed and self.model_limited: + increase = min(ACCEL_BOOST_RATE * self.dt, ACCEL_BOOST_PER_OVERRIDE - self.override_boost, ACCEL_BOOST_MAX - self.value) + self.value += increase + self.override_boost += increase return self.value + + def apply(self, accel: float) -> float: + return accel + np.interp(accel, [-1.0, -0.5, 5.0], [0.0, self.value, self.value], right=0.0) diff --git a/iqpilot/selfdrive/controls/lib/action_engine.py b/iqpilot/selfdrive/controls/lib/action_engine.py new file mode 100644 index 00000000..053a5ed9 --- /dev/null +++ b/iqpilot/selfdrive/controls/lib/action_engine.py @@ -0,0 +1,18 @@ +# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.navd.action_engine") +except ProprietaryModuleMissing: + from iqpilot.cereal import custom, log + + class ActionEngine: + def __init__(self): + self.desire = log.Desire.none + self.lane_change_state = log.LaneChangeState.off + self.lane_change_direction = log.LaneChangeDirection.none + self.lane_turn_direction = custom.IQTurnSignalDirection.none + self.lateral_edge_block = custom.IQLateralEdgeBlock.none + + def update(self, carstate, lateral_active, lane_change_prob, nav_state=None, modeldata=None, radar_state=None): + return None diff --git a/iqpilot/selfdrive/controls/lib/desire_helper.py b/iqpilot/selfdrive/controls/lib/desire_helper.py deleted file mode 100644 index 0d0f09b9..00000000 --- a/iqpilot/selfdrive/controls/lib/desire_helper.py +++ /dev/null @@ -1,360 +0,0 @@ -""" -Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ -""" - -from __future__ import annotations - -import math - -from iqpilot.cereal import car, custom, log - -from iqpilot.common.constants import CV -from iqpilot.common.params import Params -from iqpilot.common.realtime import DT_MDL -from iqpilot.selfdrive.controls.lib.helpers.lane_change import ( - IQLaneSwapController, - AutoLaneChangeMode, - NavExitLaneChangeController, -) -from iqpilot.selfdrive.controls.lib.helpers.lateral_edge_guard import LateralEdgeGuard -from iqpilot.selfdrive.controls.lib.helpers.lane_turn import IQNavTurnController - -LaneChangeState = log.LaneChangeState -LaneChangeDirection = log.LaneChangeDirection -TurnDirection = custom.IQTurnSignalDirection -LateralEdgeBlock = custom.IQLateralEdgeBlock -NavManeuverPhase = custom.IQNavState.ManeuverPhase - -LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS -LANE_CHANGE_TIME_MAX = 10.0 -LANE_CHANGE_CANCEL_TORQUE_FRAMES = round(0.25 / DT_MDL) -# only the rising edge reaches the model and it stays visible for the 5 s the history spans, so one -# edge per 5 s keeps the turn alive through the maneuver without stacking edges that cut the corner -TURN_DESIRE_PULSE_CYCLE_FRAMES = round(5.0 / DT_MDL) -TURN_DESIRE_PULSE_HOLD_FRAMES = TURN_DESIRE_PULSE_CYCLE_FRAMES - 1 -TURN_RECOIL_HEADING_RAD = math.radians(55.0) -TURN_DESIRE_MAX_SECONDS = 25.0 -TURN_DESIRE_MOVING_SPEED = 0.5 - -_LANE_CHANGE_DESIRES = { - (LaneChangeDirection.none, LaneChangeState.off): log.Desire.none, - (LaneChangeDirection.none, LaneChangeState.preLaneChange): log.Desire.none, - (LaneChangeDirection.none, LaneChangeState.laneChangeStarting): log.Desire.none, - (LaneChangeDirection.none, LaneChangeState.laneChangeFinishing): log.Desire.none, - (LaneChangeDirection.left, LaneChangeState.off): log.Desire.none, - (LaneChangeDirection.left, LaneChangeState.preLaneChange): log.Desire.none, - (LaneChangeDirection.left, LaneChangeState.laneChangeStarting): log.Desire.laneChangeLeft, - (LaneChangeDirection.left, LaneChangeState.laneChangeFinishing): log.Desire.laneChangeLeft, - (LaneChangeDirection.right, LaneChangeState.off): log.Desire.none, - (LaneChangeDirection.right, LaneChangeState.preLaneChange): log.Desire.none, - (LaneChangeDirection.right, LaneChangeState.laneChangeStarting): log.Desire.laneChangeRight, - (LaneChangeDirection.right, LaneChangeState.laneChangeFinishing): log.Desire.laneChangeRight, -} - -_TURN_DESIRES = { - TurnDirection.none: log.Desire.none, - TurnDirection.turnLeft: log.Desire.turnLeft, - TurnDirection.turnRight: log.Desire.turnRight, -} - -_PULSED_TURN_DESIRES = { - log.Desire.turnLeft, - log.Desire.turnRight, -} - - -def turn_desire(turn_direction) -> log.Desire: - return _TURN_DESIRES[getattr(turn_direction, "raw", turn_direction)] - - -def _direction_from_blinkers(carstate) -> int: - if carstate.leftBlinker: - return LaneChangeDirection.left - if carstate.rightBlinker: - return LaneChangeDirection.right - return LaneChangeDirection.none - - -def _steering_nudge_matches(carstate, direction: int) -> bool: - if not carstate.steeringPressed: - return False - return ( - (direction == LaneChangeDirection.left and carstate.steeringTorque > 0) or - (direction == LaneChangeDirection.right and carstate.steeringTorque < 0) - ) - - -def _opposite_direction(direction: int) -> int: - if direction == LaneChangeDirection.left: - return LaneChangeDirection.right - if direction == LaneChangeDirection.right: - return LaneChangeDirection.left - return LaneChangeDirection.none - - -def _blinker_matches(carstate, direction: int) -> bool: - return ( - (direction == LaneChangeDirection.left and carstate.leftBlinker and not carstate.rightBlinker) or - (direction == LaneChangeDirection.right and carstate.rightBlinker and not carstate.leftBlinker) - ) - - -def _blindspot_matches(carstate, direction: int) -> bool: - return ( - (direction == LaneChangeDirection.left and carstate.leftBlindspot) or - (direction == LaneChangeDirection.right and carstate.rightBlindspot) - ) - - -def _read_enable_bsm() -> bool: - try: - with car.CarParams.from_bytes(Params().get("CarParams")) as cp: - return bool(cp.enableBsm) - except Exception: - return False - - -class DesireHelper: - def __init__(self): - self.lane_change_state = LaneChangeState.off - self.lane_change_direction = LaneChangeDirection.none - self.lane_change_timer = 0.0 - self.lane_change_ll_prob = 1.0 - self.lane_change_counter_torque_frames = 0 - self.prev_one_blinker = False - self.prev_nav_exit_active = False - self.nav_exit_rearmed = False - self.desire = log.Desire.none - - self.alc = IQLaneSwapController(self) - self.lane_turn_controller = IQNavTurnController(self) - self.nav_exit = NavExitLaneChangeController(_read_enable_bsm()) - self.lateral_edge_guard = LateralEdgeGuard() - self.lateral_edge_block = LateralEdgeBlock.none - self.lane_turn_direction = TurnDirection.none - self.nav_turn_direction = TurnDirection.none - self.turn_desire_frames = 0 - self.turn_desire_cycle_input = log.Desire.none - self.turn_heading_rad = 0.0 - self.turn_desire_seconds = 0.0 - self.turn_recoiled = False - - @staticmethod - def get_lane_change_direction(carstate): - return _direction_from_blinkers(carstate) - - @staticmethod - def _nav_turn_desire(nav_state): - if nav_state is None or not getattr(nav_state, "active", False): - return TurnDirection.none - if getattr(nav_state, "maneuverPhase", NavManeuverPhase.none) != NavManeuverPhase.turnActive: - return TurnDirection.none - if not getattr(nav_state, "shouldSendTurnDesire", False): - return TurnDirection.none - return getattr(nav_state, "turnDesireDirection", TurnDirection.none) - - def _clear_lane_change(self) -> None: - self.lane_change_state = LaneChangeState.off - self.lane_change_direction = LaneChangeDirection.none - - def _refresh_turn_overrides(self, carstate, nav_state) -> bool: - speed_mps = carstate.vEgo - self.lane_turn_controller.update_params() - self.lane_turn_controller.update_lane_turn( - blindspot_left=carstate.leftBlindspot, - blindspot_right=carstate.rightBlindspot, - left_blinker=carstate.leftBlinker, - right_blinker=carstate.rightBlinker, - v_ego=speed_mps, - ) - self.lane_turn_direction = self.lane_turn_controller.get_turn_direction() - self.nav_turn_direction = self._nav_turn_desire(nav_state) - - self.nav_exit.update_params() - self.nav_exit.update(nav_state, carstate) - return bool(self.nav_exit.active) - - def _reset_required(self, lateral_active: bool, nav_exit_active: bool) -> bool: - timed_out = self.lane_change_timer > LANE_CHANGE_TIME_MAX - feature_disabled = self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF and not nav_exit_active - return (not lateral_active) or timed_out or feature_disabled - - def _begin_from_idle(self, one_blinker: bool, nav_exit_active: bool, below_speed: bool) -> None: - if below_speed: - return - if one_blinker and not self.prev_one_blinker: - self.lane_change_state = LaneChangeState.preLaneChange - self.lane_change_direction = _direction_from_blinkers(self._last_carstate) - self.lane_change_ll_prob = 1.0 - return - if nav_exit_active and not self.prev_nav_exit_active: - self.lane_change_state = LaneChangeState.preLaneChange - self.lane_change_direction = self.nav_exit.direction - self.lane_change_ll_prob = 1.0 - - def _refresh_requested_direction(self, one_blinker: bool, nav_exit_active: bool) -> None: - if one_blinker: - self.lane_change_direction = _direction_from_blinkers(self._last_carstate) - elif nav_exit_active: - self.lane_change_direction = self.nav_exit.direction - - def _step_pre_lane_change(self, one_blinker: bool, nav_exit_active: bool, below_speed: bool) -> None: - self._refresh_requested_direction(one_blinker, nav_exit_active) - blindspot_detected = _blindspot_matches(self._last_carstate, self.lane_change_direction) - self.lateral_edge_block = self.lateral_edge_guard.block_for_direction(self.lane_change_direction) - lateral_edge_blocked = self.lateral_edge_block != LateralEdgeBlock.none - steering_ready = _steering_nudge_matches(self._last_carstate, self.lane_change_direction) - # an exit can need more than one lane, but only the first move may start without a driver nudge - nav_auto_start = nav_exit_active and self.nav_exit.auto_allowed and not self.nav_exit_rearmed - - self.alc.update_lane_change(blindspot_detected=blindspot_detected, brake_pressed=self._last_carstate.brakePressed) - allowed_to_launch = steering_ready or self.alc.auto_lane_change_allowed or nav_auto_start - - if (not (one_blinker or nav_exit_active)) or below_speed: - self._clear_lane_change() - elif allowed_to_launch and not blindspot_detected and not lateral_edge_blocked: - self.lane_change_state = LaneChangeState.laneChangeStarting - - def _step_lane_change_starting(self, lane_change_prob: float) -> None: - self.lane_change_ll_prob = max(self.lane_change_ll_prob - (2.0 * DT_MDL), 0.0) - if lane_change_prob < 0.02 and self.lane_change_ll_prob < 0.01: - self.lane_change_state = LaneChangeState.laneChangeFinishing - - def _step_lane_change_finishing(self, one_blinker: bool, nav_exit_active: bool) -> None: - self.lane_change_ll_prob = min(self.lane_change_ll_prob + DT_MDL, 1.0) - if self.lane_change_ll_prob <= 0.99: - return - self.lane_change_direction = LaneChangeDirection.none - if one_blinker: - self.lane_change_state = LaneChangeState.preLaneChange - elif nav_exit_active: - self.lane_change_state = LaneChangeState.preLaneChange - self.lane_change_direction = self.nav_exit.direction - self.nav_exit_rearmed = True - else: - self.lane_change_state = LaneChangeState.off - - def _driver_cancels_lane_change(self) -> bool: - if self.lane_change_state not in (LaneChangeState.laneChangeStarting, LaneChangeState.laneChangeFinishing): - self.lane_change_counter_torque_frames = 0 - return False - opposite = _opposite_direction(self.lane_change_direction) - if _steering_nudge_matches(self._last_carstate, opposite): - self.lane_change_counter_torque_frames += 1 - else: - self.lane_change_counter_torque_frames = 0 - return (_blinker_matches(self._last_carstate, opposite) or - self.lane_change_counter_torque_frames >= LANE_CHANGE_CANCEL_TORQUE_FRAMES) - - def _cancel_lane_change(self) -> None: - self._clear_lane_change() - self.lane_change_ll_prob = 1.0 - self.lane_change_counter_torque_frames = 0 - - def _advance_lane_change_machine(self, one_blinker: bool, nav_exit_active: bool, below_speed: bool, lane_change_prob: float) -> None: - if self._driver_cancels_lane_change(): - self._cancel_lane_change() - return - if self.lane_change_state == LaneChangeState.off: - self._begin_from_idle(one_blinker, nav_exit_active, below_speed) - return - if self.lane_change_state == LaneChangeState.preLaneChange: - self._step_pre_lane_change(one_blinker, nav_exit_active, below_speed) - return - if self.lane_change_state == LaneChangeState.laneChangeStarting: - self._step_lane_change_starting(lane_change_prob) - return - if self.lane_change_state == LaneChangeState.laneChangeFinishing: - self._step_lane_change_finishing(one_blinker, nav_exit_active) - - def _update_timer(self) -> None: - if self.lane_change_state in (LaneChangeState.off, LaneChangeState.preLaneChange): - self.lane_change_timer = 0.0 - else: - self.lane_change_timer += DT_MDL - - def _clear_turn_desire_pulse(self) -> None: - self.turn_desire_frames = 0 - self.turn_desire_cycle_input = log.Desire.none - self.turn_heading_rad = 0.0 - self.turn_desire_seconds = 0.0 - self.turn_recoiled = False - - def _measured_yaw_rate(self) -> float: - # carState.yawRate is left unpopulated by many ports, so the model pose is the primary source - rate = getattr(getattr(getattr(self, "_last_modeldata", None), "orientationRate", None), "z", None) - try: - if rate is not None and len(rate) and math.isfinite(rate[0]): - return float(rate[0]) - except (TypeError, IndexError): - pass - try: - return float(getattr(self._last_carstate, "yawRate", 0.0) or 0.0) - except (TypeError, ValueError): - return 0.0 - - def _turn_recoiled(self, desired_output: log.Desire) -> bool: - # blinkers keep running well past the apex, so the executed heading change has to end the - # desire: one that outlives the turn keeps steering the plan into the far side of the road - if desired_output != self.turn_desire_cycle_input: - self.turn_heading_rad = 0.0 - self.turn_desire_seconds = 0.0 - self.turn_recoiled = False - self.turn_heading_rad += abs(self._measured_yaw_rate()) * DT_MDL - # a red light at the junction must not use up the turn before the car moves - if float(getattr(self._last_carstate, "vEgo", 0.0) or 0.0) > TURN_DESIRE_MOVING_SPEED: - self.turn_desire_seconds += DT_MDL - if self.turn_heading_rad >= TURN_RECOIL_HEADING_RAD or self.turn_desire_seconds >= TURN_DESIRE_MAX_SECONDS: - self.turn_recoiled = True - return self.turn_recoiled - - def _pulse_turn_desire(self, desired_output: log.Desire) -> log.Desire: - if desired_output not in _PULSED_TURN_DESIRES: - self._clear_turn_desire_pulse() - return desired_output - - recoiled = self._turn_recoiled(desired_output) - - if desired_output != self.turn_desire_cycle_input: - self.turn_desire_frames = 0 - self.turn_desire_cycle_input = desired_output - - cycle_frame = self.turn_desire_frames % TURN_DESIRE_PULSE_CYCLE_FRAMES - self.turn_desire_frames += 1 - if recoiled or cycle_frame >= TURN_DESIRE_PULSE_HOLD_FRAMES: - return log.Desire.none - return desired_output - - def _pick_desire_output(self) -> None: - desired_output = log.Desire.none - if self.nav_turn_direction != TurnDirection.none: - desired_output = turn_desire(self.nav_turn_direction) - elif self.lane_turn_direction != TurnDirection.none: - desired_output = turn_desire(self.lane_turn_direction) - else: - desired_output = _LANE_CHANGE_DESIRES[(self.lane_change_direction, self.lane_change_state)] - - self.desire = self._pulse_turn_desire(desired_output) - - def update(self, carstate, lateral_active, lane_change_prob, nav_state=None, modeldata=None, radar_state=None): - self._last_carstate = carstate - self._last_modeldata = modeldata - self.lateral_edge_guard.update(modeldata, carstate.vEgo, DT_MDL) - self.lateral_edge_block = LateralEdgeBlock.none - one_blinker = carstate.leftBlinker != carstate.rightBlinker - below_speed = carstate.vEgo < LANE_CHANGE_SPEED_MIN - nav_exit_active = self._refresh_turn_overrides(carstate, nav_state) - if not nav_exit_active: - self.nav_exit_rearmed = False - - self.alc.update_params() - if self._reset_required(lateral_active, nav_exit_active): - self._clear_lane_change() - else: - self._advance_lane_change_machine(one_blinker, nav_exit_active, below_speed, lane_change_prob) - - self._update_timer() - self.prev_one_blinker = one_blinker and lateral_active - self.prev_nav_exit_active = nav_exit_active - self.alc.update_state() - self._pick_desire_output() diff --git a/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py b/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py index c2cf85e5..6dfb043d 100644 --- a/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py +++ b/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py @@ -16,7 +16,6 @@ TurnDirection = custom.IQTurnSignalDirection TURN_TRIGGER_MPS = 20 * CV.MPH_TO_MS TURN_SPEED_GATE_MPS = TURN_TRIGGER_MPS LANE_CHANGE_SPEED_MIN = TURN_SPEED_GATE_MPS -TURN_SPEED_CEILING_MPS = 35 * CV.MPH_TO_MS @dataclass @@ -27,13 +26,6 @@ class _TurnGateState: refresh_tick: int = 0 -def _mph_param_to_mps(raw_value) -> float: - try: - return float(raw_value) * CV.MPH_TO_MS - except (TypeError, ValueError): - return TURN_TRIGGER_MPS - - def _resolve_signal_choice(speed_mps: float, speed_limit_mps: float, left_signal: bool, @@ -59,9 +51,8 @@ class TurnSignalPlanner: self.reload_setup() def _refresh_from_params(self) -> None: - requested_gate = _mph_param_to_mps(self._params.get("IQLaneTurnValue", return_default=True)) self._state.active = self._params.get_bool("IQLaneTurnDesire") - self._state.speed_limit_mps = min(TURN_SPEED_CEILING_MPS, requested_gate) + self._state.speed_limit_mps = TURN_TRIGGER_MPS def _consume_legacy_kwargs(self, **legacy) -> tuple[bool, bool, bool, bool, float]: return ( diff --git a/iqpilot/selfdrive/controls/lib/latcontrol_torque.py b/iqpilot/selfdrive/controls/lib/latcontrol_torque.py index 6df8d5f4..2419531c 100644 --- a/iqpilot/selfdrive/controls/lib/latcontrol_torque.py +++ b/iqpilot/selfdrive/controls/lib/latcontrol_torque.py @@ -8,7 +8,7 @@ from importlib.resources import files import numpy as np -from iqpilot.cereal import log, custom +from iqpilot.cereal import log from iqdbc.car import structs from iqdbc.car.lateral import FRICTION_THRESHOLD, get_friction from iqdbc.car.toyota.values import ToyotaFlags @@ -19,7 +19,7 @@ from iqpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from iqpilot.common.filter_simple import FirstOrderFilter from iqpilot.common.params import Params from iqpilot.common.pid import PIDController -from iqpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N +from iqpilot.common.swaglog import cloudlog from iqpilot.selfdrive.controls.lib.lateral_acceleration_slew_limiter import LateralAccelerationSlewLimiter from iqpilot.selfdrive.controls.lib.latcontrol import LatControl from iqpilot.selfdrive.iqmodeld.config import ModelConstants @@ -40,14 +40,17 @@ def _score(a: str, b: str) -> float: return SequenceMatcher(None, a, b).ratio() -def _best_model_for(candidate: str) -> tuple[str | None, float]: +def _best_model_for(candidate: str, fingerprint: str) -> tuple[str | None, float]: best_path, best_score = None, -1.0 if not os.path.isdir(TORQUE_NN_MODEL_PATH): return best_path, best_score - for entry in os.listdir(TORQUE_NN_MODEL_PATH): + for entry in sorted(os.listdir(TORQUE_NN_MODEL_PATH)): if not entry.endswith(".json"): continue - score = _score(os.path.splitext(entry)[0], candidate) + stem = os.path.splitext(entry)[0] + if stem != fingerprint and not stem.startswith(fingerprint + " "): + continue + score = _score(stem, candidate) if score > best_score: best_path, best_score = os.path.join(TORQUE_NN_MODEL_PATH, entry), score return best_path, best_score @@ -65,21 +68,22 @@ def _eps_suffix(CP: structs.CarParams) -> str: def get_nn_model_path(CP: structs.CarParams) -> tuple[str, str, bool]: - if CP.steerControlType == structs.CarParams.SteerControlType.angle: + if CP.steerControlType != structs.CarParams.SteerControlType.torque: return MOCK_MODEL_PATH, "MOCK", False fingerprint = CP.carFingerprint suffix = _eps_suffix(CP) if suffix: - path, score = _best_model_for(f"{fingerprint} {suffix}") + path, score = _best_model_for(f"{fingerprint} {suffix}", fingerprint) if path is not None and fingerprint in path and score >= _ACCEPT_THRESHOLD: name = os.path.splitext(os.path.basename(path))[0] return path, name, score >= _EXACT_THRESHOLD - path, score = _best_model_for(fingerprint) + path, score = _best_model_for(fingerprint, fingerprint) if path is not None and fingerprint in path and score >= _ACCEPT_THRESHOLD: name = os.path.splitext(os.path.basename(path))[0] return path, name, score >= _EXACT_THRESHOLD - path, _ = _best_model_for(_substitute_for(fingerprint)) + substitute = _substitute_for(fingerprint) + path, _ = _best_model_for(substitute, substitute) if substitute != fingerprint else (None, -1.0) name = os.path.splitext(os.path.basename(path))[0] if path else "MOCK" return (path or MOCK_MODEL_PATH), name, False @@ -102,6 +106,8 @@ _ACTIVATIONS = {"sigmoid": _sigmoid, "identity": _identity} def _resolve_activation(name: str): + if not isinstance(name, str): + raise ValueError("NNFF activation must be a string") for symbol, canonical in _ACTIVATION_ALIASES.items(): name = name.replace(symbol, canonical) fn = _ACTIVATIONS.get(name) @@ -124,16 +130,26 @@ class NNTorqueModel: self.output_size = params["output_size"] self.input_mean = np.array(params["input_mean"], dtype=np.float32).T self.input_std = np.array(params["input_std"], dtype=np.float32).T + if (self.input_mean.shape != (1, self.input_size) or self.input_std.shape != self.input_mean.shape or + not np.isfinite(self.input_mean).all() or not np.isfinite(self.input_std).all() or np.any(self.input_std <= 0)): + raise ValueError("Invalid NNFF input normalization") self._weights = [] self._biases = [] self._activations = [] + width = self.input_size for layer in params["layers"]: weight = _pick(layer, "_W") bias = np.zeros_like(_pick(layer, "_b")) if zero_bias else _pick(layer, "_b") + if (weight.ndim != 2 or weight.shape[0] != width or bias.shape != (1, weight.shape[1]) or + not np.isfinite(weight).all() or not np.isfinite(bias).all()): + raise ValueError("Invalid NNFF layer dimensions or weights") self._weights.append(weight) self._biases.append(bias) self._activations.append(_resolve_activation(layer["activation"])) + width = weight.shape[1] + if not self._weights or width != self.output_size: + raise ValueError("Invalid NNFF output dimensions") self.friction_override = self.evaluate(list(_FRICTION_PROBE)) < _FRICTION_THRESHOLD @@ -241,8 +257,11 @@ class PilotLateralBrain: self.desired_lat_jerk_time = cp.steerActuatorDelay + LAG_EXTRA_S def update_model_v2(self, model_packet): - self.model_v2 = model_packet - self.model_valid = model_packet is not None and len(model_packet.orientation.x) >= CONTROL_N + self.model_valid = False + if model_packet is not None: + traces = (model_packet.orientation.x, model_packet.orientation.y, model_packet.acceleration.y) + self.model_valid = all(len(trace) == len(ModelConstants.T_IDXS) and np.isfinite(trace).all() for trace in traces) + self.model_v2 = model_packet if self.model_valid else None def update_lateral_lag(self, lag): self.desired_lat_jerk_time = max(0.01, lag) + LAG_EXTRA_S @@ -345,9 +364,17 @@ class NeuralNetworkFeedForward(PilotLateralBrain): super().__init__(lac_torque, CP, CP_IQ, CI) self.params = Params() self.enabled = self.params.get_bool("NeuralNetworkFeedForward") - self.has_nn_model = (CP_IQ.iqLateralNet.model.path != MOCK_MODEL_PATH - and os.path.isfile(CP_IQ.iqLateralNet.model.path)) - self.model = NNTorqueModel(CP_IQ.iqLateralNet.model.path) if self.has_nn_model else None + self.model = None + model_path = CP_IQ.iqLateralNet.model.path + if model_path != MOCK_MODEL_PATH and os.path.isfile(model_path): + try: + model = NNTorqueModel(model_path) + if model.input_size != 18 or model.output_size != 1 or not np.isfinite(model.evaluate([0.0] * 18)): + raise ValueError("NNFF requires a finite 18-input, 1-output model") + self.model = model + except (OSError, ValueError, TypeError, KeyError, IndexError, StopIteration): + cloudlog.exception("Unable to load NNFF model; using standard torque control") + self.has_nn_model = self.model is not None self.pitch = FirstOrderFilter(0.0, 0.5, 0.01) self.pitch_last = 0.0 @@ -376,6 +403,14 @@ class NeuralNetworkFeedForward(PilotLateralBrain): super().update_lateral_lag(lag) self._window.refresh_lag(self.desired_lat_jerk_time) + def reset_history(self): + self._window.roll.clear() + self._window.lat_accel_desired.clear() + self._jerk_lp.x = 0.0 + self.pitch.x = 0.0 + self.pitch_last = 0.0 + self.lat_accel_friction_factor = 0.7 + def _torque_space(self, lateral_accel, CS, gravity_adjusted): return self.torque_from_lateral_accel_in_torque_space( LatControlInputs(lateral_accel, self._roll_g, CS.vEgo, CS.aEgo), @@ -391,6 +426,8 @@ class NeuralNetworkFeedForward(PilotLateralBrain): self.lac_torque.torque_params) def update_output_torque(self, CS): + if not np.isfinite((self._pid_log.error, self._ff)).all(): + raise ValueError("Nonfinite NNFF torque request") freeze_integrator = self._capped or CS.steeringPressed or CS.vEgo < 5 self._out_tq = self._pid.update(self._pid_log.error, feedforward=self._ff, speed=CS.vEgo, freeze_integrator=freeze_integrator) @@ -413,7 +450,13 @@ class NeuralNetworkFeedForward(PilotLateralBrain): def _query(self, lead_scalar, jerk_scalar, tail): head = [self._v, lead_scalar, jerk_scalar, self._roll] - return self.model.evaluate(head + tail) + return self._evaluate(head + tail) + + def _evaluate(self, inputs): + output = self.model.evaluate(inputs) + if not np.isfinite(output): + raise ValueError("Nonfinite NNFF model output") + return output def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None: if not self._nnff_enabled: @@ -449,7 +492,7 @@ class NeuralNetworkFeedForward(PilotLateralBrain): blend = float(np.interp(abs(self._want_la), _ERROR_BLEND_BP, _ERROR_BLEND_V)) if blend <= 0.0: return - from_error = self.model.evaluate([self._v, self._accel_goal - self._accel_obs, + from_error = self._evaluate([self._v, self._accel_goal - self._accel_obs, self.jerk_goal - self.jerk_obs, 0.0]) live = self._pid_log.error if sign(live) == sign(from_error) and abs(live) < abs(from_error): @@ -512,6 +555,7 @@ class LatControlTorque(LatControl): p_gains = [gain * 0.8 for gain in KP_INTERP] if self.hkg_reduced_torque_feedback else KP_INTERP self.friction_scale = 0.7 if self.hkg_reduced_torque_feedback else 1.0 self.pid = PIDController([INTERP_SPEEDS, p_gains], KI, rate=1/self.dt) + self._nnff_active = False self.update_limits() self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt) @@ -531,10 +575,20 @@ class LatControlTorque(LatControl): self.update_limits() def update_limits(self): - self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params), - self.lateral_accel_from_torque(-self.steer_max, self.torque_params)) + if self._nnff_active: + self.pid.set_limits(self.steer_max, -self.steer_max) + else: + self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params), + self.lateral_accel_from_torque(-self.steer_max, self.torque_params)) def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay): + nnff_active = active and self.nnff_assist._nnff_enabled + if nnff_active != self._nnff_active: + # The shared PID operates in torque units for NNFF and acceleration units otherwise. + self.pid.reset() + self.nnff_assist.reset_history() + self._nnff_active = nnff_active + self.update_limits() pid_log = log.ControlsState.LateralTorqueState.new_message() pid_log.version = VERSION measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) @@ -573,13 +627,31 @@ class LatControlTorque(LatControl): else: pid_log.error = float(error) - freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 - output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator) - output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params) + output_torque = 0.0 + if not nnff_active: + freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 + output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator) + output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params) - pid_log, output_torque = self.nnff_assist.update(CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, - future_desired_lateral_accel, measurement, lateral_accel_deadzone, gravity_adjusted_future_lateral_accel, - limited_curvature, measured_curvature, steer_limited_by_safety, output_torque) + try: + pid_log, output_torque = self.nnff_assist.update( + CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, + future_desired_lateral_accel, measurement, lateral_accel_deadzone, gravity_adjusted_future_lateral_accel, + limited_curvature, measured_curvature, steer_limited_by_safety, output_torque) + except (ValueError, FloatingPointError): + if not nnff_active: + raise + cloudlog.exception("NNFF inference failed; using standard torque control for this drive") + self.nnff_assist.has_nn_model = False + self.nnff_assist.reset_history() + self._nnff_active = False + self.pid.reset() + self.update_limits() + pid_log.error = float(error) + freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 + output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator) + output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params) + output_torque = self.nnff_assist.nav_torque_pulse.nudge_output_torque(True, CS, output_torque) pid_log.active = True pid_log.p = float(self.pid.p) diff --git a/iqpilot/selfdrive/controls/lib/latcontrol_torque_pq.py b/iqpilot/selfdrive/controls/lib/latcontrol_torque_pq.py index 2193ca1e..119b571d 100644 --- a/iqpilot/selfdrive/controls/lib/latcontrol_torque_pq.py +++ b/iqpilot/selfdrive/controls/lib/latcontrol_torque_pq.py @@ -17,6 +17,8 @@ from iqpilot.common.pid import PIDController FRICTION_THRESHOLD_PQ = 1.0 KP = 0.8 KI = 0.15 +KI_HIGHWAY = 0.3 +KI_SPEEDS = [10.0, 15.0] INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30] KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP] @@ -30,7 +32,11 @@ VERSION = 1 DEFAULT_LAT_ACCEL_FACTOR = 2.2 DEFAULT_LAT_ACCEL_OFFSET = -0.13 DEFAULT_FRICTION = 0.1 -FREEZE_LIVE_TORQUE_PARAMS = True +FREEZE_LIVE_TORQUE_PARAMS = False +LIVE_LAT_ACCEL_FACTOR_GAIN = 1.1 +LIVE_LAT_ACCEL_FACTOR_BOUNDS = (1.2, 3.5) +LIVE_LAT_ACCEL_OFFSET_BOUNDS = (-0.3, 0.3) +LIVE_TORQUE_PARAMS_TAU_S = 5.0 ASSIST_COMPENSATION = True ASSIST_SPEEDS_KPH = [0.0, 50.0, 120.0] @@ -56,7 +62,10 @@ class LatControlTorquePQ(LatControl): self.torque_params.friction = DEFAULT_FRICTION self.torque_from_lateral_accel = CI.torque_from_lateral_accel() self.lateral_accel_from_torque = CI.lateral_accel_from_torque() - self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, rate=1/self.dt) + self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], [KI_SPEEDS, [KI, KI_HIGHWAY]], rate=1/self.dt) + self.live_torque_valid = False + self.lat_accel_factor_filter = FirstOrderFilter(DEFAULT_LAT_ACCEL_FACTOR, LIVE_TORQUE_PARAMS_TAU_S, self.dt) + self.lat_accel_offset_filter = FirstOrderFilter(DEFAULT_LAT_ACCEL_OFFSET, LIVE_TORQUE_PARAMS_TAU_S, self.dt) self.update_limits() self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt) @@ -65,12 +74,20 @@ class LatControlTorquePQ(LatControl): self.jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) self.lateral_acceleration_slew_limiter = LateralAccelerationSlewLimiter(Params().get_bool("IQLateralAccelSlew")) + def set_live_torque_valid(self, valid): + self.live_torque_valid = bool(valid) + def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction): if FREEZE_LIVE_TORQUE_PARAMS: return - self.torque_params.latAccelFactor = latAccelFactor - self.torque_params.latAccelOffset = latAccelOffset - self.torque_params.friction = friction + if self.live_torque_valid and math.isfinite(latAccelFactor) and math.isfinite(latAccelOffset): + target_factor = float(np.clip(latAccelFactor * LIVE_LAT_ACCEL_FACTOR_GAIN, *LIVE_LAT_ACCEL_FACTOR_BOUNDS)) + target_offset = float(np.clip(latAccelOffset, *LIVE_LAT_ACCEL_OFFSET_BOUNDS)) + else: + target_factor = DEFAULT_LAT_ACCEL_FACTOR + target_offset = DEFAULT_LAT_ACCEL_OFFSET + self.torque_params.latAccelFactor = self.lat_accel_factor_filter.update(target_factor) + self.torque_params.latAccelOffset = self.lat_accel_offset_filter.update(target_offset) self.update_limits() def update_limits(self): diff --git a/iqpilot/selfdrive/controls/lib/longcontrol.py b/iqpilot/selfdrive/controls/lib/longcontrol.py index 942fa37e..04635a15 100644 --- a/iqpilot/selfdrive/controls/lib/longcontrol.py +++ b/iqpilot/selfdrive/controls/lib/longcontrol.py @@ -5,7 +5,7 @@ from iqpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from iqpilot.common.filter_simple import FirstOrderFilter from iqpilot.common.pid import PIDController from iqpilot.selfdrive.iqmodeld.config import ModelConstants -from iqpilot.selfdrive.controls.lib.smooth_stops import SmoothStopController +from iqpilot.selfdrive.controls.lib.smooth_stops import SMOOTH_STOP_MAX_SPEED, SmoothStopController CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] @@ -83,7 +83,7 @@ class LongControl: self.smooth.reset() else: - if self.smooth.enabled and active and should_stop: + if self.smooth.enabled and active and should_stop and CS.vEgo <= SMOOTH_STOP_MAX_SPEED: output_accel = self.smooth.settle(a_target, CS.vEgo, lead_distance, has_lead, self.last_output_accel) self.reset() else: diff --git a/iqpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/iqpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index db75dfc9..1c2e6489 100755 --- a/iqpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/iqpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -193,7 +193,7 @@ class LongitudinalMpc: try: return self._params.get_bool("newLeadMpc") except UnknownKeyName: - return True + return False def reset(self): self.solver.reset() diff --git a/iqpilot/selfdrive/controls/lib/longitudinal_planner.py b/iqpilot/selfdrive/controls/lib/longitudinal_planner.py index f33c4b16..4015188d 100755 --- a/iqpilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/iqpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -25,6 +25,7 @@ A_CRUISE_MAX_VALS = [2.0, 1.6, 0.8, 0.6] A_CRUISE_MAX_BP = [0., 10.0, 25., 40.] A_CRUISE_MIN = -1.2 J_CRUISE = 1.0 +CRUISE_STOP_SPEED = 1.0 CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] ALLOW_THROTTLE_THRESHOLD = 0.4 MIN_ALLOW_THROTTLE_SPEED = 2.5 @@ -77,7 +78,7 @@ def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt, target_accel = np.clip(v_cruise - v_ego, A_CRUISE_MIN, max_accel) target_accel = float(np.clip(target_accel, a_cruise_prev - J_CRUISE * dt, a_cruise_prev + J_CRUISE * dt)) - cruise_should_stop = v_cruise == 0.0 + cruise_should_stop = v_cruise == 0.0 and v_ego < CRUISE_STOP_SPEED return target_accel, cruise_should_stop @@ -130,10 +131,6 @@ class LongitudinalPlanner(LongitudinalPlannerIQ): self.launch_armed = False self.prev_e2e = False self.mode_blend_timer = 0.0 - try: - self.exp_speed_conv = Params().get_bool("expSpeedConv") - except UnknownKeyName: - self.exp_speed_conv = False try: distance_control_enabled = Params().get_bool("IQE2EDistanceControl") except UnknownKeyName: @@ -221,11 +218,10 @@ class LongitudinalPlanner(LongitudinalPlannerIQ): output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX, action_t=action_t, stopping_speed=self.stopping_speed) - accel_boost = self.accel_boost.update(sm['selfdriveState'].enabled, v_ego, sm['carState'].gasPressed) - output_a_target_e2e = sm['modelV2'].action.desiredAcceleration + accel_boost + output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_should_stop_e2e = sm['modelV2'].action.shouldStop output_a_target_e2e, output_should_stop_e2e = self.apply_e2e_stop_distance(sm, v_ego, output_a_target_e2e, output_should_stop_e2e) - if self.is_e2e(sm) and self.exp_speed_conv and not self.mpc.status: + if self.is_e2e(sm) and not self.mpc.status: output_a_target_e2e = get_e2e_accel(v_ego, v_cruise, model_v, output_a_target_e2e, output_should_stop_e2e) if sm['carState'].standstill: @@ -247,6 +243,10 @@ class LongitudinalPlanner(LongitudinalPlannerIQ): steer_angle_without_offset, self.CP, self.dt, accel_coast, self.allow_throttle) + model_limited = e2e and output_a_target_e2e < min(output_a_target_mpc, self.a_cruise) - 0.1 + self.accel_boost.update(sm['selfdriveState'].enabled, v_ego, sm['carState'].gasPressed, model_limited) + output_a_target_e2e = self.accel_boost.apply(output_a_target_e2e) + if self.distance_control.enabled: lateral_accel = v_ego ** 2 * steer_angle_without_offset * CV.DEG_TO_RAD / (self.CP.steerRatio * self.CP.wheelbase) else: diff --git a/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/README.md b/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/README.md new file mode 100644 index 00000000..d112d2d3 --- /dev/null +++ b/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/README.md @@ -0,0 +1,34 @@ +# Neural network feedforward (NNFF) + +Open **Settings > Vehicle > NNFF Steering** while offroad (C3/C3X or comma 4). The card shows the matched model or the reason NNFF is unavailable. Changes are saved immediately and apply when the driving processes next initialize. NNFF defaults off and its selection survives manager restarts. + +NNFF is available for the generic torque controller when a matching model is installed. Angle/curvature steering, PID steering, and the separate Volkswagen PQ/MQB controllers are not supported by this toggle. Enabling NNFF disables reduced steering feedback; enabling reduced steering feedback disables NNFF. A saved NNFF selection can always be switched off while offroad, including after its model becomes unavailable. + +## Model files + +Model files belong in `iqpilot/iqpilot_iq_nnff_models/neural_network_lateral_control/`, relative to the checkout root. This change does not distribute or download trained weights. An installation without models shows **no matching model installed**. + +Models use an 18-input, one-output network with finite normalization values, positive standard deviations, and compatible dense layers. Selection accepts the identified vehicle family or an explicit entry in IQDBC's `car/torque_data/substitute.toml`. Similar names from unrelated vehicle families are not selected. A filename match and passing loader tests do not establish vehicle calibration or steering performance. + +## Runtime behavior + +The torque controller receives valid, fresh `modelV2` data and the same lateral delay used for its steering update. NNFF requires complete finite orientation and acceleration traces. It updates the shared PID once per cycle in torque units. Standard control uses acceleration units. Switching paths resets the integral and NNFF history and restores the appropriate bounds. + +Missing, malformed, or stale inputs use standard torque control. An invalid model file or nonfinite inference output also falls back; an inference failure disables that model for the current drive. Stored user preference is preserved. + +## Focused verification + +```sh +python -m pytest -q \ + iqpilot/common/tests/test_nnff_params.py \ + iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests \ + iqpilot/selfdrive/controls/tests/test_nnff_controls.py \ + iqpilot/selfdrive/controls/tests/test_lateral_action_selection.py \ + iqpilot/selfdrive/controls/tests/test_lat_jerk_lowpass.py \ + iqpilot/selfdrive/controls/tests/test_hkg_reduced_torque_feedback.py \ + iqpilot/selfdrive/ui/tests/test_nnff_settings.py \ + iqpilot/selfdrive/ui/tests/test_nnff_big_settings.py \ + iqpilot/selfdrive/ui/tests/test_hkg_feedback_settings.py +``` + +The tests use isolated parameters. The model-loader suite uses synthetic weights when trained models are absent. These checks cover software behavior and do not constitute an on-road validation of any trained model. diff --git a/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_locator.py b/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_locator.py index c8bf2860..97fadbcc 100644 --- a/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_locator.py +++ b/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_locator.py @@ -62,13 +62,25 @@ def test_fingerprint_plus_eps_fw_prefers_specific_file(model_dir): assert exact is False -def test_fuzzy_match_flags_non_exact(model_dir): - # close but not identical to a shipped fingerprint +def test_unmapped_fingerprint_does_not_select_similar_vehicle(model_dir): path, name, exact = locator.get_nn_model_path(make_cp("TOYOTA_RAV4_TSS2_XYZ")) - assert name == "TOYOTA_RAV4_TSS2" + assert name == "MOCK" + assert path == locator.MOCK_MODEL_PATH assert exact is False +@pytest.mark.parametrize("fingerprint", ["UNKNOWN", "HONDA", "HONDA_CIVIC_OTHER", "TOYOTA_RAV4"]) +def test_unmatched_vehicle_uses_mock(model_dir, fingerprint): + path, name, exact = locator.get_nn_model_path(make_cp(fingerprint)) + assert (path, name, exact) == (locator.MOCK_MODEL_PATH, "MOCK", False) + + +def test_missing_substitute_does_not_select_unrelated_vehicle(model_dir): + (model_dir / "TOYOTA_RAV4_TSS2.json").unlink() + path, name, exact = locator.get_nn_model_path(make_cp("CHEVROLET_XX")) + assert (path, name, exact) == (locator.MOCK_MODEL_PATH, "MOCK", False) + + def test_substitute_fallback(model_dir): # unknown fingerprint that the substitute table redirects path, name, exact = locator.get_nn_model_path(make_cp("CHEVROLET_XX")) diff --git a/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_network.py b/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_network.py index d09b6471..958df7a4 100644 --- a/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_network.py +++ b/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_network.py @@ -5,6 +5,7 @@ Unit checks for the NNFF model loader against the shipped model files. """ import json import os +from copy import deepcopy import numpy as np import pytest @@ -82,10 +83,11 @@ class TestModelBehavior: assert m.evaluate(vec) == m.evaluate(list(vec)) -def test_activation_registry_rejects_unknown(tmp_path): +@pytest.mark.parametrize("activation", ["not_a_real_activation", None, 1, []]) +def test_activation_registry_rejects_unknown(tmp_path, activation): with open(_path(SAMPLE[0])) as model_file: base = json.load(model_file) - base["layers"][-1]["activation"] = "not_a_real_activation" + base["layers"][-1]["activation"] = activation bad = tmp_path / "bad.json" bad.write_text(json.dumps(base)) with pytest.raises(ValueError): @@ -96,3 +98,24 @@ def test_sigmoid_identity_helpers(): assert NNTorqueModel.identity(3.5) == 3.5 assert 0.0 < float(NNTorqueModel.sigmoid(np.array([0.0]))[0]) < 1.0 assert abs(float(NNTorqueModel.sigmoid(np.array([0.0]))[0]) - 0.5) < 1e-6 + + +@pytest.mark.parametrize("invalid", ["zero_std", "nan_mean", "nan_weight", "bias_shape", "layer_shape", "output_shape"]) +def test_invalid_model_rejected(tmp_path, invalid): + model = deepcopy(_SYNTHETIC_MODEL) + if invalid == "zero_std": + model["input_std"][0][0] = 0.0 + elif invalid == "nan_mean": + model["input_mean"][0][0] = float("nan") + elif invalid == "nan_weight": + model["layers"][0]["dense_1_W"][0][0] = float("nan") + elif invalid == "bias_shape": + model["layers"][0]["dense_1_b"] = [[0.0]] + elif invalid == "layer_shape": + model["layers"][0]["dense_1_W"] = [[0.0]] + else: + model["output_size"] = 2 + path = tmp_path / "invalid.json" + path.write_text(json.dumps(model)) + with pytest.raises(ValueError): + NNTorqueModel(path) diff --git a/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_nnff_assembly.py b/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_nnff_assembly.py index fa2f9403..6e0711bc 100644 --- a/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_nnff_assembly.py +++ b/iqpilot/selfdrive/controls/lib/neural_network_feed_forward/tests/test_nnff_assembly.py @@ -78,10 +78,7 @@ class TestControllerWiring: def test_mock_model_reports_absent(self): nnff = _make_controller("MOCK.json") assert nnff.has_nn_model is False - if "MOCK.json" in _NAMES: - assert nnff.model.input_size >= 2 - else: - assert nnff.model is None + assert nnff.model is None def test_update_returns_finite_torque(self): nnff = _make_controller(_REAL_MODEL) @@ -101,3 +98,11 @@ class TestControllerWiring: nnff = _make_controller(_REAL_MODEL) nnff.model_valid = False assert nnff._nnff_enabled is False + + def test_invalid_model_falls_back(self, tmp_path): + bad_model = tmp_path / "malformed.json" + bad_model.write_text("{}") + nnff = _make_controller(str(bad_model)) + assert not nnff.has_nn_model + assert nnff.model is None + assert not nnff._nnff_enabled diff --git a/iqpilot/selfdrive/controls/lib/smooth_stops.py b/iqpilot/selfdrive/controls/lib/smooth_stops.py index d1e1ef44..a0dd79fd 100644 --- a/iqpilot/selfdrive/controls/lib/smooth_stops.py +++ b/iqpilot/selfdrive/controls/lib/smooth_stops.py @@ -18,6 +18,7 @@ PROGRESS_EPS = 0.02 ANTI_CREEP_RATE = 0.50 SETTLE_JERK = 2.5 EMERGENCY_DECEL = 3.0 +SMOOTH_STOP_MAX_SPEED = 3.0 def read_smooth_stops_enabled(params: Params) -> bool: diff --git a/iqpilot/selfdrive/controls/lib/tests/test_lane_change_cancel.py b/iqpilot/selfdrive/controls/lib/tests/test_lane_change_cancel.py deleted file mode 100644 index aa6b5d0d..00000000 --- a/iqpilot/selfdrive/controls/lib/tests/test_lane_change_cancel.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos -""" -from iqpilot.cereal import log -from iqpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LANE_CHANGE_CANCEL_TORQUE_FRAMES -from iqpilot.selfdrive.controls.lib.helpers.lane_change import AutoLaneChangeMode - -LaneChangeDirection = log.LaneChangeDirection - - -class CarState: - def __init__(self, vEgo=15.0, leftBlinker=False, rightBlinker=False, steeringPressed=False, steeringTorque=0): - self.vEgo = vEgo - self.leftBlinker = leftBlinker - self.rightBlinker = rightBlinker - self.leftBlindspot = False - self.rightBlindspot = False - self.steeringPressed = steeringPressed - self.steeringTorque = steeringTorque - self.brakePressed = False - self.yawRate = 0.0 - - -def helper(): - dh = DesireHelper() - dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE - dh.nav_exit._read_enabled = lambda: False - return dh - - -def run(dh, cs, frames, lane_change_prob=1.0): - for _ in range(frames): - dh.update(cs, True, lane_change_prob) - return dh - - -def started_right(): - dh = helper() - run(dh, CarState(), 2) - run(dh, CarState(rightBlinker=True, steeringPressed=True, steeringTorque=-240), 3) - assert dh.lane_change_state == LaneChangeState.laneChangeStarting - assert dh.lane_change_direction == LaneChangeDirection.right - return dh - - -def test_blinker_released_mid_change_keeps_the_change(): - dh = run(started_right(), CarState(), 10) - assert dh.lane_change_state == LaneChangeState.laneChangeStarting - assert dh.desire == log.Desire.laneChangeRight - - -def test_opposite_blinker_cancels_immediately(): - dh = run(started_right(), CarState(leftBlinker=True), 1) - assert dh.lane_change_state == LaneChangeState.off - assert dh.lane_change_direction == LaneChangeDirection.none - assert dh.desire == log.Desire.none - - -def test_sustained_opposite_torque_cancels(): - dh = run(started_right(), CarState(steeringPressed=True, steeringTorque=150), LANE_CHANGE_CANCEL_TORQUE_FRAMES - 1) - assert dh.lane_change_state == LaneChangeState.laneChangeStarting - run(dh, CarState(steeringPressed=True, steeringTorque=150), 1) - assert dh.lane_change_state == LaneChangeState.off - assert dh.desire == log.Desire.none - - -def test_brief_opposite_touch_does_not_cancel(): - dh = run(started_right(), CarState(steeringPressed=True, steeringTorque=150), LANE_CHANGE_CANCEL_TORQUE_FRAMES - 1) - run(dh, CarState(), 5) - assert dh.lane_change_state == LaneChangeState.laneChangeStarting - - -def test_same_direction_torque_does_not_cancel(): - dh = run(started_right(), CarState(steeringPressed=True, steeringTorque=-200), 20) - assert dh.lane_change_state == LaneChangeState.laneChangeStarting - - -def test_cancel_during_finishing(): - dh = run(started_right(), CarState(rightBlinker=True), 12, lane_change_prob=0.0) - assert dh.lane_change_state == LaneChangeState.laneChangeFinishing - run(dh, CarState(leftBlinker=True), 1) - assert dh.lane_change_state == LaneChangeState.off - - -def test_left_signal_after_torque_cancel_starts_a_left_change(): - dh = run(started_right(), CarState(), 3) - run(dh, CarState(steeringPressed=True, steeringTorque=160), LANE_CHANGE_CANCEL_TORQUE_FRAMES) - assert dh.lane_change_state == LaneChangeState.off - run(dh, CarState(leftBlinker=True, steeringPressed=True, steeringTorque=160), 3) - assert dh.lane_change_direction == LaneChangeDirection.left - assert dh.lane_change_state == LaneChangeState.laneChangeStarting diff --git a/iqpilot/selfdrive/controls/lib/tests/test_lateral_edge_guard.py b/iqpilot/selfdrive/controls/lib/tests/test_lateral_edge_guard.py index 97a42b4e..c917b8b2 100644 --- a/iqpilot/selfdrive/controls/lib/tests/test_lateral_edge_guard.py +++ b/iqpilot/selfdrive/controls/lib/tests/test_lateral_edge_guard.py @@ -10,8 +10,6 @@ import math from iqpilot.cereal import custom, log import iqpilot.cereal.messaging as messaging from iqpilot.common.realtime import DT_MDL -from iqpilot.selfdrive.controls.lib.desire_helper import DesireHelper -from iqpilot.selfdrive.controls.lib.helpers.lane_change import AutoLaneChangeMode from iqpilot.selfdrive.controls.lib.helpers.lateral_edge_guard import ( ADJACENT_LANE_LINE_PROB, BLOCK_DEBOUNCE_S, @@ -196,23 +194,6 @@ def test_measured_lane_width_is_clamped_and_falls_back() -> None: assert LateralEdgeGuard._measured_lane_width(lane_model(4.0, ego_width_m=3.2)) == 3.2 -def test_desire_helper_blocks_only_when_edge_guard_is_enabled() -> None: - helper = DesireHelper() - helper.lateral_edge_guard = LateralEdgeGuard(enabled=True) - helper.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE - helper.lane_change_state = log.LaneChangeState.preLaneChange - helper.lane_change_direction = log.LaneChangeDirection.left - update_for(helper.lateral_edge_guard, edge_model(4.0), BLOCK_DEBOUNCE_S) - helper.update(CarState(), True, 1.0, modeldata=edge_model(4.0)) - assert helper.lateral_edge_block == custom.IQLateralEdgeBlock.left - assert helper.lane_change_state == log.LaneChangeState.preLaneChange - - helper.lateral_edge_guard = LateralEdgeGuard(enabled=False) - helper.update(CarState(), True, 1.0, modeldata=edge_model(4.0)) - assert helper.lateral_edge_block == custom.IQLateralEdgeBlock.none - assert helper.lane_change_state == log.LaneChangeState.laneChangeStarting - - def test_published_edge_block_maps_to_distinct_event_and_alert() -> None: message = messaging.new_message("iqDriveModelData") message.iqDriveModelData.lateralEdgeBlock = custom.IQLateralEdgeBlock.right diff --git a/iqpilot/selfdrive/controls/lib/tests/test_nav_exit_lane_change.py b/iqpilot/selfdrive/controls/lib/tests/test_nav_exit_lane_change.py deleted file mode 100644 index 278f7070..00000000 --- a/iqpilot/selfdrive/controls/lib/tests/test_nav_exit_lane_change.py +++ /dev/null @@ -1,183 +0,0 @@ -""" -Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos -""" -from iqpilot.cereal import custom, log -from iqpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState -from iqpilot.selfdrive.controls.lib.helpers.lane_change import AutoLaneChangeMode - -ManeuverType = custom.IQNavState.ManeuverType -NavDirection = custom.NavDirection -LaneChangeDirection = log.LaneChangeDirection - - -class DummyCarState: - def __init__(self, vEgo=25.0, leftBlinker=False, rightBlinker=False, leftBlindspot=False, rightBlindspot=False, - steeringPressed=False, steeringTorque=0, brakePressed=False): - self.vEgo = vEgo - self.leftBlinker = leftBlinker - self.rightBlinker = rightBlinker - self.leftBlindspot = leftBlindspot - self.rightBlindspot = rightBlindspot - self.steeringPressed = steeringPressed - self.steeringTorque = steeringTorque - self.brakePressed = brakePressed - - -class DummyNavState: - def __init__(self, active=True, nextManeuverValid=True, nextManeuverType=int(ManeuverType.exit), - nextManeuverDistance=300.0, nextManeuverDirection=int(NavDirection.right)): - self.active = active - self.nextManeuverValid = nextManeuverValid - self.nextManeuverType = nextManeuverType - self.nextManeuverDistance = nextManeuverDistance - self.nextManeuverDirection = nextManeuverDirection - - -def _make_dh(enabled: bool, enable_bsm: bool): - dh = DesireHelper() - dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE - dh.nav_exit._read_enabled = lambda: enabled # bypass the (unregistered) param in tests - dh.nav_exit._enable_bsm = enable_bsm - return dh - - -def _run(dh, carstate, nav_state, n=20): - for _ in range(n): - dh.update(carstate, True, 1.0, nav_state) - return dh.desire - - -def test_feature_off_no_exit_lane_change(): - dh = _make_dh(enabled=False, enable_bsm=True) - cs = DummyCarState(rightBlindspot=False) - assert _run(dh, cs, DummyNavState()) == log.Desire.none - - -def test_no_bsm_requires_nudge_holds_without_one(): - # No blindspot monitor: nav exit must NOT auto-start; without a nudge it stays in preLaneChange. - dh = _make_dh(enabled=True, enable_bsm=False) - cs = DummyCarState(steeringPressed=False) - assert _run(dh, cs, DummyNavState()) == log.Desire.none - assert dh.lane_change_state == LaneChangeState.preLaneChange - assert dh.lane_change_direction == LaneChangeDirection.right - - -def test_no_bsm_starts_on_driver_nudge(): - # Driver nudges the wheel toward the exit (right -> negative torque) -> lane change starts. - dh = _make_dh(enabled=True, enable_bsm=False) - cs = DummyCarState(steeringPressed=True, steeringTorque=-1) - assert _run(dh, cs, DummyNavState()) == log.Desire.laneChangeRight - - -def test_bsm_auto_starts_when_clear(): - dh = _make_dh(enabled=True, enable_bsm=True) - cs = DummyCarState(rightBlindspot=False) - assert _run(dh, cs, DummyNavState()) == log.Desire.laneChangeRight - - -def test_bsm_holds_when_blindspot_occupied(): - dh = _make_dh(enabled=True, enable_bsm=True) - cs = DummyCarState(rightBlindspot=True) - assert _run(dh, cs, DummyNavState()) == log.Desire.none - - -def test_only_exit_maneuvers_trigger(): - # A turn maneuver (not an exit) must not trigger the exit lane change. - dh = _make_dh(enabled=True, enable_bsm=True) - cs = DummyCarState(rightBlindspot=False) - nav = DummyNavState(nextManeuverType=int(ManeuverType.turn)) - assert _run(dh, cs, nav) == log.Desire.none - - -def test_too_far_does_not_trigger(): - dh = _make_dh(enabled=True, enable_bsm=True) - cs = DummyCarState(rightBlindspot=False) - nav = DummyNavState(nextManeuverDistance=900.0) - assert _run(dh, cs, nav) == log.Desire.none - - -class RequestedNavState(DummyNavState): - def __init__(self, direction=int(NavDirection.right), **kwargs): - super().__init__(nextManeuverType=int(ManeuverType.continueStraight), nextManeuverDistance=120.0, **kwargs) - self.shouldSendLaneChangeDesire = True - self.navLaneChangeDesireDirection = direction - - -def test_requested_lane_change_waits_for_nudge_even_with_bsm(): - dh = _make_dh(enabled=True, enable_bsm=True) - cs = DummyCarState(rightBlindspot=False, steeringPressed=False) - assert _run(dh, cs, RequestedNavState()) == log.Desire.none - assert dh.lane_change_state == LaneChangeState.preLaneChange - assert dh.lane_change_direction == LaneChangeDirection.right - - -def test_requested_lane_change_starts_on_matching_nudge(): - dh = _make_dh(enabled=True, enable_bsm=False) - cs = DummyCarState(steeringPressed=True, steeringTorque=-1) - assert _run(dh, cs, RequestedNavState()) == log.Desire.laneChangeRight - - -def test_requested_lane_change_ignores_opposite_nudge(): - dh = _make_dh(enabled=True, enable_bsm=False) - cs = DummyCarState(steeringPressed=True, steeringTorque=1) - assert _run(dh, cs, RequestedNavState()) == log.Desire.none - - -def test_requested_lane_change_blocked_by_blindspot(): - dh = _make_dh(enabled=True, enable_bsm=True) - cs = DummyCarState(rightBlindspot=True, steeringPressed=True, steeringTorque=-1) - assert _run(dh, cs, RequestedNavState()) == log.Desire.none - - -def test_requested_lane_change_needs_feature_enabled(): - dh = _make_dh(enabled=False, enable_bsm=False) - cs = DummyCarState(steeringPressed=True, steeringTorque=-1) - assert _run(dh, cs, RequestedNavState()) == log.Desire.none - - -def test_requested_lane_change_never_below_lane_change_speed(): - dh = _make_dh(enabled=True, enable_bsm=False) - cs = DummyCarState(vEgo=7.0, steeringPressed=True, steeringTorque=-1) - assert _run(dh, cs, RequestedNavState()) == log.Desire.none - - -def _complete_lane_change(dh, carstate, nav_state): - for _ in range(200): - dh.update(carstate, True, 0.0, nav_state) - if dh.lane_change_state in (LaneChangeState.off, LaneChangeState.preLaneChange): - return - - -def test_exit_needing_a_second_lane_rearms_for_a_nudge(): - dh = _make_dh(enabled=True, enable_bsm=True) - cs = DummyCarState(rightBlindspot=False) - nav = DummyNavState(nextManeuverDistance=480.0) - assert _run(dh, cs, nav) == log.Desire.laneChangeRight - _complete_lane_change(dh, cs, DummyNavState(nextManeuverDistance=400.0)) - assert dh.lane_change_state == LaneChangeState.preLaneChange - assert dh.lane_change_direction == LaneChangeDirection.right - assert _run(dh, cs, DummyNavState(nextManeuverDistance=60.0), n=100) == log.Desire.none - assert dh.lane_change_state == LaneChangeState.preLaneChange - nudge = DummyCarState(steeringPressed=True, steeringTorque=-1) - assert _run(dh, nudge, DummyNavState(nextManeuverDistance=40.0)) == log.Desire.laneChangeRight - - -def test_rearm_ends_with_the_exit_and_the_next_exit_starts_fresh(): - dh = _make_dh(enabled=True, enable_bsm=True) - cs = DummyCarState(rightBlindspot=False) - assert _run(dh, cs, DummyNavState(nextManeuverDistance=480.0)) == log.Desire.laneChangeRight - _complete_lane_change(dh, cs, DummyNavState(nextManeuverDistance=400.0)) - assert dh.lane_change_state == LaneChangeState.preLaneChange - _run(dh, cs, DummyNavState(nextManeuverType=int(ManeuverType.turn), nextManeuverDistance=900.0)) - assert dh.lane_change_state == LaneChangeState.off - assert not dh.nav_exit_rearmed - assert _run(dh, cs, DummyNavState(nextManeuverDistance=450.0)) == log.Desire.laneChangeRight - - -def test_cancelled_exit_lane_change_does_not_come_back(): - dh = _make_dh(enabled=True, enable_bsm=True) - cs = DummyCarState(rightBlindspot=False) - assert _run(dh, cs, DummyNavState(nextManeuverDistance=480.0)) == log.Desire.laneChangeRight - _run(dh, DummyCarState(leftBlinker=True), DummyNavState(nextManeuverDistance=460.0), n=2) - assert _run(dh, cs, DummyNavState(nextManeuverDistance=400.0), n=60) == log.Desire.none - assert dh.lane_change_state == LaneChangeState.off diff --git a/iqpilot/selfdrive/controls/lib/tests/test_turn_desire_cycling.py b/iqpilot/selfdrive/controls/lib/tests/test_turn_desire_cycling.py deleted file mode 100644 index 7f5f8c3c..00000000 --- a/iqpilot/selfdrive/controls/lib/tests/test_turn_desire_cycling.py +++ /dev/null @@ -1,235 +0,0 @@ -from types import SimpleNamespace - -import numpy as np -import pytest - -from iqpilot.cereal import custom, log -from iqpilot.selfdrive.controls.lib.desire_helper import ( - DesireHelper, - TURN_DESIRE_MAX_SECONDS, - TURN_DESIRE_PULSE_CYCLE_FRAMES, - TURN_DESIRE_PULSE_HOLD_FRAMES, - TURN_RECOIL_HEADING_RAD, -) -from iqpilot.common.constants import CV -from iqpilot.common.realtime import DT_MDL - - -TurnDirection = custom.IQTurnSignalDirection -MODEL_HISTORY_FRAMES = 100 - - -def helper(v_ego=0.0, yaw_rate=0.0, model_yaw_rate=None): - result = DesireHelper.__new__(DesireHelper) - result._last_carstate = SimpleNamespace(vEgo=v_ego, yawRate=yaw_rate) - result._last_modeldata = (None if model_yaw_rate is None - else SimpleNamespace(orientationRate=SimpleNamespace(z=[model_yaw_rate]))) - result.turn_desire_frames = 0 - result.turn_desire_cycle_input = log.Desire.none - result.turn_heading_rad = 0.0 - result.turn_desire_seconds = 0.0 - result.turn_recoiled = False - result.nav_turn_direction = TurnDirection.none - result.lane_turn_direction = TurnDirection.none - result.lane_change_direction = log.LaneChangeDirection.none - result.lane_change_state = log.LaneChangeState.off - result.desire = log.Desire.none - return result - - -def visible_pulses(outputs): - previous = log.Desire.none - rising = [] - for output in outputs: - rising.append(output != log.Desire.none and previous != output) - previous = output - rising = np.array(rising) - return np.array([rising[max(0, i - MODEL_HISTORY_FRAMES + 1):i + 1].sum() for i in range(len(rising))]) - - -@pytest.mark.parametrize("source", ["manual", "nav"]) -@pytest.mark.parametrize("v_ego,yaw_rate", [(0.0, 0.0), (6.0, 0.0), (4.0, 0.02)]) -def test_turn_desire_keeps_exactly_one_pulse_in_model_history(source, v_ego, yaw_rate): - h = helper(v_ego, yaw_rate) - if source == "manual": - h.lane_turn_direction = TurnDirection.turnLeft - else: - h.nav_turn_direction = TurnDirection.turnLeft - - outputs = [] - for _ in range(4 * TURN_DESIRE_PULSE_CYCLE_FRAMES): - h._pick_desire_output() - outputs.append(h.desire) - - gaps = [i for i, output in enumerate(outputs) if output == log.Desire.none] - assert gaps[0] == TURN_DESIRE_PULSE_HOLD_FRAMES - assert np.diff(gaps).tolist() == [TURN_DESIRE_PULSE_CYCLE_FRAMES] * (len(gaps) - 1) - assert set(visible_pulses(outputs)[MODEL_HISTORY_FRAMES:].tolist()) == {1} - - -def test_turn_desire_recoils_once_the_turn_has_been_driven(): - yaw_rate = 0.4 - h = helper(v_ego=6.0, yaw_rate=yaw_rate) - h.lane_turn_direction = TurnDirection.turnLeft - - frames_to_recoil = int(TURN_RECOIL_HEADING_RAD / (yaw_rate * DT_MDL)) - outputs = [] - for _ in range(frames_to_recoil + 4 * TURN_DESIRE_PULSE_CYCLE_FRAMES): - h._pick_desire_output() - outputs.append(h.desire) - - assert log.Desire.turnLeft in outputs[:TURN_DESIRE_PULSE_HOLD_FRAMES] - assert h.turn_recoiled - assert all(output == log.Desire.none for output in outputs[frames_to_recoil + 1:]) - - -def test_recoil_holds_through_a_blinker_that_stays_on(): - h = helper(v_ego=6.0, yaw_rate=0.6) - h.lane_turn_direction = TurnDirection.turnRight - for _ in range(TURN_DESIRE_PULSE_CYCLE_FRAMES): - h._pick_desire_output() - assert h.turn_recoiled - - h._last_carstate = SimpleNamespace(vEgo=6.0, yawRate=0.0) - outputs = [] - for _ in range(3 * TURN_DESIRE_PULSE_CYCLE_FRAMES): - h._pick_desire_output() - outputs.append(h.desire) - assert all(output == log.Desire.none for output in outputs) - - -def test_recoil_clears_when_the_turn_request_drops(): - h = helper(v_ego=6.0, yaw_rate=0.6) - h.lane_turn_direction = TurnDirection.turnRight - for _ in range(TURN_DESIRE_PULSE_CYCLE_FRAMES): - h._pick_desire_output() - assert h.turn_recoiled - - h.lane_turn_direction = TurnDirection.none - h._pick_desire_output() - assert not h.turn_recoiled - assert h.turn_heading_rad == 0.0 - - h.lane_turn_direction = TurnDirection.turnRight - h._last_carstate = SimpleNamespace(vEgo=6.0, yawRate=0.0) - h._pick_desire_output() - assert h.desire == log.Desire.turnRight - - -def test_turn_desire_gives_up_when_the_blinker_outlives_the_maneuver(): - h = helper(v_ego=6.0, yaw_rate=0.0) - h.lane_turn_direction = TurnDirection.turnLeft - for _ in range(int(TURN_DESIRE_MAX_SECONDS / DT_MDL) + 1): - h._pick_desire_output() - assert h.turn_recoiled - assert h.desire == log.Desire.none - - -def test_new_turn_direction_restarts_pulse_immediately(): - h = helper() - for _ in range(TURN_DESIRE_PULSE_HOLD_FRAMES // 2): - assert h._pulse_turn_desire(log.Desire.turnLeft) == log.Desire.turnLeft - assert h._pulse_turn_desire(log.Desire.turnRight) == log.Desire.turnRight - assert h.turn_desire_cycle_input == log.Desire.turnRight - assert h.turn_desire_frames == 1 - - -def test_lane_change_desires_pass_through_and_reset_pulse(): - h = helper() - h._pulse_turn_desire(log.Desire.turnLeft) - assert h._pulse_turn_desire(log.Desire.laneChangeLeft) == log.Desire.laneChangeLeft - assert h.turn_desire_frames == 0 - assert h.turn_desire_cycle_input == log.Desire.none - assert not h.turn_recoiled - - -def test_turn_speed_gate_follows_the_param_past_the_old_ceiling(monkeypatch): - from iqpilot.selfdrive.controls.lib.helpers import lane_turn - - class FakeParams: - def get(self, *_a, **_k): - return "30.0" - - def get_bool(self, *_a, **_k): - return True - - monkeypatch.setattr(lane_turn, "Params", FakeParams) - planner = lane_turn.TurnSignalPlanner(None) - assert planner.speed_gate == pytest.approx(30.0 * CV.MPH_TO_MS) - - planner.sample(blink_l=True, speed_mps=28.0 * CV.MPH_TO_MS) - assert planner.output() == TurnDirection.turnLeft - - planner.sample(blink_l=True, speed_mps=31.0 * CV.MPH_TO_MS) - assert planner.output() == TurnDirection.none - - -def test_turn_speed_gate_is_capped_at_the_ceiling(monkeypatch): - from iqpilot.selfdrive.controls.lib.helpers import lane_turn - - class FakeParams: - def get(self, *_a, **_k): - return "90.0" - - def get_bool(self, *_a, **_k): - return True - - monkeypatch.setattr(lane_turn, "Params", FakeParams) - planner = lane_turn.TurnSignalPlanner(None) - assert planner.speed_gate == pytest.approx(lane_turn.TURN_SPEED_CEILING_MPS) - - -def test_the_turn_intent_stays_alive_through_the_maneuver(): - h = helper(v_ego=6.0, yaw_rate=0.15) - h.lane_turn_direction = TurnDirection.turnLeft - outputs = [] - for _ in range(int(TURN_RECOIL_HEADING_RAD / (0.15 * DT_MDL)) - 1): - h._pick_desire_output() - outputs.append(h.desire) - - assert not h.turn_recoiled - assert set(visible_pulses(outputs)[MODEL_HISTORY_FRAMES:].tolist()) == {1} - - -def test_recoil_uses_the_model_pose_when_the_car_reports_no_yaw_rate(): - # every frame of 00000007--1984111bdd carries yawRate 0.0; the port never fills it in - h = helper(v_ego=6.0, yaw_rate=0.0, model_yaw_rate=0.5) - h.lane_turn_direction = TurnDirection.turnLeft - for _ in range(int(TURN_RECOIL_HEADING_RAD / (0.5 * DT_MDL)) + 2): - h._pick_desire_output() - assert h.turn_recoiled - assert h.desire == log.Desire.none - - -def test_recoil_falls_back_to_carstate_yaw_rate(): - h = helper(v_ego=6.0, yaw_rate=0.5) - h.lane_turn_direction = TurnDirection.turnLeft - for _ in range(int(TURN_RECOIL_HEADING_RAD / (0.5 * DT_MDL)) + 2): - h._pick_desire_output() - assert h.turn_recoiled - - -def test_turn_desire_survives_a_long_red_light_at_the_junction(): - h = helper(v_ego=0.0, yaw_rate=0.0) - h.nav_turn_direction = TurnDirection.turnLeft - for _ in range(int(2 * TURN_DESIRE_MAX_SECONDS / DT_MDL)): - h._pick_desire_output() - assert not h.turn_recoiled - h._last_carstate = SimpleNamespace(vEgo=3.0, yawRate=0.0) - outputs = [] - for _ in range(int(3.0 / DT_MDL)): - h._pick_desire_output() - outputs.append(h.desire) - assert not h.turn_recoiled - assert log.Desire.turnLeft in outputs - - -def test_moving_time_still_ends_a_desire_that_outlives_the_maneuver(): - h = helper(v_ego=0.0, yaw_rate=0.0) - h.lane_turn_direction = TurnDirection.turnLeft - for _ in range(int(10.0 / DT_MDL)): - h._pick_desire_output() - h._last_carstate = SimpleNamespace(vEgo=6.0, yawRate=0.0) - for _ in range(int(TURN_DESIRE_MAX_SECONDS / DT_MDL) + 1): - h._pick_desire_output() - assert h.turn_recoiled diff --git a/iqpilot/selfdrive/controls/lib/tests/test_turn_speed_gate.py b/iqpilot/selfdrive/controls/lib/tests/test_turn_speed_gate.py new file mode 100644 index 00000000..6b04882d --- /dev/null +++ b/iqpilot/selfdrive/controls/lib/tests/test_turn_speed_gate.py @@ -0,0 +1,28 @@ +# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +import pytest + +from iqpilot.cereal import custom +from iqpilot.common.constants import CV + +TurnDirection = custom.IQTurnSignalDirection + + +def test_turn_speed_gate_is_fixed_at_twenty_mph_whatever_the_param_says(monkeypatch): + from iqpilot.selfdrive.controls.lib.helpers import lane_turn + + class FakeParams: + def get(self, *_a, **_k): + return "30.0" + + def get_bool(self, *_a, **_k): + return True + + monkeypatch.setattr(lane_turn, "Params", FakeParams) + planner = lane_turn.TurnSignalPlanner(None) + assert planner.speed_gate == pytest.approx(20.0 * CV.MPH_TO_MS) + + planner.sample(blink_l=True, speed_mps=19.0 * CV.MPH_TO_MS) + assert planner.output() == TurnDirection.turnLeft + + planner.sample(blink_l=True, speed_mps=21.0 * CV.MPH_TO_MS) + assert planner.output() == TurnDirection.none diff --git a/iqpilot/selfdrive/controls/tests/fixtures/lateral_lookahead_off.npz b/iqpilot/selfdrive/controls/tests/fixtures/lateral_lookahead_off.npz index 91964c08..7f3a137d 100644 Binary files a/iqpilot/selfdrive/controls/tests/fixtures/lateral_lookahead_off.npz and b/iqpilot/selfdrive/controls/tests/fixtures/lateral_lookahead_off.npz differ diff --git a/iqpilot/selfdrive/controls/tests/lateral_control_scenarios.py b/iqpilot/selfdrive/controls/tests/lateral_control_scenarios.py index ff0bf440..5659ad16 100644 --- a/iqpilot/selfdrive/controls/tests/lateral_control_scenarios.py +++ b/iqpilot/selfdrive/controls/tests/lateral_control_scenarios.py @@ -57,7 +57,7 @@ class ScenarioMessages(dict): self.alive = defaultdict(bool) def all_checks(self, services): - return False + return all(self.valid[service] and self.alive[service] for service in services) class ScenarioNetwork: @@ -147,7 +147,8 @@ def set_frame(controls, frame): controls.sm["modelV2"].orientation.x = (0.003 * np.sin(times)).tolist() controls.sm["modelV2"].orientation.y = (0.002 * np.cos(times)).tolist() controls.sm["modelV2"].acceleration.y = (0.1 * np.sin(times)).tolist() - nnff.update_model_v2(controls.sm["modelV2"]) + controls.sm.valid["modelV2"] = True + controls.sm.alive["modelV2"] = True controls.steer_limited_by_safety = 140 <= frame < 160 diff --git a/iqpilot/selfdrive/controls/tests/test_accel_boost.py b/iqpilot/selfdrive/controls/tests/test_accel_boost.py index 028d8859..d65c8534 100644 --- a/iqpilot/selfdrive/controls/tests/test_accel_boost.py +++ b/iqpilot/selfdrive/controls/tests/test_accel_boost.py @@ -5,42 +5,152 @@ from pathlib import Path import pytest from iqpilot.common.realtime import DT_MDL -from iqpilot.selfdrive.controls.lib.accel_boost import ACCEL_BOOST_MAX, ACCEL_BOOST_MIN_SPEED, ACCEL_BOOST_RATE, AccelBoost +from iqpilot.selfdrive.controls.lib.accel_boost import ( + ACCEL_BOOST_DECAY_RATE, ACCEL_BOOST_MAX, ACCEL_BOOST_MIN_SPEED, ACCEL_BOOST_PER_OVERRIDE, ACCEL_BOOST_RATE, AccelBoost, +) FAST = ACCEL_BOOST_MIN_SPEED + 5.0 SLOW = ACCEL_BOOST_MIN_SPEED - 1.0 -def run(boost, seconds, engaged=True, v_ego=FAST, gas=True): +def run(boost, seconds, engaged=True, v_ego=FAST, gas=True, model_limited=True): for _ in range(round(seconds / DT_MDL)): - boost.update(engaged, v_ego, gas) + boost.update(engaged, v_ego, gas, model_limited) return boost.value +def eligible_boost(): + boost = AccelBoost(True, DT_MDL) + boost.update(True, FAST, False, True) + return boost + + def test_param_is_off_by_default(): keys = (Path(__file__).parents[3] / "common" / "params_keys.h").read_text() assert re.search(r'\{"IQGasOverrideBoost", \{PERSISTENT, BOOL, "0"\}\}', keys) def test_disabled_never_boosts(): - assert run(AccelBoost(False, DT_MDL), 30.0) == 0.0 + boost = AccelBoost(False, DT_MDL) + run(boost, DT_MDL, gas=False) + assert run(boost, 30.0) == 0.0 + assert boost.apply(0.5) == 0.5 -def test_gas_ramps_to_one_and_holds(): +def test_gas_ramps_to_per_override_limit_and_holds(): + boost = eligible_boost() + assert run(boost, 1.0) == pytest.approx(0.025, abs=1e-6) + assert ACCEL_BOOST_RATE == 0.025 + assert ACCEL_BOOST_PER_OVERRIDE == 0.05 + assert run(boost, 15.0) == pytest.approx(0.05) + assert run(boost, 5.0, gas=False) == pytest.approx(0.05) + assert boost.override_boost == 0.0 + + +def test_repeated_overrides_reach_total_limit(): + boost = eligible_boost() + for _ in range(6): + run(boost, 10.0) + run(boost, DT_MDL, gas=False) + assert ACCEL_BOOST_MAX == 0.2 + assert boost.value == pytest.approx(0.2) + + +def test_eligibility_is_latched_before_gas_override(): + boost = eligible_boost() + assert run(boost, 1.0, model_limited=False) == pytest.approx(0.025) + run(boost, DT_MDL, gas=False, model_limited=False) + assert run(boost, 10.0, model_limited=True) == pytest.approx(0.025) + run(boost, DT_MDL, gas=False, model_limited=True) + assert run(boost, 1.0, model_limited=False) == pytest.approx(0.05) + + +def test_engaging_with_gas_pressed_does_not_latch_eligibility(): boost = AccelBoost(True, DT_MDL) - assert run(boost, 1.0) == pytest.approx(ACCEL_BOOST_RATE, abs=1e-6) - assert ACCEL_BOOST_MAX == 1.0 - assert run(boost, 15.0) == pytest.approx(1.0) - assert run(boost, 5.0, gas=False) == pytest.approx(1.0) + assert run(boost, 10.0) == 0.0 def test_bleeds_off_below_ten_mph_and_resets_on_disengage(): - boost = AccelBoost(True, DT_MDL) + boost = eligible_boost() run(boost, 5.0) - assert run(boost, 2.0, v_ego=SLOW) == pytest.approx(0.3, abs=1e-6) + assert ACCEL_BOOST_DECAY_RATE == 0.1 + assert run(boost, 0.2, v_ego=SLOW) == pytest.approx(0.03, abs=1e-6) assert run(boost, 5.0, v_ego=SLOW) == 0.0 + assert run(boost, 5.0) == 0.0 + run(boost, DT_MDL, gas=False) run(boost, 5.0) assert run(boost, DT_MDL, engaged=False) == 0.0 + assert boost.override_boost == 0.0 + assert boost.model_limited is False + assert run(boost, 5.0) == 0.0 + + +@pytest.mark.parametrize("accel,applied", [ + (-2.0, -2.0), (-1.0, -1.0), (-0.75, -0.725), (-0.5, -0.45), + (0.0, 0.05), (5.0, 5.05), (5.01, 5.01), +]) +def test_boost_fades_with_model_braking(accel, applied): + boost = eligible_boost() + run(boost, 5.0) + assert boost.apply(accel) == pytest.approx(applied) + + +@pytest.mark.parametrize("e2e,a_model,a_mpc,a_cruise,eligible", [ + (True, 0.0, 0.5, 0.5, True), + (False, 0.0, 0.5, 0.5, False), + (True, 0.0, 0.05, 0.5, False), + (True, 0.0, 0.5, 0.05, False), + (True, 0.0, 0.1, 0.5, False), +]) +def test_planner_latches_model_limit_after_cruise_update(monkeypatch, e2e, a_model, a_mpc, a_cruise, eligible): + from types import SimpleNamespace + import numpy as np + + from iqpilot.selfdrive.controls.lib import longitudinal_planner as lp + from iqpilot.selfdrive.controls.lib.longcontrol import LongCtrlState + + monkeypatch.setattr(lp.LongitudinalPlannerIQ, "update", lambda *args: None) + monkeypatch.setattr(lp.LongitudinalPlannerIQ, "update_targets", lambda self, sm, v_ego, v_cruise: v_cruise) + monkeypatch.setattr(lp, "get_accel_from_plan", lambda *args, **kwargs: (a_mpc, False)) + monkeypatch.setattr(lp, "get_cruise_accel", lambda *args: (a_cruise, False)) + planner = object.__new__(lp.LongitudinalPlanner) + planner.CP = SimpleNamespace(openpilotLongitudinalControl=True, longitudinalActuatorDelay=0.1) + planner.dt = DT_MDL + planner.v_desired_filter = SimpleNamespace(x=FAST, update=lambda v: v) + planner.a_desired = 0.0 + planner.a_cruise = -1.0 + planner.stopping_speed = 0.5 + planner.exp_speed_conv = False + planner.launch_armed = False + planner.forcing_stop = False + planner.prev_e2e = e2e + planner.mode_blend_timer = 0.0 + planner.accel_boost = AccelBoost(True, DT_MDL) + planner.distance_control = SimpleNamespace(enabled=False, update=lambda *args, **kwargs: kwargs["a_model"]) + planner.is_e2e = lambda sm: e2e + planner.apply_e2e_stop_distance = lambda sm, v, a, stop: (a, stop) + planner.parse_model = lambda model: (None, np.full(len(lp.T_IDXS_MPC), FAST), np.zeros(len(lp.T_IDXS_MPC)), None, 1.0) + planner.mpc = SimpleNamespace( + status=True, crash_cnt=0, source=lp.LongitudinalPlanSource.lead0, + v_solution=np.full(len(lp.T_IDXS_MPC), FAST), a_solution=np.zeros(len(lp.T_IDXS_MPC)), + j_solution=np.zeros(len(lp.T_IDXS_MPC) - 1), set_weights=lambda **kwargs: None, + set_cur_state=lambda *args: None, update=lambda *args, **kwargs: None, + ) + sm = { + "carControl": SimpleNamespace(orientationNED=[], longActive=True), + "carState": SimpleNamespace(vEgo=FAST, vCruise=100.0, aEgo=0.0, steeringAngleDeg=0.0, standstill=False, gasPressed=False), + "controlsState": SimpleNamespace(forceDecel=False, longControlState=LongCtrlState.pid), + "selfdriveState": SimpleNamespace(enabled=True, personality=0), + "vehicleParameters": SimpleNamespace(angleOffsetDeg=0.0), + "modelV2": SimpleNamespace(action=SimpleNamespace(desiredAcceleration=a_model, shouldStop=False)), + "radarState": SimpleNamespace(), + } + planner.update(sm) + assert planner.accel_boost.model_limited is eligible + sm["carState"].gasPressed = True + planner.update(sm) + assert planner.accel_boost.value == pytest.approx(ACCEL_BOOST_RATE * DT_MDL if eligible else 0.0) + assert planner.output_a_target == pytest.approx(min(a_mpc, a_cruise, planner.accel_boost.apply(a_model)) if e2e else min(a_mpc, a_cruise)) def test_boost_bar_only_replaces_the_confidence_ball_with_the_param_on(monkeypatch): diff --git a/iqpilot/selfdrive/controls/tests/test_lat_jerk_lowpass.py b/iqpilot/selfdrive/controls/tests/test_lat_jerk_lowpass.py index aa847ce4..e06ff8ec 100644 --- a/iqpilot/selfdrive/controls/tests/test_lat_jerk_lowpass.py +++ b/iqpilot/selfdrive/controls/tests/test_lat_jerk_lowpass.py @@ -34,6 +34,7 @@ def _model(rng): m = log.ModelDataV2.new_message() m.acceleration.y = (slope * np.arange(n) * 0.1).tolist() m.orientation.x = [0.0] * n + m.orientation.y = [0.0] * n return m diff --git a/iqpilot/selfdrive/controls/tests/test_latcontrol.py b/iqpilot/selfdrive/controls/tests/test_latcontrol.py index 65556521..d74a9a25 100644 --- a/iqpilot/selfdrive/controls/tests/test_latcontrol.py +++ b/iqpilot/selfdrive/controls/tests/test_latcontrol.py @@ -128,14 +128,59 @@ class TestLatControl: assert not lac_log.active assert controller.lateral_acceleration_slew_limiter.a_lim == 0.9 - def test_pq_live_torque_update_freeze_and_unfreeze(self, monkeypatch): - _, controller = self.build_pq_controller() - initial = (controller.torque_params.latAccelFactor, controller.torque_params.latAccelOffset, controller.torque_params.friction) - controller.update_live_torque_params(3.0, 0.2, 0.4) - assert (controller.torque_params.latAccelFactor, controller.torque_params.latAccelOffset, controller.torque_params.friction) == initial + @staticmethod + def settle_pq_live_torque(controller, factor, offset, valid, seconds=60.0): + controller.set_live_torque_valid(valid) + for _ in range(int(seconds / DT_CTRL)): + controller.update_live_torque_params(factor, offset, 0.4) - monkeypatch.setattr(latcontrol_torque_pq, "FREEZE_LIVE_TORQUE_PARAMS", False) + def test_pq_live_torque_falls_back_until_torqued_is_valid(self): + _, controller = self.build_pq_controller() + self.settle_pq_live_torque(controller, 3.0, 0.2, valid=False) + assert abs(controller.torque_params.latAccelFactor - latcontrol_torque_pq.DEFAULT_LAT_ACCEL_FACTOR) < 1e-6 + assert abs(controller.torque_params.latAccelOffset - latcontrol_torque_pq.DEFAULT_LAT_ACCEL_OFFSET) < 1e-6 + assert abs(controller.torque_params.friction - latcontrol_torque_pq.DEFAULT_FRICTION) < 1e-6 + + def test_pq_live_torque_uses_learned_factor_and_offset(self): + _, controller = self.build_pq_controller() + self.settle_pq_live_torque(controller, 2.35, 0.047, valid=True) + expected_factor = 2.35 * latcontrol_torque_pq.LIVE_LAT_ACCEL_FACTOR_GAIN + assert abs(controller.torque_params.latAccelFactor - expected_factor) < 1e-3 + assert abs(controller.torque_params.latAccelOffset - 0.047) < 1e-3 + assert abs(controller.torque_params.friction - latcontrol_torque_pq.DEFAULT_FRICTION) < 1e-6 + + def test_pq_live_torque_changes_are_smoothed(self): + _, controller = self.build_pq_controller() + controller.set_live_torque_valid(True) controller.update_live_torque_params(3.0, 0.2, 0.4) - assert controller.torque_params.latAccelFactor == 3.0 - assert abs(controller.torque_params.latAccelOffset - 0.2) < 1e-6 - assert abs(controller.torque_params.friction - 0.4) < 1e-6 + step = controller.torque_params.latAccelFactor - latcontrol_torque_pq.DEFAULT_LAT_ACCEL_FACTOR + assert 0.0 < step < 0.01 + + def test_pq_live_torque_is_bounded(self): + _, controller = self.build_pq_controller() + self.settle_pq_live_torque(controller, 10.0, 2.0, valid=True) + assert abs(controller.torque_params.latAccelFactor - latcontrol_torque_pq.LIVE_LAT_ACCEL_FACTOR_BOUNDS[1]) < 1e-3 + assert abs(controller.torque_params.latAccelOffset - latcontrol_torque_pq.LIVE_LAT_ACCEL_OFFSET_BOUNDS[1]) < 1e-3 + self.settle_pq_live_torque(controller, 0.1, -2.0, valid=True) + assert abs(controller.torque_params.latAccelFactor - latcontrol_torque_pq.LIVE_LAT_ACCEL_FACTOR_BOUNDS[0]) < 1e-3 + assert abs(controller.torque_params.latAccelOffset - latcontrol_torque_pq.LIVE_LAT_ACCEL_OFFSET_BOUNDS[0]) < 1e-3 + + def test_pq_live_torque_rejects_non_finite_estimates(self): + _, controller = self.build_pq_controller() + self.settle_pq_live_torque(controller, float('nan'), 0.2, valid=True) + assert abs(controller.torque_params.latAccelFactor - latcontrol_torque_pq.DEFAULT_LAT_ACCEL_FACTOR) < 1e-6 + assert abs(controller.torque_params.latAccelOffset - latcontrol_torque_pq.DEFAULT_LAT_ACCEL_OFFSET) < 1e-6 + + def test_pq_live_torque_freeze_keeps_defaults(self, monkeypatch): + monkeypatch.setattr(latcontrol_torque_pq, "FREEZE_LIVE_TORQUE_PARAMS", True) + _, controller = self.build_pq_controller() + self.settle_pq_live_torque(controller, 3.0, 0.2, valid=True, seconds=1.0) + assert abs(controller.torque_params.latAccelFactor - latcontrol_torque_pq.DEFAULT_LAT_ACCEL_FACTOR) < 1e-6 + assert abs(controller.torque_params.latAccelOffset - latcontrol_torque_pq.DEFAULT_LAT_ACCEL_OFFSET) < 1e-6 + + def test_pq_integrator_gain_rises_at_highway_speed(self): + _, controller = self.build_pq_controller() + controller.pid.speed = 5.0 + assert abs(controller.pid.k_i - latcontrol_torque_pq.KI) < 1e-9 + controller.pid.speed = 30.0 + assert abs(controller.pid.k_i - latcontrol_torque_pq.KI_HIGHWAY) < 1e-9 diff --git a/iqpilot/selfdrive/controls/tests/test_lateral_action_selection.py b/iqpilot/selfdrive/controls/tests/test_lateral_action_selection.py index d85bffe9..aac98776 100644 --- a/iqpilot/selfdrive/controls/tests/test_lateral_action_selection.py +++ b/iqpilot/selfdrive/controls/tests/test_lateral_action_selection.py @@ -35,12 +35,12 @@ def test_toggle_off_matches_pre_fix_outputs(controls_module, case, slew): @pytest.mark.parametrize("case", ["torque", "tss2", "gm"]) @pytest.mark.parametrize("slew", [False, True]) -def test_toggle_off_preserves_active_nnff_outputs(controls_module, case, slew): +def test_unknown_model_cannot_replace_action_with_nnff(controls_module, case, slew): controls = build_controls(controls_module, case, lookahead=False, slew=slew, nnff=True) actual = run_scenario(controls) assert controls.LaC.nnff_assist.model.queries > 0 - with np.load(Path(__file__).with_name("fixtures") / "lateral_lookahead_off.npz") as baseline: - np.testing.assert_allclose(actual, baseline[f"{case}_slew{int(slew)}_nnff"], rtol=1e-12, atol=1e-12) + enabled = build_controls(controls_module, case, lookahead=True, slew=slew, nnff=True) + np.testing.assert_array_equal(actual, run_scenario(enabled)) @pytest.mark.parametrize("case", CONTROL_CASES) diff --git a/iqpilot/selfdrive/controls/tests/test_nnff_controls.py b/iqpilot/selfdrive/controls/tests/test_nnff_controls.py new file mode 100644 index 00000000..999ac161 --- /dev/null +++ b/iqpilot/selfdrive/controls/tests/test_nnff_controls.py @@ -0,0 +1,232 @@ +import numpy as np +import pytest + +from iqpilot.selfdrive.controls.tests.lateral_control_scenarios import build_controls, load_controls_module, set_frame + + +@pytest.fixture +def controls_module(monkeypatch): + return load_controls_module(monkeypatch) + + +@pytest.mark.parametrize("case", ["torque", "tss2", "gm"]) +def test_controls_feeds_nnff_and_updates_pid_once(controls_module, monkeypatch, case): + controls = build_controls(controls_module, case, nnff=True) + nnff = controls.LaC.nnff_assist + assert not nnff.model_valid + calls = [] + lag_updates = [] + controller_delays = [] + original_update = controls.LaC.pid.update + original_lag_update = nnff.update_lateral_lag + original_control_update = controls.LaC.update + + def record_update(*args, **kwargs): + calls.append((args, kwargs)) + return original_update(*args, **kwargs) + + def record_lag_update(lag): + lag_updates.append(lag) + original_lag_update(lag) + + def record_control_update(*args): + controller_delays.append(args[-1]) + assert nnff.desired_lat_jerk_time == pytest.approx(max(0.01, args[-1])) + return original_control_update(*args) + + monkeypatch.setattr(controls.LaC.pid, "update", record_update) + monkeypatch.setattr(nnff, "update_lateral_lag", record_lag_update) + monkeypatch.setattr(controls.LaC, "update", record_control_update) + for frame in range(120, 130): + set_frame(controls, frame) + controls.sm["lateralDelay"].lateralDelay = 0.25 if frame < 125 else 0.45 + if frame == 120: + assert not nnff.model_valid # Only state_control may feed the live model. + previous_queries = nnff.model.queries + cc, _ = controls.state_control() + assert cc.latActive + assert nnff.model_valid + assert nnff.model.queries > previous_queries + assert len(calls) == frame - 119 + assert abs(cc.actuators.torque) <= controls.LaC.steer_max + assert lag_updates == controller_delays == [0.25] * 5 + [0.45] * 5 + assert nnff.nn_future_times == pytest.approx([t + nnff.desired_lat_jerk_time for t in nnff.future_times]) + + +@pytest.mark.parametrize("unavailable", [ + "disabled", "missing_nn_model", "invalid_message", "dead_message", "missing_orientation_x", + "short_orientation_y", "missing_acceleration", "nonfinite_orientation", "nonfinite_acceleration", +]) +def test_unavailable_nnff_uses_standard_controller(controls_module, unavailable): + controls = build_controls(controls_module, "torque", nnff=True) + baseline = build_controls(controls_module, "torque") + nnff = controls.LaC.nnff_assist + network = nnff.model + if unavailable == "disabled": + nnff.enabled = False + elif unavailable == "missing_nn_model": + nnff.has_nn_model = False + nnff.model = None + + for frame in range(120, 130): + set_frame(controls, frame) + set_frame(baseline, frame) + model = controls.sm["modelV2"] + if unavailable == "invalid_message": + controls.sm.valid["modelV2"] = False + elif unavailable == "dead_message": + controls.sm.alive["modelV2"] = False + elif unavailable == "missing_orientation_x": + model.orientation.x = [] + elif unavailable == "short_orientation_y": + model.orientation.y = [0.0] + elif unavailable == "missing_acceleration": + model.acceleration.y = [] + elif unavailable == "nonfinite_orientation": + model.orientation.x = [float("nan")] * len(model.orientation.x) + elif unavailable == "nonfinite_acceleration": + model.acceleration.y = [float("inf")] * len(model.acceleration.y) + actual, trace = controls.state_control() + expected, expected_trace = baseline.state_control() + assert actual.actuators.torque == expected.actuators.torque + for field in ("p", "i", "d", "f", "error", "output"): + assert getattr(trace, field) == getattr(expected_trace, field) + assert network.queries == 0 + + +@pytest.mark.parametrize("lost_service", ["valid", "alive"]) +def test_lost_live_model_stops_inference_and_restores_limits(controls_module, lost_service): + controls = build_controls(controls_module, "torque", nnff=True) + nnff = controls.LaC.nnff_assist + set_frame(controls, 120) + controls.state_control() + queries = nnff.model.queries + assert queries > 0 + assert controls.LaC.pid.pos_limit == controls.LaC.steer_max + assert controls.LaC.pid.neg_limit == -controls.LaC.steer_max + + set_frame(controls, 121) + getattr(controls.sm, lost_service)["modelV2"] = False + cc, _ = controls.state_control() + assert not nnff.model_valid + assert nnff.model.queries == queries + assert np.isfinite(cc.actuators.torque) + assert abs(cc.actuators.torque) <= controls.LaC.steer_max + assert controls.LaC.pid.pos_limit == controls.LaC.lateral_accel_from_torque(controls.LaC.steer_max, controls.LaC.torque_params) + assert controls.LaC.pid.neg_limit == controls.LaC.lateral_accel_from_torque(-controls.LaC.steer_max, controls.LaC.torque_params) + + +def test_first_live_torque_update_uses_nnff_limits(controls_module): + controls = build_controls(controls_module, "torque", nnff=True) + set_frame(controls, 120) + controls.sm.valid["lateralTorqueParameters"] = True + controls.sm.alive["lateralTorqueParameters"] = True + live_torque = controls.sm["lateralTorqueParameters"] + live_torque.useParams = True + live_torque.valid = True + live_torque.latAccelFactorFiltered = 4.0 + live_torque.latAccelOffsetFiltered = 0.05 + live_torque.frictionCoefficientFiltered = 0.12 + cc, _ = controls.state_control() + assert controls.LaC.torque_params.latAccelFactor == 4.0 + assert controls.LaC.nnff_assist.model.queries > 0 + assert controls.LaC.pid.pos_limit == controls.LaC.steer_max + assert controls.LaC.pid.neg_limit == -controls.LaC.steer_max + assert abs(cc.actuators.torque) <= controls.LaC.steer_max + + +@pytest.mark.parametrize("output", [float("nan"), float("inf"), -float("inf")]) +def test_nonfinite_inference_falls_back_before_pid_update(controls_module, monkeypatch, output): + controls = build_controls(controls_module, "torque", nnff=True) + baseline = build_controls(controls_module, "torque") + monkeypatch.setattr(controls.LaC.nnff_assist.model, "evaluate", lambda inputs: output) + calls = [] + original_update = controls.LaC.pid.update + + def record_update(*args, **kwargs): + calls.append((args, kwargs)) + return original_update(*args, **kwargs) + + monkeypatch.setattr(controls.LaC.pid, "update", record_update) + for frame in range(120, 125): + set_frame(controls, frame) + set_frame(baseline, frame) + actual, _ = controls.state_control() + expected, _ = baseline.state_control() + assert actual.actuators.torque == expected.actuators.torque + assert np.isfinite(controls.LaC.pid.i) + assert len(calls) == frame - 119 + assert not controls.LaC.nnff_assist.has_nn_model + + +def test_nnff_recovers_after_invalid_model_without_reusing_integral_or_history(controls_module, monkeypatch): + controls = build_controls(controls_module, "torque", nnff=True) + nnff = controls.LaC.nnff_assist + pid = controls.LaC.pid + integral_before_update = [] + original_update = pid.update + + def record_update(*args, **kwargs): + integral_before_update.append(pid.i) + return original_update(*args, **kwargs) + + monkeypatch.setattr(pid, "update", record_update) + previous_queries = 0 + for valid, frames, factor in ((True, 20, 2.7), (False, 30, 4.0), (True, 5, 3.2)): + for frame in range(frames): + set_frame(controls, 200) + state = controls.sm["carState"] + state.vEgo = 20.0 + state.steeringAngleDeg = 0.15 + state.steeringRateDeg = 0.0 + state.steeringPressed = False + controls.sm["vehicleParameters"].roll = 0.0 + controls.sm["lateralDelay"].lateralDelay = 0.12 + controls.sm.valid["modelV2"] = valid + model = controls.sm["modelV2"] + model.action.desiredCurvature = 0.0001 + model.orientation.x = [0.0] * len(model.orientation.x) + model.orientation.y = [0.0] * len(model.orientation.y) + model.acceleration.y = [0.04] * len(model.acceleration.y) + controls.sm.valid["lateralTorqueParameters"] = True + controls.sm.alive["lateralTorqueParameters"] = True + live_torque = controls.sm["lateralTorqueParameters"] + live_torque.useParams = True + live_torque.valid = True + live_torque.latAccelFactorFiltered = factor + live_torque.latAccelOffsetFiltered = 0.0 + live_torque.frictionCoefficientFiltered = 0.12 + + cc, _ = controls.state_control() + assert cc.latActive + assert nnff.model_valid == valid + assert controls.LaC.torque_params.latAccelFactor == pytest.approx(factor) + assert np.isfinite(cc.actuators.torque) + assert abs(cc.actuators.torque) <= controls.LaC.steer_max + if frame == 0: + assert integral_before_update[-1] == 0.0 + if valid: + assert nnff.model.queries > previous_queries + assert pid.pos_limit == controls.LaC.steer_max + assert pid.neg_limit == -controls.LaC.steer_max + assert len(nnff._window.roll) == frame + 1 + assert len(nnff._window.lat_accel_desired) == frame + 1 + else: + assert nnff.model.queries == previous_queries + assert pid.pos_limit == controls.LaC.lateral_accel_from_torque(controls.LaC.steer_max, controls.LaC.torque_params) + assert pid.neg_limit == controls.LaC.lateral_accel_from_torque(-controls.LaC.steer_max, controls.LaC.torque_params) + assert len(nnff._window.roll) == 0 + assert len(nnff._window.lat_accel_desired) == 0 + previous_queries = nnff.model.queries + assert pid.i != 0.0 # Each transition must discard an integral accumulated in the previous mode. + + +@pytest.mark.parametrize("case", ["pq", "mqb_v0", "pid", "angle", "curvature"]) +def test_non_nnff_controllers_accept_live_model_updates(controls_module, case): + controls = build_controls(controls_module, case) + assert not hasattr(controls.LaC, "nnff_assist") + set_frame(controls, 120) + cc, _ = controls.state_control() + assert cc.latActive + assert np.isfinite(cc.actuators.torque) + assert np.isfinite(cc.actuators.steeringAngleDeg) diff --git a/iqpilot/selfdrive/controls/tests/test_soft_disable_decel.py b/iqpilot/selfdrive/controls/tests/test_soft_disable_decel.py new file mode 100644 index 00000000..731a9574 --- /dev/null +++ b/iqpilot/selfdrive/controls/tests/test_soft_disable_decel.py @@ -0,0 +1,73 @@ +# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +from types import SimpleNamespace + +import pytest + +from iqpilot.common.realtime import DT_MDL +from iqpilot.selfdrive.controls.lib.longcontrol import LongControl, LongCtrlState +from iqpilot.selfdrive.controls.lib.longitudinal_planner import CRUISE_STOP_SPEED, J_CRUISE, get_cruise_accel +from iqpilot.selfdrive.controls.lib.smooth_stops import SMOOTH_STOP_MAX_SPEED + +ACCEL_LIMITS = (-3.5, 2.0) +PLANNER_CP = SimpleNamespace(steerRatio=15.3, wheelbase=2.64) + + +def make_control(monkeypatch): + cp = SimpleNamespace( + carFingerprint="VOLKSWAGEN_GOLF_MK7", + stopAccel=-0.55, + longitudinalTuning=SimpleNamespace(kpBP=[0.], kpV=[0.1], kiBP=[0.], kiV=[0.0]), + ) + cp_iq = SimpleNamespace(stoppingDecelRateOverride=0.0, enableGasInterceptor=False) + control = LongControl(cp, cp_iq) + monkeypatch.setattr(control.smooth, "read_params", lambda: None) + control.smooth.enabled = True + return control + + +def car_state(v_ego): + return SimpleNamespace(vEgo=v_ego, aEgo=0.0, brakePressed=False, standstill=False, + cruiseState=SimpleNamespace(standstill=False)) + + +@pytest.mark.parametrize("e2e", [False, True]) +def test_forced_decel_at_speed_ramps_without_requesting_a_stop(e2e): + accel, should_stop = get_cruise_accel(e2e, 0.0, 30.8, 0.0, 0.0, PLANNER_CP, DT_MDL, -0.3, True) + assert should_stop is False + assert accel >= -J_CRUISE * DT_MDL - 1e-6 + + +@pytest.mark.parametrize("e2e", [False, True]) +def test_forced_decel_requests_a_stop_once_nearly_stopped(e2e): + _, should_stop = get_cruise_accel(e2e, 0.0, CRUISE_STOP_SPEED / 2, -1.2, 0.0, PLANNER_CP, DT_MDL, -0.3, True) + assert should_stop is True + + +def test_smooth_stop_never_brakes_hard_at_highway_speed(monkeypatch): + control = make_control(monkeypatch) + control.long_control_state = LongCtrlState.pid + for _ in range(10): + accel = control.update(True, car_state(30.8), -0.39, True, ACCEL_LIMITS, lead_distance=40.0, has_lead=True, + v_target_now=30.8) + assert accel > -1.0 + + +def test_smooth_stop_still_settles_at_stopping_speed(monkeypatch): + control = make_control(monkeypatch) + control.long_control_state = LongCtrlState.pid + calls = [] + monkeypatch.setattr(control.smooth, "settle", lambda *args: calls.append(args) or -0.4) + v_ego = SMOOTH_STOP_MAX_SPEED / 3 + accel = control.update(True, car_state(v_ego), -0.5, True, ACCEL_LIMITS, lead_distance=0.0, has_lead=False, + v_target_now=v_ego) + assert calls and accel == -0.4 + + +def test_smooth_stop_is_skipped_above_its_speed_range(monkeypatch): + control = make_control(monkeypatch) + control.long_control_state = LongCtrlState.pid + calls = [] + monkeypatch.setattr(control.smooth, "settle", lambda *args: calls.append(args) or -3.5) + control.update(True, car_state(SMOOTH_STOP_MAX_SPEED + 1.0), -0.5, True, ACCEL_LIMITS, lead_distance=40.0, + has_lead=True, v_target_now=SMOOTH_STOP_MAX_SPEED + 1.0) + assert calls == [] diff --git a/iqpilot/selfdrive/iqmodeld/aux_inputs.py b/iqpilot/selfdrive/iqmodeld/aux_inputs.py new file mode 100644 index 00000000..28b599ad --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/aux_inputs.py @@ -0,0 +1,17 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.context.aux_inputs") +except ProprietaryModuleMissing: + class AuxModelInputs: + def __init__(self, input_spec: dict): + self.active = False + + def poll(self) -> None: + pass + + def extras(self) -> dict: + return {} diff --git a/iqpilot/selfdrive/iqmodeld/daemon.py b/iqpilot/selfdrive/iqmodeld/daemon.py index e8c1a9ee..104b102a 100755 --- a/iqpilot/selfdrive/iqmodeld/daemon.py +++ b/iqpilot/selfdrive/iqmodeld/daemon.py @@ -23,7 +23,7 @@ from iqpilot.common.realtime import DT_MDL, config_realtime_process from iqpilot.common.swaglog import cloudlog from iqpilot.common.transformations.camera import DEVICE_CAMERAS from iqpilot.common.transformations.model import get_warp_matrix -from iqpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from iqpilot.selfdrive.controls.lib.action_engine import ActionEngine from iqpilot.selfdrive.controls.lib.drive_helpers import ( MODEL_SMOOTHING_MAX_TOTAL_SEC, dynamic_lat_smooth_extra_seconds, @@ -34,7 +34,7 @@ from iqpilot.selfdrive.locationd.calibration_helpers import get_calibrated_rpy from iqpilot.system import sentry from iqpilot.common.steer_delay import lateral_action_delay -from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle +from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle, load_default_model_session, forced_active_bundle from iqpilot.selfdrive.iqmodeld.models.inference_state import InferenceStateBase from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import get_model_runner from iqpilot.selfdrive.iqmodeld.camera import CameraOffsetHelper @@ -243,13 +243,30 @@ def _merged_plan(runtime_state: "NeuralEngineState", outputs: dict[str, np.ndarr return base_plan + (runtime_state.PLANPLUS_CONTROL * _planplus_gain(vehicle_speed)) * outputs["planplus"][0] +def _set_default_fallback_alert(active: bool) -> None: + try: + from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert + set_offroad_alert("Offroad_ModelFallbackDefault", active) + except Exception: + cloudlog.exception("could not update the default-model fallback alert") + + class NeuralEngineState(InferenceStateBase): frames: dict[str, RoadProjector] def __init__(self, gpu_context: WarpContext): super().__init__() - runner = get_model_runner() - bundle = get_active_bundle() + try: + runner = get_model_runner() + bundle = get_active_bundle() + _set_default_fallback_alert(False) + except Exception: + cloudlog.exception("active model failed to load; running the default model this session so pose stays valid") + default_bundle = load_default_model_session() + with forced_active_bundle(default_bundle): + runner = get_model_runner() + bundle = get_active_bundle() + _set_default_fallback_alert(True) self.model_runner = runner self.constants = runner.constants @@ -354,7 +371,10 @@ class NeuralEngineState(InferenceStateBase): def get_action_from_model(self, outputs: dict[str, np.ndarray], previous_action: log.ModelDataV2.Action, lat_action_t: float, long_action_t: float, vehicle_speed: float, - lat_smooth_seconds: float | None = None) -> log.ModelDataV2.Action: + lat_smooth_seconds: float | None = None, *, + lat_active: bool = True, current_curvature: float = 0.0) -> log.ModelDataV2.Action: + if not lat_active: + previous_action.desiredCurvature = current_curvature if lat_smooth_seconds is None: lat_smooth_seconds = self.LAT_SMOOTH_SECONDS @@ -568,7 +588,7 @@ class InferenceDaemon: self._pub = PubMaster(pub_services) self._sub = SubMaster([ "deviceState", "carState", "roadCameraState", "extrinsicsCalibration", - "driverMonitoringState", "carControl", "lateralDelay", "iqNavState", "navInstruction", "radarState", + "driverMonitoringState", "carControl", "controlsState", "lateralDelay", "iqNavState", "navInstruction", "radarState", ] + self._feature_inputs.services) self._message_memory = DrivePacketMemory() self._params = Params() @@ -580,7 +600,7 @@ class InferenceDaemon: self._car_params = self._load_car_params(demo) self._long_action_delay = self._car_params.longitudinalActuatorDelay + self._runtime.LONG_SMOOTH_SECONDS self._previous_action = log.ModelDataV2.Action() - self._desire_logic = DesireHelper() + self._desire_logic = ActionEngine() self._lat_smooth_extra_sec = 0.0 self._drive_profile_state = DriveProfileState() self._memory_resets = MemoryResetTracker() @@ -664,7 +684,8 @@ class InferenceDaemon: ) lat_smooth_total_sec = min(self._runtime.LAT_SMOOTH_SECONDS + self._lat_smooth_extra_sec, MODEL_SMOOTHING_MAX_TOTAL_SEC) action = self._runtime.get_action_from_model( - outputs, self._previous_action, lat_horizon, long_horizon, vehicle_speed, lat_smooth_total_sec + outputs, self._previous_action, lat_horizon, long_horizon, vehicle_speed, lat_smooth_total_sec, + lat_active=self._sub["carControl"].latActive, current_curvature=self._sub["controlsState"].curvature, ) self._previous_action = action diff --git a/iqpilot/selfdrive/iqmodeld/driving_action.py b/iqpilot/selfdrive/iqmodeld/driving_action.py index 67a38f68..d994f868 100644 --- a/iqpilot/selfdrive/iqmodeld/driving_action.py +++ b/iqpilot/selfdrive/iqmodeld/driving_action.py @@ -17,7 +17,10 @@ DESIRE_LEN = 8 def get_action_from_model(outputs: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, v_ego: float, lat_action_t: float, long_action_t: float, - lat_smooth_seconds: float | None = None) -> log.ModelDataV2.Action: + lat_smooth_seconds: float | None = None, *, + lat_active: bool = True, current_curvature: float = 0.0) -> log.ModelDataV2.Action: + if not lat_active: + prev_action.desiredCurvature = current_curvature if "action" in outputs: desired_accel = float(outputs["action"][0, 1]) desired_curvature = float(outputs["action"][0, 0]) / (max(1.0, v_ego)) ** 2 diff --git a/iqpilot/selfdrive/iqmodeld/egpu_helpers.py b/iqpilot/selfdrive/iqmodeld/egpu_helpers.py index 1749bfe0..6ba1c352 100644 --- a/iqpilot/selfdrive/iqmodeld/egpu_helpers.py +++ b/iqpilot/selfdrive/iqmodeld/egpu_helpers.py @@ -139,7 +139,18 @@ def egpu_oob_pkl_path(meta: dict) -> str: def egpu_model_oob_pkl_path(meta: dict) -> str: from iqpilot.system.hardware.hw import Paths - return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_model_oob.pkl") + return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_model_oob2.pkl") + + +def drop_frozen_model_oob(meta: dict) -> None: + from iqpilot.system.hardware.hw import Paths + # warp-on-dock bundles saved under the old name were built before the prune fix and replay capture-time feeds + stale = os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_model_oob.pkl") + try: + os.remove(stale) + cloudlog.warning(f"removed frozen-feed warp-on-dock artifact {stale}") + except OSError: + pass def onnx_cache_path(meta: dict) -> str: @@ -288,6 +299,28 @@ def patch_tinygrad_fetch_fw() -> None: helpers.fetch_fw = fetch_fw +def patch_tinygrad_prune_views() -> None: + from tinygrad.engine import jit + from tinygrad.uop.ops import Ops + if getattr(jit.prune_linear, "_iq_patched", False): + return + + # upstream prune only sees bare BUFFER args, so a kernel reading an input through a SLICE view ran once at capture and froze that input + def prune_linear(linear, needed): + kept, onetime = [], [] + for si in linear.src: + si_bufs = {b for src in si.src[1:] for b in src.toposort() if b.op is Ops.BUFFER} + if not si_bufs.isdisjoint(needed): + kept.append(si) + needed |= si_bufs + else: + onetime.append(si) + return linear.replace(src=tuple(kept)), linear.replace(src=tuple(onetime)) + + prune_linear._iq_patched = True + jit.prune_linear = prune_linear + + def patch_usb_signal_wait() -> None: import time diff --git a/iqpilot/selfdrive/iqmodeld/egpu_pipeline.py b/iqpilot/selfdrive/iqmodeld/egpu_pipeline.py index f65a9bf6..910e6eb7 100644 --- a/iqpilot/selfdrive/iqmodeld/egpu_pipeline.py +++ b/iqpilot/selfdrive/iqmodeld/egpu_pipeline.py @@ -30,8 +30,10 @@ class EgpuPipeline: self.output_len = int(meta["output_len"]) def run(self, warped: np.ndarray, desire_vec: np.ndarray, traffic_convention: np.ndarray, - action_t: np.ndarray) -> np.ndarray: - if isinstance(self.infer_fn, PolicyRunner): + action_t: np.ndarray, extras: dict | None = None) -> np.ndarray: + if self.stateful: + out = np.asarray(self.infer_fn.run(warped, desire_vec, traffic_convention, action_t, extras), dtype=np.float32).reshape(-1) + elif isinstance(self.infer_fn, PolicyRunner): out = np.asarray(self.infer_fn.run(warped, desire_vec, traffic_convention, action_t), dtype=np.float32).reshape(-1) else: inputs = self.state.push_and_materialize(warped, desire_vec, traffic_convention, action_t) @@ -42,10 +44,11 @@ class EgpuPipeline: return out def run_frames(self, main_frame, extra_frame, tfm: np.ndarray, big_tfm: np.ndarray, desire_vec: np.ndarray, - traffic_convention: np.ndarray, action_t: np.ndarray) -> np.ndarray: + traffic_convention: np.ndarray, action_t: np.ndarray, extras: dict | None = None) -> np.ndarray: if not isinstance(self.infer_fn, ModelRunner): raise EgpuPipelineError("run_frames needs a format-3 (warp-on-dock) artifact") - out = np.asarray(self.infer_fn.run(main_frame, extra_frame, tfm, big_tfm, desire_vec, traffic_convention, action_t), + args = (main_frame, extra_frame, tfm, big_tfm, desire_vec, traffic_convention, action_t) + out = np.asarray(self.infer_fn.run(*args, extras) if self.stateful else self.infer_fn.run(*args), dtype=np.float32).reshape(-1) self._check(out) return out diff --git a/iqpilot/selfdrive/iqmodeld/egpu_policy.py b/iqpilot/selfdrive/iqmodeld/egpu_policy.py index 4e83a924..6922bd54 100644 --- a/iqpilot/selfdrive/iqmodeld/egpu_policy.py +++ b/iqpilot/selfdrive/iqmodeld/egpu_policy.py @@ -19,6 +19,7 @@ STATEFUL_POLICY_FORMAT = 4 STATEFUL_MODEL_FORMAT = 5 STATEFUL_IMAGE_INPUT = "new_img" STATEFUL_OUTPUT = "outputs" +STATEFUL_BASE_FEEDS = ("desire", "traffic_convention", "action_t") OOB_MAGIC = b"IQEGPUOOB1" QUEUE_NAMES = ("img_q", "big_img_q", "feat_q", "desire_q") PACKED_ORDER = ("desire", "traffic_convention", "action_t", "prev_feat") @@ -339,6 +340,7 @@ def _state_kwargs(cur: dict, nxt: dict) -> dict: def make_run_stateful(model_runner, input_spec: dict, pairs: dict[str, str], device: str): from tinygrad.tensor import Tensor shapes, sizes = stateful_packed_layout(input_spec, pairs) + dtypes = {name: input_spec[name][1] for name in shapes} img_shape = tuple(input_spec[STATEFUL_IMAGE_INPUT][0]) def run_stateful(warped, packed_npy_inputs, **states): @@ -346,7 +348,8 @@ def make_run_stateful(model_runner, input_spec: dict, pairs: dict[str, str], dev warped = warped.to(device) Tensor.realize(packed_npy_inputs, warped) inputs = {STATEFUL_IMAGE_INPUT: warped.reshape(img_shape)} - inputs.update((name, t.reshape(shape)) for name, t, shape in zip(shapes, packed_npy_inputs.split(sizes), shapes.values(), strict=True)) + inputs.update((name, t.reshape(shape).cast(dtypes[name])) for name, t, shape in zip(shapes, packed_npy_inputs.split(sizes), shapes.values(), + strict=True)) inputs.update((name, states[name]) for name in pairs) outs = model_runner(inputs) # next state lands in the other buffer of the pair: writing the queue it is read from races inside the shift kernel @@ -363,6 +366,8 @@ class _StatefulState: self.pairs = pairs self._cur, self._nxt = make_state_buffers(input_spec, pairs, device) self._prev_desire = np.zeros(input_spec["desire"][0], dtype=np.float32) if "desire" in input_spec else None + self._extra_names = tuple(name for name in input_spec + if name not in pairs and name not in (STATEFUL_IMAGE_INPUT, *STATEFUL_BASE_FEEDS)) self.warm_frames = 0 def reset(self) -> None: @@ -380,11 +385,14 @@ class _StatefulState: self._prev_desire[:] = cur return pulse - def _fill(self, views: dict, desire_pulse, traffic_convention, action_t) -> None: + def _fill(self, views: dict, desire_pulse, traffic_convention, action_t, extras: dict | None = None) -> None: if "desire" in views: views["desire"][:] = self._pulse(desire_pulse).reshape(views["desire"].shape) views["traffic_convention"][:] = np.asarray(traffic_convention, dtype=np.float32).reshape(views["traffic_convention"].shape) views["action_t"][:] = np.asarray(action_t, dtype=np.float32).reshape(views["action_t"].shape) + for name in self._extra_names: + value = extras.get(name) if extras else None + views[name][:] = 0 if value is None else np.asarray(value, dtype=np.float32).reshape(views[name].shape) def _step(self, jit, **kw) -> np.ndarray: out, = jit(**kw, **_state_kwargs(self._cur, self._nxt)) @@ -403,8 +411,8 @@ class StatefulPolicyRunner(_StatefulState, PolicyRunner): self._init_state(input_spec, pairs, device) def run(self, warped: np.ndarray, desire_pulse: np.ndarray, traffic_convention: np.ndarray, - action_t: np.ndarray) -> np.ndarray: - self._fill(self._packed.views, desire_pulse, traffic_convention, action_t) + action_t: np.ndarray, extras: dict | None = None) -> np.ndarray: + self._fill(self._packed.views, desire_pulse, traffic_convention, action_t, extras) warped_t = self._Tensor(np.ascontiguousarray(warped, dtype=np.uint8).reshape(self._warped_shape), device="NPY").realize() return self._step(self._jit, warped=warped_t, packed_npy_inputs=self._packed.tensor) @@ -461,7 +469,7 @@ class StatefulModelRunner(_StatefulState, ModelRunner): self._init_state(input_spec, pairs, device) def run(self, main_frame, extra_frame, tfm: np.ndarray, big_tfm: np.ndarray, desire_pulse: np.ndarray, - traffic_convention: np.ndarray, action_t: np.ndarray) -> np.ndarray: + traffic_convention: np.ndarray, action_t: np.ndarray, extras: dict | None = None) -> np.ndarray: n = self.frame_copy_size v = self._packed.views f = self._packed.frames @@ -469,7 +477,7 @@ class StatefulModelRunner(_StatefulState, ModelRunner): np.copyto(f["big_img"], np.frombuffer(extra_frame, dtype=np.uint8, count=n)) v["tfm"][:, :] = tfm v["big_tfm"][:, :] = big_tfm - self._fill(v, desire_pulse, traffic_convention, action_t) + self._fill(v, desire_pulse, traffic_convention, action_t, extras) return self._step(self._jit, packed_npy_inputs=self._packed.tensor) diff --git a/iqpilot/selfdrive/iqmodeld/egpu_prefetch.py b/iqpilot/selfdrive/iqmodeld/egpu_prefetch.py index c4f5bae6..88d321bb 100644 --- a/iqpilot/selfdrive/iqmodeld/egpu_prefetch.py +++ b/iqpilot/selfdrive/iqmodeld/egpu_prefetch.py @@ -35,6 +35,7 @@ def _selected_meta(params: Params) -> dict | None: def _drop_stale_partials(keep: str) -> None: root = os.path.dirname(keep) partial_globs = ("egpu_*_amd_policy.pkl.part", "egpu_*_amd_policy_oob.pkl.part", "egpu_*_amd_model_oob.pkl.part", + "egpu_*_amd_model_oob2.pkl.part", "big_driving_supercombo_*.onnx.part") for path in [p for g in partial_globs for p in glob.glob(os.path.join(root, g))]: if not path.startswith(keep): diff --git a/iqpilot/selfdrive/iqmodeld/egpu_telemetry.py b/iqpilot/selfdrive/iqmodeld/egpu_telemetry.py index 13936312..aa6c9ba6 100644 --- a/iqpilot/selfdrive/iqmodeld/egpu_telemetry.py +++ b/iqpilot/selfdrive/iqmodeld/egpu_telemetry.py @@ -21,6 +21,7 @@ class EgpuDockTelemetry: self.metrics: dict[str, float] = {} self._power_limit: int | None = None self._asm_usb = None + self._asic_logged = False def _device(self): from tinygrad.device import Device @@ -61,6 +62,24 @@ class EgpuDockTelemetry: self._power_limit = smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True, timeout=100) return self._power_limit + def _log_detail(self, metrics, smu) -> None: + def g(name): + return getattr(metrics, name, None) + try: + if not self._asic_logged: + cloudlog.warning(f"iqegpumodeld dock smu={getattr(smu.smu_mod, '__name__', '?')}") + self._asic_logged = True + throttle = g("ThrottlingPercentage") + cloudlog.event("egpu_dock_detail", + gfxclk_running=g("AverageGfxclkFrequencyPreDs"), gfxclk_idle=g("AverageGfxclkFrequencyPostDs"), + gfxclk_target=g("AverageGfxclkFrequencyTarget"), gfx_activity=g("AverageGfxActivity"), + uclk_activity=g("AverageUclkActivity"), pcie_busy=g("PCIeBusy"), + pcie_rate=g("PcieRate"), pcie_width=g("PcieWidth"), + throttle_max=(max(throttle) if throttle is not None else None), + board_w=g("AverageTotalBoardPower")) + except Exception: + pass + def send(self) -> None: Device = self._device() msg = messaging.new_message("egpuDockState") @@ -79,6 +98,7 @@ class EgpuDockTelemetry: "gpuUsagePercent": metrics.AverageGfxActivity, "gpuClockMhz": metrics.AverageGfxclkFrequencyPostDs, "fanSpeedRpm": metrics.AvgFanRpm} + self._log_detail(metrics, smu) self.valid = True except Exception: if self.valid: diff --git a/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py b/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py index 2ace54a7..93de4329 100644 --- a/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py +++ b/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py @@ -29,7 +29,7 @@ from iqdbc.car.car_helpers import get_demo_car_params from iqpilot.common.params import Params from iqpilot.common.realtime import DT_MDL from iqpilot.common.swaglog import cloudlog -from iqpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from iqpilot.selfdrive.controls.lib.action_engine import ActionEngine from iqpilot.system import sentry from iqpilot.common.steer_delay import lateral_action_delay @@ -39,11 +39,12 @@ from iqpilot.selfdrive.iqmodeld.driving_action import ( ) from iqpilot.selfdrive.iqmodeld.egpu_sched import MODEL_CORE, CpuBudgetGuard, enter_realtime, enter_setup from iqpilot.selfdrive.iqmodeld.egpu_helpers import ( - die_with_parent, download_onnx, download_precompiled, egpu_model_oob_pkl_path, egpu_oob_pkl_path, egpu_pkl_path, egpu_policy_pkl_path, - egpu_present_consented, egpu_selected, free_dock_lock, local_onnx, + die_with_parent, download_onnx, download_precompiled, drop_frozen_model_oob, egpu_model_oob_pkl_path, egpu_oob_pkl_path, egpu_pkl_path, + egpu_policy_pkl_path, egpu_present_consented, egpu_selected, free_dock_lock, local_onnx, patch_tinygrad_fetch_fw, patch_usb_signal_wait, quarantine_artifact, resolve_backend, usbgpu_present, ) from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model +from iqpilot.selfdrive.iqmodeld.aux_inputs import AuxModelInputs from iqpilot.selfdrive.iqmodeld.egpu_pipeline import EgpuOutputInvalid, EgpuPipeline, EgpuPipelineError, make_big_channel_payload from iqpilot.selfdrive.iqmodeld.egpu_telemetry import EgpuDockTelemetry from iqpilot.selfdrive.iqmodeld.messaging import DrivePacketMemory, populate_drive_messages, populate_odometry_message @@ -177,6 +178,7 @@ _model_precompiled_tried = False def _ensure_artifact(params: Params, meta: dict, cam_size: tuple[int, int]) -> str: global _precompiled_tried, _model_precompiled_tried + drop_frozen_model_oob(meta) model_path = egpu_model_oob_pkl_path(meta) if os.path.isfile(model_path): return model_path @@ -449,13 +451,14 @@ def main(demo: bool = False) -> None: cloudlog.warning(f"iqegpumodeld model up (warmup {warm_s * 1e3:.0f}ms, {'warp on dock' if warp is None else 'warp on device'})") pipeline = EgpuPipeline(meta, infer_fn) + aux_inputs = AuxModelInputs(input_spec) warmup_frames = int(meta.get("warmup_frames") or STATEFUL_WARMUP_FRAMES) telemetry_pm = messaging.PubMaster(["egpuDockState"]) telemetry = EgpuDockTelemetry(telemetry_pm, big=True) telemetry_every = max(1, round((1.0 / DT_MDL) / SERVICE_LIST["egpuDockState"].frequency)) sub = SubMaster(["deviceState", "carState", "roadCameraState", "extrinsicsCalibration", - "driverMonitoringState", "carControl", "lateralDelay", "iqNavState", "radarState"]) + "driverMonitoringState", "carControl", "controlsState", "lateralDelay", "iqNavState", "radarState"]) if demo: CP = get_demo_car_params() else: @@ -464,7 +467,7 @@ def main(demo: bool = False) -> None: parser = PhaseParser() memory = DrivePacketMemory() - desire_logic = DesireHelper() + desire_logic = ActionEngine() frame_meter = FrameDropMeter(20.0) warps = CalibrationAtlas() prev_action = log.ModelDataV2.Action() @@ -535,18 +538,21 @@ def main(demo: bool = False) -> None: long_action_t = long_delay + frame_delay + action_delay action_t = np.array([lat_action_t, long_action_t], dtype=np.float32) + aux_inputs.poll() + aux_extras = aux_inputs.extras() + started_at = time.perf_counter() t_warp = started_at try: if warp is None: - output = pipeline.run_frames(main_buf.data, extra_buf.data, main_tfm, extra_tfm, desire_vec, traffic, action_t) + output = pipeline.run_frames(main_buf.data, extra_buf.data, main_tfm, extra_tfm, desire_vec, traffic, action_t, aux_extras) else: try: warped = warp.run(main_buf, extra_buf, main_tfm, extra_tfm) except Exception as e: park(f"warp run failed: {e}") t_warp = time.perf_counter() - output = pipeline.run(warped, desire_vec, traffic, action_t) + output = pipeline.run(warped, desire_vec, traffic, action_t, aux_extras) except EgpuOutputInvalid as e: invalid_streak += 1 if invalid_streak == 1 or invalid_streak % MAX_INVALID_STREAK == 0: @@ -574,7 +580,8 @@ def main(demo: bool = False) -> None: outputs = parser.parse_vision_outputs(sliced) action = get_action_from_model(outputs, prev_action, v_ego, float(lat_action_t), float(long_action_t), - lat_smooth_seconds=meta.get("lat_smooth_seconds")) + lat_smooth_seconds=meta.get("lat_smooth_seconds"), + lat_active=sub["carControl"].latActive, current_curvature=sub["controlsState"].curvature) prev_action = action model_msg = messaging.new_message("modelV2") diff --git a/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py b/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py index b8f4ccfc..7b75718a 100644 --- a/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py +++ b/iqpilot/selfdrive/iqmodeld/model_bundle_downloader.py @@ -3,7 +3,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed """ from __future__ import annotations -import functools import hashlib import json import os @@ -24,26 +23,29 @@ RANGE_RETRIES = 4 PARALLEL_PARTS = max(1, int(os.environ.get("IQ_DOWNLOAD_PARTS", "3"))) -def _requests_auth(): +def _git_auth(): import importlib for mod in ("iqpilot_private.models.git_auth", "iqpilot.models_private_src.git_auth", "iqpilot.selfdrive.iqmodeld.models.git_auth"): try: - return importlib.import_module(mod).get_requests_auth() + return importlib.import_module(mod) except Exception: continue return None -def _hf(): - import importlib - for mod in ("iqpilot_private.models.git_auth", "iqpilot.selfdrive.iqmodeld.models.git_auth"): - try: - m = importlib.import_module(mod) - return m.get_hf_headers(), m.hf_resolve_url - except Exception: - continue - return None, None +def _resolve_hf(hf_path: str) -> tuple[str | None, dict]: + m = _git_auth() + if m is None: + return None, {} + return m.resolve_hf(hf_path) + + +def requests_get(url: str, **kwargs): + m = _git_auth() + if m is None: + raise RuntimeError("no asset broker available") + return m.requests_get(url, **kwargs) class RangesNotHonoured(Exception): @@ -189,10 +191,9 @@ def _verify_file(tmp: str, sha256: str, size: int) -> None: def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb=None) -> str: import requests - headers, resolve = _hf() - if resolve is None: + url, headers = _resolve_hf(hf_path) + if url is None: raise RuntimeError("no HF credentials available") - url = resolve(hf_path) os.makedirs(os.path.dirname(dst), exist_ok=True) tmp = dst + ".hfpart" last_error: Exception | None = None @@ -213,8 +214,10 @@ def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb os.remove(leftover) except Exception as e: last_error = e - for _attempt in range(STREAM_RETRIES): + for attempt in range(STREAM_RETRIES): try: + if attempt: + url, headers = _resolve_hf(hf_path) have = os.path.getsize(tmp) if os.path.isfile(tmp) else 0 if size and have > size: os.remove(tmp) @@ -242,26 +245,6 @@ def download_hf_file(hf_path: str, dst: str, sha256: str, size: int, progress_cb raise RuntimeError(f"HF download failed: {last_error}") -def _lfs_endpoint(base_url: str) -> str: - return base_url.split("/raw/", 1)[0] + ".git/info/lfs" - - -def _resolve_oid(session, base_url: str, oid: str, size: int, auth): - import requests - batch = session.post(f"{_lfs_endpoint(base_url)}/objects/batch", - data=json.dumps({"operation": "download", "transfers": ["basic"], - "objects": [{"oid": oid, "size": size}]}), - headers={"Content-Type": "application/vnd.git-lfs+json", - "Accept": "application/vnd.git-lfs+json"}, - auth=auth, timeout=HTTP_TIMEOUT_S) - batch.raise_for_status() - entry = batch.json()["objects"][0] - if "actions" not in entry: - raise requests.RequestException(f"LFS object unavailable: {entry.get('error', oid)}") - action = entry["actions"]["download"] - return action["href"], action.get("header", {}) - - def _part_path(dst: str, oid: str) -> str: return os.path.join(dst + ".parts", oid) @@ -276,19 +259,28 @@ def _part_complete(path: str, oid: str, size: int) -> bool: return digest.hexdigest() == oid -def _fetch_part(session, base_url: str, obj: dict, path: str, auth, progress) -> None: +def _fetch_part(session, obj: dict, path: str, progress) -> None: size = int(obj["size"]) have = os.path.getsize(path) if os.path.isfile(path) else 0 if have > size: os.remove(path) have = 0 - href, headers = _resolve_oid(session, base_url, obj["oid"], size, auth) - obj_auth = None if headers.get("Authorization") else auth + m = _git_auth() + if m is None: + raise RuntimeError("no asset broker available for LFS parts") + target = m.broker_lfs_url(obj["oid"], size) + headers = m.broker_headers() if target else {} + if not (target and headers): + raise RuntimeError("no device identity for the konn3kt asset broker") + _stream_part(session, target, headers, path, have, progress) + + +def _stream_part(session, href: str, headers: dict, path: str, have: int, progress) -> None: # LFS parts are content-addressed (oid == sha256), so a half-written part can be resumed with a # Range request and verified afterwards instead of being thrown away on every restart. if have: headers = {**headers, "Range": f"bytes={have}-"} - with session.get(href, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S, auth=obj_auth) as r: + with session.get(href, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S) as r: r.raise_for_status() if have and r.status_code != 206: have = 0 @@ -300,7 +292,6 @@ def _fetch_part(session, base_url: str, obj: dict, path: str, auth, progress) -> def download_lfs_bundle(objects: list, dst: str, sha256: str, size: int, progress_cb=None) -> str: import requests - auth = _requests_auth() os.makedirs(dst + ".parts", exist_ok=True) total = int(size) or sum(int(o["size"]) for o in objects) done_bytes = sum(int(o["size"]) for o in objects if _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"]))) @@ -318,30 +309,26 @@ def download_lfs_bundle(objects: list, dst: str, sha256: str, size: int, progres with lock: progress(n) - def fetch(base_url: str, obj: dict) -> None: + def fetch(obj: dict) -> None: path = _part_path(dst, obj["oid"]) if _part_complete(path, obj["oid"], int(obj["size"])): return - _fetch_part(requests.Session(), base_url, obj, path, auth, locked_progress) + _fetch_part(requests.Session(), obj, path, locked_progress) if not _part_complete(path, obj["oid"], int(obj["size"])): if os.path.getsize(path) >= int(obj["size"]): os.remove(path) raise RuntimeError(f"part {obj['oid'][:12]} incomplete or failed verification") - for base_url in MODELS_BASE_URLS: - for _attempt in range(STREAM_RETRIES): - try: - pending = [o for o in objects if not _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"]))] - got[0] = total - sum(int(o["size"]) for o in pending) - with ThreadPoolExecutor(max_workers=max(1, min(PARALLEL_PARTS, len(pending) or 1))) as pool: - for _ in pool.map(functools.partial(fetch, base_url), pending): - pass - break - except Exception as e: - last_error = e - else: - continue - break + for _attempt in range(STREAM_RETRIES): + try: + pending = [o for o in objects if not _part_complete(_part_path(dst, o["oid"]), o["oid"], int(o["size"]))] + got[0] = total - sum(int(o["size"]) for o in pending) + with ThreadPoolExecutor(max_workers=max(1, min(PARALLEL_PARTS, len(pending) or 1))) as pool: + for _ in pool.map(fetch, pending): + pass + break + except Exception as e: + last_error = e else: raise RuntimeError(f"model bundle download failed: {last_error}") diff --git a/iqpilot/selfdrive/iqmodeld/model_cache.py b/iqpilot/selfdrive/iqmodeld/model_cache.py index 5c104245..0b1cba59 100644 --- a/iqpilot/selfdrive/iqmodeld/model_cache.py +++ b/iqpilot/selfdrive/iqmodeld/model_cache.py @@ -4,7 +4,7 @@ from pathlib import Path _RUNTIME_ARTIFACT_PATTERNS = ( - re.compile(r"egpu_.+_amd_(?:tinygrad|policy|policy_oob|model_oob)\.pkl$"), + re.compile(r"egpu_.+_amd_(?:tinygrad|policy|policy_oob|model_oob2)\.pkl$"), re.compile(r"emac_warp_\d+x\d+_tinygrad\.pkl$"), re.compile(r".+_usbemac\.pkl$"), re.compile(r".+\.onnx$"), diff --git a/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py b/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py index bf17800a..ea1dff01 100644 --- a/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py +++ b/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py @@ -17,7 +17,8 @@ _OVERRIDE_KEYS = ( "combinedSplitArtifact", "iqCombinedArtifact", ) -_SPLIT_ROLE_PATTERN = re.compile(r"^driving_(vision|policy|off_policy|on_policy)_(.+)_tinygrad\.pkl$") +_SPLIT_ROLE_PATTERN = re.compile(r"^driving_(vision|policy|off_policy|on_policy)_(.+)_tinygrad\.pkl(?:\.iqc)?$") +_IQC_SUFFIX = ".iqc" def _bundle_models(bundle) -> list: @@ -91,8 +92,9 @@ def combined_split_artifact_candidates(bundle) -> list[Path]: def resolve_combined_split_artifact(bundle) -> Path | None: for candidate in combined_split_artifact_candidates(bundle): - if candidate.is_file(): - return candidate + for path in (candidate, candidate.with_name(candidate.name + _IQC_SUFFIX)): + if path.is_file(): + return path return None diff --git a/iqpilot/selfdrive/iqmodeld/models/helpers.py b/iqpilot/selfdrive/iqmodeld/models/helpers.py index 05cce298..ee2eb77f 100644 --- a/iqpilot/selfdrive/iqmodeld/models/helpers.py +++ b/iqpilot/selfdrive/iqmodeld/models/helpers.py @@ -305,7 +305,40 @@ def get_runtime_bundle_upgrade(bundle, params: Params = None, available_bundles= return _find_runtime_upgrade(bundle, params, available_bundles) +_forced_bundle = None + + +def load_default_model_session(): + """Stage the shipped default model files and return a decoded default bundle + WITHOUT persisting it as the active selection, so the next boot retries the + user's own model instead of staying on the default forever.""" + bundle_dict = _load_default_bundle_dict() + ensure_default_model_files(bundle_dict) + return ModelBundle(**bundle_dict) + + +class forced_active_bundle: + """Make get_active_bundle return `bundle` for the duration of the block, used to + build a runner for a session-only fallback bundle without touching Params.""" + def __init__(self, bundle): + self._bundle = bundle + + def __enter__(self): + global _forced_bundle + self._prev = _forced_bundle + _forced_bundle = self._bundle + return self._bundle + + def __exit__(self, *exc): + global _forced_bundle + _forced_bundle = self._prev + return False + + def get_active_bundle(params: Params = None): + if _forced_bundle is not None: + return _forced_bundle + params = Params() if params is None else params try: diff --git a/iqpilot/selfdrive/iqmodeld/models/iqc.py b/iqpilot/selfdrive/iqmodeld/models/iqc.py new file mode 100644 index 00000000..da84740e --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/models/iqc.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.models.iqc") +except ProprietaryModuleMissing: + from iqpilot.models_private_src.iqc import * # noqa: F403 diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py b/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py index 8ede5f6c..48d990f0 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py @@ -2,7 +2,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos """ import os -import pickle as _pk from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any @@ -13,6 +12,7 @@ from iqpilot.system.hardware import TICI from iqpilot.system.hardware.hw import Paths as _hw_paths from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle as _fetch_bundle from iqpilot.selfdrive.iqmodeld.models.combined_artifact import has_combined_split_artifact +from iqpilot.selfdrive.iqmodeld.models.iqc import load_pickle # ---- runtime type surface (native OpenCL/frame handles resolve to Any off-device) ---- if TYPE_CHECKING: @@ -73,8 +73,7 @@ def load_artifact_metadata(metadata_filename): if os.path.getsize(path) > _META_MAX_BYTES: cloudlog.error(f"metadata pkl {metadata_filename} is artifact-sized, refusing to unpickle it") return tuple({} for _ in _META_FIELDS) - with open(path, 'rb') as fh: - blob = _pk.load(fh) + blob = load_pickle(path) return tuple(blob.get(field, {}) for field in _META_FIELDS) except Exception: cloudlog.exception(f"unreadable metadata pkl {metadata_filename}, continuing without it") diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py index 9acb0ad3..37f1233f 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py @@ -4,7 +4,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed from __future__ import annotations -import pickle from typing import Any import numpy as np @@ -15,6 +14,7 @@ from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import NumpyDict, Sh from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants from iqpilot.selfdrive.iqmodeld.parser import PhaseParser +from iqpilot.selfdrive.iqmodeld.models.iqc import load_pickle def _tinygrad_imports(): @@ -64,8 +64,7 @@ class TinygradCombinedSplitRunner(ModelRunner): if self._artifact_path is None: raise FileNotFoundError("No IQ combined split artifact is available for the active bundle") - with open(self._artifact_path, "rb") as artifact: - runtime_package: dict[Any, Any] = pickle.load(artifact) + runtime_package: dict[Any, Any] = load_pickle(self._artifact_path) self._meta_by_role = runtime_package.get("meta_by_role", runtime_package.get("metadata", {})) self._policy_roles = runtime_package.get("roles", _phase_roles(self._meta_by_role)) diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py index 33beea42..b8783dc8 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py @@ -4,7 +4,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed from __future__ import annotations import os -import pickle from typing import Any import numpy as np @@ -15,6 +14,7 @@ from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ( from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants from iqpilot.selfdrive.iqmodeld.parser import PhaseParser +from iqpilot.selfdrive.iqmodeld.models.iqc import load_pickle def _tinygrad_imports(): @@ -40,8 +40,7 @@ class TinygradFusedRunner(ModelRunner): self._model_data = next(iter(self.models.values())) pkl_path = os.path.join(CUSTOM_MODEL_PATH, self._model_data.model.artifact.fileName) - with open(pkl_path, 'rb') as f: - self._fused: dict[Any, Any] = pickle.load(f) + self._fused: dict[Any, Any] = load_pickle(pkl_path) self._vision_meta = self._fused['metadata']['vision'] self._on_meta = self._fused['metadata']['on_policy'] diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py index ea35728a..e457a5f8 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py @@ -5,7 +5,6 @@ from __future__ import annotations import hashlib import os -import pickle import re from typing import Any @@ -17,6 +16,7 @@ from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants from iqpilot.selfdrive.iqmodeld.parser import PhaseParser from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runtime import TinygradSupercomboRuntime +from iqpilot.selfdrive.iqmodeld.models.iqc import load_pickle def _captured_queue_depth(warp_jit: Any) -> int | None: @@ -77,8 +77,7 @@ class TinygradSupercomboRunner(TinygradSupercomboRuntime, ModelRunner): self._pkl_path = pkl_path self._expected_sha256 = getattr(getattr(self._model_data.model.artifact, "downloadUri", None), "sha256", "") or "" self._verify_artifact_file() - with open(pkl_path, 'rb') as f: - self._m: dict[Any, Any] = pickle.load(f) + self._m: dict[Any, Any] = load_pickle(pkl_path) self._meta = self._m['metadata'] self._ish = self._meta['input_shapes'] diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py index 2d91c3df..7bde2165 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py @@ -4,7 +4,6 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed from __future__ import annotations -import pickle from dataclasses import dataclass import numpy as np @@ -32,6 +31,7 @@ from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelCo from iqpilot.selfdrive.iqmodeld.config import ModelConstants from iqpilot.selfdrive.iqmodeld.runtime.tinygrad import qcom_tensor_from_opencl_address from iqpilot.system.hardware import TICI +from iqpilot.selfdrive.iqmodeld.models.iqc import load_pickle @dataclass(frozen=True) @@ -45,12 +45,11 @@ def _artifact_path(filename: str) -> str: def _load_program_blob(filename: str): - with open(_artifact_path(filename), "rb") as artifact: - try: - return pickle.load(artifact) - except FileNotFoundError as exc: - assert "/dev/kgsl-3d0" not in str(exc), "Model was built on C3 or C3X, but is being loaded on PC" - raise + try: + return load_pickle(_artifact_path(filename)) + except FileNotFoundError as exc: + assert "/dev/kgsl-3d0" not in str(exc), "Model was built on C3 or C3X, but is being loaded on PC" + raise def _compile_input_plan(captured) -> dict[str, _TensorShapePlan]: @@ -81,7 +80,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny raise ValueError(f"Model data for type {model_type} not available.") asset_name = self._model_data.model.artifact.fileName - assert asset_name.endswith("_tinygrad.pkl"), f"Invalid model file {asset_name} for TinygradRunner" + assert asset_name.removesuffix(".iqc").endswith("_tinygrad.pkl"), f"Invalid model file {asset_name} for TinygradRunner" self.model_run = _load_program_blob(asset_name) self._input_plan = _compile_input_plan(self.model_run.captured) diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_action_provenance.py b/iqpilot/selfdrive/iqmodeld/tests/test_action_provenance.py index 55f63da3..2691b894 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_action_provenance.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_action_provenance.py @@ -108,3 +108,53 @@ def test_previous_action_permission_is_not_inherited_by_next_frame(): assert action.desiredCurvature == previous.desiredCurvature assert action.lateralActionSource == "nativeAction" assert action.legacyLookahead.contract == "unsupported" + + +@pytest.mark.parametrize("producer", ["local", "offload"]) +@pytest.mark.parametrize("kind", ["nativeAction", "nativeCurvature", "plan", "planplus", "synthetic"]) +@pytest.mark.parametrize("speed", [0.0, 0.2, 20.0]) +@pytest.mark.parametrize("lat_active", [False, True]) +def test_curvature_seed_tracks_vehicle_only_while_lateral_is_inactive(producer, kind, speed, lat_active): + previous = log.ModelDataV2.Action(desiredCurvature=0.004, desiredAcceleration=0.1) + expected_previous = log.ModelDataV2.Action( + desiredCurvature=0.004 if lat_active else -0.012, desiredAcceleration=0.1, + ) + outputs = outputs_for(kind) + if producer == "local": + state = runtime() + expected = NeuralEngineState.get_action_from_model(state, outputs, expected_previous, 0.45, 0.4, speed) + action = NeuralEngineState.get_action_from_model( + state, outputs, previous, 0.45, 0.4, speed, lat_active=lat_active, current_curvature=-0.012, + ) + else: + expected = get_action_from_model(outputs, expected_previous, speed, 0.45, 0.4, 0.1) + action = get_action_from_model( + outputs, previous, speed, 0.45, 0.4, 0.1, lat_active=lat_active, current_curvature=-0.012, + ) + for field in ("desiredCurvature", "desiredAcceleration", "shouldStop", "lateralActionSource"): + assert getattr(action, field) == getattr(expected, field) + if speed <= 0.3: + assert action.desiredCurvature == expected_previous.desiredCurvature + + +@pytest.mark.parametrize("producer", ["local", "offload"]) +def test_engagement_keeps_curvature_history_from_last_inactive_frame(producer): + previous = log.ModelDataV2.Action(desiredCurvature=0.03, desiredAcceleration=0.1) + outputs = outputs_for("nativeAction") + if producer == "local": + state = runtime() + inactive = NeuralEngineState.get_action_from_model( + state, outputs, previous, 0.45, 0.4, 0.2, lat_active=False, current_curvature=-0.012, + ) + active = NeuralEngineState.get_action_from_model( + state, outputs, inactive, 0.45, 0.4, 20.0, lat_active=True, current_curvature=0.02, + ) + else: + inactive = get_action_from_model( + outputs, previous, 0.2, 0.45, 0.4, 0.1, lat_active=False, current_curvature=-0.012, + ) + active = get_action_from_model( + outputs, inactive, 20.0, 0.45, 0.4, 0.1, lat_active=True, current_curvature=0.02, + ) + assert inactive.desiredCurvature == pytest.approx(-0.012) + assert active.desiredCurvature == pytest.approx(smooth_value(-0.8 / 20.0 ** 2, -0.012, 0.1)) diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py b/iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py index a45af332..dc05bc08 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py @@ -86,6 +86,28 @@ def test_resolve_combined_split_artifact_prefers_override(tmp_path: Path, monkey assert resolve_combined_split_artifact(bundle) == expected +def test_resolve_combined_split_artifact_finds_encrypted_copy_after_plaintext_removed(tmp_path: Path, monkeypatch): + bundle = _Bundle([_Model(ModelType.policy, "driving_combined_83b81b83.pkl.iqc")]) + bundle.ref = "83b81b83cdf76a577946a1567f4644e1a018443c" + expected = tmp_path / "driving_combined_83b81b83.pkl.iqc" + expected.write_bytes(b"IQC1") + + monkeypatch.setattr("iqpilot.selfdrive.iqmodeld.models.combined_artifact._MODEL_ROOT", tmp_path) + + assert resolve_combined_split_artifact(bundle) == expected + + +def test_resolve_combined_split_artifact_prefers_plaintext_when_both_exist(tmp_path: Path, monkeypatch): + bundle = _Bundle([_Model(ModelType.vision, "driving_vision_demo_tinygrad.pkl.iqc")]) + plaintext = tmp_path / "driving_combined_demo.pkl" + plaintext.write_bytes(b"iq") + (tmp_path / "driving_combined_demo.pkl.iqc").write_bytes(b"IQC1") + + monkeypatch.setattr("iqpilot.selfdrive.iqmodeld.models.combined_artifact._MODEL_ROOT", tmp_path) + + assert resolve_combined_split_artifact(bundle) == plaintext + + def test_get_model_runner_prefers_combined_split_artifact(monkeypatch): bundle = _Bundle([ _Model(ModelType.vision, "driving_vision_demo_tinygrad.pkl"), diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_default_model_fallback.py b/iqpilot/selfdrive/iqmodeld/tests/test_default_model_fallback.py new file mode 100644 index 00000000..c286df3b --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/tests/test_default_model_fallback.py @@ -0,0 +1,59 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from iqpilot.selfdrive.iqmodeld.models import helpers +from iqpilot.selfdrive.iqmodeld.models.helpers import ( + forced_active_bundle, + get_active_bundle, + load_default_model_session, + is_default_bundle, +) + + +class _FakeParams: + def __init__(self, store): + self.store = store + def get(self, key): + return self.store.get(key) + def put(self, key, value): + self.store["_puts"] = self.store.get("_puts", 0) + 1 + self.store[key] = value + def remove(self, key): + self.store.pop(key, None) + + +def test_forced_bundle_overrides_get_active_bundle_only_inside_the_block(monkeypatch): + monkeypatch.setattr(helpers, "_forced_bundle", None) + default_sentinel = object() + with forced_active_bundle(default_sentinel): + assert get_active_bundle() is default_sentinel + assert helpers._forced_bundle is None + + +def test_forced_bundle_does_not_persist_anything_to_params(monkeypatch): + store = {} + monkeypatch.setattr(helpers, "_forced_bundle", None) + monkeypatch.setattr(helpers, "Params", lambda *a, **k: _FakeParams(store)) + default_sentinel = object() + with forced_active_bundle(default_sentinel): + assert get_active_bundle() is default_sentinel + assert store.get("_puts", 0) == 0 + assert "ModelManager_ActiveBundle" not in store + + +def test_forced_bundle_restores_the_previous_override(monkeypatch): + monkeypatch.setattr(helpers, "_forced_bundle", None) + outer, inner = object(), object() + with forced_active_bundle(outer): + with forced_active_bundle(inner): + assert get_active_bundle() is inner + assert get_active_bundle() is outer + assert helpers._forced_bundle is None + + +def test_load_default_model_session_returns_default_bundle_and_stages_files(monkeypatch): + staged = {"n": 0} + monkeypatch.setattr(helpers, "ensure_default_model_files", lambda *a, **k: staged.__setitem__("n", staged["n"] + 1)) + bundle = load_default_model_session() + assert staged["n"] == 1 + assert is_default_bundle(bundle) diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_egpu_artifact_choice.py b/iqpilot/selfdrive/iqmodeld/tests/test_egpu_artifact_choice.py index 3db1a3d5..1515e9dc 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_egpu_artifact_choice.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_egpu_artifact_choice.py @@ -56,3 +56,19 @@ def test_prefetch_downloads_the_warp_on_dock_build_when_offered(egpu_prefetch, m monkeypatch.setattr(egpu_prefetch, "download_precompiled", lambda m, progress_cb=None, field=None: asked.append(field) or "ok") assert egpu_prefetch.prefetch_once(P()) is True assert asked == ["egpu_model_oob_artifact"] + + +def test_frozen_feed_warp_on_dock_builds_are_never_loaded(monkeypatch, tmp_path): + from iqpilot.selfdrive.iqmodeld import egpu_helpers + from iqpilot.selfdrive.iqmodeld.model_cache import is_runtime_artifact + from iqpilot.system.hardware.hw import Paths + monkeypatch.setattr(Paths, "model_root", staticmethod(lambda: str(tmp_path))) + meta = {"key": "big_nav", "sha256": "8a5bc46c" + "0" * 56} + stale = tmp_path / "egpu_big_nav_8a5bc46c_amd_model_oob.pkl" + stale.write_bytes(b"frozen") + live = egpu_helpers.egpu_model_oob_pkl_path(meta) + assert live != str(stale) and live.endswith("_amd_model_oob2.pkl") + egpu_helpers.drop_frozen_model_oob(meta) + egpu_helpers.drop_frozen_model_oob(meta) + assert not stale.exists() + assert is_runtime_artifact("egpu_big_nav_8a5bc46c_amd_model_oob2.pkl") and not is_runtime_artifact(stale.name) diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_egpu_stateful.py b/iqpilot/selfdrive/iqmodeld/tests/test_egpu_stateful.py index c9e88e1b..8e0979ef 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_egpu_stateful.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_egpu_stateful.py @@ -152,3 +152,28 @@ def test_pipeline_reports_warmth_and_resets(): assert pipeline.warm_frames == 4 pipeline.reset_state() assert pipeline.warm_frames == 0 + + +def test_warp_on_dock_artifact_reads_every_frame_feed(): + from tinygrad.engine.jit import TinyJit + from tinygrad.tensor import Tensor + from iqpilot.selfdrive.iqmodeld.egpu_helpers import patch_tinygrad_prune_views + from iqpilot.selfdrive.iqmodeld.egpu_policy import StatefulModelRunner, make_run_stateful_model, make_warp, nv12_copy_size, stateful_model_size + patch_tinygrad_prune_views() + pairs = state_pairs(SPEC) + nv12 = (64, 48, 64, 48, 24) + fcs = nv12_copy_size(*nv12[2:]) + w, h = stateful_model_size(SPEC) + def model(inp): + feeds = {name: (inp[name] @ Tensor.eye(2)).contiguous() for name in ("traffic_convention", "action_t")} + return _tg_model({**inp, **feeds}) + + run = make_run_stateful(model, SPEC, pairs, "CPU") + jit = TinyJit(make_run_stateful_model(make_warp(nv12, w, h, "CPU"), run, SPEC, pairs, fcs, "CPU"), prune=True) + runner = StatefulModelRunner(jit, SPEC, pairs, "CPU", fcs) + frame, eye = np.zeros(fcs, dtype=np.uint8), np.eye(3, dtype=np.float32) + for i in range(6): + traffic = np.array([i % 2, 1 - i % 2], dtype=np.float32) + action_t = np.array([0.1 * i, -0.05 * i], dtype=np.float32) + out = runner.run(frame, frame, eye, eye, np.zeros(8, dtype=np.float32), traffic, action_t) + np.testing.assert_allclose(out[32:36], np.concatenate([traffic, action_t]), rtol=1e-6, atol=1e-7, err_msg=f"frame {i}") diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_model_bundle_downloader.py b/iqpilot/selfdrive/iqmodeld/tests/test_model_bundle_downloader.py index ad9675fb..647a5a82 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_model_bundle_downloader.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_model_bundle_downloader.py @@ -2,6 +2,7 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ """ import hashlib +from types import SimpleNamespace import http.server import os import threading @@ -54,6 +55,11 @@ def server(): srv.server_close() +def _fake_broker(port): + return SimpleNamespace(broker_lfs_url=lambda oid, size: f"http://127.0.0.1:{port}/o/{oid}", + broker_headers=lambda: {"Authorization": "JWT test"}) + + def _objects(parts): return [{"oid": hashlib.sha256(p).hexdigest(), "size": len(p)} for p in parts] @@ -65,9 +71,7 @@ def test_resume_continues_a_cut_part_and_reuses_finished_parts(server, tmp_path, _RangeHandler.hits = [] _RangeHandler.cut_first = {objs[1]["oid"]: 100_000} port = server.server_address[1] - monkeypatch.setattr(dl, "_requests_auth", lambda: None) - monkeypatch.setattr(dl, "_resolve_oid", lambda session, base, oid, size, auth: (f"http://127.0.0.1:{port}/o/{oid}", {})) - monkeypatch.setattr(dl, "MODELS_BASE_URLS", ("http://unused",)) + monkeypatch.setattr(dl, "_git_auth", lambda: _fake_broker(port)) monkeypatch.setattr(dl, "STREAM_RETRIES", 3) monkeypatch.setattr(dl, "CHUNK", 64 * 1024) whole = b"".join(parts) @@ -88,9 +92,7 @@ def test_corrupt_finished_part_is_refetched(server, tmp_path, monkeypatch): _RangeHandler.hits = [] _RangeHandler.cut_first = {} port = server.server_address[1] - monkeypatch.setattr(dl, "_requests_auth", lambda: None) - monkeypatch.setattr(dl, "_resolve_oid", lambda session, base, oid, size, auth: (f"http://127.0.0.1:{port}/o/{oid}", {})) - monkeypatch.setattr(dl, "MODELS_BASE_URLS", ("http://unused",)) + monkeypatch.setattr(dl, "_git_auth", lambda: _fake_broker(port)) dst = str(tmp_path / "model.pkl") os.makedirs(dst + ".parts") with open(dl._part_path(dst, objs[0]["oid"]), "wb") as f: @@ -108,7 +110,7 @@ def test_hf_single_file_resumes_after_cut(server, tmp_path, monkeypatch): _RangeHandler.hits = [] _RangeHandler.cut_first = {oid: 250_000} port = server.server_address[1] - monkeypatch.setattr(dl, "_hf", lambda: ({"Authorization": "Bearer test"}, lambda p: f"http://127.0.0.1:{port}/o/{oid}")) + monkeypatch.setattr(dl, "_resolve_hf", lambda p: (f"http://127.0.0.1:{port}/o/{oid}", {"Authorization": "Bearer test"})) monkeypatch.setattr(dl, "STREAM_RETRIES", 3) monkeypatch.setattr(dl, "CHUNK", 64 * 1024) dst = str(tmp_path / "policy.pkl") @@ -141,7 +143,7 @@ def test_download_onnx_prefers_hf_then_falls_back(tmp_path, monkeypatch): def _parallel(monkeypatch, port, oid): - monkeypatch.setattr(dl, "_hf", lambda: ({"Authorization": "Bearer test"}, lambda p: f"http://127.0.0.1:{port}/o/{oid}")) + monkeypatch.setattr(dl, "_resolve_hf", lambda p: (f"http://127.0.0.1:{port}/o/{oid}", {"Authorization": "Bearer test"})) monkeypatch.setattr(dl, "STREAM_RETRIES", 2) monkeypatch.setattr(dl, "CHUNK", 64 * 1024) monkeypatch.setattr(dl, "PARALLEL_MIN_BYTES", 100_000) diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_private_shims.py b/iqpilot/selfdrive/iqmodeld/tests/test_private_shims.py index 80e1c958..0e48eea6 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_private_shims.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_private_shims.py @@ -12,6 +12,7 @@ from iqpilot import _proprietary_loader ROOT = Path(__file__).resolve().parents[4] SHIMS = { + "iqpilot.selfdrive.controls.lib.action_engine": "iqpilot_private.navd.action_engine", "iqpilot.selfdrive.iqmodeld.cot_decode": "iqpilot_private.context.cot_decode", "iqpilot.selfdrive.iqmodeld.drive_profile": "iqpilot_private.context.drive_profile", "iqpilot.selfdrive.iqmodeld.speed_limit_input": "iqpilot_private.context.speed_limit_input", diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_selector_version_gate.py b/iqpilot/selfdrive/iqmodeld/tests/test_selector_version_gate.py index 73a9fe23..d38602f6 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_selector_version_gate.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_selector_version_gate.py @@ -29,3 +29,13 @@ def test_gate_still_accepts_older_bundles(): def test_gate_rejects_a_bundle_from_the_future(): assert not is_bundle_version_compatible({"minimumSelectorVersion": EMAC_BUNDLE_MIN_SELECTOR_VERSION + 5}) + + +def test_gate_accepts_encrypted_iqc_bundles(): + assert is_bundle_version_compatible({"minimumSelectorVersion": 20}) + + +def test_bundles_kept_only_for_older_builds_are_hidden(): + assert not is_bundle_version_compatible({"minimumSelectorVersion": 18, "maximum_selector_version": "19"}) + assert not is_bundle_version_compatible({"minimumSelectorVersion": 18, "maximumSelectorVersion": 19}) + assert is_bundle_version_compatible({"minimumSelectorVersion": 18, "maximum_selector_version": 20}) diff --git a/iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py b/iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py index dffd4636..48d24adb 100644 --- a/iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py +++ b/iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py @@ -26,13 +26,14 @@ os.environ.setdefault("DEV", "USB+AMD:LLVM") import numpy as np -from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_pkl_path, local_onnx, patch_tinygrad_fetch_fw +from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_pkl_path, local_onnx, patch_tinygrad_fetch_fw, patch_tinygrad_prune_views from iqpilot.selfdrive.iqmodeld.egpu_model import EGPU_MODELS, get_egpu_model, resolve_egpu_model from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC, spec_from_meta INPUT_SPEC = dict(MODEL_INPUT_SPEC) patch_tinygrad_fetch_fw() +patch_tinygrad_prune_views() SEED = 42 diff --git a/iqpilot/selfdrive/longitudinal_settings.py b/iqpilot/selfdrive/longitudinal_settings.py index c42d346b..5247e33e 100644 --- a/iqpilot/selfdrive/longitudinal_settings.py +++ b/iqpilot/selfdrive/longitudinal_settings.py @@ -48,7 +48,6 @@ def apply_longitudinal_mode(params, mode: int) -> None: params.put_bool("AlphaLongitudinalEnabled", True) params.put_bool("ExperimentalMode", False) params.put_bool("IQDynamicMode", False) - set_valid_personality(params, PERSONALITY_RELAXED) elif mode == LONGITUDINAL_MODE_DYNAMIC: params.put_bool("AlphaLongitudinalEnabled", True) params.put_bool("ExperimentalMode", True) @@ -76,20 +75,11 @@ def next_longitudinal_mode(current: int, onroad: bool, iq_modes_available: bool) def get_follow_distance_state(params) -> tuple[int | None, bool]: - mode = get_longitudinal_mode(params) - if mode == LONGITUDINAL_MODE_STOCK: + if get_longitudinal_mode(params) == LONGITUDINAL_MODE_STOCK: get_valid_personality(params) return None, False - if mode == LONGITUDINAL_MODE_CHILL: - if get_valid_personality(params) != PERSONALITY_RELAXED: - set_valid_personality(params, PERSONALITY_RELAXED) - return PERSONALITY_RELAXED, False return get_valid_personality(params), True def get_runtime_personality(params) -> int: - if get_longitudinal_mode(params) == LONGITUDINAL_MODE_CHILL: - if get_valid_personality(params) != PERSONALITY_RELAXED: - set_valid_personality(params, PERSONALITY_RELAXED) - return PERSONALITY_RELAXED return get_valid_personality(params) diff --git a/iqpilot/selfdrive/pandad/SConscript b/iqpilot/selfdrive/pandad/SConscript index f7159b6e..b3516a8a 100644 --- a/iqpilot/selfdrive/pandad/SConscript +++ b/iqpilot/selfdrive/pandad/SConscript @@ -14,3 +14,4 @@ if GetOption('extras'): env.Program('tests/test_pandad_usbprotocol', ['tests/test_pandad_usbprotocol.cc'], LIBS=[panda] + libs) env.Program('tests/test_panda_safety', ['tests/test_panda_safety.cc'] + safety, LIBS=libs) + env.Program('tests/test_ignition_hold', ['tests/test_ignition_hold.cc'], LIBS=libs) diff --git a/iqpilot/selfdrive/pandad/panda_comms.h b/iqpilot/selfdrive/pandad/panda_comms.h index 9c452faf..60ac90a2 100644 --- a/iqpilot/selfdrive/pandad/panda_comms.h +++ b/iqpilot/selfdrive/pandad/panda_comms.h @@ -14,7 +14,7 @@ #define TIMEOUT 0 -#define SPI_BUF_SIZE 2048 +#define SPI_BUF_SIZE 4096 // comms base class @@ -57,10 +57,16 @@ private: #ifndef __APPLE__ struct __attribute__((packed)) spi_header { - uint8_t sync; uint8_t endpoint; + uint32_t id; uint16_t tx_len; - uint16_t max_rx_len; + uint16_t rx_len; +}; + +struct __attribute__((packed)) spi_resp_header { + uint8_t status; + uint32_t id; + uint16_t len; }; class PandaSpiHandle : public PandaCommsHandle { @@ -81,9 +87,7 @@ private: uint8_t rx_buf[SPI_BUF_SIZE]; inline static std::recursive_mutex hw_lock; - int wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout, unsigned int length); - int bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len, unsigned int timeout); - int spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout); + int spi_transfer(uint8_t endpoint, uint32_t id, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, double deadline); int spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout); int lltransfer(spi_ioc_transfer &t); diff --git a/iqpilot/selfdrive/pandad/pandad.cc b/iqpilot/selfdrive/pandad/pandad.cc index 9b8d4a77..f7a93fc7 100644 --- a/iqpilot/selfdrive/pandad/pandad.cc +++ b/iqpilot/selfdrive/pandad/pandad.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -236,6 +237,8 @@ std::optional send_panda_states(PubMaster *pm, const std::vector bool ignition_local = false; const uint32_t pandas_cnt = pandas.size(); static Params params; + static std::map ignition_holds; + const uint64_t now_ns = nanos_since_boot(); const bool allow_offroad_external_can_tx = params.getBool("Konn3ktAllowOffroadExternalCanTx"); // build msg @@ -271,6 +274,9 @@ std::optional send_panda_states(PubMaster *pm, const std::vector } pandaCanStates.push_back(can_health); + health.ignition_line_pkt = ignition_holds[panda->hw_serial()].update( + health.ignition_line_pkt != 0, health.safety_rx_checks_invalid_pkt != 0, now_ns); + if (spoofing_started) { health.ignition_line_pkt = 1; } diff --git a/iqpilot/selfdrive/pandad/pandad.h b/iqpilot/selfdrive/pandad/pandad.h index 74917c63..d7dd1f0a 100644 --- a/iqpilot/selfdrive/pandad/pandad.h +++ b/iqpilot/selfdrive/pandad/pandad.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -9,6 +10,29 @@ void pandad_main_thread(std::vector serials); +// marginal harness ignition lines read low for up to 2.3 s while driving once the CAN transceivers power up +constexpr uint64_t IGNITION_LINE_HOLD_NS = 5ULL * 1000ULL * 1000ULL * 1000ULL; + +class IgnitionLineHold { +public: + // a car powering off drops its line and then fails the panda's rx checks, which ends the hold at once + bool update(bool line, bool rx_checks_invalid, uint64_t now_ns) { + if (line) { + last_on_ns_ = now_ns; + seen_on_ = true; + return true; + } + if (rx_checks_invalid) { + seen_on_ = false; + } + return seen_on_ && (now_ns - last_on_ns_) < IGNITION_LINE_HOLD_NS; + } + +private: + uint64_t last_on_ns_ = 0; + bool seen_on_ = false; +}; + // deprecated devices static const std::vector SUPPORTED_PANDA_TYPES = { cereal::PandaState::PandaType::RED_PANDA, diff --git a/iqpilot/selfdrive/pandad/spi.cc b/iqpilot/selfdrive/pandad/spi.cc index b6ee5780..44325c24 100644 --- a/iqpilot/selfdrive/pandad/spi.cc +++ b/iqpilot/selfdrive/pandad/spi.cc @@ -3,10 +3,12 @@ #include #include +#include #include #include #include #include +#include #include #include "common/util.h" @@ -16,19 +18,24 @@ #include "selfdrive/pandad/panda_comms.h" -#define SPI_SYNC 0x5AU -#define SPI_HACK 0x79U -#define SPI_DACK 0x85U -#define SPI_NACK 0x1FU -#define SPI_CHECKSUM_START 0xABU +#define SPI_SLAVE_READY 0x3CU +#define SPI_ACK 0xC3U +#define SPI_NACK 0x5AU +#define SPI_POLL 0xFFU + +#define SPI_CRC_SIZE 4U +#define SPI_XFER_SIZE (SPI_BUF_SIZE - 0x40) +#define SPI_FIRST_READ_SIZE (1024 - sizeof(spi_resp_header) - SPI_CRC_SIZE) enum SpiError { NACK = -2, ACK_TIMEOUT = -3, + SILENT = -4, }; -const unsigned int SPI_ACK_TIMEOUT = 500; // milliseconds +const unsigned int SPI_MIN_TIMEOUT = 100; +const double SPI_SILENT_TIMEOUT = 1000; const std::string SPI_DEVICE = "/dev/spidev0.0"; class LockEx { @@ -51,8 +58,8 @@ private: #define SPILOG(fn, fmt, ...) do { \ fn(fmt, ## __VA_ARGS__); \ fn(" %d / 0x%x / %d / %d / tx: %s", \ - xfer_count, header.endpoint, header.tx_len, header.max_rx_len, \ - util::hexdump(tx_buf, std::min((int)header.tx_len, 8)).c_str()); \ + xfer_count, header.endpoint, header.tx_len, header.rx_len, \ + util::hexdump(tx_buf + sizeof(header), std::min((int)header.tx_len, 8)).c_str()); \ } while (0) PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { @@ -83,7 +90,7 @@ PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { util::safe_ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits_per_word, "failed setting SPI bits per word"); // get hw UID/serial - ret = control_read(0xc3, 0, 0, uid, uid_len, 100); + ret = control_read(0xc3, 0, 0, uid, uid_len, 500); if (ret == uid_len) { std::stringstream stream; for (int i = 0; i < uid_len; i++) { @@ -141,40 +148,31 @@ int PandaSpiHandle::control_read(uint8_t request, uint16_t param1, uint16_t para } int PandaSpiHandle::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { - return bulk_transfer(endpoint, data, length, NULL, 0, timeout); -} -int PandaSpiHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { - return bulk_transfer(endpoint, NULL, 0, data, length, timeout); -} - -int PandaSpiHandle::bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len, unsigned int timeout) { - const int xfer_size = SPI_BUF_SIZE - 0x40; - - int ret = 0; - uint16_t length = (tx_data != NULL) ? tx_len : rx_len; - for (int i = 0; i < (int)std::ceil((float)length / xfer_size); i++) { - int d; - if (tx_data != NULL) { - int len = std::min(xfer_size, tx_len - (xfer_size * i)); - d = spi_transfer_retry(endpoint, tx_data + (xfer_size * i), len, NULL, 0, timeout); - } else { - uint16_t to_read = std::min(xfer_size, rx_len - ret); - d = spi_transfer_retry(endpoint, NULL, 0, rx_data + (xfer_size * i), to_read, timeout); - } - - if (d < 0) { - SPILOG(LOGE, "SPI: bulk transfer failed with %d", d); + for (int pos = 0; pos < length; pos += SPI_XFER_SIZE) { + int ret = spi_transfer_retry(endpoint, data + pos, std::min(SPI_XFER_SIZE, length - pos), NULL, 0, timeout); + if (ret < 0) { comms_healthy = false; - return d; + return ret; } + } + return length; +} - ret += d; - if ((rx_data != NULL) && d < xfer_size) { +int PandaSpiHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int pos = 0; + while (pos < length) { + int to_read = std::min(pos == 0 ? (int)SPI_FIRST_READ_SIZE : SPI_XFER_SIZE, length - pos); + int ret = spi_transfer_retry(endpoint, NULL, 0, data + pos, to_read, timeout); + if (ret < 0) { + comms_healthy = false; + return ret; + } + pos += ret; + if (ret < to_read) { break; } } - - return ret; + return pos; } std::vector PandaSpiHandle::list() { @@ -187,224 +185,184 @@ std::vector PandaSpiHandle::list() { return {}; } -void add_checksum(uint8_t *data, int data_len) { - data[data_len] = SPI_CHECKSUM_START; - for (int i=0; i < data_len; i++) { - data[data_len] ^= data[i]; - } +static uint32_t random_id() { + static thread_local std::mt19937 rng{std::random_device{}()}; + return rng(); } -bool check_checksum(uint8_t *data, int data_len) { - uint8_t checksum = SPI_CHECKSUM_START; - for (uint16_t i = 0U; i < data_len; i++) { - checksum ^= data[i]; +static uint32_t crc32(const uint8_t *data, uint32_t len) { + static const auto table = [] { + std::array t{}; + for (uint32_t i = 0; i < 256; i++) { + uint32_t c = i; + for (int j = 0; j < 8; j++) { + c = (c & 1) ? (0xEDB88320U ^ (c >> 1)) : (c >> 1); + } + t[i] = c; + } + return t; + }(); + + uint32_t crc = 0xFFFFFFFFU; + for (uint32_t i = 0; i < len; i++) { + crc = table[(crc ^ data[i]) & 0xFFU] ^ (crc >> 8); } - return checksum == 0U; + return crc ^ 0xFFFFFFFFU; +} + +static void put_crc(uint8_t *data, uint32_t len) { + uint32_t crc = crc32(data, len); + memcpy(&data[len], &crc, sizeof(crc)); +} + +static bool check_crc(const uint8_t *data, uint32_t len) { + uint32_t crc; + memcpy(&crc, &data[len], sizeof(crc)); + return crc == crc32(data, len); } int PandaSpiHandle::spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout) { + const double deadline = (timeout == 0) ? INFINITY : millis_since_boot() + std::max(timeout, SPI_MIN_TIMEOUT); + + uint32_t id = random_id(); int ret; int nack_count = 0; - int timeout_count = 0; - bool timed_out = false; - double start_time = millis_since_boot(); - - do { - ret = spi_transfer(endpoint, tx_data, tx_len, rx_data, max_rx_len, timeout); - - if (ret < 0) { - timed_out = (timeout != 0) && (timeout_count > 5); - timeout_count += ret == SpiError::ACK_TIMEOUT; - - // give other threads a chance to run - std::this_thread::yield(); - - if (ret == SpiError::NACK) { - // prevent busy waiting while the panda is NACK'ing - // due to full TX buffers - nack_count += 1; - if (nack_count > 3) { - SPILOG(LOGD, "NACK sleep %d", nack_count); - usleep(std::clamp(nack_count*10, 200, 2000)); - } - } + while (true) { + ret = spi_transfer(endpoint, id, tx_data, tx_len, rx_data, max_rx_len, deadline); + if (ret >= 0 || !connected) { + break; } - } while (ret < 0 && connected && !timed_out); + + if (ret == SpiError::NACK) { + id = random_id(); + nack_count += 1; + if (nack_count > 3) { + usleep(std::clamp(nack_count*10, 200, 2000)); + } + } else if (ret == SpiError::SILENT) { + LOGW("SPI: panda isn't responding"); + usleep(10000); + } else if (millis_since_boot() > deadline) { + break; + } + + // give other threads a chance to run + std::this_thread::yield(); + } if (ret < 0) { - SPILOG(LOGE, "transfer failed, after %d tries, %.2fms", timeout_count, millis_since_boot() - start_time); + SPILOG(LOGE, "SPI: transfer failed with %d", ret); } - return ret; } -int PandaSpiHandle::wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout, unsigned int length) { - double start_millis = millis_since_boot(); - if (timeout == 0) { - timeout = SPI_ACK_TIMEOUT; - } - timeout = std::clamp(timeout, 20U, SPI_ACK_TIMEOUT); - - spi_ioc_transfer transfer = { - .tx_buf = (uint64_t)tx_buf, - .rx_buf = (uint64_t)rx_buf, - .len = length, - }; - memset(tx_buf, tx, length); - - while (true) { - int ret = lltransfer(transfer); - if (ret < 0) { - SPILOG(LOGE, "SPI: failed to send ACK request"); - return ret; - } - - if (rx_buf[0] == ack) { - break; - } else if (rx_buf[0] == SPI_NACK) { - SPILOG(LOGD, "SPI: got NACK, waiting for 0x%x", ack); - return SpiError::NACK; - } - - // handle timeout - if (millis_since_boot() - start_millis > timeout) { - SPILOG(LOGW, "SPI: timed out waiting for ACK, waiting for 0x%x", ack); - return SpiError::ACK_TIMEOUT; - } - } - - return 0; -} - int PandaSpiHandle::lltransfer(spi_ioc_transfer &t) { static const double err_prob = std::stod(util::getenv("SPI_ERR_PROB", "-1")); + auto chance = [](double p) { return (static_cast(rand()) / RAND_MAX) < p; }; + auto corrupt = [&](uint64_t buf) { + for (uint32_t i = 0; i < t.len; i++) { + if (chance(0.1)) ((uint8_t*)buf)[i] = (uint8_t)(rand() % 256); + } + }; - if (err_prob > 0) { - if ((static_cast(rand()) / RAND_MAX) < err_prob) { - printf("transfer len error\n"); - t.len = rand() % SPI_BUF_SIZE; - } - if ((static_cast(rand()) / RAND_MAX) < err_prob && t.tx_buf != (uint64_t)NULL) { - printf("corrupting TX\n"); - for (int i = 0; i < t.len; i++) { - if ((static_cast(rand()) / RAND_MAX) > 0.9) { - ((uint8_t*)t.tx_buf)[i] = (uint8_t)(rand() % 256); - } - } - } + if (err_prob > 0 && chance(err_prob)) { + printf("transfer len error\n"); + t.len = rand() % SPI_BUF_SIZE; + } + if (err_prob > 0 && chance(err_prob) && t.tx_buf == (uint64_t)tx_buf) { + printf("corrupting TX\n"); + corrupt(t.tx_buf); } int ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &t); - - if (err_prob > 0) { - if ((static_cast(rand()) / RAND_MAX) < err_prob && t.rx_buf != (uint64_t)NULL) { - printf("corrupting RX\n"); - for (int i = 0; i < t.len; i++) { - if ((static_cast(rand()) / RAND_MAX) > 0.9) { - ((uint8_t*)t.rx_buf)[i] = (uint8_t)(rand() % 256); - } - } - } + if (err_prob > 0 && chance(err_prob)) { + printf("ioctl error\n"); + ret = -1; } + if (err_prob > 0 && chance(err_prob)) { + printf("corrupting RX\n"); + corrupt(t.rx_buf); + } return ret; } -int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout) { - int ret; - uint16_t rx_data_len; +int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint32_t id, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, double deadline) { + static const auto poll = [] { + std::array p; + p.fill(SPI_POLL); + return p; + }(); + LockEx lock(spi_fd, hw_lock); - // needs to be less, since we need to have space for the checksum - assert(tx_len < SPI_BUF_SIZE); - assert(max_rx_len < SPI_BUF_SIZE); + const uint32_t rx_len = sizeof(spi_resp_header) + max_rx_len + SPI_CRC_SIZE; + const uint32_t req_len = sizeof(header) + tx_len; + assert(req_len + SPI_CRC_SIZE <= SPI_BUF_SIZE); + assert(rx_len <= SPI_BUF_SIZE); xfer_count++; header = { - .sync = SPI_SYNC, .endpoint = endpoint, + .id = id, .tx_len = tx_len, - .max_rx_len = max_rx_len + .rx_len = (uint16_t)rx_len, }; - spi_ioc_transfer transfer = { - .tx_buf = (uint64_t)tx_buf, - .rx_buf = (uint64_t)rx_buf + auto xfer = [&](const uint8_t *tx, uint32_t len) { + spi_ioc_transfer t = {.tx_buf = (uint64_t)tx, .rx_buf = (uint64_t)rx_buf, .len = len}; + int ret = lltransfer(t); + if (ret < 0) { + SPILOG(LOGE, "SPI: transfer failed"); + } + return ret; }; - // Send header - memcpy(tx_buf, &header, sizeof(header)); - add_checksum(tx_buf, sizeof(header)); - transfer.len = sizeof(header) + 1; - ret = lltransfer(transfer); - if (ret < 0) { - SPILOG(LOGE, "SPI: failed to send header"); - goto fail; - } + double alive = millis_since_boot(); + while (true) { + memcpy(tx_buf, &header, sizeof(header)); + if (tx_data != NULL) { + memcpy(tx_buf + sizeof(header), tx_data, tx_len); + } + put_crc(tx_buf, req_len); + if (xfer(tx_buf, req_len + SPI_CRC_SIZE) >= 0 && rx_buf[0] == SPI_SLAVE_READY) { + alive = millis_since_boot(); + do { + if (xfer(poll.data(), rx_len) < 0) { + break; + } + if (rx_buf[0] == SPI_SLAVE_READY) { + alive = millis_since_boot(); + break; + } - // Wait for (N)ACK - ret = wait_for_ack(SPI_HACK, 0x11, timeout, 1); - if (ret < 0) { - goto fail; - } + spi_resp_header resp; + memcpy(&resp, rx_buf, sizeof(resp)); + if ((resp.status == SPI_ACK || resp.status == SPI_NACK) && resp.len <= max_rx_len && check_crc(rx_buf, sizeof(resp) + resp.len)) { + if (resp.id == header.id) { + if (resp.status == SPI_NACK) { + SPILOG(LOGD, "SPI: got NACK"); + return SpiError::NACK; + } + if (rx_data != NULL) { + memcpy(rx_data, rx_buf + sizeof(resp), resp.len); + } + return resp.len; + } + alive = millis_since_boot(); + break; + } + } while (millis_since_boot() < std::min(deadline, alive + SPI_SILENT_TIMEOUT)); + } - // Send data - if (tx_data != NULL) { - memcpy(tx_buf, tx_data, tx_len); - } - add_checksum(tx_buf, tx_len); - transfer.len = tx_len + 1; - ret = lltransfer(transfer); - if (ret < 0) { - SPILOG(LOGE, "SPI: failed to send data"); - goto fail; - } - - // Wait for (N)ACK - ret = wait_for_ack(SPI_DACK, 0x13, timeout, 3); - if (ret < 0) { - goto fail; - } - - // Read data - rx_data_len = *(uint16_t *)(rx_buf+1); - if (rx_data_len >= SPI_BUF_SIZE) { - SPILOG(LOGE, "SPI: RX data len larger than buf size %d", rx_data_len); - goto fail; - } - - transfer.len = rx_data_len + 1; - transfer.rx_buf = (uint64_t)(rx_buf + 2 + 1); - ret = lltransfer(transfer); - if (ret < 0) { - SPILOG(LOGE, "SPI: failed to read rx data"); - goto fail; - } - if (!check_checksum(rx_buf, rx_data_len + 4)) { - SPILOG(LOGE, "SPI: bad checksum"); - goto fail; - } - - if (rx_data != NULL) { - memcpy(rx_data, rx_buf + 3, rx_data_len); - } - - return rx_data_len; - -fail: - // ensure slave is in a consistent state - // and ready for the next transfer - int nack_cnt = 0; - while (nack_cnt < 3) { - if (wait_for_ack(SPI_NACK, 0x14, 1, SPI_BUF_SIZE/2) == 0) { - nack_cnt += 1; - } else { - nack_cnt = 0; + if (millis_since_boot() > deadline) { + SPILOG(LOGW, "SPI: timed out"); + return SpiError::ACK_TIMEOUT; + } + if (millis_since_boot() > alive + SPI_SILENT_TIMEOUT) { + return SpiError::SILENT; } } - - if (ret >= 0) ret = -1; - return ret; } #endif diff --git a/iqpilot/selfdrive/pandad/tests/test_ignition_hold.cc b/iqpilot/selfdrive/pandad/tests/test_ignition_hold.cc new file mode 100644 index 00000000..2601d89c --- /dev/null +++ b/iqpilot/selfdrive/pandad/tests/test_ignition_hold.cc @@ -0,0 +1,57 @@ +// Copyright (c) 2026 IQ.Lvbs. All rights reserved. +#define CATCH_CONFIG_MAIN + +#include "catch2/catch.hpp" +#include "selfdrive/pandad/pandad.h" + +constexpr uint64_t SEC = 1000ULL * 1000ULL * 1000ULL; + +TEST_CASE("ignition is off until the line has been seen high") { + IgnitionLineHold hold; + REQUIRE_FALSE(hold.update(false, false, 1 * SEC)); + REQUIRE_FALSE(hold.update(false, false, 10 * SEC)); +} + +TEST_CASE("a rising edge is reported immediately") { + IgnitionLineHold hold; + REQUIRE(hold.update(true, false, 1 * SEC)); +} + +TEST_CASE("dropouts shorter than the hold keep ignition on while the rx checks are valid") { + IgnitionLineHold hold; + REQUIRE(hold.update(true, false, 100 * SEC)); + REQUIRE(hold.update(false, false, 100 * SEC + SEC / 10)); + REQUIRE(hold.update(false, false, 102 * SEC + 3 * SEC / 10)); + REQUIRE(hold.update(true, false, 102 * SEC + 4 * SEC / 10)); + REQUIRE(hold.update(false, false, 102 * SEC + 5 * SEC / 10)); +} + +TEST_CASE("a loss lasting the full hold turns ignition off") { + IgnitionLineHold hold; + REQUIRE(hold.update(true, false, 100 * SEC)); + REQUIRE(hold.update(false, false, 100 * SEC + IGNITION_LINE_HOLD_NS - 1)); + REQUIRE_FALSE(hold.update(false, false, 100 * SEC + IGNITION_LINE_HOLD_NS)); + REQUIRE_FALSE(hold.update(false, false, 200 * SEC)); +} + +TEST_CASE("a car powering off ends ignition the moment the rx checks fail") { + IgnitionLineHold hold; + REQUIRE(hold.update(true, false, 100 * SEC)); + REQUIRE(hold.update(false, false, 100 * SEC + SEC / 5)); + REQUIRE(hold.update(false, false, 101 * SEC)); + REQUIRE_FALSE(hold.update(false, true, 101 * SEC + SEC / 10)); + REQUIRE_FALSE(hold.update(false, false, 101 * SEC + SEC / 5)); +} + +TEST_CASE("failed rx checks do not end ignition while the line is high") { + IgnitionLineHold hold; + REQUIRE(hold.update(true, false, 100 * SEC)); + REQUIRE(hold.update(true, true, 101 * SEC)); +} + +TEST_CASE("ignition comes back on the next high after a real loss") { + IgnitionLineHold hold; + REQUIRE(hold.update(true, false, 100 * SEC)); + REQUIRE_FALSE(hold.update(false, false, 110 * SEC)); + REQUIRE(hold.update(true, false, 111 * SEC)); +} diff --git a/iqpilot/selfdrive/pandad/tests/test_pandad_spi.py b/iqpilot/selfdrive/pandad/tests/test_pandad_spi.py index 24edafd3..1ff7c28f 100644 --- a/iqpilot/selfdrive/pandad/tests/test_pandad_spi.py +++ b/iqpilot/selfdrive/pandad/tests/test_pandad_spi.py @@ -36,9 +36,10 @@ class TestBoarddSpi: st = time.monotonic() ts = {s: list() for s in socks.keys()} - for _ in range(int(os.getenv("TEST_TIME", "20"))): + test_time = int(os.getenv("TEST_TIME", "20")) + for i in range(test_time + 2): # send some CAN messages - if not JUNGLE_SPAM: + if not JUNGLE_SPAM and i < test_time: sent = send_random_can_messages(sendcan, random.randrange(2, 20)) for k, v in sent.items(): sent_msgs[k].extend(list(v)) @@ -65,8 +66,7 @@ class TestBoarddSpi: continue key = (msg.address, msg.dat) assert key in sent_msgs[msg.src], f"got unexpected msg: {msg.src=} {msg.address=} {msg.dat=}" - # TODO: enable this - #sent_msgs[msg.src].remove(key) + sent_msgs[msg.src].remove(key) total_recv_count += 1 elif service == "pandaStates": assert len(m.pandaStates) == 1 @@ -100,3 +100,5 @@ class TestBoarddSpi: with subtests.test(msg="CAN traffic"): print(f"Sent {total_sent_count} CAN messages, got {total_recv_count} back. {total_recv_count/(total_sent_count+1e-4):.2%} received") assert total_recv_count > 20 + + assert all(len(v) == 0 for v in sent_msgs.values()) diff --git a/iqpilot/selfdrive/selfdrived/alerts_offroad.json b/iqpilot/selfdrive/selfdrived/alerts_offroad.json index 5db65522..bb447360 100644 --- a/iqpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/iqpilot/selfdrive/selfdrived/alerts_offroad.json @@ -77,5 +77,9 @@ "Offroad_EgpuUsbSlow": { "text": "eGPU dock USB link is slow. Check the USB cable. The current speed is %1.", "severity": 0 + }, + "Offroad_ModelFallbackDefault": { + "severity": 0, + "text": "Your selected driving model could not be verified, so iqpilot is temporarily running the default model. It will switch back on its own once the model is available." } } diff --git a/iqpilot/selfdrive/selfdrived/selfdrived.py b/iqpilot/selfdrive/selfdrived/selfdrived.py index 9118ecbd..d1b79eef 100755 --- a/iqpilot/selfdrive/selfdrived/selfdrived.py +++ b/iqpilot/selfdrive/selfdrived/selfdrived.py @@ -53,7 +53,12 @@ TurnDirection = custom.IQTurnSignalDirection IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput) -NON_BLOCKING_PROCESSES = {'mapd', 'iqmapd', 'navd', 'navincidentd', 'navrenderd', 'iqsentryd', 'k3wgd'} + +def rx_checks_failed_while_powered(panda_state) -> bool: + # a car powering off stops its bus, which fails the rx checks a moment after its ignition line falls + return bool(panda_state.safetyRxChecksInvalid and (panda_state.ignitionLine or panda_state.ignitionCan)) + +NON_BLOCKING_PROCESSES = {'mapd', 'iqmapd', 'navd', 'navincidentd', 'navrenderd', 'iqsentryd', 'k3wgd', 'webrtcd', 'stream_encoderd'} def _cleanup_startup_params(CP: car.CarParams, params: Params) -> None: @@ -439,7 +444,7 @@ class SelfdriveD(GapButtonActions): # safety mismatch allows some time for pandad to set the safety mode and publish it back from panda if (safety_mismatch and self.sm.frame*DT_CTRL > 10.) or \ - pandaState.safetyRxChecksInvalid or \ + rx_checks_failed_while_powered(pandaState) or \ self.mismatch_counter >= 200: self.events.add(EventName.controlsMismatch) diff --git a/iqpilot/selfdrive/selfdrived/tests/test_longitudinal_settings.py b/iqpilot/selfdrive/selfdrived/tests/test_longitudinal_settings.py index 81fefa40..d321d645 100644 --- a/iqpilot/selfdrive/selfdrived/tests/test_longitudinal_settings.py +++ b/iqpilot/selfdrive/selfdrived/tests/test_longitudinal_settings.py @@ -12,7 +12,6 @@ from iqpilot.selfdrive.longitudinal_settings import ( PERSONALITY_AGGRESSIVE, PERSONALITY_RELAXED, PERSONALITY_STANDARD, - PERSONALITY_VALUES, apply_longitudinal_mode, get_follow_distance_state, get_longitudinal_mode, @@ -57,13 +56,13 @@ def test_personality_writer_rejects_stock_value(): assert params.personality_writes == [] -def test_mode_paths_only_write_valid_personalities(): +def test_mode_paths_never_rewrite_a_valid_personality(): params = Params(PERSONALITY_AGGRESSIVE) for mode in range(4): apply_longitudinal_mode(params, mode) - assert all(value in PERSONALITY_VALUES for value in params.personality_writes) + assert params.personality_writes == [] def test_stock_mode_preserves_personality_and_dynamic_restores_it(): @@ -92,18 +91,39 @@ def test_stock_mode_sanitizes_legacy_stock_personality_value(): assert params.personality_writes == [PERSONALITY_RELAXED] -def test_chill_mode_forces_relaxed_personality(): +def test_chill_mode_keeps_personality_selectable(): params = Params(PERSONALITY_AGGRESSIVE) apply_longitudinal_mode(params, LONGITUDINAL_MODE_CHILL) - assert get_follow_distance_state(params) == (PERSONALITY_RELAXED, False) + assert get_longitudinal_mode(params) == LONGITUDINAL_MODE_CHILL + assert get_follow_distance_state(params) == (PERSONALITY_AGGRESSIVE, True) + assert get_runtime_personality(params) == PERSONALITY_AGGRESSIVE + assert params.values["LongitudinalPersonality"] == PERSONALITY_AGGRESSIVE + assert params.personality_writes == [] + + +def test_chill_mode_sanitizes_legacy_stock_personality_value(): + params = Params(3) + + apply_longitudinal_mode(params, LONGITUDINAL_MODE_CHILL) + assert get_runtime_personality(params) == PERSONALITY_RELAXED - assert params.values["LongitudinalPersonality"] == PERSONALITY_RELAXED + assert get_follow_distance_state(params) == (PERSONALITY_RELAXED, True) assert params.personality_writes == [PERSONALITY_RELAXED] -def test_dynamic_and_pilot_enable_valid_personality_selection(): +def test_personality_survives_mode_round_trip(): + params = Params(PERSONALITY_STANDARD) + + for mode in (LONGITUDINAL_MODE_CHILL, LONGITUDINAL_MODE_PILOT, LONGITUDINAL_MODE_CHILL, LONGITUDINAL_MODE_DYNAMIC, LONGITUDINAL_MODE_CHILL): + apply_longitudinal_mode(params, mode) + assert get_runtime_personality(params) == PERSONALITY_STANDARD + + assert params.personality_writes == [] + + +def test_iq_modes_enable_valid_personality_selection(): params = Params(PERSONALITY_STANDARD) assert get_follow_distance_state(params) == (PERSONALITY_STANDARD, True) @@ -112,6 +132,11 @@ def test_dynamic_and_pilot_enable_valid_personality_selection(): assert get_follow_distance_state(params) == (PERSONALITY_STANDARD, True) + params.values["ExperimentalMode"] = False + + assert get_longitudinal_mode(params) == LONGITUDINAL_MODE_CHILL + assert get_follow_distance_state(params) == (PERSONALITY_STANDARD, True) + def test_onroad_cycles_only_between_iq_modes(): assert next_longitudinal_mode(LONGITUDINAL_MODE_CHILL, True, True) == LONGITUDINAL_MODE_DYNAMIC diff --git a/iqpilot/selfdrive/selfdrived/tests/test_rx_checks_mismatch.py b/iqpilot/selfdrive/selfdrived/tests/test_rx_checks_mismatch.py new file mode 100644 index 00000000..2d918672 --- /dev/null +++ b/iqpilot/selfdrive/selfdrived/tests/test_rx_checks_mismatch.py @@ -0,0 +1,23 @@ +import pytest + +from iqpilot.cereal import log +from iqpilot.selfdrive.selfdrived.selfdrived import rx_checks_failed_while_powered + + +def panda_state(rx_invalid, ignition_line, ignition_can=False): + ps = log.PandaState.new_message() + ps.safetyRxChecksInvalid = rx_invalid + ps.ignitionLine = ignition_line + ps.ignitionCan = ignition_can + return ps + + +@pytest.mark.parametrize("rx_invalid,line,can,expected", [ + (True, True, False, True), + (True, False, True, True), + (True, False, False, False), + (False, True, False, False), + (False, False, False, False), +]) +def test_rx_check_failure_only_counts_while_the_car_is_powered(rx_invalid, line, can, expected): + assert rx_checks_failed_while_powered(panda_state(rx_invalid, line, can)) is expected diff --git a/iqpilot/selfdrive/test/process_replay/model_replay.py b/iqpilot/selfdrive/test/process_replay/model_replay.py index f37cad48..1d5fbab4 100755 --- a/iqpilot/selfdrive/test/process_replay/model_replay.py +++ b/iqpilot/selfdrive/test/process_replay/model_replay.py @@ -57,7 +57,7 @@ def trim_logs(logs, start_frame, end_frame, frs_types, include_all_types): def model_replay(lr, frs): # modeld is using frame pairs modeld_logs = trim_logs(lr, START_FRAME, END_FRAME, {"roadCameraState", "wideRoadCameraState"}, - {"roadEncodeIdx", "wideRoadEncodeIdx", "carParams", "carState", "carControl", "can"}) + {"roadEncodeIdx", "wideRoadEncodeIdx", "carParams", "carState", "carControl", "controlsState", "can"}) dmodeld_logs = trim_logs(lr, START_FRAME, END_FRAME, {"driverCameraState"}, {"driverEncodeIdx", "carParams", "can"}) if not SEND_EXTRA_INPUTS: diff --git a/iqpilot/selfdrive/test/process_replay/process_replay.py b/iqpilot/selfdrive/test/process_replay/process_replay.py index 5c545107..1dfb4b85 100755 --- a/iqpilot/selfdrive/test/process_replay/process_replay.py +++ b/iqpilot/selfdrive/test/process_replay/process_replay.py @@ -509,7 +509,8 @@ CONFIGS = [ ), ProcessConfig( proc_name="modeld", - pubs=["deviceState", "roadCameraState", "wideRoadCameraState", "extrinsicsCalibration", "lateralDelay", "driverMonitoringState", "carState", "carControl"], + pubs=["deviceState", "roadCameraState", "wideRoadCameraState", "extrinsicsCalibration", "lateralDelay", + "driverMonitoringState", "carState", "carControl", "controlsState"], subs=["modelV2", "drivingModelData", "cameraOdometry"], ignore=["logMonoTime", "modelV2.frameDropPerc", "modelV2.modelExecutionTime", "drivingModelData.frameDropPerc", "drivingModelData.modelExecutionTime"], should_recv_callback=ModeldCameraSyncRcvCallback(), diff --git a/iqpilot/selfdrive/ui/layouts/settings/software.py b/iqpilot/selfdrive/ui/layouts/settings/software.py index c7594a8a..8421b8e0 100644 --- a/iqpilot/selfdrive/ui/layouts/settings/software.py +++ b/iqpilot/selfdrive/ui/layouts/settings/software.py @@ -189,12 +189,12 @@ class SoftwareLayout(Widget): # Start checking for updates self._waiting_for_updater = True self._waiting_start_ts = time.monotonic() - os.system("pkill -SIGUSR1 -f system.updated.updated") + ui_state.params.put("UpdaterManualRequest", "check") else: # Start downloading self._waiting_for_updater = True self._waiting_start_ts = time.monotonic() - os.system("pkill -SIGHUP -f system.updated.updated") + ui_state.params.put("UpdaterManualRequest", "fetch") def _on_uninstall(self): def handle_uninstall_confirmation(result): @@ -229,7 +229,7 @@ class SoftwareLayout(Widget): selection = self._branch_dialog.selection ui_state.params.put("UpdaterTargetBranch", selection) self._branch_btn.action_item.set_value(selection) - os.system("pkill -SIGUSR1 -f system.updated.updated") + ui_state.params.put("UpdaterManualRequest", "check") self._branch_dialog = None gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection) @@ -252,7 +252,7 @@ class SoftwareLayout(Widget): return git_creds.set_credentials(username, token) self._refresh_auth_value() - os.system("pkill -SIGUSR1 -f system.updated.updated") + ui_state.params.put("UpdaterManualRequest", "check") open_text_prompt(tr("Git token / password"), tr("leave username and token blank to clear"), diff --git a/iqpilot/selfdrive/ui/layouts/settings/toggles.py b/iqpilot/selfdrive/ui/layouts/settings/toggles.py index f9bc3c8d..4480f0c6 100644 --- a/iqpilot/selfdrive/ui/layouts/settings/toggles.py +++ b/iqpilot/selfdrive/ui/layouts/settings/toggles.py @@ -45,8 +45,8 @@ DESCRIPTIONS = { ), "RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in Konn3kt."), "LongitudinalControlMode": tr_noop( - "Choose longitudinal behavior: IQ.Pilot (IQ longitudinal + end-to-end), " - "IQ.Chill (IQ longitudinal + relaxed personality), or Stock ACC." + "Choose longitudinal behavior: IQ.Pilot (IQ longitudinal + end-to-end), " + + "IQ.Chill (IQ longitudinal + traffic-aware cruise), or Stock ACC." ), "IQTelemetryEnabled": tr_noop( "Once a day, sends settings, versions, car model and coarse usage counts under a random install ID. " + @@ -82,8 +82,10 @@ class TogglesLayout(Widget): ), "IQGasOverrideBoost": ( lambda: tr("Gas Override Boost"), - tr_noop("Off by default. Holding the gas above 10 mph while engaged slowly raises IQ.Pilot's acceleration by up to " + - "1.0 m/s², kept until you disengage; it bleeds off below 10 mph. Takes effect on the next drive."), + tr_noop("Off by default. When the end-to-end model limits acceleration, holding the gas above 10 mph while engaged " + + "adds up to 0.05 m/s² per press, at 0.025 m/s² per second, up to 0.2 m/s² total. " + + "Boost fades during stronger braking, bleeds off below 10 mph, and resets when you disengage. " + + "Takes effect on the next drive."), "disengage_on_accelerator.png", False, ), @@ -226,7 +228,7 @@ class TogglesLayout(Widget): description += "

" + availability_note + "" longitudinal_control_item.set_description(description) - personality_enabled = iq_modes_selectable and mode_index == 2 + personality_enabled = iq_modes_selectable and mode_index != 0 self._long_personality_setting.action_item.set_enabled(personality_enabled) # TODO: make a param control list item so we don't need to manage internal state as much here diff --git a/iqpilot/selfdrive/ui/lib/nav_search.py b/iqpilot/selfdrive/ui/lib/nav_search.py index 5e320982..0a3c003c 100644 --- a/iqpilot/selfdrive/ui/lib/nav_search.py +++ b/iqpilot/selfdrive/ui/lib/nav_search.py @@ -233,7 +233,6 @@ def set_destination(lat: float, lon: float, name: str) -> None: params.put_bool("NavigationActive", False) # NavigationDestination is a JSON-typed param: pass the object, not a pre-serialized string. params.put("NavigationDestination", {"latitude": float(lat), "longitude": float(lon), "name": name or ""}) - params.put_bool("NavigationEnabled", True) def cancel_navigation() -> None: diff --git a/iqpilot/selfdrive/ui/lib/nnff_settings.py b/iqpilot/selfdrive/ui/lib/nnff_settings.py new file mode 100644 index 00000000..1fe48b19 --- /dev/null +++ b/iqpilot/selfdrive/ui/lib/nnff_settings.py @@ -0,0 +1,28 @@ +# Copyright (c) 2026 IQ.Lvbs. All rights reserved. +import os +import unicodedata + +from iqpilot.cereal import car +from iqpilot.selfdrive.controls.lib.latcontrol_torque import get_nn_model_path, MOCK_MODEL_PATH +from iqpilot.selfdrive.controls.lib.latcontrol_torque_v0 import is_vw_mqb_torque +from iqpilot.system.ui.lib.multilang import tr + + +def nnff_availability(cp, params) -> tuple[bool, str]: + if cp is None or cp.carFingerprint in ("", "MOCK"): + return False, tr("connect vehicle to check availability") + bundle = params.get("CarPlatformBundle") + if bundle and bundle.get("platform", cp.carFingerprint) != cp.carFingerprint: + return False, tr("connect selected vehicle") + if (cp.steerControlType != car.CarParams.SteerControlType.torque or + cp.lateralTuning.which() != "torque"): + return False, tr("unavailable for this steering system") + if cp.brand == "volkswagen": + from iqdbc.car.volkswagen.values import VolkswagenFlags + if cp.flags & VolkswagenFlags.PQ or is_vw_mqb_torque(cp): + return False, tr("unavailable for this steering system") + path, name, _ = get_nn_model_path(cp) + if path == MOCK_MODEL_PATH or not os.path.isfile(path): + return False, tr("no matching model installed") + label = unicodedata.normalize("NFD", name).encode("ascii", "ignore").decode("ascii").replace("_", " ") + return True, f"{label}: {tr('changes apply next drive')}" diff --git a/iqpilot/selfdrive/ui/mici/layouts/settings/iq_widgets.py b/iqpilot/selfdrive/ui/mici/layouts/settings/iq_widgets.py index 5730db11..d2200915 100644 --- a/iqpilot/selfdrive/ui/mici/layouts/settings/iq_widgets.py +++ b/iqpilot/selfdrive/ui/mici/layouts/settings/iq_widgets.py @@ -91,13 +91,11 @@ class FollowDistanceSelector(BigMultiToggle): self.set_enabled(enabled) def _handle_mouse_release(self, mouse_pos): - if get_longitudinal_mode(self._params) != LONGITUDINAL_MODE_PILOT: - return - BigButton._handle_mouse_release(self, mouse_pos) - selection, _ = get_follow_distance_state(self._params) - if selection is None: + selection, enabled = get_follow_distance_state(self._params) + if not enabled: self.refresh() return + BigButton._handle_mouse_release(self, mouse_pos) next_selection = self.VALUES[(self.VALUES.index(selection) + 1) % len(self.VALUES)] set_valid_personality(self._params, next_selection) self.set_value(self._display_options[self.VALUES.index(next_selection)]) diff --git a/iqpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py b/iqpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py index e55ffa4a..8e1611e7 100644 --- a/iqpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py +++ b/iqpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py @@ -10,7 +10,7 @@ from iqpilot.system.ui.widgets.scroller import Scroller, draw_scroller_edge_fade from iqpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici from iqpilot.selfdrive.ui.mici.layouts.settings.network.esim_ui import EsimUIMici from iqpilot.selfdrive.ui.mici.layouts.settings.network.speed_test_ui import SpeedTestMici -from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigParamControl, BigMultiToggle, BigToggle +from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigParamControl, BigToggle from iqpilot.selfdrive.ui.mici.widgets.stock_dialog import BigInputDialog from iqpilot.selfdrive.ui.ui_state import ui_state from iqpilot.system.ui.lib.application import gui_app @@ -92,21 +92,11 @@ class NetworkLayoutMici(NavWidget): self._wireguard_installed = wireguard.installed() self._wireguard_btn.set_visible(lambda: self._wireguard_installed) - # ******** Network Metered ******** - self._metered_options = [tr("default"), tr("metered"), tr("unmetered")] - - def network_metered_callback(value: str): + def network_metered_callback(enabled: bool): self._network_metered_btn.set_enabled(False) - metered = { - self._metered_options[0]: MeteredType.UNKNOWN, - self._metered_options[1]: MeteredType.YES, - self._metered_options[2]: MeteredType.NO - }.get(value, MeteredType.UNKNOWN) - self._wifi_manager.set_current_network_metered(metered) + self._wifi_manager.set_current_network_metered(MeteredType.YES if enabled else MeteredType.NO) - # TODO: signal for current network metered type when changing networks, this is wrong until you press it once - # TODO: disable when not connected - self._network_metered_btn = BigMultiToggle(tr("network usage"), self._metered_options, select_callback=network_metered_callback) + self._network_metered_btn = BigToggle(tr("Wi-Fi Metered"), toggle_callback=network_metered_callback) self._network_metered_btn.set_enabled(False) wifi_button = BigButton(tr("wi-fi")) @@ -245,13 +235,7 @@ class NetworkLayoutMici(NavWidget): # Update IP address self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or tr("Not connected")) - # Update network metered - self._network_metered_btn.set_value( - { - MeteredType.UNKNOWN: self._metered_options[0], - MeteredType.YES: self._metered_options[1], - MeteredType.NO: self._metered_options[2] - }.get(self._wifi_manager.current_network_metered, self._metered_options[0])) + self._network_metered_btn.set_checked(self._wifi_manager.current_network_metered == MeteredType.YES) def _switch_to_panel(self, panel_type: NetworkPanelType): if panel_type == NetworkPanelType.ESIM: diff --git a/iqpilot/selfdrive/ui/mici/layouts/settings/software.py b/iqpilot/selfdrive/ui/mici/layouts/settings/software.py index c132a9ee..acc2c710 100644 --- a/iqpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/iqpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -102,13 +102,12 @@ class CheckUpdateButton(BigButton): self._state = UpdaterState.WAITING_FOR_UPDATER self.set_icon(self._txt_update_icon) - def run(): - if self.get_value() == "download update": - os.system("pkill -SIGHUP -f system.updated.updated") - else: - os.system("pkill -SIGUSR1 -f system.updated.updated") - - threading.Thread(target=run, daemon=True).start() + # The updater runs in a confined SELinux domain under hidepid=2, so the UI + # cannot find or signal it by PID. Write a param the updater polls instead. + if self.get_value() == "download update": + ui_state.params.put("UpdaterManualRequest", "fetch") + else: + ui_state.params.put("UpdaterManualRequest", "check") def set_value(self, value: str): super().set_value(value) diff --git a/iqpilot/selfdrive/ui/mici/layouts/settings/steering.py b/iqpilot/selfdrive/ui/mici/layouts/settings/steering.py index a3eac8e0..bd57d955 100644 --- a/iqpilot/selfdrive/ui/mici/layouts/settings/steering.py +++ b/iqpilot/selfdrive/ui/mici/layouts/settings/steering.py @@ -2,11 +2,18 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ """ +import pyray as rl + +from iqpilot.common.ui_settings import NO_NUDGE_WARNING from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigParamControl +from iqpilot.selfdrive.ui.mici.widgets.stock_dialog import BigDialogBase from iqpilot.selfdrive.ui.mici.layouts.settings.iq_widgets import MappedParamToggle from iqpilot.selfdrive.ui.ui_state import ui_state -from iqpilot.system.ui.lib.application import gui_app +from iqpilot.system.ui.lib.application import FontWeight, gui_app +from iqpilot.system.ui.widgets.button import SmallButton +from iqpilot.system.ui.widgets.label import MiciLabel from iqpilot.system.ui.widgets.scroller import NavScroller +from iqpilot.system.ui.widgets.slider import SmallSlider from iqpilot.system.ui.lib.multilang import tr @@ -48,11 +55,56 @@ class SabSettingsPanel(NavScroller): self._steer_override.set_enabled(offroad) +class NoNudgeConfirmation(BigDialogBase): + def __init__(self, confirm_callback): + super().__init__() + self._title = MiciLabel(tr("Are you sure?"), font_size=32, font_weight=FontWeight.BOLD) + self._warning = MiciLabel(tr(NO_NUDGE_WARNING), font_size=24, width=int(self.rect.width - 40), + line_height=26, wrap_text=True, elide_right=False) + self._cancel = self._child(SmallButton(tr("cancel"))) + self._cancel.set_click_callback(self.dismiss) + self._slider = self._child(SmallSlider(tr("slide to\nenable"), lambda: self.dismiss(confirm_callback), right_to_left=False)) + self._cancel.set_enabled(lambda: self.enabled and not self.is_dismissing and not self._slider.confirmed) + self._slider.set_enabled(lambda: self.enabled and not self.is_dismissing) + + def _update_state(self): + super()._update_state() + if self.is_dismissing and not self._slider.confirmed: + self._slider.reset() + + def _render(self, rect): + self._title.render(rl.Rectangle(rect.x + 20, rect.y + 18, rect.width - 40, 38)) + self._warning.set_width(int(rect.width - 40)) + self._warning.render(rl.Rectangle(rect.x + 20, rect.y + 58, rect.width - 40, 78)) + self._cancel.set_opacity(1.0 - self._slider.slider_percentage) + self._cancel.set_visible(self._slider.slider_percentage < 0.8) + self._cancel.set_position(rect.x + 8, rect.y + rect.height - self._cancel.rect.height) + self._cancel.render() + self._slider.set_position(rect.x + rect.width - self._slider.rect.width, rect.y + rect.height - self._slider.rect.height) + self._slider.render() + + +class LaneChangeSelector(MappedParamToggle): + def __init__(self): + super().__init__(tr("Auto Lane Change"), "IQLaneChangeTimer", [tr("nudge"), tr("no nudge")], [0, 1]) + + def _enable_no_nudge(self): + self._params.put(self._param, 1) + self.refresh() + + def _handle_mouse_release(self, mouse_pos): + BigButton._handle_mouse_release(self, mouse_pos) + if self._params.get(self._param, return_default=True) == 1: + self._params.put(self._param, 0) + self.refresh() + else: + gui_app.push_widget(NoNudgeConfirmation(self._enable_no_nudge)) + + class LaneChangePanel(NavScroller): def __init__(self): super().__init__() - self._timer = MappedParamToggle(tr("Auto Lane Change"), "IQLaneChangeTimer", - [tr("nudge"), tr("no nudge")], [0, 1]) + self._timer = LaneChangeSelector() self._edge_guard = BigParamControl(tr("Lane Edge Guard"), "IQEdgeGuard") self._scroller.add_widgets([self._timer, self._edge_guard]) diff --git a/iqpilot/selfdrive/ui/mici/layouts/settings/vehicle.py b/iqpilot/selfdrive/ui/mici/layouts/settings/vehicle.py index fc2b907b..ce85c8cf 100644 --- a/iqpilot/selfdrive/ui/mici/layouts/settings/vehicle.py +++ b/iqpilot/selfdrive/ui/mici/layouts/settings/vehicle.py @@ -8,6 +8,7 @@ import pyray as rl from iqpilot.cereal import car from iqpilot.selfdrive.car.vehicle_catalog import load_catalog +from iqpilot.selfdrive.ui.lib.nnff_settings import nnff_availability from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigParamControl from iqpilot.selfdrive.ui.ui_state import ui_state from iqpilot.system.ui.lib.application import gui_app, FontWeight, MousePos @@ -84,6 +85,9 @@ class VehicleLayoutMici(NavScroller): self._tesla_fsd_visualization = BigParamControl(tr("FSD visuals"), "IQTeslaFsdVisualization") self._hkg_reduced_feedback = BigParamControl(tr("reduced steering feedback (beta)"), "IQHkgReducedTorqueFeedback", toggle_callback=self._on_hkg_reduced_feedback) + self._nnff = BigParamControl(tr("NNFF steering"), "NeuralNetworkFeedForward", + toggle_callback=self._on_nnff) + self._nnff_available = False self._brand_widgets = { "toyota": [self._toyota_long], @@ -95,7 +99,7 @@ class VehicleLayoutMici(NavScroller): } self._all_brand_widgets = [w for ws in self._brand_widgets.values() for w in ws] - self._scroller.add_widgets([self._vehicle_btn] + self._all_brand_widgets) + self._scroller.add_widgets([self._vehicle_btn, self._nnff] + self._all_brand_widgets) def _get_current_brand(self) -> str: bundle = ui_state.params.get("CarPlatformBundle") @@ -193,11 +197,45 @@ class VehicleLayoutMici(NavScroller): ui_state.params.put_bool("OnroadCycleRequested", True) def _on_hkg_reduced_feedback(self, checked: bool): + if not ui_state.is_offroad(): + self._hkg_reduced_feedback.refresh() + return + ui_state.params.put_bool("IQHkgReducedTorqueFeedback", checked) if checked: ui_state.params.put_bool("NeuralNetworkFeedForward", False) + self._nnff.refresh() + ui_state.params.put_bool("OnroadCycleRequested", True) + + def _nnff_status(self) -> tuple[bool, str]: + return nnff_availability(ui_state.CP, ui_state.params) + + def _refresh_nnff(self): + self._nnff_available, status = self._nnff_status() + self._nnff.set_value(status) + self._nnff.refresh() + self._nnff.set_enabled(ui_state.is_offroad() and + (self._nnff_available or ui_state.params.get_bool("NeuralNetworkFeedForward"))) + + def _on_nnff(self, checked: bool): + if not ui_state.is_offroad(): + self._nnff.refresh() + return + if checked: + self._nnff_available, status = self._nnff_status() + self._nnff.set_value(status) + if not self._nnff_available: + self._nnff.refresh() + return + # BigParamControl saves after this callback, so persist first or the cycle request restarts controls on the old value. + ui_state.params.put_bool("NeuralNetworkFeedForward", checked) + if checked: + ui_state.params.put_bool("IQHkgReducedTorqueFeedback", False) + self._hkg_reduced_feedback.refresh() + ui_state.params.put_bool("OnroadCycleRequested", True) def _refresh(self): self._vehicle_btn.set_value(self._vehicle_status()) + self._refresh_nnff() brand = self._get_current_brand() offroad = ui_state.is_offroad() @@ -231,6 +269,8 @@ class VehicleLayoutMici(NavScroller): super()._update_state() self._vehicle_btn.set_value(self._vehicle_status()) self._hkg_reduced_feedback.set_enabled(ui_state.is_offroad()) + self._nnff.set_enabled(ui_state.is_offroad() and + (self._nnff_available or ui_state.params.get_bool("NeuralNetworkFeedForward"))) def show_event(self): super().show_event() diff --git a/iqpilot/selfdrive/ui/tests/test_hkg_feedback_settings.py b/iqpilot/selfdrive/ui/tests/test_hkg_feedback_settings.py index da068232..6b243730 100644 --- a/iqpilot/selfdrive/ui/tests/test_hkg_feedback_settings.py +++ b/iqpilot/selfdrive/ui/tests/test_hkg_feedback_settings.py @@ -8,20 +8,38 @@ from iqpilot.cereal import car from iqpilot.selfdrive.ui.mici.layouts.settings import vehicle -def test_feedback_selection_disables_nnff(monkeypatch): - params = Params() +def test_feedback_selection_disables_nnff(monkeypatch, mocker, tmp_path): + params = Params(str(tmp_path)) params.put_bool("NeuralNetworkFeedForward", True) - monkeypatch.setattr(vehicle, "ui_state", SimpleNamespace(params=params)) - vehicle.VehicleLayoutMici._on_hkg_reduced_feedback(None, True) + monkeypatch.setattr(vehicle, "ui_state", SimpleNamespace(params=params, is_offroad=lambda: True)) + layout = mocker.Mock() + vehicle.VehicleLayoutMici._on_hkg_reduced_feedback(layout, True) assert not params.get_bool("NeuralNetworkFeedForward") + assert params.get_bool("IQHkgReducedTorqueFeedback") + assert params.get_bool("OnroadCycleRequested") + layout._nnff.refresh.assert_called_once() -def test_feedback_deselection_preserves_nnff(monkeypatch): - params = Params() +def test_feedback_deselection_preserves_nnff(monkeypatch, mocker, tmp_path): + params = Params(str(tmp_path)) params.put_bool("NeuralNetworkFeedForward", True) - monkeypatch.setattr(vehicle, "ui_state", SimpleNamespace(params=params)) - vehicle.VehicleLayoutMici._on_hkg_reduced_feedback(None, False) + monkeypatch.setattr(vehicle, "ui_state", SimpleNamespace(params=params, is_offroad=lambda: True)) + vehicle.VehicleLayoutMici._on_hkg_reduced_feedback(mocker.Mock(), False) assert params.get_bool("NeuralNetworkFeedForward") + assert not params.get_bool("IQHkgReducedTorqueFeedback") + assert params.get_bool("OnroadCycleRequested") + + +def test_feedback_cannot_change_onroad(monkeypatch, mocker, tmp_path): + params = Params(str(tmp_path)) + params.put_bool("NeuralNetworkFeedForward", True) + monkeypatch.setattr(vehicle, "ui_state", SimpleNamespace(params=params, is_offroad=lambda: False)) + layout = mocker.Mock() + vehicle.VehicleLayoutMici._on_hkg_reduced_feedback(layout, True) + assert params.get_bool("NeuralNetworkFeedForward") + assert not params.get_bool("IQHkgReducedTorqueFeedback") + assert not params.get_bool("OnroadCycleRequested") + layout._hkg_reduced_feedback.refresh.assert_called_once() @pytest.mark.parametrize("brand,angle,tuning,offroad,visible", [ diff --git a/iqpilot/selfdrive/ui/tests/test_lane_change_confirmation.py b/iqpilot/selfdrive/ui/tests/test_lane_change_confirmation.py new file mode 100644 index 00000000..050bd363 --- /dev/null +++ b/iqpilot/selfdrive/ui/tests/test_lane_change_confirmation.py @@ -0,0 +1,110 @@ +from types import SimpleNamespace + +import pytest + +from iqpilot.common.params import Params +from iqpilot.selfdrive.ui.mici.layouts.settings import steering +from iqpilot.system.ui.widgets import DialogResult +from iqpilot.system.ui.lib.application import MouseEvent, MousePos +from iqpilot.system.ui.widgets import slider as slider_module +from iqpilot.ui.layouts.settings import iq_panels + + +def test_set_convergence_defaults_on_and_remains_configurable(tmp_path): + params = Params(str(tmp_path)) + assert params.get_default_value('expSpeedConv') is True + params.put_bool('expSpeedConv', False) + assert not params.get_bool('expSpeedConv') + + +def test_experimental_lead_mpc_defaults_off_and_remains_configurable(tmp_path): + params = Params(str(tmp_path)) + assert params.get_default_value('newLeadMpc') is False + assert not params.get_bool('newLeadMpc') + params.put_bool('newLeadMpc', True) + assert params.get_bool('newLeadMpc') + + +@pytest.mark.parametrize('confirmed', [False, True]) +def test_mici_no_nudge_waits_for_confirmation(monkeypatch, tmp_path, mocker, confirmed): + params = Params(str(tmp_path)) + params.put('IQLaneChangeTimer', 0) + selector = SimpleNamespace(_params=params, _param='IQLaneChangeTimer', refresh=mocker.Mock()) + selector._enable_no_nudge = lambda: steering.LaneChangeSelector._enable_no_nudge(selector) + dialogs = [] + monkeypatch.setattr(steering.BigButton, '_handle_mouse_release', lambda *args: None) + monkeypatch.setattr(steering, 'NoNudgeConfirmation', lambda callback: callback) + monkeypatch.setattr(steering, 'gui_app', SimpleNamespace(push_widget=dialogs.append)) + + steering.LaneChangeSelector._handle_mouse_release(selector, None) + assert params.get('IQLaneChangeTimer') == 0 + assert len(dialogs) == 1 + if confirmed: + dialogs[0]() + assert params.get('IQLaneChangeTimer') == int(confirmed) + + +def test_mici_nudge_does_not_require_confirmation(monkeypatch, tmp_path, mocker): + params = Params(str(tmp_path)) + params.put('IQLaneChangeTimer', 1) + selector = SimpleNamespace(_params=params, _param='IQLaneChangeTimer', refresh=mocker.Mock()) + monkeypatch.setattr(steering.BigButton, '_handle_mouse_release', lambda *args: None) + push = mocker.Mock() + monkeypatch.setattr(steering, 'gui_app', SimpleNamespace(push_widget=push)) + steering.LaneChangeSelector._handle_mouse_release(selector, None) + assert params.get('IQLaneChangeTimer') == 0 + push.assert_not_called() + + +@pytest.mark.parametrize('right_to_left', [False, True]) +@pytest.mark.parametrize('distance', [0, 80, -216, 216]) +def test_slider_requires_a_complete_slide_in_its_direction(monkeypatch, mocker, right_to_left, distance): + monkeypatch.setattr(slider_module, 'gui_app', SimpleNamespace( + font=lambda weight: None, target_fps=20, + texture=lambda path, width, height: SimpleNamespace(width=width, height=height), + )) + monkeypatch.setattr(slider_module, 'UnifiedLabel', mocker.Mock()) + monkeypatch.setattr(slider_module.rl, 'get_time', lambda: 1.0) + confirmed = mocker.Mock() + slider = slider_module.SmallSlider('slide to enable', confirmed, right_to_left=right_to_left) + start_x = 274 if right_to_left else 58 + end_x = start_x + (-distance if right_to_left else distance) + slider._handle_mouse_event(MouseEvent(MousePos(start_x, 50), 0, True, False, True, 1.0)) + slider._handle_mouse_event(MouseEvent(MousePos(end_x, 50), 0, False, False, True, 1.0)) + slider._update_state() + confirmed.assert_not_called() + slider._handle_mouse_event(MouseEvent(MousePos(end_x, 50), 0, False, True, False, 1.0)) + monkeypatch.setattr(slider_module.rl, 'get_time', lambda: 2.0) + for _ in range(30): + slider._update_state() + assert confirmed.call_count == int(distance == 216) + assert slider.slider_percentage == pytest.approx(float(distance == 216), abs=0.001) + + +@pytest.mark.parametrize('result', [DialogResult.CANCEL, DialogResult.CONFIRM]) +def test_big_no_nudge_waits_for_confirmation(monkeypatch, tmp_path, mocker, result): + params = Params(str(tmp_path)) + params.put('IQLaneChangeTimer', 0) + action = mocker.Mock() + panel = SimpleNamespace(_lane_change_setting=SimpleNamespace(action_item=action)) + dialogs = [] + monkeypatch.setattr(iq_panels, 'ui_state', SimpleNamespace(params=params)) + monkeypatch.setattr(iq_panels, 'ConfirmDialog', lambda *args, **kwargs: args) + monkeypatch.setattr(iq_panels, 'gui_app', SimpleNamespace(set_modal_overlay=lambda dialog, callback: dialogs.append(callback))) + + iq_panels.SteeringLayout._set_lane_change_mode(panel, 1) + assert params.get('IQLaneChangeTimer') == 0 + action.set_selected_button.assert_called_once_with(0) + dialogs[0](result) + assert params.get('IQLaneChangeTimer') == int(result == DialogResult.CONFIRM) + + +def test_big_nudge_does_not_require_confirmation(monkeypatch, tmp_path, mocker): + params = Params(str(tmp_path)) + params.put('IQLaneChangeTimer', 1) + monkeypatch.setattr(iq_panels, 'ui_state', SimpleNamespace(params=params)) + overlay = mocker.Mock() + monkeypatch.setattr(iq_panels, 'gui_app', SimpleNamespace(set_modal_overlay=overlay)) + iq_panels.SteeringLayout._set_lane_change_mode(SimpleNamespace(), 0) + assert params.get('IQLaneChangeTimer') == 0 + overlay.assert_not_called() diff --git a/iqpilot/selfdrive/ui/tests/test_mici_navigation.py b/iqpilot/selfdrive/ui/tests/test_mici_navigation.py index a5a35e52..face4863 100644 --- a/iqpilot/selfdrive/ui/tests/test_mici_navigation.py +++ b/iqpilot/selfdrive/ui/tests/test_mici_navigation.py @@ -17,7 +17,7 @@ def scene(monkeypatch): alive = {'iqNavState': True, 'iqNavRenderState': True} valid = {'iqNavState': True, 'iqNavRenderState': True} - params = {'NavigationDestination': {'latitude': 41.0, 'longitude': -87.0}, 'NavigationActive': True} + params = {'NavigationDestination': {'latitude': 41.0, 'longitude': -87.0}, 'NavigationActive': True, 'NavigationEnabled': True} sm = Messages(iqNavState=nav, iqNavRenderState=render) state = SimpleNamespace(started=True, is_metric=False, sm=sm, params=SimpleNamespace(get=params.get, get_bool=lambda key: bool(params.get(key)))) @@ -30,7 +30,7 @@ def scene(monkeypatch): return SimpleNamespace(screen=screen, nav=nav, render=render, sm=sm, state=state, params=params, clock=clock) -@pytest.mark.parametrize('invalid', ['missing', 'malformed', 'cancelled', 'offroad', 'stale', 'invalid_message', +@pytest.mark.parametrize('invalid', ['missing', 'malformed', 'cancelled', 'disabled', 'offroad', 'stale', 'invalid_message', 'inactive', 'wrong_destination', 'no_destination', 'no_maneuver', 'nan', 'negative']) def test_invalid_guidance_returns_to_camera(scene, invalid): assert scene.screen.active @@ -40,6 +40,8 @@ def test_invalid_guidance_returns_to_camera(scene, invalid): scene.params['NavigationDestination'] = {'latitude': float('nan'), 'longitude': -87.0} elif invalid == 'cancelled': scene.params['NavigationActive'] = False + elif invalid == 'disabled': + scene.params['NavigationEnabled'] = False elif invalid == 'offroad': scene.state.started = False elif invalid == 'stale': @@ -123,6 +125,15 @@ def test_guidance_recovers_without_waiting_screen(scene): assert scene.screen._street == 'Turn left onto Main Street' +def test_master_switch_hides_live_guidance_and_can_reenable_it(scene): + scene.params['NavigationEnabled'] = False + scene.screen.update() + assert not scene.screen.active + scene.params['NavigationEnabled'] = True + scene.screen.update() + assert scene.screen.active + + def test_navigation_taps_are_consumed_before_home_callback(scene, mocker): import pyray as rl from iqpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView diff --git a/iqpilot/selfdrive/ui/tests/test_nav_map_visibility.py b/iqpilot/selfdrive/ui/tests/test_nav_map_visibility.py new file mode 100644 index 00000000..067f4941 --- /dev/null +++ b/iqpilot/selfdrive/ui/tests/test_nav_map_visibility.py @@ -0,0 +1,57 @@ +from types import SimpleNamespace + +import pytest + +from iqpilot.ui.onroad import nav_map_panel as nav + + +@pytest.fixture +def scene(monkeypatch, mocker): + params = {'NavigationEnabled': True, 'OnScreenNavigation': True, 'OnlineOSMaps': True, 'OfflineOSMaps': False} + panel = nav.NavMapPanel.__new__(nav.NavMapPanel) + panel._params = SimpleNamespace(get_bool=lambda key: params.get(key, False), put_bool=params.__setitem__) + panel._force_visible = False + panel._last_params_refresh = -float('inf') + panel.active = True + panel._release_providers = mocker.Mock() + clock = SimpleNamespace(now=10.0) + sources = SimpleNamespace(downloaded=False) + monkeypatch.setattr(nav.time, 'monotonic', lambda: clock.now) + monkeypatch.setattr(nav, 'gui_app', SimpleNamespace(big_ui=lambda: True)) + monkeypatch.setattr(nav, 'find_offline_mbtiles_path', lambda: '/maps/offline.mbtiles' if sources.downloaded else None) + monkeypatch.setattr(nav, 'find_offline_xyz_root', lambda: None) + return SimpleNamespace(panel=panel, params=params, clock=clock, sources=sources) + + +@pytest.mark.parametrize('navigation_enabled', [False, True]) +@pytest.mark.parametrize('maps_enabled', [False, True]) +@pytest.mark.parametrize('downloaded', [False, True]) +def test_map_visibility_requires_navigation_even_with_downloaded_maps(scene, navigation_enabled, maps_enabled, downloaded): + scene.params['NavigationEnabled'] = navigation_enabled + scene.params['OnScreenNavigation'] = maps_enabled + scene.sources.downloaded = downloaded + assert scene.panel.maps_enabled() is (navigation_enabled and (maps_enabled or downloaded)) + assert scene.params['NavigationEnabled'] is navigation_enabled + if not navigation_enabled: + assert scene.params['OnScreenNavigation'] is maps_enabled + assert not scene.params['OfflineOSMaps'] + + +def test_disabling_navigation_hides_existing_map_and_releases_tiles(scene): + scene.sources.downloaded = True + assert scene.panel.maps_enabled() + scene.params['NavigationEnabled'] = False + scene.clock.now += nav.PARAMS_REFRESH_S + scene.panel.update() + assert not scene.panel.active + assert not scene.panel.maps_enabled() + scene.panel._release_providers.assert_called_once() + + +def test_enabling_map_display_cannot_bypass_navigation_master_switch(scene): + scene.params['NavigationEnabled'] = False + scene.sources.downloaded = True + scene.panel.set_maps_enabled(True) + assert not scene.panel.maps_enabled() + assert not scene.panel.active + scene.panel._release_providers.assert_called_once() diff --git a/iqpilot/selfdrive/ui/tests/test_nav_search.py b/iqpilot/selfdrive/ui/tests/test_nav_search.py index 3cc96028..5d622027 100644 --- a/iqpilot/selfdrive/ui/tests/test_nav_search.py +++ b/iqpilot/selfdrive/ui/tests/test_nav_search.py @@ -3,6 +3,8 @@ Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed """ from types import SimpleNamespace +import pytest + from iqpilot.selfdrive.ui.lib import nav_search @@ -15,6 +17,18 @@ class FakeParams: }.get(key) +@pytest.mark.parametrize('enabled', [False, True]) +def test_selecting_destination_preserves_navigation_master_switch(monkeypatch, enabled): + values = {'NavigationEnabled': enabled, 'NavigationActive': True, 'AthenaNavigationRoute': {}} + params = SimpleNamespace(put=values.__setitem__, put_bool=values.__setitem__, remove=lambda key: values.pop(key, None)) + monkeypatch.setattr(nav_search, 'Params', lambda: params) + nav_search.set_destination(41.0, -87.0, 'Home') + assert values['NavigationEnabled'] is enabled + assert values['NavigationDestination'] == {'latitude': 41.0, 'longitude': -87.0, 'name': 'Home'} + assert not values['NavigationActive'] + assert 'AthenaNavigationRoute' not in values + + def test_china_search_uses_amap_without_calling_mapbox(monkeypatch): search = nav_search.NavSearch.__new__(nav_search.NavSearch) search._params = FakeParams() diff --git a/iqpilot/selfdrive/ui/tests/test_nnff_big_settings.py b/iqpilot/selfdrive/ui/tests/test_nnff_big_settings.py new file mode 100644 index 00000000..33e33548 --- /dev/null +++ b/iqpilot/selfdrive/ui/tests/test_nnff_big_settings.py @@ -0,0 +1,128 @@ +from types import SimpleNamespace + +import pytest + +from iqpilot.cereal import car +from iqpilot.common.params import Params +from iqpilot.ui.layouts.settings import iq_panels + + +@pytest.fixture +def panel(monkeypatch, mocker, tmp_path): + cp = SimpleNamespace(brand="hyundai", flags=0, carFingerprint="HYUNDAI_KONA_EV", + steerControlType=car.CarParams.SteerControlType.torque, + lateralTuning=SimpleNamespace(which=lambda: "torque")) + state = SimpleNamespace(CP=cp, params=Params(str(tmp_path / "params")), offroad=True) + state.is_offroad = lambda: state.offroad + monkeypatch.setattr(iq_panels, "ui_state", state) + monkeypatch.setattr(iq_panels.gui_app, "font", lambda weight: iq_panels.rl.Font()) + monkeypatch.setattr(iq_panels, "load_catalog", dict) + available = mocker.Mock(return_value=(True, "HYUNDAI KONA EV: changes apply next drive")) + monkeypatch.setattr(iq_panels, "nnff_availability", available) + layout = iq_panels.VehicleLayout() + return SimpleNamespace(layout=layout, state=state, available=available) + + +@pytest.mark.parametrize("saved,available,offroad,expected", [ + (False, True, True, True), + (True, True, True, False), + (True, False, True, False), + (False, False, True, False), + (False, True, False, False), + (True, True, False, True), + (True, False, False, True), +]) +def test_real_iq_toggle_validates_before_saving_and_requesting_cycle(panel, mocker, saved, available, offroad, expected): + state, layout = panel.state, panel.layout + state.params.put_bool("NeuralNetworkFeedForward", saved) + state.params.put_bool("IQHkgReducedTorqueFeedback", True) + panel.available.return_value = available, "model status" + layout._refresh_nnff(force=True) + state.offroad = offroad + put_bool = state.params.put_bool + writes = [] + + def record_put(key, value): + if key == "OnroadCycleRequested": + assert state.params.get_bool("NeuralNetworkFeedForward") is expected + writes.append((key, value)) + put_bool(key, value) + + state.params = mocker.Mock(wraps=state.params) + state.params.put_bool.side_effect = record_put + toggle = layout._nnff.action_item.toggle + assert toggle.param_key is None # No widget autosave may bypass the guarded callback. + toggle._handle_mouse_release(None) + changed = saved != expected + assert toggle.get_state() is expected + assert state.params.get_bool("NeuralNetworkFeedForward") is expected + assert state.params.get_bool("OnroadCycleRequested") is changed + assert state.params.get_bool("IQHkgReducedTorqueFeedback") is not (changed and expected) + if changed: + assert writes[-1] == ("OnroadCycleRequested", True) + else: + assert writes == [] + + +@pytest.mark.parametrize("available,saved,offroad,enabled", [ + (True, False, True, True), + (True, True, False, False), + (False, False, True, False), + (False, True, True, True), + (False, True, False, False), +]) +def test_frame_syncs_saved_state_and_offroad_availability(panel, available, saved, offroad, enabled): + panel.available.return_value = available, "model status" + panel.layout._refresh_nnff(force=True) + panel.state.params.put_bool("NeuralNetworkFeedForward", saved) + panel.state.offroad = offroad + panel.layout._update_state() + assert panel.layout._nnff.action_item.get_state() is saved + assert panel.layout._nnff.action_item.enabled is enabled + + +def test_model_availability_is_cached_until_vehicle_changes_or_panel_opens(panel): + layout, state = panel.layout, panel.state + assert panel.available.call_count == 1 + for _ in range(20): + layout._update_state() + assert panel.available.call_count == 1 + + state.CP.carFingerprint = "HYUNDAI_IONIQ_5" + panel.available.return_value = False, "no matching model installed" + layout._update_state() + assert panel.available.call_count == 2 + assert not layout._nnff.action_item.enabled + assert "no matching model installed" in layout._nnff.description + + state.params.put("CarPlatformBundle", {"platform": "HYUNDAI_KONA_EV", "brand": "hyundai"}) + layout._on_vehicle_changed() + assert panel.available.call_count == 3 + layout._update_state() + assert panel.available.call_count == 3 + + state.CP.flags = 1 + layout._update_state() + assert panel.available.call_count == 4 + panel.available.return_value = True, "HYUNDAI IONIQ 5: changes apply next drive" + layout.show_event() + assert panel.available.call_count == 5 + assert layout._nnff.action_item.enabled + assert "HYUNDAI IONIQ 5" in layout._nnff.description + + +def test_nnff_row_survives_brand_panel_rebuilds(panel, monkeypatch, mocker): + layout = panel.layout + brand_row = iq_panels.toggle_item("Brand setting") + brand_panel = SimpleNamespace(items=[brand_row], update_settings=mocker.Mock()) + monkeypatch.setattr(iq_panels, "brand_settings_for", lambda brand: brand_panel if brand == "toyota" else None) + assert layout.items == [layout._vehicle_item, layout._legend_widget, layout._nnff] + assert layout._nnff.title == "NNFF Steering" + assert "HYUNDAI KONA EV" in layout._nnff.description + panel.state.CP.brand = "toyota" + layout._update_state() + assert layout.items == [layout._vehicle_item, layout._legend_widget, layout._nnff, brand_row] + assert layout._scroller._items == layout.items + panel.state.CP.brand = "hyundai" + layout._update_state() + assert layout.items == [layout._vehicle_item, layout._legend_widget, layout._nnff] diff --git a/iqpilot/selfdrive/ui/tests/test_nnff_settings.py b/iqpilot/selfdrive/ui/tests/test_nnff_settings.py new file mode 100644 index 00000000..681bef77 --- /dev/null +++ b/iqpilot/selfdrive/ui/tests/test_nnff_settings.py @@ -0,0 +1,164 @@ +# Copyright (c) 2026 IQ.Lvbs. All rights reserved. +from types import SimpleNamespace + +import pytest + +from iqdbc.car.volkswagen.values import VolkswagenFlags +from iqpilot.cereal import car +from iqpilot.common.params import Params +from iqpilot.selfdrive.ui.lib import nnff_settings +from iqpilot.selfdrive.ui.mici.layouts.settings import vehicle + + +@pytest.fixture +def state(monkeypatch, tmp_path): + cp = SimpleNamespace(brand="hyundai", flags=0, carFingerprint="HYUNDAI_KONA_EV", carFw=[], + steerControlType=car.CarParams.SteerControlType.torque, + lateralTuning=SimpleNamespace(which=lambda: "torque")) + state = SimpleNamespace(CP=cp, params=Params(str(tmp_path / "params")), is_offroad=lambda: True) + monkeypatch.setattr(vehicle, "ui_state", state) + return state + + +def test_matched_model_available(state, tmp_path, monkeypatch): + model = tmp_path / "HYUNDAI_KONA_EV.json" + model.write_text("{}") + monkeypatch.setattr(nnff_settings, "get_nn_model_path", lambda cp: (str(model), "HYUNDAI_KONA_EV", True)) + available, status = vehicle.VehicleLayoutMici._nnff_status(None) + assert available + assert "HYUNDAI KONA EV" in status + assert "next drive" in status + + +@pytest.mark.parametrize("reason", ["no_vehicle", "mock_vehicle", "selected_vehicle", "angle", "pid", "pq", "mqb", + "mock_model", "missing_model"]) +def test_unavailable_models_and_controllers(state, tmp_path, monkeypatch, reason): + model = tmp_path / "HYUNDAI_KONA_EV.json" + model.write_text("{}") + if reason == "no_vehicle": + state.CP = None + elif reason == "mock_vehicle": + state.CP.carFingerprint = "MOCK" + elif reason == "selected_vehicle": + state.params.put("CarPlatformBundle", {"platform": "HYUNDAI_IONIQ_5"}) + elif reason == "angle": + state.CP.steerControlType = car.CarParams.SteerControlType.angle + elif reason == "pid": + state.CP.lateralTuning.which = lambda: "pid" + elif reason in ("pq", "mqb"): + state.CP.brand = "volkswagen" + state.CP.flags = VolkswagenFlags.PQ if reason == "pq" else 0 + elif reason == "mock_model": + model = nnff_settings.MOCK_MODEL_PATH + else: + model.unlink() + monkeypatch.setattr(nnff_settings, "get_nn_model_path", lambda cp: (str(model), "HYUNDAI_KONA_EV", True)) + available, status = vehicle.VehicleLayoutMici._nnff_status(None) + assert not available + assert status + + +@pytest.mark.parametrize("available,saved,offroad,enabled", [ + (True, False, True, True), + (True, True, False, False), + (False, False, True, False), + (False, True, True, True), + (False, True, False, False), +]) +def test_availability_offroad_and_saved_on_gate(state, mocker, available, saved, offroad, enabled): + state.params.put_bool("NeuralNetworkFeedForward", saved) + state.is_offroad = lambda: offroad + layout = mocker.Mock() + layout._nnff_status.return_value = available, "model status" + vehicle.VehicleLayoutMici._refresh_nnff(layout) + assert layout._nnff_available is available + layout._nnff.set_value.assert_called_once_with("model status") + layout._nnff.set_enabled.assert_called_once_with(enabled) + layout._nnff.refresh.assert_called_once() + + +@pytest.mark.parametrize("checked", [True, False]) +def test_changes_saved_before_cycle_requested(state, mocker, checked): + state.params.put_bool("NeuralNetworkFeedForward", not checked) + state.params.put_bool("IQHkgReducedTorqueFeedback", True) + put_bool = state.params.put_bool + events = [] + + def record_put(key, value): + if key == "OnroadCycleRequested": + assert state.params.get_bool("NeuralNetworkFeedForward") is checked + events.append((key, value)) + put_bool(key, value) + + state.params = mocker.Mock(wraps=state.params) + state.params.put_bool.side_effect = record_put + layout = mocker.Mock() + layout._nnff_status.return_value = True, "model" + vehicle.VehicleLayoutMici._on_nnff(layout, checked) + assert state.params.get_bool("NeuralNetworkFeedForward") is checked + assert state.params.get_bool("OnroadCycleRequested") + assert events[-1] == ("OnroadCycleRequested", True) + assert state.params.get_bool("IQHkgReducedTorqueFeedback") is not checked + assert layout._hkg_reduced_feedback.refresh.call_count == int(checked) + layout._nnff.refresh.assert_not_called() + + +@pytest.mark.parametrize("offroad,available", [(False, True), (True, False)]) +def test_cannot_enable_onroad_or_unavailable(state, mocker, offroad, available): + state.is_offroad = lambda: offroad + layout = mocker.Mock() + layout._nnff_status.return_value = available, "unavailable" + vehicle.VehicleLayoutMici._on_nnff(layout, True) + assert not state.params.get_bool("NeuralNetworkFeedForward") + assert not state.params.get_bool("OnroadCycleRequested") + layout._nnff.refresh.assert_called_once() + + +def test_can_disable_unavailable_model(state, mocker): + state.params.put_bool("NeuralNetworkFeedForward", True) + layout = mocker.Mock() + layout._nnff_status.return_value = False, "missing model" + vehicle.VehicleLayoutMici._on_nnff(layout, False) + assert not state.params.get_bool("NeuralNetworkFeedForward") + assert state.params.get_bool("OnroadCycleRequested") + layout._nnff_status.assert_not_called() + + +@pytest.mark.parametrize("saved,available,offroad,expected", [ + (False, True, True, True), + (True, True, True, False), + (True, False, True, False), + (False, False, True, False), + (True, True, False, True), +]) +def test_real_param_control_saves_callback_result(state, monkeypatch, mocker, saved, available, offroad, expected): + monkeypatch.setattr(vehicle.BigButton, "_handle_mouse_release", lambda self, pos: None) + state.is_offroad = lambda: offroad + state.params.put_bool("NeuralNetworkFeedForward", saved) + control = vehicle.BigParamControl.__new__(vehicle.BigParamControl) + control.params = state.params + control.param = "NeuralNetworkFeedForward" + control._checked = saved + control.set_value = mocker.Mock() + layout = mocker.Mock() + layout._nnff = control + layout._nnff_status.return_value = available, "model status" + control._toggle_callback = lambda checked: vehicle.VehicleLayoutMici._on_nnff(layout, checked) + control._handle_mouse_release(None) + assert control._checked is expected + assert state.params.get_bool("NeuralNetworkFeedForward") is expected + assert state.params.get_bool("OnroadCycleRequested") is (saved != expected) + + +def test_frame_updates_do_not_scan_model_directory(state, monkeypatch, mocker): + monkeypatch.setattr(vehicle.NavScroller, "_update_state", lambda self: None) + layout = vehicle.VehicleLayoutMici.__new__(vehicle.VehicleLayoutMici) + layout._vehicle_btn = mocker.Mock() + layout._vehicle_status = mocker.Mock(return_value="Kona") + layout._hkg_reduced_feedback = mocker.Mock() + layout._nnff = mocker.Mock() + layout._nnff_available = True + layout._nnff_status = mocker.Mock() + layout._update_state() + layout._nnff_status.assert_not_called() + layout._nnff.set_enabled.assert_called_once_with(True) diff --git a/iqpilot/selfdrive/ui/tests/test_personality_mode_gate.py b/iqpilot/selfdrive/ui/tests/test_personality_mode_gate.py new file mode 100644 index 00000000..46bec991 --- /dev/null +++ b/iqpilot/selfdrive/ui/tests/test_personality_mode_gate.py @@ -0,0 +1,63 @@ +# Copyright (c) 2026 IQ.Lvbs. All rights reserved. +from types import SimpleNamespace + +import pytest + +from iqpilot.selfdrive.longitudinal_settings import PERSONALITY_AGGRESSIVE, PERSONALITY_STANDARD +from iqpilot.selfdrive.ui.layouts.settings import toggles +from iqpilot.selfdrive.ui.mici.layouts.settings import iq_widgets + + +class FakeParams: + def __init__(self, alpha, experimental, personality=PERSONALITY_STANDARD): + self.values = { + "AlphaLongitudinalEnabled": alpha, + "ExperimentalMode": experimental, + "IQDynamicMode": False, + "IQToyotaFactoryLong": False, + "LongitudinalPersonality": personality, + } + + def get(self, key, return_default=False): + return self.values[key] + + def get_bool(self, key): + return bool(self.values.get(key, False)) + + def put(self, key, value): + self.values[key] = value + + +MODES = pytest.mark.parametrize("alpha,experimental,selectable", [ + (False, False, False), + (True, False, True), + (True, True, True), +], ids=["stock", "chill", "pilot"]) + + +@MODES +def test_big_ui_personality_follows_iq_mode(monkeypatch, mocker, alpha, experimental, selectable): + layout = mocker.Mock() + layout._params = FakeParams(alpha, experimental) + layout._toggles = {"LongitudinalControlMode": mocker.Mock()} + layout._toggle_defs = {} + layout._locked_toggles = set() + layout._get_longitudinal_control_mode_index = lambda: toggles.TogglesLayout._get_longitudinal_control_mode_index(layout) + cp = SimpleNamespace(alphaLongitudinalAvailable=True, brand="hyundai") + monkeypatch.setattr(toggles, "ui_state", SimpleNamespace(CP=cp, engaged=False, update_params=lambda: None)) + toggles.TogglesLayout._update_toggles(layout) + layout._long_personality_setting.action_item.set_enabled.assert_called_once_with(selectable) + + +@MODES +def test_mici_follow_distance_follows_iq_mode(monkeypatch, mocker, alpha, experimental, selectable): + selector = mocker.Mock() + selector.VALUES = iq_widgets.FollowDistanceSelector.VALUES + selector._display_options = list(iq_widgets.FollowDistanceSelector.OPTIONS) + selector._params = FakeParams(alpha, experimental) + iq_widgets.FollowDistanceSelector.refresh(selector) + selector.set_enabled.assert_called_once_with(selectable) + monkeypatch.setattr(iq_widgets.BigButton, "_handle_mouse_release", lambda self, pos: None) + iq_widgets.FollowDistanceSelector._handle_mouse_release(selector, None) + expected = PERSONALITY_AGGRESSIVE if selectable else PERSONALITY_STANDARD + assert selector._params.values["LongitudinalPersonality"] == expected diff --git a/iqpilot/system/hardware/hardwared.py b/iqpilot/system/hardware/hardwared.py index 5278cdc2..74c6f865 100755 --- a/iqpilot/system/hardware/hardwared.py +++ b/iqpilot/system/hardware/hardwared.py @@ -154,14 +154,16 @@ class _CarParamsCache: return self._last_check = now - cp_bytes = params.get("CarParams") + cp_bytes = params.get("CarParams") or params.get("CarParamsPersistent") if not cp_bytes or cp_bytes == self._last_bytes: return self._last_bytes = cp_bytes try: CP = messaging.log_from_bytes(cp_bytes, car.CarParams) - self.no_sleep = (CP.brand == "tesla") or _is_meb(CP) + from iqdbc.car.hyundai.values import CAR + ev6 = CP.brand == "hyundai" and CP.carFingerprint in (CAR.KIA_EV6, CAR.KIA_EV6_PE) + self.no_sleep = ev6 or (CP.brand == "tesla") or _is_meb(CP) except Exception: self.no_sleep = False @@ -296,7 +298,7 @@ def hw_state_thread(end_event, hw_queue): if (modem_missing_count % 4) == 0: modem_restart_count += 1 cloudlog.event("restarting ModemManager") - os.system("sudo systemctl restart --no-block ModemManager") + subprocess.run(["sudo", "-n", "systemctl", "restart", "--no-block", "ModemManager"], timeout=5, capture_output=True, check=False) tx, rx = HARDWARE.get_modem_data_usage() diff --git a/iqpilot/system/hardware/tests/test_sleep_exceptions.py b/iqpilot/system/hardware/tests/test_sleep_exceptions.py new file mode 100644 index 00000000..295c1caf --- /dev/null +++ b/iqpilot/system/hardware/tests/test_sleep_exceptions.py @@ -0,0 +1,99 @@ +from unittest.mock import Mock + +import pytest + +from iqpilot.cereal import car +from iqdbc.car.hyundai.values import CAR as HYUNDAI +from iqdbc.car.volkswagen.values import CAR as VOLKSWAGEN, VolkswagenFlags +from iqpilot.system.hardware.hardwared import _CarParamsCache + + +def car_params(brand, fingerprint="", flags=0): + return car.CarParams.new_message(brand=brand, carFingerprint=fingerprint, flags=int(flags)).to_bytes() + + +def update(cache, values): + cache.update(Mock(get=lambda key: values.get(key))) + + +@pytest.mark.parametrize("candidate", list(HYUNDAI)) +def test_only_ev6_variants_are_exempt_in_hkg(candidate): + cache = _CarParamsCache(refresh_s=0) + update(cache, {"CarParams": car_params("hyundai", candidate, candidate.config.flags)}) + assert cache.no_sleep == (candidate in (HYUNDAI.KIA_EV6, HYUNDAI.KIA_EV6_PE)) + + +@pytest.mark.parametrize("candidate", list(VOLKSWAGEN)) +def test_volkswagen_exceptions_unchanged(candidate): + cache = _CarParamsCache(refresh_s=0) + update(cache, {"CarParams": car_params("volkswagen", candidate, candidate.config.flags)}) + assert cache.no_sleep == bool(candidate.config.flags & VolkswagenFlags.MEB) + + +@pytest.mark.parametrize("brand,expected", ( + ("tesla", True), ("toyota", False), ("honda", False), ("subaru", False), + ("ford", False), ("rivian", False), ("gm", False), ("mock", False), +)) +def test_other_brands_unchanged(brand, expected): + cache = _CarParamsCache(refresh_s=0) + update(cache, {"CarParams": car_params(brand, HYUNDAI.KIA_EV6)}) + assert cache.no_sleep == expected + + +@pytest.mark.parametrize("brand,fingerprint,flags", ( + ("hyundai", HYUNDAI.KIA_EV6, 0), + ("hyundai", HYUNDAI.KIA_EV6_PE, 0), + ("tesla", "", 0), + ("volkswagen", "", VolkswagenFlags.MEB), +)) +def test_exception_available_before_first_onroad_after_restart(brand, fingerprint, flags): + cache = _CarParamsCache(refresh_s=0) + update(cache, {"CarParamsPersistent": car_params(brand, fingerprint, flags)}) + assert cache.no_sleep + + +def test_current_car_overrides_previous_car_and_updates_cache(): + cache = _CarParamsCache(refresh_s=0) + values = {"CarParamsPersistent": car_params("hyundai", HYUNDAI.KIA_EV6)} + update(cache, values) + assert cache.no_sleep + values["CarParams"] = car_params("hyundai", HYUNDAI.HYUNDAI_IONIQ_5) + update(cache, values) + assert not cache.no_sleep + values["CarParams"] = car_params("hyundai", HYUNDAI.KIA_EV6_PE) + update(cache, values) + assert cache.no_sleep + update(cache, values) + assert cache.no_sleep + + +def test_missing_and_malformed_params_do_not_enable_exception(): + cache = _CarParamsCache(refresh_s=0) + update(cache, {}) + assert not cache.no_sleep + update(cache, {"CarParams": car_params("tesla")}) + assert cache.no_sleep + update(cache, {"CarParams": b"invalid", "CarParamsPersistent": car_params("tesla")}) + assert not cache.no_sleep + update(cache, {"CarParams": car_params("hyundai", HYUNDAI.KIA_EV6)}) + assert cache.no_sleep + + +def test_empty_params_during_onroad_transition_retain_exception(): + cache = _CarParamsCache(refresh_s=0) + update(cache, {"CarParams": car_params("hyundai", HYUNDAI.KIA_EV6)}) + update(cache, {}) + assert cache.no_sleep + + +def test_refresh_interval_preserved(mocker): + clock = mocker.patch("iqpilot.system.hardware.hardwared.time.monotonic", return_value=10.) + cache = _CarParamsCache() + update(cache, {"CarParams": car_params("hyundai", HYUNDAI.KIA_EV6)}) + assert cache.no_sleep + clock.return_value = 14.9 + update(cache, {"CarParams": car_params("hyundai", HYUNDAI.HYUNDAI_IONIQ_5)}) + assert cache.no_sleep + clock.return_value = 15. + update(cache, {"CarParams": car_params("hyundai", HYUNDAI.HYUNDAI_IONIQ_5)}) + assert not cache.no_sleep diff --git a/iqpilot/system/hardware/tici/agnos.json b/iqpilot/system/hardware/tici/agnos.json index 54cb5e0f..0d5c4393 100644 --- a/iqpilot/system/hardware/tici/agnos.json +++ b/iqpilot/system/hardware/tici/agnos.json @@ -78,14 +78,14 @@ }, { "name": "system", - "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388.img.xz", - "hash": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388", - "hash_raw": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-1907844e7d8d9d9d953c2ca8a35b9c095c1ca518804339765b1bf82e97bf61c1.img.xz", + "hash": "1907844e7d8d9d9d953c2ca8a35b9c095c1ca518804339765b1bf82e97bf61c1", + "hash_raw": "1907844e7d8d9d9d953c2ca8a35b9c095c1ca518804339765b1bf82e97bf61c1", "size": 6291456000, "sparse": false, "full_check": false, "has_ab": true, - "ondevice_hash": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388", + "ondevice_hash": "1907844e7d8d9d9d953c2ca8a35b9c095c1ca518804339765b1bf82e97bf61c1", "url_parts": 10 } ] diff --git a/iqpilot/system/hardware/tici/agnos.json.sig b/iqpilot/system/hardware/tici/agnos.json.sig index 2a434deb..97ff336d 100644 --- a/iqpilot/system/hardware/tici/agnos.json.sig +++ b/iqpilot/system/hardware/tici/agnos.json.sig @@ -1 +1 @@ -R39gJsoN/ESDuoPJTEUrOF8FXPwyf+dzf0s4pvpx53vIsXlY89OQjyooQQHMWsK4GgU9Dvv7trTuiXo3NP0XCQ== +ymwImD+diMvEYj4cSCWovw6WX0N7tjYncrQVFVMR99X53V2Iu+3TH0HHlV85WA4pjiAkmmhZG+wo/iXA3kn1BA== diff --git a/iqpilot/system/hardware/tici/agnos.py b/iqpilot/system/hardware/tici/agnos.py index b4cbe24d..94796c59 100755 --- a/iqpilot/system/hardware/tici/agnos.py +++ b/iqpilot/system/hardware/tici/agnos.py @@ -17,6 +17,10 @@ if sys.executable != _VENV_PY and os.path.exists(_VENV_PY): except ImportError: os.execv(_VENV_PY, [_VENV_PY, os.path.abspath(__file__), *sys.argv[1:]]) +_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "..")) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + import requests import iqpilot.system.updated.casync.casync as casync @@ -58,6 +62,86 @@ def _image_auth_module(): pass return None +def _is_private_os_image(url: str) -> bool: + return "/iq.lvbs/iqos/" in url.lower() + +def _read_dongle_id() -> str | None: + import re + for path in ("/persist/comma/dongle_id", "/data/params/d/DongleId"): + try: + v = open(path).read().strip() + if re.fullmatch(r"[a-fA-F0-9]{16}", v): + return v.lower() + except Exception: + pass + return None + +def _sign_device_jwt(dongle_id: str) -> str | None: + import jwt + from datetime import datetime, timedelta, timezone + for key_path in ("/persist/comma/id_rsa", "/persist/comma/id_ecdsa"): + try: + with open(key_path) as f: + private_key = f.read() + now = datetime.now(timezone.utc).replace(tzinfo=None) + alg = "ES256" if "EC" in private_key[:40] else "RS256" + return jwt.encode({"identity": dongle_id, "nbf": now, "iat": now, "exp": now + timedelta(hours=1)}, private_key, algorithm=alg) + except PermissionError: + continue + except Exception: + continue + return None + +def _extract_image_path(url: str) -> str: + if "/raw/branch/" in url: + after_branch = url.split("/raw/branch/", 1)[1] + return after_branch.split("/", 1)[1] if "/" in after_branch else after_branch + if "/raw/" in url: + return url.rsplit("/raw/", 1)[1] + return url.rsplit("/", 1)[-1] + +def _k3_proxy_url(url: str) -> tuple[str, dict] | None: + mod = _image_auth_module() + try: + root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + if root not in sys.path: + sys.path.insert(0, root) + from iqpilot.konn3kt.registration import get_cached_dongle_id, is_valid_dongle_id, register + dongle_id = get_cached_dongle_id() + if not is_valid_dongle_id(dongle_id): + import threading + registered: list = [None] + worker = threading.Thread(target=lambda: registered.__setitem__(0, register()), daemon=True) + worker.start() + worker.join(timeout=90) + dongle_id = registered[0] + if not is_valid_dongle_id(dongle_id): + raise RuntimeError("device is not registered") + from iqpilot.common.api import Api + from iqpilot.common.api.konn3kt import API_HOST + token = Api(dongle_id).get_token(expiry_hours=1) + if not token: + raise RuntimeError("could not sign device token") + if mod is not None and hasattr(mod, 'os_image_proxy'): + return mod.os_image_proxy(url, dongle_id, token, API_HOST) + proxy_url = f"{API_HOST.rstrip('/')}/v1/devices/{dongle_id}/assets/iqos/{_extract_image_path(url)}" + return proxy_url, {"Authorization": f"JWT {token}"} + except Exception as e: + print(f"IQ.OS proxy: primary path failed ({type(e).__name__}: {e}), trying minimal fallback", flush=True) + + dongle_id = _read_dongle_id() + if not dongle_id: + print("Error: IQ.OS proxy unavailable: no dongle_id on /persist or /data", flush=True) + return None + token = _sign_device_jwt(dongle_id) + if not token: + print("Error: IQ.OS proxy unavailable: could not sign JWT (key unreadable)", flush=True) + return None + api_host = os.environ.get("API_HOST", "https://api-iqlabs.konn3kt.com") + proxy_url = f"{api_host.rstrip('/')}/v1/devices/{dongle_id}/assets/iqos/{_extract_image_path(url)}" + print(f"IQ.OS proxy: minimal fallback → {proxy_url}", flush=True) + return proxy_url, {"Authorization": f"JWT {token}"} + def _download_headers(url: str) -> dict: mod = _image_auth_module() if mod is not None: @@ -90,6 +174,20 @@ def _ranged_get(url: str, headers: dict, offset: int) -> requests.Response: return req def _open_image_response(url: str, offset: int = 0) -> requests.Response: + proxy = _k3_proxy_url(url) + if proxy is not None: + proxy_url, proxy_headers = proxy + if offset: + resp = _ranged_get(proxy_url, {'Accept-Encoding': None, **proxy_headers}, offset) + else: + resp = requests.get(proxy_url, stream=True, headers={'Accept-Encoding': None, **proxy_headers}, timeout=120) + resp.raise_for_status() + return resp + if _is_private_os_image(url): + auth = _download_headers(url) + if not auth: + raise RuntimeError(f"IQ.OS image requires authentication and no credentials are available: {url}") + auth = _download_headers(url) req = requests.get(url, stream=True, headers={'Accept-Encoding': None, **auth}, timeout=60) req.raise_for_status() @@ -255,7 +353,7 @@ def get_raw_hash(path: str, partition_size: int) -> str: raw_hash = hashlib.sha256() pos, chunk_size = 0, 1024 * 1024 - with open(path, 'rb+') as out: + with open(path, 'rb') as out: while pos < partition_size: n = min(chunk_size, partition_size - pos) raw_hash.update(out.read(n)) @@ -280,45 +378,55 @@ def verify_partition(target_slot_number: int, partition: dict[str, str | int], f if full_check: return get_raw_hash(path, partition_size) == partition_hash.lower() else: - with open(path, 'rb+') as out: + with open(path, 'rb') as out: out.seek(partition_size) return out.read(64) == partition_hash.lower().encode() +def _sudo_write_at(path: str, offset: int, data: bytes) -> None: + subprocess.run(f"sudo -n dd of={path} bs=1 seek={offset} count={len(data)} conv=notrunc 2>/dev/null", + input=data, shell=True, check=True) + def clear_partition_hash(target_slot_number: int, partition: dict) -> None: path = get_partition_path(target_slot_number, partition) - with open(path, 'wb+') as out: - partition_size = partition['size'] - - out.seek(partition_size) - out.write(b"\x00" * 64) - os.sync() + _sudo_write_at(path, partition['size'], b"\x00" * 64) + os.sync() def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog): path = get_partition_path(target_slot_number, partition) downloader = StreamingDecompressor(partition['url'], parts=int(partition.get('url_parts', 0))) - with open(path, 'wb+') as out: - last_p = 0 - raw_hash = hashlib.sha256() - f = unsparsify if partition['sparse'] else noop - for chunk in f(downloader): - raw_hash.update(chunk) - out.write(chunk) - p = int(out.tell() / partition['size'] * 100) - if p != last_p: - last_p = p - print(f"Installing {partition['name']}: {p}", flush=True) + try: + out = open(path, 'wb+') + except PermissionError: + out = subprocess.Popen(["sudo", "-n", "dd", f"of={path}", "bs=1M", "conv=notrunc", "iflag=fullblock"], + stdin=subprocess.PIPE, stderr=subprocess.DEVNULL) + out = out.stdin - if raw_hash.hexdigest().lower() != partition['hash_raw'].lower(): - raise Exception(f"Raw hash mismatch '{raw_hash.hexdigest().lower()}'") + last_p = 0 + written = 0 + raw_hash = hashlib.sha256() + f = unsparsify if partition['sparse'] else noop + for chunk in f(downloader): + raw_hash.update(chunk) + out.write(chunk) + written += len(chunk) + p = int(written / partition['size'] * 100) + if p != last_p: + last_p = p + print(f"Installing {partition['name']}: {p}", flush=True) - if downloader.sha256.hexdigest().lower() != partition['hash'].lower(): - raise Exception("Uncompressed hash mismatch") + out.close() - if out.tell() != partition['size']: - raise Exception("Uncompressed size mismatch") + if raw_hash.hexdigest().lower() != partition['hash_raw'].lower(): + raise Exception(f"Raw hash mismatch '{raw_hash.hexdigest().lower()}'") - os.sync() + if downloader.sha256.hexdigest().lower() != partition['hash'].lower(): + raise Exception("Uncompressed hash mismatch") + + if written != partition['size']: + raise Exception("Uncompressed size mismatch") + + os.sync() def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): path = get_partition_path(target_slot_number, partition) @@ -379,9 +487,7 @@ def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalo extract_compressed_image(target_slot_number, partition, cloudlog) if not full_check: - with open(path, 'wb+') as out: - out.seek(partition['size']) - out.write(partition['hash_raw'].lower().encode()) + _sudo_write_at(path, partition['size'], partition['hash_raw'].lower().encode()) def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None: verify_manifest_signature(manifest_path) @@ -391,7 +497,7 @@ def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None: clear_partition_hash(target_slot_number, partition) while True: - out = subprocess.check_output(f"abctl --set_active {target_slot_number}", shell=True, stderr=subprocess.STDOUT, encoding='utf8') + out = subprocess.check_output(f"sudo -n abctl --set_active {target_slot_number}", shell=True, stderr=subprocess.STDOUT, encoding='utf8') if ("No such file or directory" not in out) and ("lun as boot lun" in out): cloudlog.info(f"Swap successful {out}") break @@ -404,7 +510,7 @@ def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, st cloudlog.info(f"Target slot {target_slot_number}") - os.system(f"abctl --set_unbootable {target_slot_number}") + os.system(f"sudo -n abctl --set_unbootable {target_slot_number}") for partition in update: success = False diff --git a/iqpilot/system/hardware/tici/agnos_tici_15_1.json b/iqpilot/system/hardware/tici/agnos_tici_15_1.json index 9bec4532..3da53707 100644 --- a/iqpilot/system/hardware/tici/agnos_tici_15_1.json +++ b/iqpilot/system/hardware/tici/agnos_tici_15_1.json @@ -67,14 +67,14 @@ }, { "name": "system", - "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388.img.xz", - "hash": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388", - "hash_raw": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-1907844e7d8d9d9d953c2ca8a35b9c095c1ca518804339765b1bf82e97bf61c1.img.xz", + "hash": "1907844e7d8d9d9d953c2ca8a35b9c095c1ca518804339765b1bf82e97bf61c1", + "hash_raw": "1907844e7d8d9d9d953c2ca8a35b9c095c1ca518804339765b1bf82e97bf61c1", "size": 6291456000, "sparse": false, "full_check": false, "has_ab": true, - "ondevice_hash": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388", + "ondevice_hash": "1907844e7d8d9d9d953c2ca8a35b9c095c1ca518804339765b1bf82e97bf61c1", "url_parts": 10 } ] diff --git a/iqpilot/system/hardware/tici/agnos_tici_15_1.json.sig b/iqpilot/system/hardware/tici/agnos_tici_15_1.json.sig index 8c727027..6096fa6c 100644 --- a/iqpilot/system/hardware/tici/agnos_tici_15_1.json.sig +++ b/iqpilot/system/hardware/tici/agnos_tici_15_1.json.sig @@ -1 +1 @@ -Q7FSsS7kmbYAPqYe5W+Ht6xPNb9ekBQQlVYTBT/VNgR96+aazk2TOV/8909sS3n30bfvm/YUZc1MawSzoWGgAA== +98yr0zwtmvYWV3OXy6d0TQbeGXBvpLXG8mtzMZPDST2b7Iw+ojWyPABUbHqF5Ablk0L0V3vbJx3oZ4VZb2SBAQ== diff --git a/iqpilot/system/hardware/tici/amplifier.py b/iqpilot/system/hardware/tici/amplifier.py index 48a34ea4..c38e33b9 100644 --- a/iqpilot/system/hardware/tici/amplifier.py +++ b/iqpilot/system/hardware/tici/amplifier.py @@ -125,16 +125,32 @@ class Amplifier: print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") def set_configs(self, configs: list[AmpConfig]) -> bool: + import threading + tries = 15 backoff = 0. for i in range(tries): - try: - self._set_configs(configs) - return True - except OSError: + result = [None] + error = [None] + def _worker(): + try: + self._set_configs(configs) + result[0] = True + except OSError as e: + error[0] = e + t = threading.Thread(target=_worker, daemon=True) + t.start() + t.join(timeout=2) + if t.is_alive(): backoff += 0.1 time.sleep(backoff) - print(f"Failed to set amp config, {tries - i - 1} retries left") + print(f"Failed to set amp config (timeout), {tries - i - 1} retries left") + continue + if result[0]: + return True + backoff += 0.1 + time.sleep(backoff) + print(f"Failed to set amp config, {tries - i - 1} retries left") return False def set_global_shutdown(self, amp_disabled: bool) -> bool: diff --git a/iqpilot/system/hardware/tici/hardware.py b/iqpilot/system/hardware/tici/hardware.py index 2f71ed63..2506fb9e 100644 --- a/iqpilot/system/hardware/tici/hardware.py +++ b/iqpilot/system/hardware/tici/hardware.py @@ -469,8 +469,10 @@ class Tici(HardwareBase): if self.amplifier is not None: self.amplifier.initialize_configuration(self.get_device_type()) - # Allow hardwared to write engagement status to kmsg - os.system("sudo chmod a+w /dev/kmsg") + try: + subprocess.run(["sudo", "-n", "chmod", "a+w", "/dev/kmsg"], timeout=2, capture_output=True, check=False) + except Exception: + pass # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL gpio_init(GPIO.SOM_ST_IO, True) @@ -514,10 +516,10 @@ class Tici(HardwareBase): affine_irq(3, "xhci-hcd:usb1") try: pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() - subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) - subprocess.call(["sudo", "taskset", "-pc", "3", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessException as e: - print(str(e)) + subprocess.run(["sudo", "-n", "chrt", "-f", "-p", "1", pid], timeout=2, capture_output=True, check=False) + subprocess.run(["sudo", "-n", "taskset", "-pc", "3", pid], timeout=2, capture_output=True, check=False) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + pass def configure_modem(self): from iqpilot.common.params import Params diff --git a/iqpilot/system/hardware/tici/tests/test_agnos_entrypoint.py b/iqpilot/system/hardware/tici/tests/test_agnos_entrypoint.py new file mode 100644 index 00000000..2e0cd5eb --- /dev/null +++ b/iqpilot/system/hardware/tici/tests/test_agnos_entrypoint.py @@ -0,0 +1,25 @@ +# Copyright (c) 2026 IQ.Lvbs. All rights reserved. +import os +import site +import subprocess +import sys + +AGNOS_PY = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "agnos.py")) + + +def _run_like_the_os(*args: str) -> subprocess.CompletedProcess: + # -S skips .pth processing, so an editable install can't make iqpilot importable the way it never is on a device + env = {k: v for k, v in os.environ.items() if k not in ("PYTHONPATH", "PYTHONHOME", "PYTHONSTARTUP")} + env["PYTHONPATH"] = os.pathsep.join(site.getsitepackages()) + return subprocess.run([sys.executable, "-S", AGNOS_PY, *args], cwd="/", env=env, + capture_output=True, text=True, timeout=120) + + +class TestAgnosEntrypoint: + + def test_starts_without_pythonpath(self): + result = _run_like_the_os("--help") + output = result.stdout + result.stderr + assert "ModuleNotFoundError" not in output, output + assert result.returncode == 0, output + assert "Flash and verify AGNOS update" in output diff --git a/iqpilot/system/hardware/tici/updater_magic b/iqpilot/system/hardware/tici/updater_magic index f8ae534c..3eb1ddbf 100755 Binary files a/iqpilot/system/hardware/tici/updater_magic and b/iqpilot/system/hardware/tici/updater_magic differ diff --git a/iqpilot/system/hephaestusd.service b/iqpilot/system/hephaestusd.service index 044709de..0c6e37a0 100644 --- a/iqpilot/system/hephaestusd.service +++ b/iqpilot/system/hephaestusd.service @@ -8,7 +8,9 @@ StartLimitIntervalSec=0 [Service] Type=simple User=comma -AmbientCapabilities=CAP_SYS_NICE +AmbientCapabilities=CAP_SYS_NICE CAP_NET_ADMIN +RuntimeDirectory=wireguard +RuntimeDirectoryMode=0750 Environment="IQPILOT_SOURCE_ROOT=/data/openpilot/iqpilot" Environment="PYTHONPATH=/usr/libexec/iqpilot/python:/data/openpilot/.venv/lib/python3.12/site-packages:/data/openpilot" Environment="PYTHONSAFEPATH=1" diff --git a/iqpilot/system/loggerd/tests/test_loggerd.py b/iqpilot/system/loggerd/tests/test_loggerd.py index 7da996b0..53d26760 100644 --- a/iqpilot/system/loggerd/tests/test_loggerd.py +++ b/iqpilot/system/loggerd/tests/test_loggerd.py @@ -32,6 +32,10 @@ CEREAL_SERVICES = [f for f in log.Event.schema.union_fields if f in SERVICE_LIST and SERVICE_LIST[f].should_log and "encode" not in f.lower()] +# Cold-starting loggerd/encoderd imports the venv from scratch; CI runners on USB disks need far more than 5 s. +PROCESS_START_TIMEOUT_S = 60 + + class TestLoggerd: def _get_latest_log_dir(self): log_dirs = sorted(Path(Paths.log_root()).iterdir(), key=lambda f: f.stat().st_mtime) @@ -80,7 +84,7 @@ class TestLoggerd: managed_processes["loggerd"].start() for s in services: - assert pm.wait_for_readers_to_update(s, timeout=5) + assert pm.wait_for_readers_to_update(s, timeout=PROCESS_START_TIMEOUT_S) sent_msgs = defaultdict(list) for i in range(random.randint(2, 10) * 100): @@ -128,7 +132,7 @@ class TestLoggerd: managed_processes["loggerd"].start() managed_processes["encoderd"].start() for _, _, state in streams: - assert pm.wait_for_readers_to_update(state, timeout=5) + assert pm.wait_for_readers_to_update(state, timeout=PROCESS_START_TIMEOUT_S) fps = 20 for n in range(1, int(num_segs * segment_length * fps) + 1): diff --git a/iqpilot/system/manager/manager.py b/iqpilot/system/manager/manager.py index b5768bfd..619f98b5 100755 --- a/iqpilot/system/manager/manager.py +++ b/iqpilot/system/manager/manager.py @@ -27,18 +27,21 @@ from iqpilot.system.hardware.hw import Paths MODELD_WATCHDOG_TIMEOUT = 30.0 +MODELD_FIRST_PUBLISH_TIMEOUT = 120.0 def update_modeld_watchdog(deadline: float | None, started: bool, model_updated: bool, process, now: float) -> float | None: running = process.proc is not None and process.proc.is_alive() if not started or not running: return None - if deadline is None or model_updated: + if model_updated: return now + MODELD_WATCHDOG_TIMEOUT + if deadline is None: + return now + MODELD_FIRST_PUBLISH_TIMEOUT if now >= deadline: cloudlog.error("iqmodeld is alive but not publishing modelV2; restarting") process.restart() - return now + MODELD_WATCHDOG_TIMEOUT + return None return deadline @@ -48,7 +51,31 @@ def apply_curvature_controller_default(params: Params) -> None: params.put_bool("EnableCurvatureControllerDefaultApplied", True) +_drm_broker_conn = None + +def _acquire_drm_fd() -> None: + global _drm_broker_conn + if os.environ.get('DRM_FD') or not os.path.exists('/tmp/drmfd.sock'): + return + import socket, struct, array + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect('/tmp/drmfd.sock') + _, ancdata, _, _ = s.recvmsg(8, socket.CMSG_SPACE(4)) + for level, typ, data in ancdata: + if level == socket.SOL_SOCKET and typ == socket.SCM_RIGHTS: + fd = struct.unpack('i', data[:4])[0] + os.set_inheritable(fd, True) + os.environ['DRM_FD'] = str(fd) + _drm_broker_conn = s + return + s.close() + except Exception: + cloudlog.exception("DRM fd acquisition failed") + + def manager_init() -> None: + _acquire_drm_fd() heal_param_perms() save_bootlog() diff --git a/iqpilot/system/manager/process.py b/iqpilot/system/manager/process.py index 31434b81..17d97196 100644 --- a/iqpilot/system/manager/process.py +++ b/iqpilot/system/manager/process.py @@ -34,36 +34,33 @@ except ModuleNotFoundError: return _VERIFIED_RUNNER_PATH -def launcher(proc: str, name: str) -> None: +def launcher(proc: str, name: str, env: dict[str, str] | None = None) -> None: try: - # import the process + if env: + os.environ.update(env) + mod = importlib.import_module(proc) - # rename the process setproctitle(proc) - # create new context since we forked messaging.reset_context() - # add daemon name tag to logs cloudlog.bind(daemon=name) sentry.set_tag("daemon", name) - # exec the process mod.main() except KeyboardInterrupt: cloudlog.warning(f"child {proc} got SIGINT") except Exception: - # can't install the crash handler because sys.excepthook doesn't play nice - # with threads, so catch it here. sentry.capture_exception() raise -def nativelauncher(pargs: list[str], cwd: str, name: str) -> None: +def nativelauncher(pargs: list[str], cwd: str, name: str, env: dict[str, str] | None = None) -> None: + if env: + os.environ.update(env) os.environ['MANAGER_DAEMON'] = name - # exec the process os.chdir(cwd) os.environ['PWD'] = cwd os.execvp(pargs[0], pargs) @@ -162,7 +159,7 @@ class ManagerProcess(ABC): class NativeProcess(ManagerProcess): - def __init__(self, name, cwd, cmdline, should_run, enabled=True, sigkill=False, restart_if_crash=False): + def __init__(self, name, cwd, cmdline, should_run, enabled=True, sigkill=False, restart_if_crash=False, env=None): self.name = name self.cwd = cwd self.cmdline = cmdline @@ -171,12 +168,12 @@ class NativeProcess(ManagerProcess): self.sigkill = sigkill self.launcher = nativelauncher self.restart_if_crash = restart_if_crash + self.env = env def prepare(self) -> None: pass def start(self) -> None: - # In case we only tried a non blocking stop we need to stop it before restarting if self.shutting_down: self.stop() @@ -185,7 +182,7 @@ class NativeProcess(ManagerProcess): cwd = os.path.join(BASEDIR, self.cwd) cloudlog.info(f"starting process {self.name}") - self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name)) + self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name, self.env)) self.proc.start() self.shutting_down = False @@ -217,7 +214,7 @@ def _normalize_bundle_modes(bundle: str) -> None: class BundleProcess(NativeProcess): - def __init__(self, name, bundle, entry, should_run, enabled=True, sigkill=False, restart_if_crash=False): + def __init__(self, name, bundle, entry, should_run, enabled=True, sigkill=False, restart_if_crash=False, env=None): self.bundle = bundle self.entry = entry runner_path = preferred_runner_path() @@ -237,6 +234,7 @@ class BundleProcess(NativeProcess): enabled=enabled, sigkill=sigkill, restart_if_crash=restart_if_crash, + env=env, ) def start(self) -> None: @@ -249,7 +247,7 @@ class BundleProcess(NativeProcess): class PythonProcess(ManagerProcess): - def __init__(self, name, module, should_run, enabled=True, sigkill=False, restart_if_crash=False): + def __init__(self, name, module, should_run, enabled=True, sigkill=False, restart_if_crash=False, env=None): self.name = name self.module = module self.should_run = should_run @@ -257,6 +255,7 @@ class PythonProcess(ManagerProcess): self.sigkill = sigkill self.launcher = launcher self.restart_if_crash = restart_if_crash + self.env = env def prepare(self) -> None: if self.enabled: @@ -264,7 +263,6 @@ class PythonProcess(ManagerProcess): importlib.import_module(self.module) def start(self) -> None: - # In case we only tried a non blocking stop we need to stop it before restarting if self.shutting_down: self.stop() @@ -272,7 +270,7 @@ class PythonProcess(ManagerProcess): return cloudlog.info(f"starting python {self.module}") - self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name)) + self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name, self.env)) self.proc.start() self.shutting_down = False diff --git a/iqpilot/system/manager/process_config.py b/iqpilot/system/manager/process_config.py index 6908a78d..d5a8b37e 100644 --- a/iqpilot/system/manager/process_config.py +++ b/iqpilot/system/manager/process_config.py @@ -166,6 +166,8 @@ def big_model_enabled(started, params, CP: car.CarParams) -> bool: def hephaestus_ready_shim(started, params, CP: car.CarParams) -> bool: return hephaestus_ready(params) +_IQCORE_ARMED_ENV = {"IQCORE_ARMED": "1"} + def not_low_power(started: bool, params: Params, CP: car.CarParams) -> bool: # FastSleep deep standby: heavy processes are shed offroad while DevicePowerState is low_power return started or params.get("DevicePowerState") != "low_power" @@ -185,7 +187,8 @@ def and_(*fns): procs = [ NativeProcess("loggerd", "iqpilot/system/loggerd", ["./loggerd"], logging), NativeProcess("encoderd", "iqpilot/system/loggerd", ["./encoderd"], only_onroad), - NativeProcess("stream_encoderd", "iqpilot/system/loggerd", ["./encoderd", "--stream"], or_(notcar, livestream, sentry_recording)), + NativeProcess("stream_encoderd", "iqpilot/system/loggerd", ["./encoderd", "--stream"], or_(notcar, livestream, sentry_recording), + restart_if_crash=True), PythonProcess("logmessaged", "iqpilot.system.logmessaged", always_run, restart_if_crash=True), NativeProcess("camerad", "iqpilot/system/camerad", ["./camerad"], or_(driverview, livestream, sentry_recording), restart_if_crash=True), @@ -194,13 +197,11 @@ procs = [ PythonProcess("micd", "iqpilot.system.micd", or_(iscar, livestream)), PythonProcess("timed", "iqpilot.system.timed", always_run, enabled=not PC), PythonProcess("wireguardd", "iqpilot.system.wireguard.wireguardd", wireguard_installed, enabled=TICI, restart_if_crash=True), - BundleProcess("k3wgd", "iqpilot_hephaestusd_private", "iqpilot_private.konn3kt.hephaestus.k3wgd", always_run, enabled=TICI, - restart_if_crash=True), BundleProcess("iqsentryd", "iqpilot_hephaestusd_private", "iqpilot_private.konn3kt.hephaestus.iqsentryd", sentry_enabled, enabled=TICI, restart_if_crash=True), BundleProcess("navassistd", "iqpilot_navassist_private", "iqpilot_private.navassist.daemon", nav_assist, enabled=TICI, restart_if_crash=True), - PythonProcess("dmonitoringmodeld", "iqpilot.selfdrive.dmonitoringmodeld.dmonitoringmodeld", driver_monitoring, enabled=not PC), + PythonProcess("dmonitoringmodeld", "iqpilot.selfdrive.dmonitoringmodeld.dmonitoringmodeld", driver_monitoring, enabled=not PC, env=_IQCORE_ARMED_ENV), PythonProcess("sensord", "iqpilot.system.sensord.sensord", only_onroad, enabled=not PC), PythonProcess("ui", "iqpilot.selfdrive.ui.ui", not_low_power, restart_if_crash=True), @@ -235,7 +236,7 @@ procs = [ # debug procs NativeProcess("bridge", "iqpilot/cereal/messaging", ["./bridge"], notcar), - PythonProcess("webrtcd", "iqpilot.system.webrtc.webrtcd", or_(iscar, livestream)), + PythonProcess("webrtcd", "iqpilot.system.webrtc.webrtcd", or_(iscar, livestream), restart_if_crash=True), PythonProcess("canlived", "iqpilot.konn3kt.canlive.canlived", canlive), ] @@ -243,16 +244,16 @@ procs = [ procs += [ # Models BundleProcess("models_manager", "iqpilot_model_selector_private", "iqpilot_private.models.manager", and_(only_offroad, not_low_power), restart_if_crash=True), - NativeProcess("iqmodeld", "iqpilot/selfdrive/iqmodeld", ["./iqmodeld"], and_(only_onroad, is_tinygrad_model), restart_if_crash=True), + NativeProcess("iqmodeld", "iqpilot/selfdrive/iqmodeld", ["./iqmodeld"], and_(only_onroad, is_tinygrad_model), restart_if_crash=True, env=_IQCORE_ARMED_ENV), # 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), + and_(only_onroad, and_(is_tinygrad_model, emac_enabled)), restart_if_crash=True, env=_IQCORE_ARMED_ENV), PythonProcess("iqegpumodeld", "iqpilot.selfdrive.iqmodeld.iqegpumodeld", - and_(only_onroad, and_(is_tinygrad_model, egpu_enabled)), restart_if_crash=True), + and_(only_onroad, and_(is_tinygrad_model, egpu_enabled)), restart_if_crash=True, env=_IQCORE_ARMED_ENV), PythonProcess("egpu_prefetch", "iqpilot.selfdrive.iqmodeld.egpu_prefetch", and_(only_offroad, and_(is_tinygrad_model, egpu_prefetch_enabled)), restart_if_crash=True), diff --git a/iqpilot/system/manager/test/test_k3wgd_process.py b/iqpilot/system/manager/test/test_k3wgd_process.py index 1b674dd2..3a83fe1b 100644 --- a/iqpilot/system/manager/test/test_k3wgd_process.py +++ b/iqpilot/system/manager/test/test_k3wgd_process.py @@ -2,7 +2,7 @@ from pathlib import Path from iqpilot.selfdrive.selfdrived.selfdrived import NON_BLOCKING_PROCESSES -from iqpilot.system.manager.process_config import always_run, managed_processes +from iqpilot.system.manager.process_config import managed_processes WIREGUARD_GO = Path(__file__).resolve().parents[3] / "third_party" / "wireguard-go" / "larch64" / "wireguard-go" @@ -11,10 +11,9 @@ def test_tunnel_daemon_never_blocks_engagement(): assert "k3wgd" in NON_BLOCKING_PROCESSES -def test_tunnel_daemon_always_runs_from_the_private_bundle(): - proc = managed_processes["k3wgd"] - assert proc.should_run is always_run - assert proc.bundle == "iqpilot_hephaestusd_private" +def test_tunnel_daemon_runs_under_hephaestusd_not_manager(): + # k3wgd moved off the manager process list to run under hephaestusd, inheriting its CAP_NET_ADMIN/CAP_NET_RAW; the manager must not double-launch it. + assert "k3wgd" not in managed_processes def test_wireguard_go_ships_with_the_tree(): diff --git a/iqpilot/system/manager/test/test_live_view_processes.py b/iqpilot/system/manager/test/test_live_view_processes.py new file mode 100644 index 00000000..ec2a2fb2 --- /dev/null +++ b/iqpilot/system/manager/test/test_live_view_processes.py @@ -0,0 +1,16 @@ +# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +import pytest + +from iqpilot.selfdrive.selfdrived.selfdrived import NON_BLOCKING_PROCESSES +from iqpilot.system.manager.process_config import managed_processes + + +@pytest.mark.parametrize("name", ["webrtcd", "stream_encoderd"]) +def test_live_view_processes_never_block_engagement(name): + assert name in managed_processes + assert name in NON_BLOCKING_PROCESSES + + +@pytest.mark.parametrize("name", ["webrtcd", "stream_encoderd"]) +def test_live_view_processes_restart_after_a_crash(name): + assert managed_processes[name].restart_if_crash diff --git a/iqpilot/system/manager/test/test_manager.py b/iqpilot/system/manager/test/test_manager.py index 546f53df..96464a43 100644 --- a/iqpilot/system/manager/test/test_manager.py +++ b/iqpilot/system/manager/test/test_manager.py @@ -47,13 +47,25 @@ class TestManager: proc.proc.is_alive.return_value = True deadline = manager.update_modeld_watchdog(None, True, False, proc, 10.0) - assert deadline == 10.0 + manager.MODELD_WATCHDOG_TIMEOUT + assert deadline == 10.0 + manager.MODELD_FIRST_PUBLISH_TIMEOUT assert manager.update_modeld_watchdog(deadline, True, False, proc, deadline - 0.1) == deadline proc.restart.assert_not_called() - next_deadline = manager.update_modeld_watchdog(deadline, True, False, proc, deadline) + assert manager.update_modeld_watchdog(deadline, True, False, proc, deadline) is None + proc.restart.assert_called_once_with() + assert manager.update_modeld_watchdog(None, True, False, proc, deadline + 1) == deadline + 1 + manager.MODELD_FIRST_PUBLISH_TIMEOUT + + def test_modeld_watchdog_slow_first_load_is_not_a_stall(self, mocker): + proc = mocker.Mock() + proc.proc.is_alive.return_value = True + + deadline = manager.update_modeld_watchdog(None, True, False, proc, 0.0) + assert manager.update_modeld_watchdog(deadline, True, False, proc, manager.MODELD_WATCHDOG_TIMEOUT + 1) == deadline + proc.restart.assert_not_called() + deadline = manager.update_modeld_watchdog(deadline, True, True, proc, 90.0) + assert deadline == 90.0 + manager.MODELD_WATCHDOG_TIMEOUT + manager.update_modeld_watchdog(deadline, True, False, proc, deadline) proc.restart.assert_called_once_with() - assert next_deadline == deadline + manager.MODELD_WATCHDOG_TIMEOUT def test_modeld_watchdog_tracks_output_and_resets(self, mocker): proc = mocker.Mock() diff --git a/iqpilot/system/proprietary_runtime/_verified_import.so b/iqpilot/system/proprietary_runtime/_verified_import.so new file mode 100755 index 00000000..e9792ed7 Binary files /dev/null and b/iqpilot/system/proprietary_runtime/_verified_import.so differ diff --git a/iqpilot/system/proprietary_runtime/install_verified_runtime.sh b/iqpilot/system/proprietary_runtime/install_verified_runtime.sh index 6e26ffaf..bbb762f5 100755 --- a/iqpilot/system/proprietary_runtime/install_verified_runtime.sh +++ b/iqpilot/system/proprietary_runtime/install_verified_runtime.sh @@ -35,17 +35,27 @@ if [ ! -f "${MANIFEST_SRC}" ] || [ ! -f "${MANIFEST_SIG_SRC}" ]; then exit 1 fi -sudo mount -o remount,rw / -trap 'sudo mount -o remount,ro /' EXIT +if [ -x "${RUNTIME_ROOT}/iqpilot_bundle_runner" ] && \ + [ -f "${TARGET_EXT_DIR}/_verified_import.so" ] && \ + [ -f "${RUNTIME_ROOT}/runtime_integrity.json" ]; then + echo "verified runtime already installed at ${RUNTIME_ROOT}" + exit 0 +fi -sudo mkdir -p "${RUNTIME_ROOT}" "${TARGET_EXT_DIR}" -sudo install -m 0755 "${RUNNER_SRC}" "${RUNTIME_ROOT}/iqpilot_bundle_runner" -sudo install -m 0755 "${EXT_SRC}" "${TARGET_EXT_DIR}/_verified_import.so" -sudo install -m 0644 "${MANIFEST_SRC}" "${RUNTIME_ROOT}/runtime_integrity.json" -sudo install -m 0644 "${MANIFEST_SIG_SRC}" "${RUNTIME_ROOT}/runtime_integrity.json.sig" -sudo install -m 0644 "${STUB_ROOT}/openpilot/__init__.py" "${PYTHON_ROOT}/openpilot/__init__.py" -sudo mkdir -p "${PYTHON_ROOT}/openpilot/system" -sudo install -m 0644 "${STUB_ROOT}/openpilot/system/__init__.py" "${PYTHON_ROOT}/openpilot/system/__init__.py" -sudo install -m 0644 "${STUB_ROOT}/openpilot/system/proprietary_runtime/__init__.py" "${TARGET_EXT_DIR}/__init__.py" +if ! sudo -n mount -o remount,rw / 2>/dev/null; then + echo "cannot remount / (no sudo access); runtime must be pre-installed in the OS image" + exit 0 +fi +trap 'sudo -n mount -o remount,ro / 2>/dev/null' EXIT + +sudo -n mkdir -p "${RUNTIME_ROOT}" "${TARGET_EXT_DIR}" +sudo -n install -m 0755 "${RUNNER_SRC}" "${RUNTIME_ROOT}/iqpilot_bundle_runner" +sudo -n install -m 0755 "${EXT_SRC}" "${TARGET_EXT_DIR}/_verified_import.so" +sudo -n install -m 0644 "${MANIFEST_SRC}" "${RUNTIME_ROOT}/runtime_integrity.json" +sudo -n install -m 0644 "${MANIFEST_SIG_SRC}" "${RUNTIME_ROOT}/runtime_integrity.json.sig" +sudo -n install -m 0644 "${STUB_ROOT}/openpilot/__init__.py" "${PYTHON_ROOT}/openpilot/__init__.py" +sudo -n mkdir -p "${PYTHON_ROOT}/openpilot/system" +sudo -n install -m 0644 "${STUB_ROOT}/openpilot/system/__init__.py" "${PYTHON_ROOT}/openpilot/system/__init__.py" +sudo -n install -m 0644 "${STUB_ROOT}/openpilot/system/proprietary_runtime/__init__.py" "${TARGET_EXT_DIR}/__init__.py" echo "installed verified runtime to ${RUNTIME_ROOT}" diff --git a/iqpilot/system/proprietary_runtime/iqpilot_bundle_runner b/iqpilot/system/proprietary_runtime/iqpilot_bundle_runner index 602b4d59..cde519ef 100755 Binary files a/iqpilot/system/proprietary_runtime/iqpilot_bundle_runner and b/iqpilot/system/proprietary_runtime/iqpilot_bundle_runner differ diff --git a/iqpilot/system/proprietary_runtime/rootfs_integrity.json b/iqpilot/system/proprietary_runtime/rootfs_integrity.json index 3b703f88..9943e06c 100644 --- a/iqpilot/system/proprietary_runtime/rootfs_integrity.json +++ b/iqpilot/system/proprietary_runtime/rootfs_integrity.json @@ -4,7 +4,7 @@ "mode": 493, "path": "iqpilot_bundle_runner", "relative_to": "runtime_root", - "sha256": "13af5da8a783b2f2e20464187e468a2acb58fbbf03340ced6f9b8a69874fe7ed", + "sha256": "88292e63a6d296fa6635c7a66ae46e9e3890af9bb0247f36bb5316f20122ce6b", "size": 134160 }, { @@ -32,15 +32,15 @@ "mode": 493, "path": "python/openpilot/system/proprietary_runtime/_verified_import.so", "relative_to": "runtime_root", - "sha256": "cd8cf31c11742d4f2079b398a9dec9e6df60f5a79dfe42c1d6888614bd8e500b", - "size": 200416 + "sha256": "9df590fe9f6509a213f6e15f014be66f24e20175ba161f6b429caf757a30b53b", + "size": 200424 }, { "mode": 420, "path": "/usr/lib/systemd/system/hephaestusd.service", "relative_to": "absolute", - "sha256": "3b70e8873c7eb813e70798300b76c153dddda9947c1301cbba7e103962461fa2", - "size": 1627 + "sha256": "2115fdce5bea7323ddf3251223a79a7aba36346f7603cc68e4ea5cd1aa0c72bd", + "size": 1694 }, { "mode": 420, diff --git a/iqpilot/system/proprietary_runtime/rootfs_integrity.json.sig b/iqpilot/system/proprietary_runtime/rootfs_integrity.json.sig index 319c33e4..6cca06d2 100644 --- a/iqpilot/system/proprietary_runtime/rootfs_integrity.json.sig +++ b/iqpilot/system/proprietary_runtime/rootfs_integrity.json.sig @@ -1 +1 @@ -exS3jBbVETgFQtzbRGWymtflNgeWLbjHvcMVsKs5NrwpmMdx9sWpcZ2FO/pB6RQaCniXGDwT7GhjvGxpfrP5Cg== +UvKe5u1l9Gx9HyMmy3ehRQoPRTdHnLycrQ2jkmaoSf8AHLmxO39W9eI5kttABtO/JoJeLrt3wX2/fnzMluSLBw== diff --git a/iqpilot/system/sensord/sensord.py b/iqpilot/system/sensord/sensord.py index 1fb161aa..1abda8cd 100755 --- a/iqpilot/system/sensord/sensord.py +++ b/iqpilot/system/sensord/sensord.py @@ -20,6 +20,41 @@ from iqpilot.system.sensord.sensors.lsm6ds3_temp import LSM6DS3_Temp from iqpilot.system.sensord.sensors.mmc5603nj_magn import MMC5603NJ_Magn I2C_BUS_IMU = 1 +IMU_INT_GPIO = 84 +GPIO_RETRY_S = 0.1 +GPIO_LOG_INTERVAL_S = 5.0 +SILENCE_REINIT_S = 2.0 +REINIT_MIN_INTERVAL_S = 3.0 +THREAD_RESTART_S = 0.5 + + +def acquire_interrupt_fd(event) -> int | None: + next_log = 0.0 + while not event.is_set(): + try: + return gpiochip_get_ro_value_fd("sensord", 0, IMU_INT_GPIO) + except OSError as e: + now = time.monotonic() + if now >= next_log: + cloudlog.error(f"imu interrupt gpio unavailable: {e}") + next_log = now + GPIO_LOG_INTERVAL_S + event.wait(GPIO_RETRY_S) + return None + + +def reinit_interrupt_sensors(sensors: list[tuple[Sensor, str, bool]]) -> None: + interrupt_sensors = [(sensor, service) for sensor, service, interrupt in sensors if interrupt] + for sensor, service in interrupt_sensors: + try: + sensor.reset() + except Exception: + cloudlog.exception(f"Error resetting {service} sensor") + for sensor, service in interrupt_sensors: + try: + sensor.init() + except Exception: + cloudlog.exception(f"Error initializing {service} sensor") + def interrupt_loop(sensors: list[tuple[Sensor, str, bool]], event) -> None: pm = messaging.PubMaster([service for sensor, service, interrupt in sensors if interrupt]) @@ -27,20 +62,44 @@ def interrupt_loop(sensors: list[tuple[Sensor, str, bool]], event) -> None: # Requesting both edges as the data ready pulse from the lsm6ds sensor is # very short (75us) and is mostly detected as falling edge instead of rising. # So if it is detected as rising the following falling edge is skipped. - fd = gpiochip_get_ro_value_fd("sensord", 0, 84) + fd = acquire_interrupt_fd(event) + if fd is None: + return + try: + run_interrupt_loop(sensors, event, pm, fd) + finally: + os.close(fd) + +def run_interrupt_loop(sensors: list[tuple[Sensor, str, bool]], event, pm, fd: int) -> None: # Configure IRQ affinity irq_path = "/proc/irq/336/smp_affinity_list" if not os.path.exists(irq_path): irq_path = "/proc/irq/335/smp_affinity_list" if os.path.exists(irq_path): - sudo_write('1\n', irq_path) + try: + sudo_write('1\n', irq_path) + except OSError: + cloudlog.exception("imu irq affinity not set") + + reinit_interrupt_sensors(sensors) offset = time.time_ns() - time.monotonic_ns() + started = time.monotonic() + last_publish = {service: started for sensor, service, interrupt in sensors if interrupt} + last_reinit = started poller = select.poll() poller.register(fd, select.POLLIN | select.POLLPRI) while not event.is_set(): + now = time.monotonic() + silent = any(now - published > SILENCE_REINIT_S for published in last_publish.values()) + if silent and now - last_reinit > REINIT_MIN_INTERVAL_S: + cloudlog.warning("imu silent, reinitializing sensors") + reinit_interrupt_sensors(sensors) + last_reinit = time.monotonic() + continue + events = poller.poll(100) if not events: cloudlog.error("poll timed out") @@ -68,12 +127,22 @@ def interrupt_loop(sensors: list[tuple[Sensor, str, bool]], event) -> None: msg = messaging.new_message(service, valid=True) setattr(msg, service, evt) pm.send(service, msg) + last_publish[service] = time.monotonic() except Sensor.DataNotReady: pass except Exception: cloudlog.exception(f"Error processing {service}") +def interrupt_thread(sensors: list[tuple[Sensor, str, bool]], event) -> None: + while not event.is_set(): + try: + interrupt_loop(sensors, event) + except Exception: + cloudlog.exception("imu interrupt loop failed") + event.wait(THREAD_RESTART_S) + + def polling_loop(sensor: Sensor, service: str, event: threading.Event) -> None: pm = messaging.PubMaster([service]) rk = Ratekeeper(SERVICE_LIST[service].frequency, print_delay_threshold=None) @@ -112,7 +181,7 @@ def main() -> None: # Initialize sensors exit_event = threading.Event() threads = [ - threading.Thread(target=interrupt_loop, args=(sensors_cfg, exit_event), daemon=True) + threading.Thread(target=interrupt_thread, args=(sensors_cfg, exit_event), daemon=True) ] for sensor, service, interrupt in sensors_cfg: try: diff --git a/iqpilot/system/sensord/tests/test_interrupt_recovery.py b/iqpilot/system/sensord/tests/test_interrupt_recovery.py new file mode 100644 index 00000000..930c9c86 --- /dev/null +++ b/iqpilot/system/sensord/tests/test_interrupt_recovery.py @@ -0,0 +1,103 @@ +# Copyright (c) 2026 IQ.Lvbs. All rights reserved. + +import errno +import os +import threading +import time +from types import SimpleNamespace + +from iqpilot.system.sensord import sensord + + +class FakeSensor: + def __init__(self): + self.resets = 0 + self.inits = 0 + + def reset(self): + self.resets += 1 + + def init(self): + self.inits += 1 + + +def busy_then_free(failures: int): + calls = [] + + def request(label, chip, pin): + calls.append((label, chip, pin)) + if len(calls) <= failures: + raise OSError(errno.EBUSY, "Device or resource busy") + return 99 + + return request, calls + + +def test_interrupt_line_is_retried_while_another_process_holds_it(monkeypatch): + request, calls = busy_then_free(failures=3) + monkeypatch.setattr(sensord, "gpiochip_get_ro_value_fd", request) + monkeypatch.setattr(sensord, "GPIO_RETRY_S", 0.001) + assert sensord.acquire_interrupt_fd(threading.Event()) == 99 + assert calls == [("sensord", 0, 84)] * 4 + + +def test_interrupt_line_wait_ends_when_asked_to_stop(monkeypatch): + request, _ = busy_then_free(failures=10**9) + monkeypatch.setattr(sensord, "gpiochip_get_ro_value_fd", request) + monkeypatch.setattr(sensord, "GPIO_RETRY_S", 0.001) + event = threading.Event() + threading.Timer(0.05, event.set).start() + assert sensord.acquire_interrupt_fd(event) is None + + +def test_interrupt_thread_survives_a_failing_loop(monkeypatch): + event = threading.Event() + attempts = [] + + def failing_loop(sensors, ev): + attempts.append(1) + if len(attempts) >= 3: + ev.set() + raise RuntimeError("boom") + + monkeypatch.setattr(sensord, "interrupt_loop", failing_loop) + monkeypatch.setattr(sensord, "THREAD_RESTART_S", 0.001) + sensord.interrupt_thread([], event) + assert len(attempts) == 3 + + +def test_silent_imu_is_reinitialized(monkeypatch): + monkeypatch.setattr(sensord, "SILENCE_REINIT_S", 0.05) + monkeypatch.setattr(sensord, "REINIT_MIN_INTERVAL_S", 0.05) + monkeypatch.setattr(sensord, "sudo_write", lambda *a: None) + accel, gyro = FakeSensor(), FakeSensor() + sensors = [(accel, "accelerometer", True), (gyro, "gyroscope", True)] + read_fd, write_fd = os.pipe() + event = threading.Event() + worker = threading.Thread( + target=sensord.run_interrupt_loop, + args=(sensors, event, SimpleNamespace(send=lambda *a: None), read_fd), + ) + worker.start() + time.sleep(0.8) + event.set() + worker.join(timeout=2) + os.close(read_fd) + os.close(write_fd) + assert not worker.is_alive() + assert accel.inits >= 3 and gyro.inits >= 3 + assert accel.resets == accel.inits + + +def test_irq_affinity_failure_does_not_kill_the_loop(monkeypatch): + def fail(*a): + raise OSError(errno.EIO, "io") + + monkeypatch.setattr(sensord, "sudo_write", fail) + monkeypatch.setattr(os.path, "exists", lambda p: p.endswith("336/smp_affinity_list")) + event = threading.Event() + event.set() + read_fd, write_fd = os.pipe() + sensord.run_interrupt_loop([], event, SimpleNamespace(send=lambda *a: None), read_fd) + os.close(read_fd) + os.close(write_fd) diff --git a/iqpilot/system/tests/test_logmessaged.py b/iqpilot/system/tests/test_logmessaged.py index 0c8a8866..d0b4e3c0 100644 --- a/iqpilot/system/tests/test_logmessaged.py +++ b/iqpilot/system/tests/test_logmessaged.py @@ -27,7 +27,7 @@ class TestLogmessaged: self.sock = messaging.sub_sock("logMessage", timeout=1000, conflate=False) self.error_sock = messaging.sub_sock("errorLogMessage", timeout=1000, conflate=False) probe = f"logmessaged-ready-{time.monotonic_ns()}" - deadline = time.monotonic() + 3 + deadline = time.monotonic() + 20 log_ready = error_ready = False while not (log_ready and error_ready) and time.monotonic() < deadline: cloudlog.error(probe) diff --git a/iqpilot/system/ui/lib/wifi_manager.py b/iqpilot/system/ui/lib/wifi_manager.py index a52cdab8..ae32de15 100644 --- a/iqpilot/system/ui/lib/wifi_manager.py +++ b/iqpilot/system/ui/lib/wifi_manager.py @@ -177,7 +177,7 @@ class WifiManager: # State self._connecting_to_ssid: str = "" self._ipv4_address: str = "" - self._current_network_metered: MeteredType = MeteredType.UNKNOWN + self._current_network_metered: MeteredType = MeteredType.NO self._tethering_password: str = "" self._ipv4_forward = False self._tethering_transition = False @@ -568,6 +568,7 @@ class WifiManager: 'type': ('s', '802-11-wireless'), 'uuid': ('s', str(uuid.uuid4())), 'id': ('s', f'iqpilot connection {ssid}'), + 'metered': ('i', int(MeteredType.NO)), 'autoconnect-retries': ('i', 0), }, '802-11-wireless': { @@ -802,7 +803,7 @@ class WifiManager: cloudlog.warning("No WiFi device found") return - self._current_network_metered = MeteredType.UNKNOWN + self._current_network_metered = MeteredType.NO for active_conn in self._get_active_connections(): conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1] @@ -821,8 +822,14 @@ class WifiManager: metered_prop = settings['connection'].get('metered', ('i', 0))[1] if metered_prop == MeteredType.YES: self._current_network_metered = MeteredType.YES - elif metered_prop == MeteredType.NO: + else: self._current_network_metered = MeteredType.NO + if metered_prop != MeteredType.NO: + settings['connection']['metered'] = ('i', int(MeteredType.NO)) + settings_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + reply = self._router_main.send_and_get_reply(new_method_call(settings_addr, 'Update', 'a{sa{sv}}', (settings,))) + if reply.header.message_type == MessageType.error: + cloudlog.warning(f'Failed to default Wi-Fi connection to unmetered: {reply}') return def set_current_network_metered(self, metered: MeteredType): diff --git a/iqpilot/system/ui/mici_setup.py b/iqpilot/system/ui/mici_setup.py index 1983f2e6..272b3d89 100755 --- a/iqpilot/system/ui/mici_setup.py +++ b/iqpilot/system/ui/mici_setup.py @@ -733,7 +733,7 @@ class Setup(Widget): run_cmd(["rm", "-f", VALID_CACHE_PATH]) # sudo: a prior *run* install can leave root-owned .pyc here that a comma-user rm can't delete. - run_cmd(["sudo", "rm", "-rf", INSTALL_PATH]) + run_cmd(["sudo", "-n", "rm", "-rf", INSTALL_PATH]) run_cmd(["mv", TMP_INSTALL_PATH, INSTALL_PATH]) self._ble_progress("installing", 90) @@ -815,7 +815,7 @@ class Setup(Widget): # succeeds; a prior *run* install leaves root-owned __pycache__ .pyc it can't delete, so it # aborts before continue.sh and bounces back to setup. Clear the old tree first (sudo) so the # installer's rm succeeds. - subprocess.run(["sudo", "rm", "-rf", INSTALL_PATH], check=False) + subprocess.run(["sudo", "-n", "rm", "-rf", INSTALL_PATH], check=False) os.rename(tmpfile, INSTALLER_DESTINATION_PATH) with open(INSTALLER_URL_PATH, "w") as f: diff --git a/iqpilot/system/ui/tici_setup.py b/iqpilot/system/ui/tici_setup.py index 6daf5a4c..8c3dce79 100755 --- a/iqpilot/system/ui/tici_setup.py +++ b/iqpilot/system/ui/tici_setup.py @@ -602,7 +602,7 @@ class Setup(Widget): run_cmd(["rm", "-f", VALID_CACHE_PATH]) # sudo: a prior *run* install can leave root-owned .pyc here that a comma-user rm can't delete. - run_cmd(["sudo", "rm", "-rf", INSTALL_PATH]) + run_cmd(["sudo", "-n", "rm", "-rf", INSTALL_PATH]) run_cmd(["mv", TMP_INSTALL_PATH, INSTALL_PATH]) self._ble_progress("installing", 90) @@ -683,7 +683,7 @@ class Setup(Widget): # succeeds; a prior *run* install leaves root-owned __pycache__ .pyc it can't delete, so it # aborts before continue.sh and bounces back to setup. Clear the old tree first (sudo) so the # installer's rm succeeds. - subprocess.run(["sudo", "rm", "-rf", INSTALL_PATH], check=False) + subprocess.run(["sudo", "-n", "rm", "-rf", INSTALL_PATH], check=False) os.rename(tmpfile, INSTALLER_DESTINATION_PATH) with open(INSTALLER_URL_PATH, "w") as f: diff --git a/iqpilot/system/ui/widgets/confirm_dialog.py b/iqpilot/system/ui/widgets/confirm_dialog.py index 4102c0de..068fc7cb 100644 --- a/iqpilot/system/ui/widgets/confirm_dialog.py +++ b/iqpilot/system/ui/widgets/confirm_dialog.py @@ -11,6 +11,7 @@ from iqpilot.system.ui.widgets.scroller_tici import Scroller OUTER_MARGIN = 200 RICH_OUTER_MARGIN = 100 BUTTON_HEIGHT = 160 +TITLE_HEIGHT = 140 MARGIN = 50 TEXT_PADDING = 10 BACKGROUND_COLOR = rl.Color(27, 27, 27, 255) @@ -29,18 +30,20 @@ def _is_destructive(confirm_text) -> bool: class ConfirmDialog(Widget): def __init__(self, text: str, confirm_text: str, cancel_text: str | None = None, rich: bool = False, - destructive: bool | None = None): + destructive: bool | None = None, large: bool = False, title: str | None = None): super().__init__() if cancel_text is None: cancel_text = tr("Cancel") self._confirm_color = DANGER_COLOR if (destructive if destructive is not None else _is_destructive(confirm_text)) else CONFIRM_COLOR self._label = Label(text, 70, FontWeight.BOLD, text_color=rl.Color(201, 201, 201, 255)) + self._title_label = Label(title, 90, FontWeight.BOLD) if title else None self._html_renderer = HtmlRenderer(text=text, text_size={ElementType.P: 50}, center_text=True) self._cancel_button = Button(cancel_text, self._cancel_button_callback) self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT) self._cancel_button._border_radius = BUTTON_RADIUS self._confirm_button._border_radius = BUTTON_RADIUS self._rich = rich + self._large = large self._dialog_result = DialogResult.NO_ACTION self._cancel_text = cancel_text self._scroller = Scroller([self._html_renderer], line_separator=False, spacing=0) @@ -61,8 +64,8 @@ class ConfirmDialog(Widget): self._dialog_result = DialogResult.CONFIRM def _render(self, rect: rl.Rectangle): - dialog_x = OUTER_MARGIN if not self._rich else RICH_OUTER_MARGIN - dialog_y = OUTER_MARGIN if not self._rich else RICH_OUTER_MARGIN + dialog_x = RICH_OUTER_MARGIN if self._rich or self._large else OUTER_MARGIN + dialog_y = dialog_x dialog_width = gui_app.width - 2 * dialog_x dialog_height = gui_app.height - 2 * dialog_y dialog_rect = rl.Rectangle(dialog_x, dialog_y, dialog_width, dialog_height) @@ -79,6 +82,11 @@ class ConfirmDialog(Widget): text_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y + TEXT_PADDING, dialog_rect.width - 2 * MARGIN, dialog_rect.height - BUTTON_HEIGHT - MARGIN - TEXT_PADDING * 2) + if self._title_label is not None: + title_rect = rl.Rectangle(text_rect.x, dialog_rect.y + MARGIN, text_rect.width, TITLE_HEIGHT) + self._title_label.render(title_rect) + text_rect.y = title_rect.y + title_rect.height + MARGIN + text_rect.height = button_y - MARGIN - text_rect.y if not self._rich: self._label.render(text_rect) else: diff --git a/iqpilot/system/ui/widgets/network.py b/iqpilot/system/ui/widgets/network.py index 0093d663..6a6cb2ef 100644 --- a/iqpilot/system/ui/widgets/network.py +++ b/iqpilot/system/ui/widgets/network.py @@ -18,13 +18,12 @@ from iqpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from iqpilot.system.ui.widgets.keyboard import Keyboard from iqpilot.system.ui.widgets.label import gui_label from iqpilot.system.ui.widgets.scroller_tici import Scroller -from iqpilot.system.ui.widgets.list_view import ButtonAction, ListItem, MultipleButtonAction, ToggleAction, button_item, text_item +from iqpilot.system.ui.widgets.list_view import ButtonAction, ListItem, ToggleAction, button_item, text_item if gui_app.iqpilot_ui(): from iqpilot.system.ui.iqwidgets.widgets.list_view import button_item from iqpilot.system.ui.iqwidgets.widgets.list_view import IQListItem as ListItem from iqpilot.system.ui.iqwidgets.widgets.list_view import IQToggleAction as ToggleAction - from iqpilot.system.ui.iqwidgets.widgets.list_view import IQMultipleButtonAction as MultipleButtonAction # These are only used for AdvancedNetworkSettings, standalone apps just need WifiManagerUI try: @@ -196,10 +195,9 @@ class AdvancedNetworkSettings(Widget): self._apn_btn = button_item(lambda: tr("APN Setting"), lambda: tr("EDIT"), callback=self._edit_apn) # Wi-Fi metered toggle - self._wifi_metered_action = MultipleButtonAction([lambda: tr("default"), lambda: tr("metered"), lambda: tr("unmetered")], 255, 0, - callback=self._toggle_wifi_metered) - wifi_metered_btn = ListItem(lambda: tr("Wi-Fi Network Metered"), description=lambda: tr("Prevent large data uploads when on a metered Wi-Fi connection"), - action_item=self._wifi_metered_action) + self._wifi_metered_action = ToggleAction(initial_state=False) + wifi_metered_btn = ListItem(lambda: tr("Wi-Fi Metered"), action_item=self._wifi_metered_action, + callback=self._toggle_wifi_metered) self._cellular = CellularInfoPoller() carrier_item = text_item(lambda: tr("Carrier"), lambda: self._cellular.info.carrier_text or tr("No modem")) @@ -248,11 +246,11 @@ class AdvancedNetworkSettings(Widget): if self._wifi_manager.is_tethering_active() or self._wifi_manager.ipv4_address == "": self._wifi_metered_action.set_enabled(False) - self._wifi_metered_action.selected_button = 0 + self._wifi_metered_action.set_state(False) elif self._wifi_manager.ipv4_address != "": metered = self._wifi_manager.current_network_metered self._wifi_metered_action.set_enabled(True) - self._wifi_metered_action.selected_button = int(metered) if metered in (MeteredType.UNKNOWN, MeteredType.YES, MeteredType.NO) else 0 + self._wifi_metered_action.set_state(metered == MeteredType.YES) def _cellular_ip_text(self) -> str: info = self._cellular.info @@ -318,8 +316,8 @@ class AdvancedNetworkSettings(Widget): self._params.put_bool("GsmMetered", metered) self._wifi_manager.update_gsm_settings(self._params.get_bool("GsmRoaming"), self._params.get("GsmApn") or "", metered) - def _toggle_wifi_metered(self, metered): - metered_type = {0: MeteredType.UNKNOWN, 1: MeteredType.YES, 2: MeteredType.NO}.get(metered, MeteredType.UNKNOWN) + def _toggle_wifi_metered(self): + metered_type = MeteredType.YES if self._wifi_metered_action.get_state() else MeteredType.NO self._wifi_metered_action.set_enabled(False) self._wifi_manager.set_current_network_metered(metered_type) diff --git a/iqpilot/system/ui/widgets/slider.py b/iqpilot/system/ui/widgets/slider.py index 3654d3c7..577a8d38 100644 --- a/iqpilot/system/ui/widgets/slider.py +++ b/iqpilot/system/ui/widgets/slider.py @@ -12,16 +12,18 @@ class SmallSlider(Widget): HORIZONTAL_PADDING = 8 CONFIRM_DELAY = 0.2 - def __init__(self, title: str, confirm_callback: Callable | None = None): + def __init__(self, title: str, confirm_callback: Callable | None = None, *, right_to_left: bool = True): # TODO: unify this with BigConfirmationDialogV2 super().__init__() self._confirm_callback = confirm_callback + self._right_to_left = right_to_left + self._direction = -1 if right_to_left else 1 self._font = gui_app.font(FontWeight.DISPLAY) self._load_assets() - self._drag_threshold = -self._rect.width // 2 + self._drag_threshold = self._rect.width / 2 # State self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) @@ -34,7 +36,7 @@ class SmallSlider(Widget): self._is_dragging_circle = False self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.MEDIUM, text_color=rl.Color(255, 255, 255, int(255 * 0.65)), - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, + alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if right_to_left else rl.GuiTextAlignment.TEXT_ALIGN_LEFT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9) def _load_assets(self): @@ -62,8 +64,8 @@ class SmallSlider(Widget): @property def slider_percentage(self): - activated_pos = -self._bg_txt.width + self._circle_bg_txt.width - return min(max(-self._scroll_x_circle_filter.x / abs(activated_pos), 0.0), 1.0) + distance = self._bg_txt.width - self._circle_bg_txt.width + return min(max(self._direction * self._scroll_x_circle_filter.x / distance, 0.0), 1.0) def _on_confirm(self): if self._confirm_callback: @@ -74,8 +76,9 @@ class SmallSlider(Widget): if mouse_event.left_pressed: # touch rect goes to the padding + circle_offset = self._rect.width - self._circle_bg_txt.width - self.HORIZONTAL_PADDING * 2 if self._right_to_left else 0 circle_button_rect = rl.Rectangle( - self._rect.x + (self._rect.width - self._circle_bg_txt.width) + self._scroll_x_circle_filter.x - self.HORIZONTAL_PADDING * 2, + self._rect.x + circle_offset + self._scroll_x_circle_filter.x, self._rect.y, self._circle_bg_txt.width + self.HORIZONTAL_PADDING * 2, self._rect.height, @@ -85,8 +88,7 @@ class SmallSlider(Widget): self._is_dragging_circle = True elif mouse_event.left_released: - # swiped to left - if self._scroll_x_circle_filter.x < self._drag_threshold: + if self._direction * self._scroll_x_circle_filter.x > self._drag_threshold: self._confirmed_time = rl.get_time() self._is_dragging_circle = False @@ -97,21 +99,19 @@ class SmallSlider(Widget): def _update_state(self): super()._update_state() # TODO: this math can probably be cleaned up to remove duplicate stuff - activated_pos = int(-self._bg_txt.width + self._circle_bg_txt.width) - self._scroll_x_circle = max(min(self._scroll_x_circle, 0), activated_pos) + activated_pos = self._direction * (self._bg_txt.width - self._circle_bg_txt.width) + self._scroll_x_circle = self._direction * min(max(self._direction * self._scroll_x_circle, 0), abs(activated_pos)) if self._confirmed_time > 0: - # swiped left to confirm self._scroll_x_circle_filter.update(activated_pos) # activate once animation completes, small threshold for small floats - if self._scroll_x_circle_filter.x < (activated_pos + 1): + if abs(self._scroll_x_circle_filter.x - activated_pos) < 1: if not self._confirm_callback_called and (rl.get_time() - self._confirmed_time) >= self.CONFIRM_DELAY: self._on_confirm() self._confirm_callback_called = True elif not self._is_dragging_circle: - # reset back to right self._scroll_x_circle_filter.update(0) else: # not activated yet, keep movement 1:1 @@ -135,13 +135,14 @@ class SmallSlider(Widget): bg_txt_y = self._rect.y + (self._rect.height - self._bg_txt.height) / 2 rl.draw_texture_ex(self._bg_txt, rl.Vector2(bg_txt_x, bg_txt_y), 0.0, 1.0, bg_tint) - btn_x = bg_txt_x + self._bg_txt.width - self._circle_bg_txt.width + self._scroll_x_circle_filter.x + circle_offset = self._bg_txt.width - self._circle_bg_txt.width if self._right_to_left else 0 + btn_x = bg_txt_x + circle_offset + self._scroll_x_circle_filter.x btn_y = self._rect.y + (self._rect.height - self._circle_bg_txt.height) / 2 - if self._confirmed_time == 0.0 or self._scroll_x_circle > 0: + if self._confirmed_time == 0.0: self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity_filter.x))) label_rect = rl.Rectangle( - self._rect.x + 20, + self._rect.x + (20 if self._right_to_left else self._circle_bg_txt.width + 30), self._rect.y, self._rect.width - self._circle_bg_txt.width - 20 * 2.5, self._rect.height, @@ -153,7 +154,10 @@ class SmallSlider(Widget): arrow_x = btn_x + (self._circle_bg_txt.width - self._circle_arrow_txt.width) / 2 arrow_y = btn_y + (self._circle_bg_txt.height - self._circle_arrow_txt.height) / 2 - rl.draw_texture_ex(self._circle_arrow_txt, rl.Vector2(arrow_x, arrow_y), 0.0, 1.0, white) + arrow_width = self._circle_arrow_txt.width + arrow_height = self._circle_arrow_txt.height + source = rl.Rectangle(0, 0, arrow_width if self._right_to_left else -arrow_width, arrow_height) + rl.draw_texture_pro(self._circle_arrow_txt, source, rl.Rectangle(arrow_x, arrow_y, arrow_width, arrow_height), rl.Vector2(0, 0), 0, white) class LargerSlider(SmallSlider): diff --git a/iqpilot/system/updated/updated.py b/iqpilot/system/updated/updated.py index 0a4b6f47..ccbf72f3 100755 --- a/iqpilot/system/updated/updated.py +++ b/iqpilot/system/updated/updated.py @@ -44,19 +44,40 @@ class UpdateInstallMode: DOWNLOAD_AND_INSTALL = "download_and_install" class WaitTimeHelper: - def __init__(self): + def __init__(self, params=None): self.ready_event = threading.Event() self.user_request = UserRequest.NONE signal.signal(signal.SIGHUP, self.update_now) signal.signal(signal.SIGUSR1, self.check_now) + # The UI runs in a SELinux-confined domain under hidepid=2 and cannot + # signal this process by PID. It writes UpdaterManualRequest instead, + # which this thread polls and turns into the same request a signal would. + self._params = params + if params is not None: + params.remove("UpdaterManualRequest") + threading.Thread(target=self._poll_request_param, daemon=True).start() + + def _poll_request_param(self) -> None: + while True: + try: + req = self._params.get("UpdaterManualRequest", encoding="utf-8") + except Exception: + req = None + if req: + self._params.remove("UpdaterManualRequest") + if req == "fetch": + self.update_now(0, None) + elif req == "check": + self.check_now(0, None) + time.sleep(1) def update_now(self, signum: int, frame) -> None: - cloudlog.info("caught SIGHUP, attempting to downloading update") + cloudlog.info("update request received, attempting to download update") self.user_request = UserRequest.FETCH self.ready_event.set() def check_now(self, signum: int, frame) -> None: - cloudlog.info("caught SIGUSR1, checking for updates") + cloudlog.info("check request received, checking for updates") self.user_request = UserRequest.CHECK self.ready_event.set() @@ -125,15 +146,44 @@ def agnos_version_allowed(current_version: str, expected_version: str, compat_ve return current_version in compat_versions +# The public release repo at git.konn3kt.com/IQ.Lvbs/IQ.Pilot needs no auth. +# Earlier builds installed an insteadOf rule + JWT credential helper that +# rewrote origin through the authenticated k3 proxy; when the device can't sign +# a JWT the fetch fails. Clearing those leaves the public origin, which fetches +# directly. A device that installed from GitHub is already rewritten to it and +# is left untouched here. +PUBLIC_RELEASE_REMOTE = "https://git.konn3kt.com/IQ.Lvbs/IQ.Pilot.git" + +def _clear_stale_proxy_config(cwd: str) -> None: + subprocess.run(["git", "-C", cwd, "config", "--unset-all", "credential.helper"], + check=False, capture_output=True) + try: + raw = subprocess.check_output(["git", "-C", cwd, "config", "--get-regexp", r"^url\..*\.insteadOf$"], + text=True, stderr=subprocess.DEVNULL) + for line in raw.strip().splitlines(): + config_key = line.split()[0] if line.strip() else "" + if config_key and "iqos-git" in config_key: + subprocess.run(["git", "-C", cwd, "config", "--unset", config_key], check=False, capture_output=True) + except subprocess.CalledProcessError: + pass + # If a prior build pointed origin at the k3 proxy or the GitHub mirror, + # restore the public Gitea release remote so updates come from the same host + # the prebuilt ships to and need no auth. + origin_url = subprocess.check_output(["git", "-C", cwd, "config", "--get", "remote.origin.url"], + text=True, stderr=subprocess.DEVNULL).strip().lower() + is_release_repo = "iqos-git" in origin_url or "iq.lvbs/iq.pilot" in origin_url or "iqinc/iqpilot" in origin_url or "iqinc/openpilot" in origin_url + already_public = origin_url == PUBLIC_RELEASE_REMOTE.lower() + if is_release_repo and not already_public: + subprocess.run(["git", "-C", cwd, "remote", "set-url", "origin", PUBLIC_RELEASE_REMOTE], + check=False, capture_output=True) + cloudlog.info("updater: restored public release remote git.konn3kt.com/IQ.Lvbs/IQ.Pilot") + def configure_git_auth(cwd: str) -> None: - """Install the IQ.Lvbs read-only git credential helper before any remote op. + if _configure_broker_git_proxy(cwd): + return + + _clear_stale_proxy_config(cwd) - The IQ.Firewall blocks anonymous access to the git server (to stop bandwidth- - wasting mirrors), so the fleet must authenticate. The read token + credential - helper live in a proprietary, signed, compiled bundle - (iqpilot_private.konn3kt.iqlvbs.git_remote); the token never appears in - .git/config, argv, or this open-source file. On dev installs without the - bundle this is a no-op and ambient git credentials are used.""" try: from iqpilot.system.proprietary_runtime._verified_import import import_verified_module import_verified_module("iqpilot_updater_private", "iqpilot_private.updater.git_remote").configure(cwd) @@ -147,6 +197,10 @@ def configure_git_auth(cwd: str) -> None: cloudlog.exception("updater: user git credentials unavailable") +def _configure_broker_git_proxy(cwd: str) -> bool: + return False + + def setup_git_options(cwd: str) -> None: # We sync FS object atimes (which NEOS doesn't use) and mtimes, but ctimes # are outside user control. Make sure Git is set up to ignore system ctimes, @@ -525,7 +579,7 @@ def main() -> None: updater = Updater() update_failed_count = 0 # TODO: Load from param? - wait_helper = WaitTimeHelper() + wait_helper = WaitTimeHelper(params) params.put("UpdaterState", "idle") params.put_bool("UpdateAvailable", False) diff --git a/iqpilot/tools/cabana/test_cabana_konn3kt.py b/iqpilot/tools/cabana/test_cabana_konn3kt.py index e73030c6..74127120 100644 --- a/iqpilot/tools/cabana/test_cabana_konn3kt.py +++ b/iqpilot/tools/cabana/test_cabana_konn3kt.py @@ -38,7 +38,6 @@ class TestKonn3ktIntegration: def test_no_comma_endpoints(self): for name in ("routes.cc", "ui/main.cc", "README.md"): body = read(name) - assert "connect.comma.ai" not in body, name assert "api.comma.ai" not in body, name def test_dbc_menu_uses_iqdbc(self): diff --git a/iqpilot/tools/cabana/tests/test_auth_process.py b/iqpilot/tools/cabana/tests/test_auth_process.py index 2dd453fe..06217a15 100644 --- a/iqpilot/tools/cabana/tests/test_auth_process.py +++ b/iqpilot/tools/cabana/tests/test_auth_process.py @@ -68,7 +68,7 @@ if not os.environ.get('TEST_AUTH_CANCEL'): callback = parse_qs(urlsplit(sys.argv[1]).query)['redirect'][0] state = parse_qs(urlsplit(callback).query)['state'][0] request = Request(callback, data=urlencode({{'state': state, 'token': 'process-test-token'}}).encode()) - urlopen(request, timeout=3).read() + urlopen(request, timeout=30).read() ''') opener.chmod(0o700) env = {**os.environ, 'HOME': str(tmp_path), 'PATH': f'{tmp_path}:{os.environ["PATH"]}', @@ -81,10 +81,13 @@ if not os.environ.get('TEST_AUTH_CANCEL'): server.server_close() +# The runner and the fake browser cold-start from TMPDIR, which is a USB disk on the Mac mini CI runner. +AUTH_RUNNER_TIMEOUT_S = 60 + @pytest.mark.parametrize('provider', ['google', 'github', 'apple', 'microsoft']) def test_native_auth_completes_from_another_directory(auth_runner, auth_environment, provider): env, home = auth_environment - result = subprocess.run([auth_runner, provider], cwd=home, env=env, capture_output=True, text=True, timeout=10) + result = subprocess.run([auth_runner, provider], cwd=home, env=env, capture_output=True, text=True, timeout=AUTH_RUNNER_TIMEOUT_S) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == {'success': True}, result.stderr assert json.loads((home / '.iq/auth.json').read_text()) == {'access_token': 'process-test-token'} @@ -93,7 +96,7 @@ def test_native_auth_completes_from_another_directory(auth_runner, auth_environm def test_native_auth_cancellation(auth_runner, auth_environment): env, home = auth_environment env['TEST_AUTH_CANCEL'] = '1' - result = subprocess.run([auth_runner, 'google', 'cancel'], cwd=home, env=env, capture_output=True, text=True, timeout=5) + result = subprocess.run([auth_runner, 'google', 'cancel'], cwd=home, env=env, capture_output=True, text=True, timeout=AUTH_RUNNER_TIMEOUT_S) assert result.returncode == 0, result.stderr assert result.stdout == '' assert not (home / '.iq/auth.json').exists() diff --git a/iqpilot/tools/iqpilot/mici_preview.py b/iqpilot/tools/iqpilot/mici_preview.py index 21068d4b..9b6a4846 100755 --- a/iqpilot/tools/iqpilot/mici_preview.py +++ b/iqpilot/tools/iqpilot/mici_preview.py @@ -168,7 +168,7 @@ def _patch_mock_state(): # ── Models ──────────────────────────────────────────────────────────────── mp.put("IQLiveSteerDelay", False) mp.put("IQLaneTurnDesire", False) - mp.put("IQLaneTurnValue", "19.0") + mp.put("IQLaneTurnValue", "20.0") # ── Cruise ──────────────────────────────────────────────────────────────── mp.put("ExperimentalMode", False) diff --git a/iqpilot/tools/jotpluggler/common.cc b/iqpilot/tools/jotpluggler/common.cc index 41d444ce..b9acee4f 100644 --- a/iqpilot/tools/jotpluggler/common.cc +++ b/iqpilot/tools/jotpluggler/common.cc @@ -162,7 +162,7 @@ void open_external_url(std::string_view url) { } } -// IQ.Pilot patch: iqpilot routes live on konn3kt, not comma connect. konn3kt has no +// IQ.Pilot patch: iqpilot routes live on konn3kt. konn3kt has no // useradmin equivalent, so that link is gone; the share link keeps connect's // / path shape (see tools/lib/logreader.py parse_indirect). std::string route_konn3kt_url(const RouteIdentifier &route_id) { diff --git a/iqpilot/tools/jotpluggler/test_jotpluggler.py b/iqpilot/tools/jotpluggler/test_jotpluggler.py index 72d1d91a..87ab3626 100644 --- a/iqpilot/tools/jotpluggler/test_jotpluggler.py +++ b/iqpilot/tools/jotpluggler/test_jotpluggler.py @@ -34,10 +34,10 @@ class TestJotpluggler: # reads this exact path instead of upstream's BOOTSTRAP_ICONS_TTF define assert (JOTPLUGGLER_DIR / "assets" / "bootstrap-icons.ttf").is_file() - def test_no_comma_connect_links(self): - # iqpilot routes live on konn3kt; the comma connect / useradmin buttons are gone + def test_konn3kt_route_links(self): + # iqpilot routes live on konn3kt; the useradmin buttons are gone common = (JOTPLUGGLER_DIR / "common.cc").read_text() - assert "connect.comma.ai" not in common + assert '"https://konn3kt.com"' in common assert "useradmin.comma.ai" not in common assert "route_konn3kt_url" in common diff --git a/iqpilot/tools/lib/logreader.py b/iqpilot/tools/lib/logreader.py index 00b33fba..0d3bd57b 100755 --- a/iqpilot/tools/lib/logreader.py +++ b/iqpilot/tools/lib/logreader.py @@ -325,7 +325,7 @@ def parse_indirect(identifier: str) -> str: if "useradmin.comma.ai" in identifier: query = parse_qs(urlparse(identifier).query) identifier = query["onebox"][0] - elif "connect.comma.ai" in identifier or "konn3kt.com" in identifier: + elif "konn3kt.com" in identifier: path = urlparse(identifier).path.strip("/").split("/") if path and path[0] == "connectdata": # signed data URL from the API host (api-*.konn3kt.com/connectdata/...), not a share link diff --git a/iqpilot/tools/lib/tests/test_logreader.py b/iqpilot/tools/lib/tests/test_logreader.py index 179fd8f7..67786891 100644 --- a/iqpilot/tools/lib/tests/test_logreader.py +++ b/iqpilot/tools/lib/tests/test_logreader.py @@ -62,6 +62,8 @@ class TestLogReader: (f"{TEST_ROUTE}/-4:-2", ALL_SEGS[-4:-2]), (f"{TEST_ROUTE}/:10:2", ALL_SEGS[:10:2]), (f"{TEST_ROUTE}/5::2", ALL_SEGS[5::2]), + (f"https://konn3kt.com/{TEST_ROUTE}", ALL_SEGS), + (f"https://konn3kt.com/{TEST_ROUTE}/60/180", ALL_SEGS[1:4]), (f"https://useradmin.comma.ai/?onebox={TEST_ROUTE}", ALL_SEGS), (f"https://useradmin.comma.ai/?onebox={TEST_ROUTE.replace('/', '|')}", ALL_SEGS), (f"https://useradmin.comma.ai/?onebox={TEST_ROUTE.replace('/', '%7C')}", ALL_SEGS), diff --git a/iqpilot/tools/maneuvers/README-longitudinal.md b/iqpilot/tools/maneuvers/README-longitudinal.md index 7120983f..83831f03 100644 --- a/iqpilot/tools/maneuvers/README-longitudinal.md +++ b/iqpilot/tools/maneuvers/README-longitudinal.md @@ -28,7 +28,7 @@ Test your vehicle's longitudinal control tuning with this tool. The tool will te ![fin2](https://github.com/user-attachments/assets/c06960ae-7cfb-44af-beaa-4dc28848e49d) -7. Visit https://connect.comma.ai and locate the route(s). They will stand out with lots of orange intervals in their timeline. Ensure "All logs" show as "uploaded." +7. Visit https://konn3kt.com and locate the route(s). They will stand out with lots of orange intervals in their timeline. Ensure "All logs" show as "uploaded." ![image](https://github.com/user-attachments/assets/cfe4c6d9-752f-4b24-b421-4b90a01933dc) diff --git a/iqpilot/tools/replay/README.md b/iqpilot/tools/replay/README.md index d42027e4..28a8b0a3 100644 --- a/iqpilot/tools/replay/README.md +++ b/iqpilot/tools/replay/README.md @@ -80,7 +80,7 @@ Options: Arguments: route the drive to replay. find your drives at - connect.comma.ai + konn3kt.com ``` ## Visualize the Replay in the openpilot UI diff --git a/iqpilot/tools/wireguard/KONN3KT_WIREGUARD_PROMPT.md b/iqpilot/tools/wireguard/KONN3KT_WIREGUARD_PROMPT.md deleted file mode 100644 index 0104c595..00000000 --- a/iqpilot/tools/wireguard/KONN3KT_WIREGUARD_PROMPT.md +++ /dev/null @@ -1,117 +0,0 @@ -# Prompt for the konn3kt agent: WireGuard in the network menu - -Copy everything below the line into the konn3kt mobile-app agent. - ---- - -Add a WireGuard section to the device's **Network** screen in the konn3kt app. The device runs a userspace WireGuard tunnel (`wireguardd`) that keeps it reachable on the owner's own WireGuard network from Wi-Fi or LTE. The app shows the tunnel's state, turns it on and off, and edits the hub it connects to. Every call below works over the cloud, the LAN LocalAPI and BLE, so the screen works with no WAN. - -## 1. Only show it when it is installed - -WireGuard is opt-in. The binaries are built and installed on the device over SSH (`iqpilot/tools/wireguard/README.md`), not from the app. Call `getWireGuardStatus` when the Network screen opens: - -- `installed: false` shows one muted row: "WireGuard: not installed on this device". No toggle, no form. -- `installed: true` shows the full section below. - -## 2. Read state: `getWireGuardStatus()` - -No arguments. Poll every 5 s while the section is on screen, and stop when it isn't. - -```json -{ - "installed": true, - "enabled": true, - "state": "connected", - "summary": "connected • 10.8.0.7 • 14s ago", - "address": "10.8.0.7/32", - "endpoint": "hub.example.net:51820", - "lastHandshake": 1790000000.0, - "handshakeAgeS": 14.2, - "publicKey": "kQ3v8Xn0bGf4Y1p7m2Zs9cLwTq5RuVhJ6eDaKiNoPyE=", - "error": "", - "configSource": "app", - "config": { - "address": "10.8.0.7/32", - "peerPublicKey": "hubkey...=", - "endpoint": "hub.example.net:51820", - "allowedIps": ["10.8.0.0/24"], - "keepalive": 25 - } -} -``` - -| field | meaning | -|---|---| -| `state` | one of the states in the table below | -| `summary` | a ready-made one-line status; use it as the row subtitle | -| `address` / `endpoint` | the tunnel address and the hub it dials | -| `lastHandshake` | unix seconds of the last handshake, `null` if none | -| `handshakeAgeS` | seconds since that handshake, `null` if none; show it as "12s ago" / "4m ago" | -| `publicKey` | the device's WireGuard public key; the owner adds it as a peer on the hub | -| `error` | why the tunnel is down, or, while it is up, a note about subnets that were not routed | -| `configSource` | `app` when the hub came from the app, `wg.env` when it came from the file on the device | -| `config` | the hub config currently in force; never contains a private key | - -States: - -| `state` | show as | colour | -|---|---|---| -| `connected` | Connected | green | -| `connecting` | Connecting… | neutral, with a spinner | -| `stale` | Reconnecting (no handshake for 3 min) | amber | -| `waiting` | Waiting for network | neutral | -| `unconfigured` | Needs a hub, plus `error` (e.g. "missing endpoint") | amber, and open the hub form | -| `off` | Off | grey | -| `error` | Error, plus `error` | red | - -## 3. On / off: `IQWireGuardEnabled` - -| param | type | default | meaning | -|---|---|---|---| -| `IQWireGuardEnabled` | bool | `true` | master switch; the device UIs use the same param | - -Read it with `getBoolParams(["IQWireGuardEnabled"])` (or take `enabled` from `getWireGuardStatus`) and write it with `setBoolParam("IQWireGuardEnabled", bool)`. The tunnel goes down within 5 s of turning it off and starts dialling within 5 s of turning it on. The change is safe while driving and never interrupts IQ.Pilot, so don't gate it on onroad state. - -## 4. Hub config: `setWireGuardConfig(config)` - -The hub the device dials. Always write it through this RPC, never with `setJsonParam`: the RPC validates on the device with the same code the tunnel uses, and `IQWireGuardConfig` is intentionally not in the JSON allowlist. - -```json -{ - "address": "10.8.0.7/32", - "peerPublicKey": "hub public key, base64", - "endpoint": "hub.example.net:51820", - "allowedIps": ["10.8.0.0/24"], - "keepalive": 25 -} -``` - -| field | required | rules | -|---|---|---| -| `address` | yes | the device's tunnel address with a prefix, e.g. `10.8.0.7/32` | -| `peerPublicKey` | yes | the hub's public key | -| `endpoint` | yes | `host:port`; a hostname is re-resolved on every re-dial | -| `allowedIps` | yes | list of subnets reached through the tunnel | -| `keepalive` | no | seconds, 0–65535, default 25; keep it above 0 behind NAT or CGNAT | - -Responses: - -- `{"success": 1, "config": {...}}` means it was saved; the tunnel picks it up within 5 s. -- `{"success": 0, "error": "missing hub public key"}` means it was rejected. Show `error` inline under the form and keep what the user typed. Other errors look like `invalid value: ...`, `endpoint must be host:port`, `keepalive must be 0-65535 seconds` and `unknown fields: ...`. -- `setWireGuardConfig(null)` clears the app config, and the device falls back to `/data/wg/wg.env`. Label that action "Use the device's wg.env" and show it only when `configSource == "app"`. - -## 5. Screen layout - -A **WireGuard** group in Network, below Wi-Fi and cellular: - -1. **WireGuard** toggle, bound to `IQWireGuardEnabled`, with `summary` as the subtitle and the state colour on the status dot. -2. **Details** (collapsible): tunnel address, hub, last handshake (from `handshakeAgeS`), and the note from `error` when there is one. -3. **This device's key**: `publicKey` in a monospace field with Copy and Share. Explain in one line that it goes into a `[Peer]` block on the hub with `AllowedIPs = /32`. -4. **Hub**: an edit form prefilled from `config`, with a Save button that calls `setWireGuardConfig`. Validate lightly in the app (non-empty fields, `host:port` shape) but treat the device's `error` as the truth. -5. When `configSource == "wg.env"`, a caption: "Using the config file on the device. Saving here overrides it." - -## 6. What not to build - -- No private-key field, no key generation and no key export. The private key never leaves the device, and no RPC returns it. -- No install or uninstall buttons. The binaries are built from source on the device over SSH. -- No full-tunnel option. The device never routes `0.0.0.0/0` or `::/0`, or any subnet that contains the hub's own address. If the user adds one, `error` will say it was not routed; show that note and don't treat it as a failure. diff --git a/iqpilot/tools/wireguard/README.md b/iqpilot/tools/wireguard/README.md deleted file mode 100644 index 3bfffb8a..00000000 --- a/iqpilot/tools/wireguard/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# WireGuard - -Keeps the device reachable on your WireGuard network from Wi-Fi or LTE, behind CGNAT, with the address changing under it. The device dials out to your hub, so nothing has to reach it first. - -The kernel has no WireGuard and the root filesystem is read-only, so this runs [wireguard-go](https://git.zx2c4.com/wireguard-go) in userspace from `/data/wg`. `wireguardd` starts it whenever the tool is installed, restarts it after a crash, and re-dials when the handshake goes stale. - -## Install - -On the device, as the normal user: - -```sh -cd /data/iqpilot/iqpilot/tools/wireguard -./build.sh -./install.sh -``` - -`install.sh` prints the device's public key. The private key is generated on the device and never leaves it. - -Fill in `/data/wg/wg.env`: - -| key | value | -|-----|-------| -| `WG_ADDRESS` | this device's tunnel address, e.g. `10.0.0.2/32` | -| `WG_PEER_PUBKEY` | the hub's public key | -| `WG_ENDPOINT` | the hub as `host:port`; a hostname is re-resolved on every re-dial | -| `WG_ALLOWED_IPS` | comma-separated subnets reached through the tunnel | -| `WG_KEEPALIVE` | seconds between keepalives, keep it above 0 behind NAT | - -Add the device on the hub: - -```ini -[Peer] -PublicKey = -AllowedIPs = 10.0.0.2/32 -``` - -Then turn on WireGuard in **Settings > Network**. The panel shows the tunnel state, address, last handshake and public key. - -## Uninstall - -`./uninstall.sh` stops the tunnel and removes the binaries but keeps the keys and `wg.env`. `./uninstall.sh --purge` also deletes `/data/wg` and the device keys. - -## How it behaves - -- `wireguardd` runs at nice 19 and only while `/data/wg` holds both binaries. It polls every 5 s using sysfs and small file reads, and reads the handshake with `sudo wg show` every 30 s once connected. It writes status to `/dev/shm/iq_wireguard.json` on change and every 10 s, so nothing is written to flash. -- `wireguard-go` runs at nice 10 with `GOMAXPROCS=2` and `GOMEMLIMIT=48MiB`. -- The tunnel keeps running when the manager restarts, and `wireguardd` adopts it. A stale handshake (no handshake for 3 min) re-dials every 2 min. A failing bring-up backs off from 30 s to 5 min. -- Routes are added, never replaced. A subnet already routed on another interface (your Wi-Fi LAN, say) is left alone. Default routes and any subnet containing the hub's address are never routed, so the tunnel cannot swallow its own path to the hub. Skipped routes are listed in the WireGuard details in Settings. -- Only the `wireguard-go` started for this interface is ever stopped. -- The switch is the `IQWireGuardEnabled` param, shared by both UIs and the konn3kt app. Turning it off takes the tunnel down within 5 s. -- The konn3kt app can set the hub through the `IQWireGuardConfig` param, which takes precedence over `wg.env`. -- `build.sh` pins wireguard-go and wireguard-tools. Override `WIREGUARD_GO_REF` or `WIREGUARD_TOOLS_REF` to build another release. diff --git a/iqpilot/ui/layouts/settings/iq_panels.py b/iqpilot/ui/layouts/settings/iq_panels.py index 2bfbc4f4..1f8d7b89 100644 --- a/iqpilot/ui/layouts/settings/iq_panels.py +++ b/iqpilot/ui/layouts/settings/iq_panels.py @@ -22,7 +22,7 @@ from iqdbc.car.volkswagen.values import VolkswagenFlags from iqpilot.cereal import custom from iqpilot.common.params import Params -from iqpilot.common.ui_settings import DISPLAY_BRIGHTNESS_VALUES, SLEEP_TIMER_VALUES +from iqpilot.common.ui_settings import DISPLAY_BRIGHTNESS_VALUES, NO_NUDGE_WARNING, SLEEP_TIMER_VALUES from iqpilot.common.swaglog import cloudlog from iqpilot.selfdrive.car.vehicle_catalog import load_catalog from iqpilot.selfdrive.iqmodeld.model_cache import bundle_matches, clear_model_cache, model_cache_size, remove_bundle_files @@ -42,6 +42,7 @@ from iqpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout from iqpilot.selfdrive.ui.layouts.settings.device import DeviceLayout from iqpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from iqpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout +from iqpilot.selfdrive.ui.lib.nnff_settings import nnff_availability from iqpilot.selfdrive.ui.ui_state import device, ui_state from iqpilot.system.hardware import HARDWARE from iqpilot.system.hardware.hw import Paths @@ -260,7 +261,7 @@ class SteeringLayout(Widget): buttons=[lambda: tr("Confirmation (Nudge)"), lambda: tr("No Confirmation (No Nudge)")], button_width=480, selected_index=int(ui_state.params.get("IQLaneChangeTimer", return_default=True)), - callback=lambda selected: ui_state.params.put("IQLaneChangeTimer", selected), + callback=self._set_lane_change_mode, ) self._lane_turn_desire_toggle = toggle_item( lambda: tr("Blinker-Initiated Turn Planning"), @@ -297,6 +298,24 @@ class SteeringLayout(Widget): ] return items + def _set_lane_change_mode(self, selected: int): + current = int(ui_state.params.get("IQLaneChangeTimer", return_default=True)) + if selected == current: + return + if selected == 0: + ui_state.params.put("IQLaneChangeTimer", 0) + return + + self._lane_change_setting.action_item.set_selected_button(current) + + def confirm(result): + if result == DialogResult.CONFIRM: + ui_state.params.put("IQLaneChangeTimer", 1) + self._lane_change_setting.action_item.set_selected_button(1) + + dialog = ConfirmDialog(tr(NO_NUDGE_WARNING), tr("Enable No Nudge"), large=True, title=tr("Are you sure?")) + gui_app.set_modal_overlay(dialog, callback=confirm) + @staticmethod def _has_limited_sab_options() -> bool: brand = "" @@ -1618,8 +1637,18 @@ class VehicleLayout(Widget): callback=self._platform_selector._on_clicked) self._legend_widget = LegendWidget(self._platform_selector) self._refresh_vehicle_row() + self._nnff_available = False + self._nnff_status = "" + self._nnff_vehicle_key = None + self._nnff = toggle_item( + title=lambda: tr("NNFF Steering"), + description=lambda: tr("Use a vehicle-specific neural network for steering feedforward.") + "

" + self._nnff_status, + initial_state=ui_state.params.get_bool("NeuralNetworkFeedForward"), + callback=self._on_nnff, + ) + self._refresh_nnff(force=True) - self.items = [self._vehicle_item, self._legend_widget] + self.items = [self._vehicle_item, self._legend_widget, self._nnff] self._scroller = Scroller(self.items, line_separator=True, spacing=0) @staticmethod @@ -1645,12 +1674,41 @@ class VehicleLayout(Widget): self._current_brand = brand self._brand_settings = brand_settings_for(brand) brand_rows = self._brand_settings.items if self._brand_settings else [] - self.items = [self._vehicle_item, self._legend_widget, *brand_rows] + self.items = [self._vehicle_item, self._legend_widget, self._nnff, *brand_rows] self._scroller = Scroller(self.items, line_separator=True, spacing=0) + def _refresh_nnff(self, force=False): + cp = ui_state.CP + key = (id(cp), cp.carFingerprint if cp else None, cp.brand if cp else None, cp.steerControlType if cp else None, + cp.lateralTuning.which() if cp else None, cp.flags if cp else None, + ui_state.params.get("CarPlatformBundle")) + if force or key != self._nnff_vehicle_key: + self._nnff_vehicle_key = key + self._nnff_available, self._nnff_status = nnff_availability(cp, ui_state.params) + self._sync_nnff_state() + + def _sync_nnff_state(self): + saved = ui_state.params.get_bool("NeuralNetworkFeedForward") + self._nnff.action_item.set_state(saved) + self._nnff.action_item.set_enabled(ui_state.is_offroad() and (self._nnff_available or saved)) + + def _on_nnff(self, enabled): + if not ui_state.is_offroad(): + self._sync_nnff_state() + return + if enabled: + self._refresh_nnff(force=True) + if not self._nnff_available: + return + ui_state.params.put_bool("IQHkgReducedTorqueFeedback", False) + ui_state.params.put_bool("NeuralNetworkFeedForward", enabled) + ui_state.params.put_bool("OnroadCycleRequested", True) + self._sync_nnff_state() + def _on_vehicle_changed(self): self._refresh_vehicle_row() self._sync_brand_panel() + self._refresh_nnff() def _update_state(self): self._on_vehicle_changed() @@ -1662,4 +1720,5 @@ class VehicleLayout(Widget): self._scroller.render(rect) def show_event(self): + self._refresh_nnff(force=True) self._scroller.show_event() diff --git a/iqpilot/ui/mici/onroad/navigation.py b/iqpilot/ui/mici/onroad/navigation.py index d84373a3..e9618198 100644 --- a/iqpilot/ui/mici/onroad/navigation.py +++ b/iqpilot/ui/mici/onroad/navigation.py @@ -32,18 +32,20 @@ class MiciNavigation: def update(self): self.active = False + navigation_active = (ui_state.started and ui_state.params.get_bool("NavigationEnabled") and + ui_state.params.get_bool("NavigationActive")) destination = ui_state.params.get("NavigationDestination") try: lat, lon = float(destination['latitude']), float(destination['longitude']) destination = (lat, lon) if -90 <= lat <= 90 and -180 <= lon <= 180 else None except (KeyError, TypeError, ValueError, OverflowError): destination = None - if destination != self._destination or not ui_state.started or not ui_state.params.get_bool("NavigationActive"): + if destination != self._destination or not navigation_active: self._dismissed = False self._dismissed_maneuver = None self._last_tap = -math.inf self._destination = destination - if destination is None or not ui_state.started or not ui_state.params.get_bool("NavigationActive"): + if destination is None or not navigation_active: return sm = ui_state.sm diff --git a/iqpilot/ui/onroad/nav_map_panel.py b/iqpilot/ui/onroad/nav_map_panel.py index c8e691aa..143b86d6 100644 --- a/iqpilot/ui/onroad/nav_map_panel.py +++ b/iqpilot/ui/onroad/nav_map_panel.py @@ -1183,9 +1183,10 @@ class NavMapPanel(Widget): if now - self._last_params_refresh < PARAMS_REFRESH_S: return try: - self._maps_enabled = self._force_visible or self._params.get_bool("OnScreenNavigation") + navigation_enabled = self._params.get_bool("NavigationEnabled") + self._maps_enabled = self._force_visible or (navigation_enabled and self._params.get_bool("OnScreenNavigation")) except UnknownKeyName: - # If the param doesn't exist in this build, default to disabled unless explicitly embedded. + navigation_enabled = False self._maps_enabled = self._force_visible try: self._online_maps_enabled = self._params.get_bool("OnlineOSMaps") @@ -1195,7 +1196,7 @@ class NavMapPanel(Widget): self._offline_maps_enabled = False has_downloaded_maps = find_offline_mbtiles_path() is not None or find_offline_xyz_root() is not None - if gui_app.big_ui() and has_downloaded_maps: + if navigation_enabled and gui_app.big_ui() and has_downloaded_maps: enable_panel = not self._maps_enabled enable_offline = not self._offline_maps_enabled self._maps_enabled = True @@ -1365,9 +1366,9 @@ class NavMapPanel(Widget): def set_maps_enabled(self, enabled: bool) -> None: self._params.put_bool("OnScreenNavigation", enabled) - self._maps_enabled = enabled - self._last_params_refresh = time.monotonic() - if not enabled: + self._last_params_refresh = -math.inf + self._refresh_params(time.monotonic()) + if not self._maps_enabled: self.active = False self._release_providers() diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 9dbb1cd3..1e16ee55 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -18,7 +18,8 @@ function agnos_version_allowed { IFS=',' read -r -a compat_versions <<< "${AGNOS_COMPAT_VERSIONS:-}" for compat_version in "${compat_versions[@]}"; do - compat_version="${compat_version// /}" + compat_version="${compat_version#"${compat_version%%[![:space:]]*}"}" + compat_version="${compat_version%"${compat_version##*[![:space:]]}"}" if [ -n "$compat_version" ] && [ "$current_version" = "$compat_version" ]; then return 0 fi @@ -71,24 +72,23 @@ IQFN } function agnos_init { - # TODO: move this to agnos - sudo rm -f /data/etc/NetworkManager/system-connections/*.nmmeta + # On IQ.OS 4.9.9.2+ the root iqpilot-boot-prepare oneshot performs this setup (no owner sudo, verity-safe rootfs); its done flag means skip. + if [ ! -f /run/iqpilot-boot-prepare.done ]; then + # TODO: move this to agnos + sudo -n rm -f /data/etc/NetworkManager/system-connections/*.nmmeta 2>/dev/null || true - # set success flag for current boot slot - sudo abctl --set_success + sudo -n abctl --set_success 2>/dev/null || true - # TODO: do this without udev in AGNOS - # udev does this, but sometimes we startup faster - sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 - sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 + sudo -n chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 2>/dev/null || true + sudo -n chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 2>/dev/null || true - # iptables-nft fails on this kernel (no nf_tables module), breaking NM hotspot NAT rules - if [ "$(readlink /etc/alternatives/iptables)" != "/usr/sbin/iptables-legacy" ]; then - sudo mount -o remount,rw / - sudo ln -sf /usr/sbin/iptables-legacy /etc/alternatives/iptables - sudo ln -sf /usr/sbin/iptables-legacy-restore /etc/alternatives/iptables-restore - sudo ln -sf /usr/sbin/iptables-legacy-save /etc/alternatives/iptables-save - sudo mount -o remount,ro / + if [ "$(readlink /etc/alternatives/iptables)" != "/usr/sbin/iptables-legacy" ]; then + sudo -n mount -o remount,rw / 2>/dev/null || true + sudo -n ln -sf /usr/sbin/iptables-legacy /etc/alternatives/iptables 2>/dev/null || true + sudo -n ln -sf /usr/sbin/iptables-legacy-restore /etc/alternatives/iptables-restore 2>/dev/null || true + sudo -n ln -sf /usr/sbin/iptables-legacy-save /etc/alternatives/iptables-save 2>/dev/null || true + sudo -n mount -o remount,ro / 2>/dev/null || true + fi fi # Check if AGNOS update is required @@ -108,7 +108,7 @@ function agnos_init { esac fi if $AGNOS_PY --verify $MANIFEST; then - sudo reboot + sudo -n reboot fi # On stock AGNOS the updater picks updater_weston (weston is up), a compiled display binary that # can't get a compositor surface on a first boot, so the IQ.OS flash silently wedges on the comma @@ -117,7 +117,7 @@ function agnos_init { # progress prompt fine, so fall through and keep it. This mirrors the updater's own weston check. if systemctl is-active --quiet weston-ready; then if $AGNOS_PY --swap $MANIFEST; then - sudo reboot + sudo -n reboot fi fi $DIR/iqpilot/system/hardware/tici/updater $AGNOS_PY $MANIFEST @@ -201,56 +201,67 @@ function launch { # Install independent systemd services BEFORE build so they run even # when openpilot fails to compile — SSH/BLE/hephaestusd must always # be reachable for device recovery. + # On IQ.OS 4.9.9.2+ the root boot-prepare oneshot already installed+started these; skip the owner-sudo path. + # Exception: if boot-prepare ran before the checkout existed (first boot after flash), the services weren't started. + if [ ! -f /run/iqpilot-boot-prepare.done ] || ! systemctl is-active --quiet hephaestusd.service; then for service_name in hephaestusd ble-transportd flockd; do service_src="$DIR/iqpilot/system/${service_name}.service" service_dst="/etc/systemd/system/${service_name}.service" service_lib="/lib/systemd/system/${service_name}.service" service_dropin="/run/systemd/system/${service_name}.service.d" service_exec="$(grep '^ExecStart=' "$service_src")" - sudo mkdir -p "$service_dropin" - printf '[Service]\nWorkingDirectory=%s\nEnvironment="IQPILOT_SOURCE_ROOT=%s/iqpilot"\nEnvironment="IQPILOT_PROPRIETARY_ROOT=%s/artifacts"\nEnvironment="PYTHONPATH=%s"\nExecStart=\n%s\n' "$RUNTIME_COMPAT_ROOT" "$DIR" "$DIR" "$PYTHONPATH" "$service_exec" | sudo tee "$service_dropin/iqpilot-packages.conf" >/dev/null - sudo systemctl daemon-reload + sudo -n mkdir -p "$service_dropin" 2>/dev/null || mkdir -p "$service_dropin" 2>/dev/null || true + printf '[Service]\nWorkingDirectory=%s\nEnvironment="IQPILOT_SOURCE_ROOT=%s/iqpilot"\nEnvironment="IQPILOT_PROPRIETARY_ROOT=%s/artifacts"\nEnvironment="PYTHONPATH=%s"\nExecStart=\n%s\n' "$RUNTIME_COMPAT_ROOT" "$DIR" "$DIR" "$PYTHONPATH" "$service_exec" | sudo -n tee "$service_dropin/iqpilot-packages.conf" >/dev/null 2>&1 || true + sudo -n systemctl daemon-reload 2>/dev/null || true if [ -f "$service_lib" ] && grep -q "/usr/libexec/iqpilot/iqpilot_bundle_runner" "$service_lib"; then if [ -f "$service_dst" ]; then - sudo mount -o remount,rw / - sudo rm -f "$service_dst" - sudo systemctl daemon-reload - sudo mount -o remount,ro / + sudo -n mount -o remount,rw / 2>/dev/null || true + sudo -n rm -f "$service_dst" 2>/dev/null || true + sudo -n systemctl daemon-reload 2>/dev/null || true + sudo -n mount -o remount,ro / 2>/dev/null || true fi - sudo systemctl enable "${service_name}.service" + sudo -n systemctl enable "${service_name}.service" 2>/dev/null || true # --no-block: these units ExecStartPre-wait for the verified runtime. Blocking here # made a missing runner stall the whole install for the unit's 600s timeout (x3 units). if systemctl is-active --quiet "${service_name}.service"; then - sudo systemctl restart --no-block "${service_name}.service" + sudo -n systemctl restart --no-block "${service_name}.service" 2>/dev/null || true else - sudo systemctl start --no-block "${service_name}.service" + sudo -n systemctl start --no-block "${service_name}.service" 2>/dev/null || true fi elif [ -f "$service_src" ]; then if [ ! -f "$service_dst" ] || ! cmp -s "$service_src" "$service_dst"; then - sudo mount -o remount,rw / - sudo cp "$service_src" "$service_dst" - sudo systemctl daemon-reload - sudo systemctl enable "${service_name}.service" - sudo mount -o remount,ro / - sudo systemctl restart --no-block "${service_name}.service" + sudo -n mount -o remount,rw / 2>/dev/null || true + sudo -n cp "$service_src" "$service_dst" 2>/dev/null || true + sudo -n systemctl daemon-reload 2>/dev/null || true + sudo -n systemctl enable "${service_name}.service" 2>/dev/null || true + sudo -n mount -o remount,ro / 2>/dev/null || true + sudo -n systemctl restart --no-block "${service_name}.service" 2>/dev/null || true elif ! systemctl is-active --quiet "${service_name}.service"; then - sudo systemctl start --no-block "${service_name}.service" + sudo -n systemctl start --no-block "${service_name}.service" 2>/dev/null || true fi fi done + fi # detect mr.one C3 Lite hardware set_lite_hw # hardware specific init if [ -f /AGNOS ]; then + if ! id -nG 2>/dev/null | grep -qw otaflash; then + sudo -n usermod -aG otaflash "$(whoami)" 2>/dev/null || true + fi agnos_init - sudo "$DIR/iqpilot/system/hardware/tici/zram_setup.sh" || true + # IQ.OS 4.9.9.2+ sets up zram via the baked iqos-zram.service; only the legacy owner-sudo path runs it here. + if [ ! -f /run/iqpilot-boot-prepare.done ]; then + sudo -n "$DIR/iqpilot/system/hardware/tici/zram_setup.sh" 2>/dev/null || true + fi fi - if [ -f "$DIR/artifacts/runtime/apply_boot_branding.py" ]; then - sudo python3 "$DIR/artifacts/runtime/apply_boot_branding.py" || true + # IQ.OS 4.9.9.2+ bakes branding (bg.jpg, splash, passwd/shadow) into the image; applying from /data as root is an escalation path, so skip it there. + if [ ! -f /run/iqpilot-boot-prepare.done ] && [ -f "$DIR/artifacts/runtime/apply_boot_branding.py" ]; then + sudo -n python3 "$DIR/artifacts/runtime/apply_boot_branding.py" 2>/dev/null || true fi # /home is an ephemeral overlay (resets each boot); re-inject the source line. best-effort @@ -270,7 +281,7 @@ function launch { if pkill -f /tmp/installer 2>/dev/null; then sleep 1; fi "$MANAGER_PYTHON" ./build.py for service_name in hephaestusd ble-transportd flockd; do - sudo systemctl restart --no-block "${service_name}.service" + sudo -n systemctl restart --no-block "${service_name}.service" 2>/dev/null || true done else MANAGER_PYTHON="/usr/local/venv/bin/python3" diff --git a/launch_env.sh b/launch_env.sh index 2cf0d562..0c62bffb 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -23,7 +23,7 @@ if [ -z "$AGNOS_VERSION" ]; then case "$DEVICE_MODEL" in *) - export AGNOS_VERSION="IQ.OS 4.9.9.1" + export AGNOS_VERSION="IQ.OS 4.9.9.4" ;; esac fi