diff --git a/artifacts/iqpilot_emac_private/manifest.json b/artifacts/iqpilot_emac_private/manifest.json index ed66bffe9..a339c1150 100644 --- a/artifacts/iqpilot_emac_private/manifest.json +++ b/artifacts/iqpilot_emac_private/manifest.json @@ -191,8 +191,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/daemon.py": { "mode": 420, - "sha256": "15002cb6c81cca8c7aec572568da1e202c55777bbd21fefaf8fda36fd0d39b6e", - "size": 30295 + "sha256": "581e57ffcafab0f796346e657f8cd975daf7b22a92878f20603fd367cb127346", + "size": 30513 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/driving_action.py": { "mode": 420, @@ -231,8 +231,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_channel.py": { "mode": 420, - "sha256": "2d706993df418501d93dc766ffe452131e565bfbc2e8412b527dc14915e92832", - "size": 1872 + "sha256": "092d4762710cf453f94741ebd5f63dc8e1c6aac97a177c4cdc5afb7fc868b1d7", + "size": 2112 }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_warp.py": { "mode": 420, @@ -341,8 +341,8 @@ }, "python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py": { "mode": 420, - "sha256": "2cc299b2cfda88ee2dbc52658f145fb7840e2dfc49a7371e22fe671beed0e57f", - "size": 5436 + "sha256": "6b3992f2bd68774511fe8fe313ee801387f89ef8a52d88e772cf47ca09b75bfe", + "size": 5438 }, "python/_iqclosure/iqpilot/selfdrive/locationd/__init__.py": { "mode": 420, @@ -536,28 +536,28 @@ }, "python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "c069d85362528e1f1e36c419950096eb81dd07e96fdb14fa6aac6c50178bad27", + "sha256": "fea0dc4e8eb2f65ff5ef03158534aad4e862b2d35522c6a69bb84cd745aafa0b", "size": 333808 }, "python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "846394e2620f16d5bb457a22a01bf44b0c7ba2c3511e90a1b6a2f57bf60e65ef", + "sha256": "b5c161b71fb5cfaded8810938a3270124b99fbf18ae4759945555a19c258aaa0", "size": 68072 }, "python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "af7708ec07d580ed547985fd45b11ae92c5aa44b3b3222c2550b694e3caaf110", + "sha256": "e140b44ab0dcd34ac2d33afd89d440af3ccfae04b4c75b1148ff8773ecb5a122", "size": 267864 }, "python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "abefe36ce82767927f9c258a08ef13421b724d85736291043d3062fe19451dcd", + "sha256": "45f0062fe6c79527a635cccf85e68fcb1294561096d67dcb9f832ce2eecb30de", "size": 203552 }, "python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "518157f68e397db8bc4ac345989b62abccddd5fa91f41c4a0c42d5408cde4477", - "size": 399040 + "sha256": "ee6a34c2d0f42c45338c0cf97d55c3a7575c5c1cc5879c6fd49805e3101eed3d", + "size": 399128 }, "runtime": { "entries": { @@ -568,10 +568,10 @@ } }, "signatures": { - "python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so": "U9GH0luMCR2czyM8p33BMAvmT48ZKjlSndkx1YpTDKAvCvmksXlrhckumbk65JX9vXR8xLRLYGksR7muXvDpBw==", - "python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so": "I1r0dLpsIGW/UufeQJ2s/DVEzB1t1ANW5HwsILHbf7nh6ifasd7JrfEIkQPl+U+fbu2N5D0ciSKaMNJXFwXYAQ==", - "python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so": "zxJFn+T/NY/qBNRYJ/s7Xqp8PFVO9GCTiMqH03Y2TiZGiisgwrUGDEcBjBVAIjDDYqjYTn8s01p7cN2PCK92Dg==", - "python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so": "y0TNy549j35aJ7lnS4N21Fx5XRwfWjCZxyObB+SQnZWJQ8sW63r4lVS+t01koyQUXLmzT3/y1mYJ3WujlkxACQ==", - "python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so": "K1glyNgXfg7KVRXYhfsGS53iH9hxv+k5TOzylw6Cu3KA2JvyOYUgMhQlVDg+p+t4DJYfOr69H/d5ENtgHGVNDg==" + "python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so": "LYNTQr1MnMp3btqtnZ+IcvQYgE9VnRVPyAGms0QUARkSUs3poUIIuV/iYKA14wIUS5/gxHrqiGTNZcyJdNuBCw==", + "python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so": "8mWAxk4HILq0BLA2SQSwjuZuqx+Dl/WZdOf8JY2leDUMxDXla1KoMzLPBL/ngL1uQ1r2MqFlLohli4jBYPmtBA==", + "python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so": "T744Rj5GuQl9Xyzc7nxEtE+Wx8rIteZDC+TiPNcSqsyBOBvf5luN7gtrIWfM+dB5fp36EJhCJZFl48E/WgqNAQ==", + "python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so": "WCv8ts3H+9N+OtgxaESxadcS7eRu/bDxvIQIRkj24LLeHJ3hL/Z/0zWJlALqkQuL/eBTjs/nTaziIDwwu9UHAA==", + "python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so": "o+2jZmIPfaJV8QPleFWyB24sLCVvkJ3Hwg+7kFYXIE7cHXQm2tzkrgdPhUlWhslTk/gRKFBYg67FrjFvBpjRAw==" } } diff --git a/artifacts/iqpilot_emac_private/manifest.json.sig b/artifacts/iqpilot_emac_private/manifest.json.sig index c126d077b..53e75d345 100644 --- a/artifacts/iqpilot_emac_private/manifest.json.sig +++ b/artifacts/iqpilot_emac_private/manifest.json.sig @@ -1 +1 @@ -xwY/6EveM0uSrV6q37dNH9aAkFdYr5Wr1YSawQtQNWBSyzDwA5qacLqCqZFbLbLAx1A9d8wUSEDPeAESmMHMBA== +NIDyUgCyKnLKNpWgrqG5yIPAYBaD61T6Yv4KV4uzFhKpqo0OInBCXl1AcEGupEFLfTXdsrS5gDnm/4KdU2jiDw== 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 97327992f..8828032d3 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 @@ -513,6 +513,9 @@ class InferenceDaemon: self._lat_smooth_extra_sec = 0.0 def _load_car_params(self, demo: bool): + if not demo and self._params.get_bool("IQBenchIgnition") and self._params.get("CarParams") is None: + cloudlog.warning("iqmodeld: bench ignition with no CarParams; running with the demo car") + demo = True car_params = get_demo_car_params() if demo else messaging.log_from_bytes( self._params.get("CarParams", block=True), car.CarParams) cloudlog.info("iqmodeld got CarParams: %s", car_params.brand) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_channel.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_channel.py index a9fa710bc..937a2d932 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_channel.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_channel.py @@ -44,6 +44,14 @@ class ModelChannel: return None return frame_id + def peek(self) -> tuple[int | None, bool]: + seq, frame_id, length = HEADER.unpack(self.mm[:HEADER.size]) + if seq % 2 != 0: + return None, True + if seq == 0 or length == 0: + return None, False + return frame_id, False + def read(self) -> tuple[int, dict] | None: seq1, frame_id, length = HEADER.unpack(self.mm[:HEADER.size]) if seq1 == 0 or seq1 % 2 != 0 or length == 0: diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py index 07e4886eb..03fe6a88d 100644 --- a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py @@ -87,8 +87,6 @@ def main() -> None: print(f"saved warp JIT to {out} ({os.path.getsize(out) / 1e6:.2f} MB)") -if __name__ == "__main__": - main() def selftest_inputs(cam_w: int, cam_h: int, nv12_size: int): @@ -117,3 +115,7 @@ def selftest_digest(compiled, cam_w: int, cam_h: int, nv12_size: int) -> str: frame=Tensor(frame, device=dev).realize(), big_frame=Tensor(big_frame, device=dev).realize()) return hashlib.sha256(out.numpy().astype(np.uint8).tobytes()).hexdigest() + + +if __name__ == "__main__": + main() 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 index 079dda44c..ff196001a 100755 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 index 97372609f..605719d38 100755 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 index 6430d9a7e..74ff33ea9 100755 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 index 50a7ef9e3..6a6d20727 100755 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 index e03fa1469..46e270fbd 100755 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_navassist_private/manifest.json b/artifacts/iqpilot_navassist_private/manifest.json new file mode 100644 index 000000000..3558cff4a --- /dev/null +++ b/artifacts/iqpilot_navassist_private/manifest.json @@ -0,0 +1,174 @@ +{ + "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": "c69185d8f6482c8b021f687b793246d2e2ede853e89a88748c64b85f60711b59", + "size": 3638 + }, + "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 + }, + "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": "d07a56d54a8dfd8dd66b0a58a709cd13585de455b7eeecbb50616792757193e1", + "size": 67640 + }, + "python/iqpilot_private/navassist/__init__.py": { + "mode": 420, + "sha256": "ee4237375e199e74a35b5c1851f32eec3d0799966b7f1211c86ce00a0f756088", + "size": 112 + }, + "python/iqpilot_private/navassist/appsetup.cpython-312-aarch64-linux-gnu.so": { + "mode": 493, + "sha256": "d7ba6546b659c439dfd747ee80419117493db6bdfee5305965428896aa75e720", + "size": 135328 + }, + "python/iqpilot_private/navassist/assets.cpython-312-aarch64-linux-gnu.so": { + "mode": 493, + "sha256": "45f9529ee808c55bd2e8965a6c72e5050dd8d12f47fd71e996a5aad161aa2e6b", + "size": 201704 + }, + "python/iqpilot_private/navassist/container.cpython-312-aarch64-linux-gnu.so": { + "mode": 493, + "sha256": "16d2d214e9d4a793ead2b623fade799cf842e739b25cf74ab5ed60cb7c26f2f3", + "size": 134016 + }, + "python/iqpilot_private/navassist/daemon.cpython-312-aarch64-linux-gnu.so": { + "mode": 493, + "sha256": "f2d8fd727cb6002c174ea47ccd2945977143e5abe6d188732217474bcbd931bc", + "size": 203048 + }, + "python/iqpilot_private/navassist/guard.cpython-312-aarch64-linux-gnu.so": { + "mode": 493, + "sha256": "2aed237187dcfa009ebbd5f0f6beab7a5da4d89cd3b4e37968a2bb62f7fc6468", + "size": 68240 + }, + "python/iqpilot_private/navassist/paths.cpython-312-aarch64-linux-gnu.so": { + "mode": 493, + "sha256": "0e63117bb686de08c73176138cc3851cbdadf491774b04e752995c059dcd281a", + "size": 69656 + }, + "runtime": { + "entries": { + "iqpilot_private.navassist.daemon": { + "path": "python/iqpilot_private/navassist/daemon.cpython-312-aarch64-linux-gnu.so", + "type": "python-module" + } + } + }, + "signatures": { + "python/iqpilot_private/navassist/__init__.cpython-312-aarch64-linux-gnu.so": "grzvl6Szgy0DhX0A2RHxMH+EZYP4GDUwRwrXCI278zqBo9A+xdZPxBSQ0FIxoDzMZVrAfrK5yOADYYyF1gFkDQ==", + "python/iqpilot_private/navassist/appsetup.cpython-312-aarch64-linux-gnu.so": "e4WP9/AodSGn0e1aCmUfLvDCA36Z3lyLQXTAXeuMpFamR2uu7SRYCKBoW7gIMtrqZvNi3DNeDfmPaWdITSlDDA==", + "python/iqpilot_private/navassist/assets.cpython-312-aarch64-linux-gnu.so": "K9jDY0eQxqtE7RKPmnnjJhOYogK4mkUXxucZmkY+K798BWThYxT+Q0q2D50eW9vc33fKH0lk4+L4w4P/qkTzDA==", + "python/iqpilot_private/navassist/container.cpython-312-aarch64-linux-gnu.so": "qE6DWECE/wsj58n9ewIFAEts8xTCKjWGQ0p+7fZ3XKFJR7hvFt7fNTZAQm1sBSAd55697ow7kq7Y1wyicvBBDw==", + "python/iqpilot_private/navassist/daemon.cpython-312-aarch64-linux-gnu.so": "h2yssDIlxVDJMaPW+K3TzGKwzrGcGDdQdOrZ75LmTS02UN34tmPWzv0j7WV2ombz5n8gu8+FcxYB+o1VapqkDA==", + "python/iqpilot_private/navassist/guard.cpython-312-aarch64-linux-gnu.so": "kTPke8BA2muzet1xLQWUKnFtlVboJSZ6Ep6O79y2zQ8xMkEPum2fHiYZr8wkqu+2NMEB/j1HO0BvwJPdl/qOBg==", + "python/iqpilot_private/navassist/paths.cpython-312-aarch64-linux-gnu.so": "Sd7DGPwD4DDrbqrnCiIwGgGAnTXU5MTS7rNdpq2rsixxvXhFzaaxQgMbGFi6CoylBAHeOwqi9uwjtY6aiaYkCQ==" + } +} diff --git a/artifacts/iqpilot_navassist_private/manifest.json.sig b/artifacts/iqpilot_navassist_private/manifest.json.sig new file mode 100644 index 000000000..ad4844acd --- /dev/null +++ b/artifacts/iqpilot_navassist_private/manifest.json.sig @@ -0,0 +1 @@ ++k8uY8dYPLT5xuTlqHHgo9YUJkOQM6KQxhPn4leJUi2t9DsHDEMBWJwdtyqzQi5Zes9eRuG9GmF7Hircxj88DQ== diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/_vendored.json b/artifacts/iqpilot_navassist_private/python/_iqclosure/_vendored.json new file mode 100644 index 000000000..f535c129d --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/_vendored.json @@ -0,0 +1,19 @@ +[ + "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/iqpilot/system/android/__init__.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/__init__.py similarity index 100% rename from iqpilot/system/android/__init__.py rename to artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/__init__.py diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/__init__.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/gpio.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/gpio.py new file mode 100644 index 000000000..8f025a2da --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/gpio.py @@ -0,0 +1,89 @@ +import os +import fcntl +import ctypes +from functools import cache + +def gpio_init(pin: int, output: bool) -> None: + try: + with open(f"/sys/class/gpio/gpio{pin}/direction", 'wb') as f: + f.write(b"out" if output else b"in") + except Exception as e: + print(f"Failed to set gpio {pin} direction: {e}") + +def gpio_set(pin: int, high: bool) -> None: + try: + with open(f"/sys/class/gpio/gpio{pin}/value", 'wb') as f: + f.write(b"1" if high else b"0") + except Exception as e: + print(f"Failed to set gpio {pin} value: {e}") + +def gpio_read(pin: int) -> bool | None: + val = None + try: + with open(f"/sys/class/gpio/gpio{pin}/value", 'rb') as f: + val = bool(int(f.read().strip())) + except Exception as e: + print(f"Failed to set gpio {pin} value: {e}") + + 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: + with open(f"/sys/kernel/irq/{irq}/actions") as f: + actions = f.read().strip().split(',') + return actions + except FileNotFoundError: + return [] + +def get_irqs_for_action(action: str) -> list[str]: + ret = [] + with open("/proc/interrupts") as f: + for l in f.readlines(): + irq = l.split(':')[0].strip() + if irq.isdigit() and action in get_irq_action(irq): + ret.append(irq) + return ret + +# *** gpiochip *** + +class gpioevent_data(ctypes.Structure): + _fields_ = [ + ("timestamp", ctypes.c_uint64), + ("id", ctypes.c_uint32), + ] + +class gpioevent_request(ctypes.Structure): + _fields_ = [ + ("lineoffset", ctypes.c_uint32), + ("handleflags", ctypes.c_uint32), + ("eventflags", ctypes.c_uint32), + ("label", ctypes.c_char * 32), + ("fd", ctypes.c_int) + ] + +def gpiochip_get_ro_value_fd(label: str, gpiochip_id: int, pin: int) -> int: + GPIOEVENT_REQUEST_BOTH_EDGES = 0x3 + GPIOHANDLE_REQUEST_INPUT = 0x1 + GPIO_GET_LINEEVENT_IOCTL = 0xc030b404 + + rq = gpioevent_request() + rq.lineoffset = pin + rq.handleflags = GPIOHANDLE_REQUEST_INPUT + rq.eventflags = GPIOEVENT_REQUEST_BOTH_EDGES + rq.label = label.encode('utf-8')[:31] + b'\0' + + fd = os.open(f"/dev/gpiochip{gpiochip_id}", os.O_RDONLY) + fcntl.ioctl(fd, GPIO_GET_LINEEVENT_IOCTL, rq) + os.close(fd) + return int(rq.fd) diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/i2c.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/i2c.py new file mode 100644 index 000000000..1dfaa659a --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/i2c.py @@ -0,0 +1,81 @@ +import os +import fcntl +import ctypes + +# I2C constants from /usr/include/linux/i2c-dev.h +I2C_SLAVE = 0x0703 +I2C_SLAVE_FORCE = 0x0706 +I2C_SMBUS = 0x0720 + +# SMBus transfer types +I2C_SMBUS_READ = 1 +I2C_SMBUS_WRITE = 0 +I2C_SMBUS_BYTE_DATA = 2 +I2C_SMBUS_I2C_BLOCK_DATA = 8 + +I2C_SMBUS_BLOCK_MAX = 32 + + +class _I2cSmbusData(ctypes.Union): + _fields_ = [ + ("byte", ctypes.c_uint8), + ("word", ctypes.c_uint16), + ("block", ctypes.c_uint8 * (I2C_SMBUS_BLOCK_MAX + 2)), + ] + + +class _I2cSmbusIoctlData(ctypes.Structure): + _fields_ = [ + ("read_write", ctypes.c_uint8), + ("command", ctypes.c_uint8), + ("size", ctypes.c_uint32), + ("data", ctypes.POINTER(_I2cSmbusData)), + ] + + +class SMBus: + def __init__(self, bus: int): + self._fd = os.open(f'/dev/i2c-{bus}', os.O_RDWR) + + def __enter__(self) -> 'SMBus': + return self + + def __exit__(self, *args) -> None: + self.close() + + def close(self) -> None: + if hasattr(self, '_fd') and self._fd >= 0: + os.close(self._fd) + self._fd = -1 + + def _set_address(self, addr: int, force: bool = False) -> None: + ioctl_arg = I2C_SLAVE_FORCE if force else I2C_SLAVE + fcntl.ioctl(self._fd, ioctl_arg, addr) + + def _smbus_access(self, read_write: int, command: int, size: int, data: _I2cSmbusData) -> None: + ioctl_data = _I2cSmbusIoctlData(read_write, command, size, ctypes.pointer(data)) + fcntl.ioctl(self._fd, I2C_SMBUS, ioctl_data) + + def read_byte_data(self, addr: int, register: int, force: bool = False) -> int: + self._set_address(addr, force) + data = _I2cSmbusData() + self._smbus_access(I2C_SMBUS_READ, register, I2C_SMBUS_BYTE_DATA, data) + return int(data.byte) + + def write_byte_data(self, addr: int, register: int, value: int, force: bool = False) -> None: + self._set_address(addr, force) + data = _I2cSmbusData() + data.byte = value & 0xFF + self._smbus_access(I2C_SMBUS_WRITE, register, I2C_SMBUS_BYTE_DATA, data) + + def read_i2c_block_data(self, addr: int, register: int, length: int, force: bool = False) -> list[int]: + self._set_address(addr, force) + if not (0 <= length <= I2C_SMBUS_BLOCK_MAX): + raise ValueError(f"length must be 0..{I2C_SMBUS_BLOCK_MAX}") + + data = _I2cSmbusData() + data.block[0] = length + self._smbus_access(I2C_SMBUS_READ, register, I2C_SMBUS_I2C_BLOCK_DATA, data) + read_len = int(data.block[0]) or length + read_len = min(read_len, length) + return [int(b) for b in data.block[1 : read_len + 1]] 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 new file mode 100644 index 000000000..ceaf083cd --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/logging_extra.py @@ -0,0 +1,249 @@ +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 new file mode 100644 index 000000000..9fd2f695d --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/realtime.py @@ -0,0 +1,134 @@ +"""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 new file mode 100644 index 000000000..f8fd9d392 --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/swaglog.py @@ -0,0 +1,165 @@ +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/common/time_helpers.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/time_helpers.py new file mode 100644 index 000000000..8564e270c --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/time_helpers.py @@ -0,0 +1,15 @@ +import datetime +from pathlib import Path + +MIN_DATE = datetime.datetime(year=2025, month=2, day=21) + +def min_date(): + # on systemd systems, the default time is the systemd build time + systemd_path = Path("/lib/systemd/systemd") + if systemd_path.exists(): + d = datetime.datetime.fromtimestamp(systemd_path.stat().st_mtime) + return max(MIN_DATE, d + datetime.timedelta(days=1)) + return MIN_DATE + +def system_time_valid(): + return datetime.datetime.now() > min_date() diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/utils.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/utils.py new file mode 100644 index 000000000..e0aa1605d --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/common/utils.py @@ -0,0 +1,271 @@ +import io +import os +import tempfile +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 + +class Timer: + """Simple lap timer for profiling sequential operations.""" + + def __init__(self): + self._start = self._lap = time.monotonic() + self._sections = {} + + def lap(self, name): + now = time.monotonic() + self._sections[name] = now - self._lap + self._lap = now + + @property + def total(self): + return time.monotonic() - self._start + + def fmt(self, duration): + parts = ", ".join(f"{k}={v:.2f}s" + (f" ({duration/v:.0f}x)" if k == 'render' and v > 0 else "") for k, v in self._sections.items()) + total = self.total + realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" + return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" + +def sudo_write(val: str, path: str) -> None: + try: + with open(path, 'w') as f: + f.write(str(val)) + except PermissionError: + os.system(f"sudo chmod a+w {path}") + try: + 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}'") + + +def sudo_read(path: str) -> str: + try: + return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + except Exception: + return "" + + +class MovingAverage: + def __init__(self, window_size: int): + self.window_size: int = window_size + self.buffer: list[float] = [0.0] * window_size + self.index: int = 0 + self.count: int = 0 + self.sum: float = 0.0 + + def add_value(self, new_value: float): + # Update the sum: subtract the value being replaced and add the new value + self.sum -= self.buffer[self.index] + self.buffer[self.index] = new_value + self.sum += new_value + + # Update the index in a circular manner + self.index = (self.index + 1) % self.window_size + + # Track the number of added values (for partial windows) + self.count = min(self.count + 1, self.window_size) + + def get_average(self) -> float: + if self.count == 0: + return float('nan') + return self.sum / self.count + + +class CallbackReader: + """Wraps a file, but overrides the read method to also + call a callback function with the number of bytes read so far.""" + + def __init__(self, f, callback, *args): + self.f = f + self.callback = callback + self.cb_args = args + self.total_read = 0 + + def __getattr__(self, attr): + return getattr(self.f, attr) + + def read(self, *args, **kwargs): + chunk = self.f.read(*args, **kwargs) + self.total_read += len(chunk) + self.callback(*self.cb_args, self.total_read) + return chunk + + +@contextlib.contextmanager +def atomic_write(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None, + overwrite: bool = False): + """Write to a file atomically using a temporary file in the same directory as the destination file.""" + dir_name = os.path.dirname(path) + + if not overwrite and os.path.exists(path): + raise FileExistsError(f"File '{path}' already exists. To overwrite it, set 'overwrite' to True.") + + with tempfile.NamedTemporaryFile(mode=mode, buffering=buffering, encoding=encoding, newline=newline, dir=dir_name, delete=False) as tmp_file: + yield tmp_file + tmp_file_name = tmp_file.name + os.replace(tmp_file_name, path) + + +def get_upload_stream(filepath: str, should_compress: bool) -> tuple[io.BufferedIOBase, int]: + if not should_compress: + file_size = os.path.getsize(filepath) + file_stream = open(filepath, "rb") + return file_stream, file_size + + # Compress the file on the fly + compressed_stream = io.BytesIO() + compressor = zstd.ZstdCompressor(level=LOG_COMPRESSION_LEVEL) + + with open(filepath, "rb") as f: + compressor.copy_stream(f, compressed_stream) + compressed_size = compressed_stream.tell() + compressed_stream.seek(0) + return compressed_stream, compressed_size + + +# remove all keys that end in DEPRECATED +def strip_deprecated_keys(d): + for k in list(d.keys()): + if isinstance(k, str): + if k.endswith('DEPRECATED'): + d.pop(k) + elif isinstance(d[k], dict): + strip_deprecated_keys(d[k]) + return d + + +def run_cmd(cmd: list[str], cwd=None, env=None) -> str: + return subprocess.check_output(cmd, encoding='utf8', cwd=cwd, env=env).strip() + + +def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> str: + try: + return run_cmd(cmd, cwd=cwd, env=env) + except subprocess.CalledProcessError: + 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) + def wrapper(*args, **kwargs): + for _ in range(attempts): + try: + return func(*args, **kwargs) + except Exception: + print(f"{func.__name__} failed, trying again") + time.sleep(delay) + + if ignore_failure: + print(f"{func.__name__} failed after retry") + else: + raise Exception(f"{func.__name__} failed after retry") + return wrapper + return decorator diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/__init__.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/__init__.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/__init__.py new file mode 100644 index 000000000..1338aa2cf --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/__init__.py @@ -0,0 +1,22 @@ +import os +from typing import cast + +from iqpilot.system.hardware.base import HardwareBase +from iqpilot.system.hardware.tici.hardware import Tici +from iqpilot.system.hardware.pc.hardware import Pc + +TICI = os.path.isfile('/TICI') +AGNOS = os.path.isfile('/AGNOS') +PC = not TICI + + +if TICI: + HARDWARE = cast(HardwareBase, Tici()) +else: + HARDWARE = cast(HardwareBase, Pc()) + +# Only comma 3/3X expose the DMA-BUF EGL extensions used by the zero-copy +# camera renderer and the direct EGL frame-pacing calls. /TICI is also present +# on comma 4, so it identifies the AGNOS hardware family rather than this GPU +# capability. +EGL_DMA_BUF_SUPPORTED = TICI and HARDWARE.get_device_type() in ("tici", "tizi") diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/base.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/base.py new file mode 100644 index 000000000..217696e09 --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/base.py @@ -0,0 +1,228 @@ +import os +from abc import abstractmethod, ABC +from dataclasses import dataclass, fields + +from iqpilot.cereal import log + +NetworkType = log.DeviceState.NetworkType +NetworkStrength = log.DeviceState.NetworkStrength + +class LPAError(RuntimeError): + pass + +class LPAProfileNotFoundError(LPAError): + pass + +@dataclass +class Profile: + iccid: str + nickname: str + enabled: bool + provider: str + +@dataclass +class ThermalZone: + # a zone from /sys/class/thermal/thermal_zone* + name: str # a.k.a type + scale: float = 1000. # scale to get degrees in C + zone_number = -1 + + def read(self) -> float: + if self.zone_number < 0: + for n in os.listdir("/sys/devices/virtual/thermal"): + if not n.startswith("thermal_zone"): + continue + with open(os.path.join("/sys/devices/virtual/thermal", n, "type")) as f: + if f.read().strip() == self.name: + self.zone_number = int(n.removeprefix("thermal_zone")) + break + + try: + with open(f"/sys/devices/virtual/thermal/thermal_zone{self.zone_number}/temp") as f: + return int(f.read()) / self.scale + except FileNotFoundError: + return 0 + +@dataclass +class ThermalConfig: + cpu: list[ThermalZone] | None = None + gpu: list[ThermalZone] | None = None + dsp: ThermalZone | None = None + pmic: list[ThermalZone] | None = None + memory: ThermalZone | None = None + intake: ThermalZone | None = None + exhaust: ThermalZone | None = None + case: ThermalZone | None = None + + def get_msg(self): + ret = {} + for f in fields(ThermalConfig): + v = getattr(self, f.name) + if v is not None: + if isinstance(v, list): + ret[f.name + "TempC"] = [x.read() for x in v] + else: + ret[f.name + "TempC"] = v.read() + return ret + +class LPABase(ABC): + @abstractmethod + def list_profiles(self) -> list[Profile]: + pass + + @abstractmethod + def get_active_profile(self) -> Profile | None: + pass + + @abstractmethod + def delete_profile(self, iccid: str) -> None: + pass + + @abstractmethod + def bootstrap(self) -> None: + pass + + @abstractmethod + def download_profile(self, qr: str, nickname: str | None = None) -> None: + pass + + @abstractmethod + def nickname_profile(self, iccid: str, nickname: str) -> None: + pass + + @abstractmethod + def switch_profile(self, iccid: str) -> None: + pass + + def is_comma_profile(self, iccid: str) -> bool: + return any(iccid.startswith(prefix) for prefix in ('8985235',)) + +class HardwareBase(ABC): + @staticmethod + def get_cmdline() -> dict[str, str]: + with open('/proc/cmdline') as f: + cmdline = f.read() + return {kv[0]: kv[1] for kv in [s.split('=') for s in cmdline.split(' ')] if len(kv) == 2} + + @staticmethod + def read_param_file(path, parser, default=0): + try: + with open(path) as f: + return parser(f.read()) + except Exception: + return default + + def booted(self) -> bool: + return True + + def reboot(self, reason=None): + print("REBOOT!") + + def uninstall(self): + print("uninstall") + + def get_os_version(self): + return None + + @abstractmethod + def get_device_type(self): + pass + + def get_imei(self, slot) -> str: + return "" + + def get_serial(self): + return "" + + def get_network_info(self): + return None + + def get_network_type(self): + return NetworkType.none + + def get_sim_info(self): + return { + 'sim_id': '', + 'mcc_mnc': None, + 'network_type': ["Unknown"], + 'sim_state': ["ABSENT"], + 'data_connected': False + } + + def get_sim_lpa(self) -> LPABase: + raise NotImplementedError("SIM LPA not available") + + def get_network_strength(self, network_type): + return NetworkStrength.unknown + + def get_network_metered(self, network_type) -> bool: + return network_type not in (NetworkType.none, NetworkType.wifi, NetworkType.ethernet) + + def get_current_power_draw(self): + return 0 + + def get_som_power_draw(self): + return 0 + + def shutdown(self): + print("SHUTDOWN!") + + def get_thermal_config(self): + return ThermalConfig() + + def set_display_power(self, on: bool): + pass + + def set_screen_brightness(self, percentage): + pass + + def get_screen_brightness(self): + return 0 + + def set_power_save(self, powersave_enabled): + pass + + def get_gpu_usage_percent(self): + return 0 + + def get_modem_version(self): + return None + + def get_modem_temperatures(self): + return [] + + def initialize_hardware(self): + pass + + def configure_modem(self): + pass + + def reboot_modem(self): + pass + + def recover_sim_detection(self) -> bool: + return False + + def get_networks(self): + return None + + def has_internal_panda(self) -> bool: + return False + + def reset_internal_panda(self): + pass + + def recover_internal_panda(self): + pass + + def get_modem_data_usage(self): + return -1, -1 + + def get_voltage(self) -> float: + return 0. + + def get_current(self) -> float: + return 0. + + def set_ir_power(self, percent: int): + pass 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 new file mode 100644 index 000000000..7263ddbfc --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/hw.py @@ -0,0 +1,137 @@ +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 log_root_external() -> str: + return '/mnt/external_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 stats_iq_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "stats") + else: + return "/data/stats_iq/" + + @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 new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py new file mode 100644 index 000000000..72ef2f235 --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py @@ -0,0 +1,12 @@ +from iqpilot.cereal import log +from iqpilot.system.hardware.base import HardwareBase + +NetworkType = log.DeviceState.NetworkType + + +class Pc(HardwareBase): + def get_device_type(self): + return "pc" + + def get_network_type(self): + return NetworkType.wifi 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 new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py new file mode 100644 index 000000000..48a34ea48 --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +import time +from collections import namedtuple + +from iqpilot.common.i2c import SMBus + +# https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf + +AmpConfig = namedtuple('AmpConfig', ['name', 'value', 'register', 'offset', 'mask']) +EQParams = namedtuple('EQParams', ['K', 'k1', 'k2', 'c1', 'c2']) + + +def configs_from_eq_params(base, eq_params): + return [ + AmpConfig("K (high)", (eq_params.K >> 8), base, 0, 0xFF), + AmpConfig("K (low)", (eq_params.K & 0xFF), base + 1, 0, 0xFF), + AmpConfig("k1 (high)", (eq_params.k1 >> 8), base + 2, 0, 0xFF), + AmpConfig("k1 (low)", (eq_params.k1 & 0xFF), base + 3, 0, 0xFF), + AmpConfig("k2 (high)", (eq_params.k2 >> 8), base + 4, 0, 0xFF), + AmpConfig("k2 (low)", (eq_params.k2 & 0xFF), base + 5, 0, 0xFF), + AmpConfig("c1 (high)", (eq_params.c1 >> 8), base + 6, 0, 0xFF), + AmpConfig("c1 (low)", (eq_params.c1 & 0xFF), base + 7, 0, 0xFF), + AmpConfig("c2 (high)", (eq_params.c2 >> 8), base + 8, 0, 0xFF), + AmpConfig("c2 (low)", (eq_params.c2 & 0xFF), base + 9, 0, 0xFF), + ] + + +BASE_CONFIG = [ + AmpConfig("MCLK prescaler", 0b01, 0x10, 4, 0b00110000), + AmpConfig("PM: enable speakers", 0b11, 0x4D, 4, 0b00110000), + AmpConfig("PM: enable DACs", 0b11, 0x4D, 0, 0b00000011), + AmpConfig("Enable PLL1", 0b1, 0x12, 7, 0b10000000), + AmpConfig("Enable PLL2", 0b1, 0x1A, 7, 0b10000000), + AmpConfig("DAI1: I2S mode", 0b00100, 0x14, 2, 0b01111100), + AmpConfig("DAI2: I2S mode", 0b00100, 0x1C, 2, 0b01111100), + AmpConfig("DAI1 Passband filtering: music mode", 0b1, 0x18, 7, 0b10000000), + AmpConfig("DAI1 voice mode gain (DV1G)", 0b00, 0x2F, 4, 0b00110000), + AmpConfig("DAI1 attenuation (DV1)", 0x0, 0x2F, 0, 0b00001111), + AmpConfig("DAI2 attenuation (DV2)", 0x0, 0x31, 0, 0b00001111), + AmpConfig("DAI2: DC blocking", 0b1, 0x20, 0, 0b00000001), + AmpConfig("DAI2: High sample rate", 0b0, 0x20, 3, 0b00001000), + AmpConfig("ALC enable", 0b1, 0x43, 7, 0b10000000), + AmpConfig("ALC/excursion limiter release time", 0b101, 0x43, 4, 0b01110000), + AmpConfig("ALC multiband enable", 0b1, 0x43, 3, 0b00001000), + AmpConfig("DAI1 EQ enable", 0b0, 0x49, 0, 0b00000001), + AmpConfig("DAI2 EQ clip detection disabled", 0b1, 0x32, 4, 0b00010000), + AmpConfig("DAI2 EQ attenuation", 0x5, 0x32, 0, 0b00001111), + AmpConfig("Excursion limiter upper corner freq", 0b100, 0x41, 4, 0b01110000), + AmpConfig("Excursion limiter lower corner freq", 0b00, 0x41, 0, 0b00000011), + AmpConfig("Excursion limiter threshold", 0b000, 0x42, 0, 0b00001111), + AmpConfig("Distortion limit (THDCLP)", 0x6, 0x46, 4, 0b11110000), + AmpConfig("Distortion limiter release time constant", 0b0, 0x46, 0, 0b00000001), + AmpConfig("Right DAC input mixer: DAI1 left", 0b0, 0x22, 3, 0b00001000), + AmpConfig("Right DAC input mixer: DAI1 right", 0b0, 0x22, 2, 0b00000100), + AmpConfig("Right DAC input mixer: DAI2 left", 0b1, 0x22, 1, 0b00000010), + AmpConfig("Right DAC input mixer: DAI2 right", 0b0, 0x22, 0, 0b00000001), + AmpConfig("DAI1 audio port selector", 0b10, 0x16, 6, 0b11000000), + AmpConfig("DAI2 audio port selector", 0b01, 0x1E, 6, 0b11000000), + AmpConfig("Enable left digital microphone", 0b1, 0x48, 5, 0b00100000), + AmpConfig("Enable right digital microphone", 0b1, 0x48, 4, 0b00010000), + AmpConfig("Enhanced volume smoothing disabled", 0b0, 0x49, 7, 0b10000000), + AmpConfig("Volume adjustment smoothing disabled", 0b0, 0x49, 6, 0b01000000), + AmpConfig("Zero-crossing detection disabled", 0b0, 0x49, 5, 0b00100000), +] + +CONFIGS = { + "tici": [ + AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111), + AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100), + AmpConfig("Right speaker output volume", 0x1c, 0x3E, 0, 0b00011111), + AmpConfig("DAI2 EQ enable", 0b1, 0x49, 1, 0b00000010), + *configs_from_eq_params(0x84, EQParams(0x274F, 0xC0FF, 0x3BF9, 0x0B3C, 0x1656)), + *configs_from_eq_params(0x8E, EQParams(0x1009, 0xC6BF, 0x2952, 0x1C97, 0x30DF)), + *configs_from_eq_params(0x98, EQParams(0x0F75, 0xCBE5, 0x0ED2, 0x2528, 0x3E42)), + *configs_from_eq_params(0xA2, EQParams(0x091F, 0x3D4C, 0xCE11, 0x1266, 0x2807)), + *configs_from_eq_params(0xAC, EQParams(0x0A9E, 0x3F20, 0xE573, 0x0A8B, 0x3A3B)), + ], + "tizi": [ + AmpConfig("Left speaker output from left DAC", 0b1, 0x2B, 0, 0b11111111), + AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111), + AmpConfig("Left Speaker Mixer Gain", 0b00, 0x2D, 0, 0b00000011), + AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100), + AmpConfig("Left speaker output volume", 0x17, 0x3D, 0, 0b00011111), + AmpConfig("Right speaker output volume", 0x17, 0x3E, 0, 0b00011111), + AmpConfig("DAI2 EQ enable", 0b0, 0x49, 1, 0b00000010), + AmpConfig("DAI2: DC blocking", 0b0, 0x20, 0, 0b00000001), + AmpConfig("ALC enable", 0b0, 0x43, 7, 0b10000000), + AmpConfig("DAI2 EQ attenuation", 0x2, 0x32, 0, 0b00001111), + AmpConfig("Excursion limiter upper corner freq", 0b001, 0x41, 4, 0b01110000), + AmpConfig("Excursion limiter threshold", 0b100, 0x42, 0, 0b00001111), + AmpConfig("Distortion limit (THDCLP)", 0x0, 0x46, 4, 0b11110000), + AmpConfig("Distortion limiter release time constant", 0b1, 0x46, 0, 0b00000001), + AmpConfig("Left DAC input mixer: DAI1 left", 0b0, 0x22, 7, 0b10000000), + AmpConfig("Left DAC input mixer: DAI1 right", 0b0, 0x22, 6, 0b01000000), + AmpConfig("Left DAC input mixer: DAI2 left", 0b1, 0x22, 5, 0b00100000), + AmpConfig("Left DAC input mixer: DAI2 right", 0b0, 0x22, 4, 0b00010000), + AmpConfig("Right DAC input mixer: DAI2 left", 0b0, 0x22, 1, 0b00000010), + AmpConfig("Right DAC input mixer: DAI2 right", 0b1, 0x22, 0, 0b00000001), + AmpConfig("Volume adjustment smoothing disabled", 0b1, 0x49, 6, 0b01000000), + ], +} + + +class Amplifier: + AMP_I2C_BUS = 0 + AMP_ADDRESS = 0x10 + + def __init__(self, debug=False): + self.debug = debug + + def _get_shutdown_config(self, amp_disabled: bool) -> AmpConfig: + return AmpConfig("Global shutdown", 0b0 if amp_disabled else 0b1, 0x51, 7, 0b10000000) + + def _set_configs(self, configs: list[AmpConfig]) -> None: + with SMBus(self.AMP_I2C_BUS) as bus: + for config in configs: + if self.debug: + print(f"Setting \"{config.name}\" to {config.value}:") + + old_value = bus.read_byte_data(self.AMP_ADDRESS, config.register, force=True) + new_value = (old_value & (~config.mask)) | ((config.value << config.offset) & config.mask) + bus.write_byte_data(self.AMP_ADDRESS, config.register, new_value, force=True) + + if self.debug: + print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") + + def set_configs(self, configs: list[AmpConfig]) -> bool: + tries = 15 + backoff = 0. + for i in range(tries): + try: + self._set_configs(configs) + return True + except OSError: + 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: + return self.set_configs([self._get_shutdown_config(amp_disabled), ]) + + def initialize_configuration(self, model: str) -> bool: + cfgs = [ + self._get_shutdown_config(True), + *BASE_CONFIG, + *CONFIGS[model], + self._get_shutdown_config(False), + ] + return self.set_configs(cfgs) + + +if __name__ == "__main__": + with open("/sys/firmware/devicetree/base/model") as f: + model = f.read().strip('\x00') + model = model.split('comma ')[-1] + + amp = Amplifier() + amp.initialize_configuration(model) diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py new file mode 100644 index 000000000..b9fa7fbff --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py @@ -0,0 +1,740 @@ +import math +import os +import sys +import subprocess +import time +import tempfile +from enum import IntEnum +from functools import cached_property, lru_cache +from pathlib import Path + +from iqpilot.cereal import log +from iqpilot.common.utils import sudo_read, sudo_write +from iqpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action +from iqpilot.system.hardware.base import HardwareBase, LPABase, ThermalConfig, ThermalZone +from iqpilot.system.hardware.tici import iwlist +from iqpilot.system.hardware.tici.lpa import TiciLPA +from iqpilot.system.hardware.tici.pins import GPIO +from iqpilot.system.hardware.tici.amplifier import Amplifier + +NM = 'org.freedesktop.NetworkManager' +NM_CON_ACT = NM + '.Connection.Active' +NM_DEV = NM + '.Device' +NM_DEV_WL = NM + '.Device.Wireless' +NM_DEV_STATS = NM + '.Device.Statistics' +NM_AP = NM + '.AccessPoint' +DBUS_PROPS = 'org.freedesktop.DBus.Properties' + +MM = 'org.freedesktop.ModemManager1' +MM_MODEM = MM + ".Modem" +MM_MODEM_SIMPLE = MM + ".Modem.Simple" +MM_SIM = MM + ".Sim" + +class MM_MODEM_STATE(IntEnum): + FAILED = -1 + UNKNOWN = 0 + INITIALIZING = 1 + LOCKED = 2 + DISABLED = 3 + DISABLING = 4 + ENABLING = 5 + ENABLED = 6 + SEARCHING = 7 + REGISTERED = 8 + DISCONNECTING = 9 + CONNECTING = 10 + CONNECTED = 11 + +class NMActiveConnectionState(IntEnum): + UNKNOWN = 0 + ACTIVATING = 1 + ACTIVATED = 2 + DEACTIVATING = 3 + DEACTIVATED = 4 + +class NMMetered(IntEnum): + NM_METERED_UNKNOWN = 0 + NM_METERED_YES = 1 + NM_METERED_NO = 2 + NM_METERED_GUESS_YES = 3 + NM_METERED_GUESS_NO = 4 + +TIMEOUT = 0.1 +REFRESH_RATE_MS = 1000 + +NetworkType = log.DeviceState.NetworkType +NetworkStrength = log.DeviceState.NetworkStrength + +# https://developer.gnome.org/ModemManager/unstable/ModemManager-Flags-and-Enumerations.html#MMModemAccessTechnology +MM_MODEM_ACCESS_TECHNOLOGY_UMTS = 1 << 5 +MM_MODEM_ACCESS_TECHNOLOGY_LTE = 1 << 14 + +# MMModemStateFailedReason +MM_MODEM_STATE_FAILED_REASON_SIM_MISSING = 2 + + +def affine_irq(val, action): + irqs = get_irqs_for_action(action) + if len(irqs) == 0: + return + + for i in irqs: + sudo_write(str(val), f"/proc/irq/{i}/smp_affinity_list") + +@lru_cache +def get_device_type(): + # lru_cache and cache can cause memory leaks when used in classes + try: + with open("/sys/firmware/devicetree/base/model") as f: + model = f.read().strip('\x00') + except FileNotFoundError: + # off-device (e.g. the prebuilt build container fakes /TICI but has no + # devicetree); import must not crash. Not a real device type. + return "unknown" + return model.split('comma ')[-1] + +class Tici(HardwareBase): + @staticmethod + def _ensure_system_python_path() -> None: + system_site = "/usr/lib/python3/dist-packages" + if system_site not in sys.path and os.path.isdir(system_site): + sys.path.append(system_site) + + @staticmethod + def _run_direct_modem_command(command: str) -> None: + import serial + + last_error: Exception | None = None + for device in ("/dev/ttyUSB2", "/dev/ttyUSB3"): + if not os.path.exists(device): + continue + + try: + with serial.Serial(device, baudrate=9600, timeout=2) as modem: + modem.reset_input_buffer() + modem.write((command + "\r").encode("ascii")) + + deadline = time.monotonic() + 3.0 + while time.monotonic() < deadline: + line = modem.readline().decode(errors="ignore").strip() + if not line: + continue + if line == "OK": + return + if line == "ERROR" or "ERROR" in line: + raise RuntimeError(f"{device}: {line}") + raise TimeoutError(f"{device}: timed out waiting for modem response") + except Exception as e: + last_error = e + + if last_error is not None: + raise last_error + raise RuntimeError("No modem AT port available") + + @cached_property + def bus(self): + try: + import dbus + except ModuleNotFoundError: + self._ensure_system_python_path() + import dbus + return dbus.SystemBus() + + @cached_property + def nm(self): + return self.bus.get_object(NM, '/org/freedesktop/NetworkManager') + + @property # this should not be cached, in case the modemmanager restarts + def mm(self): + return self.bus.get_object(MM, '/org/freedesktop/ModemManager1') + + @cached_property + def amplifier(self): + if self.get_device_type() == "mici": + return None + if os.path.exists('/tmp/lite_hw') or os.environ.get('LITE') == '1': + return None + return Amplifier() + + def get_os_version(self): + with open("/VERSION") as f: + return f.read().strip() + + def get_device_type(self): + return get_device_type() + + def reboot(self, reason=None): + subprocess.check_output(["sudo", "reboot"]) + + def uninstall(self): + Path("/data/__system_reset__").touch() + os.sync() + self.reboot() + + def get_serial(self): + return self.get_cmdline()['androidboot.serialno'] + + def get_voltage(self): + with open("/sys/class/hwmon/hwmon1/in1_input") as f: + return int(f.read()) + + def get_current(self): + with open("/sys/class/hwmon/hwmon1/curr1_input") as f: + return int(f.read()) + + def set_ir_power(self, percent: int): + if self.get_device_type() in ("tici", "tizi"): + return + + value = int((percent / 100) * 300) + with open("/sys/class/leds/led:switch_2/brightness", "w") as f: + f.write("0\n") + with open("/sys/class/leds/led:torch_2/brightness", "w") as f: + f.write(f"{value}\n") + with open("/sys/class/leds/led:switch_2/brightness", "w") as f: + f.write(f"{value}\n") + + def get_network_type(self): + try: + primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + primary_connection = self.bus.get_object(NM, primary_connection) + primary_type = primary_connection.Get(NM_CON_ACT, 'Type', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + + if primary_type == '802-3-ethernet': + return NetworkType.ethernet + elif primary_type == '802-11-wireless': + return NetworkType.wifi + else: + active_connections = self.nm.Get(NM, 'ActiveConnections', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + for conn in active_connections: + c = self.bus.get_object(NM, conn) + tp = c.Get(NM_CON_ACT, 'Type', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if tp == 'gsm': + modem = self.get_modem() + modem_state = modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if modem_state < MM_MODEM_STATE.REGISTERED: + return NetworkType.none + access_t = modem.Get(MM_MODEM, 'AccessTechnologies', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if access_t >= MM_MODEM_ACCESS_TECHNOLOGY_LTE: + return NetworkType.cell4G + elif access_t >= MM_MODEM_ACCESS_TECHNOLOGY_UMTS: + return NetworkType.cell3G + else: + return NetworkType.cell2G + except Exception: + pass + + return NetworkType.none + + def get_modem(self): + objects = self.mm.GetManagedObjects(dbus_interface="org.freedesktop.DBus.ObjectManager", timeout=TIMEOUT) + if not objects: + raise RuntimeError("ModemManager returned no modems") + modem_path = next(iter(objects)) + return self.bus.get_object(MM, modem_path) + + def get_wlan(self): + wlan_path = self.nm.GetDeviceByIpIface('wlan0', dbus_interface=NM, timeout=TIMEOUT) + return self.bus.get_object(NM, wlan_path) + + def get_wwan(self): + wwan_path = self.nm.GetDeviceByIpIface('wwan0', dbus_interface=NM, timeout=TIMEOUT) + return self.bus.get_object(NM, wwan_path) + + def get_sim_info(self): + modem = self.get_modem() + sim_path = modem.Get(MM_MODEM, 'Sim', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + + if sim_path == "/": + return { + 'sim_id': '', + 'mcc_mnc': None, + 'network_type': ["Unknown"], + 'sim_state': ["ABSENT"], + 'data_connected': False + } + else: + sim = self.bus.get_object(MM, sim_path) + return { + 'sim_id': str(sim.Get(MM_SIM, 'SimIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)), + 'mcc_mnc': str(sim.Get(MM_SIM, 'OperatorIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)), + 'network_type': ["Unknown"], + 'sim_state': ["READY"], + 'data_connected': modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) == MM_MODEM_STATE.CONNECTED, + } + + def get_sim_lpa(self) -> LPABase: + return TiciLPA() + + def get_imei(self, slot): + if slot != 0: + return "" + + return str(self.get_modem().Get(MM_MODEM, 'EquipmentIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)) + + def get_network_info(self): + if self.get_device_type() == "mici": + return None + try: + modem = self.get_modem() + info = modem.Command("AT+QNWINFO", math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + extra = modem.Command('AT+QENG="servingcell"', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + state = modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + except Exception: + return None + + if info and info.startswith('+QNWINFO: '): + info = info.replace('+QNWINFO: ', '').replace('"', '').split(',') + extra = "" if extra is None else extra.replace('+QENG: "servingcell",', '').replace('"', '') + state = "" if state is None else MM_MODEM_STATE(state).name + + if len(info) != 4: + return None + + technology, operator, band, channel = info + + return({ + 'technology': technology, + 'operator': operator, + 'band': band, + 'channel': int(channel), + 'extra': extra, + 'state': state, + }) + else: + return None + + def parse_strength(self, percentage): + if percentage < 25: + return NetworkStrength.poor + elif percentage < 50: + return NetworkStrength.moderate + elif percentage < 75: + return NetworkStrength.good + else: + return NetworkStrength.great + + def get_network_strength(self, network_type): + network_strength = NetworkStrength.unknown + + try: + if network_type == NetworkType.none: + pass + elif network_type == NetworkType.wifi: + wlan = self.get_wlan() + active_ap_path = wlan.Get(NM_DEV_WL, 'ActiveAccessPoint', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if active_ap_path != "/": + active_ap = self.bus.get_object(NM, active_ap_path) + strength = int(active_ap.Get(NM_AP, 'Strength', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)) + network_strength = self.parse_strength(strength) + else: # Cellular + modem = self.get_modem() + strength = int(modem.Get(MM_MODEM, 'SignalQuality', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)[0]) + network_strength = self.parse_strength(strength) + except Exception: + pass + + return network_strength + + def get_network_metered(self, network_type) -> bool: + try: + primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + primary_connection = self.bus.get_object(NM, primary_connection) + primary_devices = primary_connection.Get(NM_CON_ACT, 'Devices', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + + for dev in primary_devices: + dev_obj = self.bus.get_object(NM, str(dev)) + metered_prop = dev_obj.Get(NM_DEV, 'Metered', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + + if network_type == NetworkType.wifi: + if metered_prop in [NMMetered.NM_METERED_YES, NMMetered.NM_METERED_GUESS_YES]: + return True + elif network_type in [NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G]: + if metered_prop == NMMetered.NM_METERED_NO: + return False + except Exception: + pass + + return super().get_network_metered(network_type) + + def get_modem_version(self): + try: + modem = self.get_modem() + return modem.Get(MM_MODEM, 'Revision', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + except Exception: + return None + + def get_modem_temperatures(self): + timeout = 0.2 # Default timeout is too short + try: + modem = self.get_modem() + temps = modem.Command("AT+QTEMP", math.ceil(timeout), dbus_interface=MM_MODEM, timeout=timeout) + return list(filter(lambda t: t != 255, map(int, temps.split(' ')[1].split(',')))) + except Exception: + return [] + + + def get_current_power_draw(self): + return (self.read_param_file("/sys/class/hwmon/hwmon1/power1_input", int) / 1e6) + + def get_som_power_draw(self): + return (self.read_param_file("/sys/class/power_supply/bms/voltage_now", int) * self.read_param_file("/sys/class/power_supply/bms/current_now", int) / 1e12) + + def shutdown(self): + os.system("sudo poweroff") + + def get_thermal_config(self): + intake, exhaust, case = None, None, None + if self.get_device_type() == "mici": + case = ThermalZone("case") + intake = ThermalZone("intake") + exhaust = ThermalZone("exhaust") + return ThermalConfig(cpu=[ThermalZone(f"cpu{i}-silver-usr") for i in range(4)] + + [ThermalZone(f"cpu{i}-gold-usr") for i in range(4)], + gpu=[ThermalZone("gpu0-usr"), ThermalZone("gpu1-usr")], + dsp=ThermalZone("compute-hvx-usr"), + memory=ThermalZone("ddr-usr"), + pmic=[ThermalZone("pm8998_tz"), ThermalZone("pm8005_tz")], + intake=intake, + exhaust=exhaust, + case=case) + + def set_display_power(self, on): + try: + with open("/sys/class/backlight/panel0-backlight/bl_power", "w") as f: + f.write("0" if on else "4") + except Exception: + pass + + def set_screen_brightness(self, percentage): + try: + with open("/sys/class/backlight/panel0-backlight/max_brightness") as f: + max_brightness = float(f.read().strip()) + + val = int(percentage * (max_brightness / 100.)) + with open("/sys/class/backlight/panel0-backlight/brightness", "w") as f: + f.write(str(val)) + except Exception: + pass + + def get_screen_brightness(self): + try: + with open("/sys/class/backlight/panel0-backlight/max_brightness") as f: + max_brightness = float(f.read().strip()) + + with open("/sys/class/backlight/panel0-backlight/brightness") as f: + return int(float(f.read()) / (max_brightness / 100.)) + except Exception: + return 0 + + def set_power_save(self, powersave_enabled): + # amplifier, 100mW at idle + if self.amplifier is not None: + self.amplifier.set_global_shutdown(amp_disabled=powersave_enabled) + if not powersave_enabled: + self.amplifier.initialize_configuration(self.get_device_type()) + + # *** CPU config *** + + # offline big cluster + for i in range(4, 8): + val = '0' if powersave_enabled else '1' + sudo_write(val, f'/sys/devices/system/cpu/cpu{i}/online') + + for n in ('0', '4'): + if powersave_enabled and n == '4': + continue + gov = 'ondemand' if powersave_enabled else 'performance' + sudo_write(gov, f'/sys/devices/system/cpu/cpufreq/policy{n}/scaling_governor') + + # *** IRQ config *** + + # GPU, modeld core + affine_irq(7, "kgsl-3d0") + + # camerad core + camera_irqs = ("a5", "cci", "cpas_camnoc", "cpas-cdm", "csid", "ife", "csid-lite", "ife-lite") + for n in camera_irqs: + affine_irq(6, n) + + def get_gpu_usage_percent(self): + try: + with open('/sys/class/kgsl/kgsl-3d0/gpubusy') as f: + used, total = f.read().strip().split() + return 100.0 * int(used) / int(total) + except Exception: + return 0 + + def initialize_hardware(self): + 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") + + # 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) + gpio_set(GPIO.SOM_ST_IO, 1) + + # *** IRQ config *** + + # mask off big cluster from default affinity + sudo_write("f", "/proc/irq/default_smp_affinity") + + # move these off the default core + affine_irq(1, "msm_vidc") # encoders + affine_irq(1, "i2c_geni") # sensors + + # *** GPU config *** + # https://github.com/commaai/agnos-kernel-sdm845/blob/master/arch/arm64/boot/dts/qcom/sdm845-gpu.dtsi#L216 + affine_irq(5, "fts_ts") # touch + affine_irq(5, "msm_drm") # display + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/min_pwrlevel") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/max_pwrlevel") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_bus_on") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_clk_on") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_rail_on") + sudo_write("1000", "/sys/class/kgsl/kgsl-3d0/idle_timer") + sudo_write("performance", "/sys/class/kgsl/kgsl-3d0/devfreq/governor") + sudo_write("710", "/sys/class/kgsl/kgsl-3d0/max_clock_mhz") + + # setup governors + sudo_write("performance", "/sys/class/devfreq/soc:qcom,cpubw/governor") + sudo_write("performance", "/sys/class/devfreq/soc:qcom,memlat-cpu0/governor") + sudo_write("performance", "/sys/class/devfreq/soc:qcom,memlat-cpu4/governor") + + # *** VIDC (encoder) config *** + sudo_write("N", "/sys/kernel/debug/msm_vidc/clock_scaling") + sudo_write("Y", "/sys/kernel/debug/msm_vidc/disable_thermal_mitigation") + + # pandad core + affine_irq(3, "spi_geni") # SPI + if "tici" in self.get_device_type(): + affine_irq(3, "xhci-hcd:usb3") + 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)) + + def configure_modem(self): + from iqpilot.common.params import Params + + sim_info = self.get_sim_info() + sim_id = sim_info.get('sim_id', '') + params = Params() + manual_apn = params.get("GsmApn", encoding="utf-8") or "" + metered_enabled = params.get_bool("GsmMetered") + + modem = self.get_modem() + try: + manufacturer = str(modem.Get(MM_MODEM, 'Manufacturer', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)) + except Exception: + manufacturer = None + + cmds = [] + is_comma_profile = self.get_sim_lpa().is_comma_profile(sim_id) + roaming_enabled = params.get_bool("GsmRoaming") + initial_eps_apn = "" if is_comma_profile else manual_apn + + if not is_comma_profile and params.get("GsmRoaming") is None: + params.put_bool("GsmRoaming", True) + roaming_enabled = True + + subprocess.call([ + "nmcli", "connection", "modify", "lte", + "gsm.auto-config", "no" if manual_apn else "yes", + "gsm.apn", manual_apn, + "gsm.home-only", "no" if roaming_enabled else "yes", + "gsm.network-id", "", + "gsm.initial-eps-bearer-configure", "yes" if initial_eps_apn else "no", + "gsm.initial-eps-bearer-apn", initial_eps_apn, + "connection.metered", "unknown" if metered_enabled else "no", + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + 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: + subprocess.call(["mmcli", "-m", "any", '--3gpp-set-initial-eps-bearer-settings=apn=']) + + cmds += [ + # configure modem as data-centric + 'AT+QNVW=5280,0,"0102000000000000"', + 'AT+QNVFW="/nv/item_files/ims/IMS_enable",00', + 'AT+QNVFW="/nv/item_files/modem/mmode/ue_usage_setting",01', + ] + if self.get_device_type() == "tizi": + cmds += [ + 'AT+QSIMDET=1,0', + 'AT+QSIMSTAT=1', + ] + elif manufacturer == 'Cavli Inc.': + cmds += [ + 'AT^SIMSWAP=1', # use SIM slot, instead of internal eSIM + 'AT$QCSIMSLEEP=0', # disable SIM sleep + 'AT$QCSIMCFG=SimPowerSave,0', # more sleep disable + + # ethernet config + 'AT$QCPCFG=usbNet,0', + 'AT$QCNETDEVCTL=3,1', + ] + else: + # this modem gets upset with too many AT commands + if sim_id is None or len(sim_id) == 0: + cmds += [ + # SIM sleep disable + 'AT$QCSIMSLEEP=0', + 'AT$QCSIMCFG=SimPowerSave,0', + + # ethernet config + 'AT$QCPCFG=usbNet,1', + ] + + for cmd in cmds: + try: + modem.Command(cmd, math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + except Exception: + pass + + # eSIM prime + dest = "/etc/NetworkManager/system-connections/esim.nmconnection" + if self.get_sim_lpa().is_comma_profile(sim_id) and not os.path.exists(dest): + with open(Path(__file__).parent/'esim.nmconnection') as f, tempfile.NamedTemporaryFile(mode='w') as tf: + dat = f.read() + dat = dat.replace("sim-id=", f"sim-id={sim_id}") + tf.write(dat) + tf.flush() + + # needs to be root + os.system(f"sudo cp {tf.name} {dest}") + os.system(f"sudo nmcli con load {dest}") + + def recover_sim_detection(self) -> bool: + # A worn SIM-tray presence switch can read "removed" while the SIM pads still make + # contact; with hot-swap detect armed (AT+QSIMDET=1) the modem never powers the SIM + # and lands in failed/sim-missing. Disabling detect and rebooting the modem makes it + # probe the SIM electrically. Safe to retry on failure: firing disarms the QSIMDET + # gate, so a genuinely SIM-less device gets at most one extra modem reboot per boot. + if self.get_device_type() not in ("tici", "tizi"): + return False + + try: + modem = self.get_modem() + state = modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if state != MM_MODEM_STATE.FAILED: + return False + reason = modem.Get(MM_MODEM, 'StateFailedReason', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if reason != MM_MODEM_STATE_FAILED_REASON_SIM_MISSING: + return False + detect = str(modem.Command('AT+QSIMDET?', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)).strip() + if not detect.startswith('+QSIMDET: 1'): + return False + modem.Command('AT+QSIMDET=0,0', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + modem.Command('AT+CFUN=1,1', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + return True + except Exception: + return False + + def reboot_modem(self): + modem = None + try: + modem = self.get_modem() + except Exception: + pass + + if modem is not None: + for state in (0, 1): + try: + modem.Command(f'AT+CFUN={state}', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + except Exception: + pass + return + + for state in (0, 1): + try: + self._run_direct_modem_command(f"AT+CFUN={state}") + except Exception: + pass + + def get_networks(self): + r = {} + + wlan = iwlist.scan() + if wlan is not None: + r['wlan'] = wlan + + lte_info = self.get_network_info() + if lte_info is not None: + extra = lte_info['extra'] + + # ,"LTE",,,,,,,, + # ,,,,,,, + if 'LTE' in extra: + extra = extra.split(',') + try: + r['lte'] = [{ + "mcc": int(extra[3]), + "mnc": int(extra[4]), + "cid": int(extra[5], 16), + "nmr": [{"pci": int(extra[6]), "earfcn": int(extra[7])}], + }] + except (ValueError, IndexError): + pass + + return r + + def get_modem_data_usage(self): + try: + wwan = self.get_wwan() + + # Ensure refresh rate is set so values don't go stale + refresh_rate = wwan.Get(NM_DEV_STATS, 'RefreshRateMs', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if refresh_rate != REFRESH_RATE_MS: + u = type(refresh_rate) + wwan.Set(NM_DEV_STATS, 'RefreshRateMs', u(REFRESH_RATE_MS), dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + + tx = wwan.Get(NM_DEV_STATS, 'TxBytes', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + rx = wwan.Get(NM_DEV_STATS, 'RxBytes', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + return int(tx), int(rx) + except Exception: + return -1, -1 + + def has_internal_panda(self): + return True + + def reset_internal_panda(self): + gpio_init(GPIO.STM_RST_N, True) + gpio_init(GPIO.STM_BOOT0, True) + + gpio_set(GPIO.STM_RST_N, 1) + gpio_set(GPIO.STM_BOOT0, 0) + time.sleep(1) + gpio_set(GPIO.STM_RST_N, 0) + + def recover_internal_panda(self): + gpio_init(GPIO.STM_RST_N, True) + gpio_init(GPIO.STM_BOOT0, True) + + gpio_set(GPIO.STM_RST_N, 1) + gpio_set(GPIO.STM_BOOT0, 1) + time.sleep(0.5) + gpio_set(GPIO.STM_RST_N, 0) + time.sleep(0.5) + gpio_set(GPIO.STM_BOOT0, 0) + + def booted(self): + # this normally boots within 8s, but on rare occasions takes 30+s + encoder_state = sudo_read("/sys/kernel/debug/msm_vidc/core0/info") + if "Core state: 0" in encoder_state and (time.monotonic() < 60*2): + return False + return True + +if __name__ == "__main__": + t = Tici() + t.configure_modem() + t.initialize_hardware() + t.set_power_save(False) + print(t.get_sim_info()) diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py new file mode 100644 index 000000000..1e7c428b4 --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py @@ -0,0 +1,35 @@ +import subprocess + + +def scan(interface="wlan0"): + result = [] + try: + r = subprocess.check_output(["iwlist", interface, "scan"], encoding='utf8') + + mac = None + for line in r.split('\n'): + if "Address" in line: + # Based on the adapter eithere a percentage or dBm is returned + # Add previous network in case no dBm signal level was seen + if mac is not None: + result.append({"mac": mac}) + mac = None + + mac = line.split(' ')[-1] + elif "dBm" in line: + try: + level = line.split('Signal level=')[1] + rss = int(level.split(' ')[0]) + result.append({"mac": mac, "rss": rss}) + mac = None + except ValueError: + continue + + # Add last network if no dBm was found + if mac is not None: + result.append({"mac": mac}) + + return result + + except Exception: + return None diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py new file mode 100644 index 000000000..a636742d5 --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py @@ -0,0 +1,1482 @@ +#!/usr/bin/env python3 +# SGP.22 v2.3: https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2021/07/SGP.22-v2.3.pdf + +import argparse +import atexit +import base64 +import hashlib +import json +import math +import os +import re +import requests +import serial +import subprocess +import sys +import time + +from collections.abc import Generator +from contextlib import contextmanager +from pathlib import Path +from typing import Literal +from iqpilot.common.time_helpers import system_time_valid +from iqpilot.system.hardware.base import LPABase, LPAError, LPAProfileNotFoundError, Profile + +GSMA_CI_BUNDLE = str(Path(__file__).parent / 'gsma_ci_bundle.pem') + + +DEFAULT_DEVICE = "/dev/modem_at0" +DEFAULT_BAUD = 9600 +DEFAULT_TIMEOUT = 5.0 +# https://euicc-manual.osmocom.org/docs/lpa/applet-id/ +ISDR_AID = "A0000005591010FFFFFFFF8900000100" +ES10X_MSS = 120 +MM = "org.freedesktop.ModemManager1" +MM_MODEM = MM + ".Modem" + +# TLV Tags +TAG_ICCID = 0x5A +TAG_STATUS = 0x80 +TAG_EUICC_INFO = 0xBF20 +TAG_PREPARE_DOWNLOAD = 0xBF21 +TAG_PROFILE_INFO_LIST = 0xBF2D +TAG_EUICC_CHALLENGE = 0xBF2E +TAG_SET_NICKNAME = 0xBF29 +TAG_LIST_NOTIFICATION = 0xBF28 +TAG_RETRIEVE_NOTIFICATION = 0xBF2B +TAG_NOTIFICATION_METADATA = 0xBF2F +TAG_NOTIFICATION_SENT = 0xBF30 +TAG_ENABLE_PROFILE = 0xBF31 +TAG_DISABLE_PROFILE = 0xBF32 +TAG_DELETE_PROFILE = 0xBF33 +TAG_BPP = 0xBF36 +TAG_PROFILE_INSTALL_RESULT = 0xBF37 +TAG_AUTH_SERVER = 0xBF38 +TAG_CANCEL_SESSION = 0xBF41 + +STATE_LABELS = {0: "disabled", 1: "enabled", 255: "unknown"} +ICON_LABELS = {0: "jpeg", 1: "png", 255: "unknown"} +CLASS_LABELS = {0: "test", 1: "provisioning", 2: "operational", 255: "unknown"} +PROFILE_ERROR_CODES = { + 0x01: "iccidOrAidNotFound", 0x02: "profileNotInDisabledState", + 0x03: "disallowedByPolicy", 0x04: "wrongProfileReenabling", + 0x05: "catBusy", 0x06: "undefinedError", +} +AUTH_SERVER_ERROR_CODES = { + 0x01: "eUICCVerificationFailed", 0x02: "eUICCCertificateExpired", + 0x03: "eUICCCertificateRevoked", 0x05: "invalidServerSignature", + 0x06: "euiccCiPKUnknown", 0x0A: "matchingIdRefused", + 0x10: "insufficientMemory", +} +BPP_COMMAND_NAMES = { + 0: "initialiseSecureChannel", 1: "configureISDP", 2: "storeMetadata", + 3: "storeMetadata2", 4: "replaceSessionKeys", 5: "loadProfileElements", +} +BPP_ERROR_REASONS = { + 1: "incorrectInputValues", 2: "invalidSignature", 3: "invalidTransactionId", + 4: "unsupportedCrtValues", 5: "unsupportedRemoteOperationType", + 6: "unsupportedProfileClass", 7: "scp03tStructureError", 8: "scp03tSecurityError", + 9: "iccidAlreadyExistsOnEuicc", 10: "insufficientMemoryForProfile", + 11: "installInterrupted", 12: "peProcessingError", 13: "dataMismatch", + 14: "invalidNAA", +} +CANCEL_SESSION_REASON = { + 0: "endUserRejection", 1: "postponed", 2: "timeout", + 3: "pprNotAllowed", 127: "undefinedReason", +} + +ESIM_TLS_BUNDLE_ENV = "IQPILOT_ESIM_CA_BUNDLE" +ESIM_QESIM_INIT_ENV = "IQPILOT_ESIM_QESIM_INIT" +ESIM_EXCLUSIVE_MODEM_ENV = "IQPILOT_ESIM_EXCLUSIVE_MODEM" + + +def _ensure_dbus_import(): + try: + import dbus # noqa: F401 + return + except ModuleNotFoundError: + dist_packages = "/usr/lib/python3/dist-packages" + if dist_packages not in sys.path and os.path.isdir(dist_packages): + sys.path.append(dist_packages) + import dbus # noqa: F401 + + +def _tls_verify_option() -> bool | str: + # Default to the GSMA CI bundle used by the reference eSIM branch. + # Allow explicit override for local testing or forced system trust behavior. + override = os.getenv(ESIM_TLS_BUNDLE_ENV, "").strip() + if not override: + return GSMA_CI_BUNDLE + if override.lower() in ("0", "false", "off"): + return False + if override.lower() in ("1", "true", "on", "system"): + return True + return override + + +def b64e(data: bytes) -> str: + return base64.b64encode(data).decode("ascii") + + +def b64d(s: str) -> bytes: + return base64.b64decode(base64_trim(s)) + + +def _decode_apdu_response_blob(raw: str, label: str) -> bytes: + filtered = re.sub(r"[^0-9A-Fa-f]", "", raw) + if len(filtered) < 2 or len(filtered) % 2: + raise RuntimeError(f"Malformed {label} response payload") + return bytes.fromhex(filtered) + + +class AtClient: + def __init__(self, device: str, baud: int, timeout: float, verbose: bool) -> None: + self._device = device + self._baud = baud + self._timeout = timeout + self.verbose = verbose + self.channel: str | None = None + self._serial: serial.Serial | None = None + self._use_csim = False + self._has_qesim = False + self._qesim_lpa_enabled = False + try: + self._serial = serial.Serial(device, baudrate=baud, timeout=timeout) + self._disable_echo() + except (serial.SerialException, PermissionError, OSError): + self._serial = None + if self.verbose: + print(f"!! using ModemManager DBus AT transport for {device}", file=sys.stderr) + + def close(self) -> None: + try: + if self.channel: + try: + self.query(f"AT+CCHC={self.channel}") + except Exception as e: + if self.verbose: + print(f"!! ignoring channel close failure: {e}", file=sys.stderr) + finally: + self.channel = None + finally: + if self._serial is not None: + self._serial.close() + + @staticmethod + def _is_busy_open_error(error: Exception) -> bool: + text = str(error).lower() + return "errno 16" in text or "device or resource busy" in text + + def _disable_echo(self) -> None: + if self._serial is None: + return + self._serial.reset_input_buffer() + self._serial.write(b"ATE0\r") + time.sleep(0.1) + self._serial.reset_input_buffer() + + def _send(self, cmd: str) -> None: + if self.verbose: + print(f">> {cmd}", file=sys.stderr) + assert self._serial is not None + self._serial.write((cmd + "\r").encode("ascii")) + + def _expect(self) -> list[str]: + lines: list[str] = [] + while True: + assert self._serial is not None + raw = self._serial.readline() + if not raw: + raise TimeoutError("AT command timed out") + line = raw.decode(errors="ignore").strip() + if not line: + continue + if self.verbose: + print(f"<< {line}", file=sys.stderr) + if line == "OK": + return lines + if line == "ERROR": + raise RuntimeError("AT command failed") + if "ERROR" in line and line.startswith("+"): + # Catch +CME ERROR / +CMS ERROR and similar modem-specific failures. + raise RuntimeError(f"AT command failed: {line}") + lines.append(line) + + def _get_modem(self): + _ensure_dbus_import() + import dbus + + bus = dbus.SystemBus() + mm = bus.get_object(MM, '/org/freedesktop/ModemManager1') + objects = mm.GetManagedObjects(dbus_interface="org.freedesktop.DBus.ObjectManager", timeout=self._timeout) + modem_paths = [path for path in objects.keys() if "/Modem/" in str(path)] + if not modem_paths: + raise RuntimeError("ModemManager returned no modems") + return bus.get_object(MM, modem_paths[0]) + + def _dbus_query(self, cmd: str) -> list[str]: + if self.verbose: + print(f"DBUS >> {cmd}", file=sys.stderr) + try: + result = str(self._get_modem().Command(cmd, math.ceil(self._timeout), dbus_interface=MM_MODEM, timeout=self._timeout)) + except Exception as e: + raise RuntimeError(f"AT command failed: {e}") from e + lines = [line.strip() for line in result.splitlines() if line.strip()] + if self.verbose: + for line in lines: + print(f"DBUS << {line}", file=sys.stderr) + return lines + + def _reconnect_serial(self) -> None: + self.channel = None + try: + if self._serial is not None: + self._serial.close() + except Exception: + pass + self._serial = serial.Serial(self._device, baudrate=self._baud, timeout=self._timeout) + self._disable_echo() + + def query(self, cmd: str) -> list[str]: + if self._serial is None: + return self._dbus_query(cmd) + try: + self._send(cmd) + return self._expect() + except (OSError, serial.SerialException) as e: + try: + self._reconnect_serial() + self._send(cmd) + return self._expect() + except Exception: + raise RuntimeError(f"AT transport error ({cmd}): {e}") from e + except TimeoutError as e: + raise TimeoutError(f"AT command timed out ({cmd})") from e + except RuntimeError as e: + raise RuntimeError(f"AT command failed ({cmd}): {e}") from e + + def _supports_qesim(self) -> bool: + try: + lines = self.query("AT+QESIM=?") + except Exception: + return False + return any(line.startswith("+QESIM:") for line in lines) + + def _set_quectel_lpa_mode(self) -> None: + self._has_qesim = self._supports_qesim() + self._qesim_lpa_enabled = False + if not self._has_qesim: + return + + # Some Quectel firmwares require enabling internal LPA mode before + # ISO7816 channel open commands (CCHO/CGLA) will succeed. + lpa_enable_cmds = ( + 'AT+QESIM="lpa_enable",1', + 'AT+QESIM="lpa_enable"', + ) + for cmd in lpa_enable_cmds: + try: + self.query(cmd) + self._qesim_lpa_enabled = True + break + except Exception: + continue + + # Keep BIP auth enabled when supported; this is needed by some firmware + # revisions for profile download/auth paths. + try: + lines = self.query('AT+QCFG="bip/auth"?') + needs_enable = any('"bip/auth",0' in line for line in lines) + if needs_enable: + self.query('AT+QCFG="bip/auth",1') + except Exception: + pass + + def ensure_capabilities(self) -> None: + if self._serial is None: + return + + # Some modem firmwares reject command test forms (e.g. AT+CCHO=?) + # while still supporting real execution. Keep these probes non-fatal. + self.query("AT") + # Keep IQ-specific QESIM initialization opt-in for debugging only. + # Default behavior should match upstream greatgitsby/esim transport flow. + if os.getenv(ESIM_QESIM_INIT_ENV, "").strip() == "1": + try: + self.query("AT+CMEE=2") + except Exception: + pass + self._set_quectel_lpa_mode() + for command in ("AT+CCHO", "AT+CCHC", "AT+CGLA", "AT+CSIM"): + try: + self.query(f"{command}=?") + except Exception as e: + if self.verbose: + print(f"!! capability probe failed for {command}: {e}", file=sys.stderr) + + def open_isdr(self) -> None: + try: + self.query(f'AT+CCHC=1') + except Exception: + pass + ccho_candidates = [ + f'AT+CCHO="{ISDR_AID}"', + f"AT+CCHO={ISDR_AID}", + ] + for cmd in ccho_candidates: + try: + lines = self.query(cmd) + except Exception: + continue + for line in lines: + if line.startswith("+CCHO:") and (ch := line.split(":", 1)[1].strip()): + self.channel = ch + return + + # Fallback for modems that reject CCHO but allow APDUs over basic channel. + self.channel = "0" + aid_len = len(ISDR_AID) // 2 + select_isdr = bytes.fromhex(f"00A40400{aid_len:02X}{ISDR_AID}") + try: + _, sw1, sw2 = self.send_apdu(select_isdr) + if (sw1, sw2) == (0x90, 0x00): + return + raise RuntimeError(f"ISD-R SELECT failed on basic channel SW={sw1:02X}{sw2:02X}") + except Exception: + self.channel = None + # Final fallback for modems that only support APDU via CSIM. + try: + _, sw1, sw2 = self._send_csim_apdu(select_isdr) + if (sw1, sw2) == (0x90, 0x00): + self._use_csim = True + self.channel = "csim" + if self.verbose: + print("!! using CSIM APDU transport fallback", file=sys.stderr) + return + raise RuntimeError(f"ISD-R SELECT failed on CSIM SW={sw1:02X}{sw2:02X}") + except Exception: + self.channel = None + if self._has_qesim and not self._qesim_lpa_enabled and os.getenv(ESIM_QESIM_INIT_ENV, "").strip() == "1": + raise RuntimeError( + "Modem exposes AT+QESIM but rejects LPA enable and ISD-R access; " + "this firmware/SKU likely does not provide usable eSIM functionality" + ) + raise RuntimeError("Failed to access eUICC ISD-R (CCHO/CGLA/CSIM all failed)") + + def send_apdu(self, apdu: bytes) -> tuple[bytes, int, int]: + for attempt in range(3): + try: + if not self.channel: + self.open_isdr() + if self._use_csim: + return self._send_csim_apdu(apdu) + hex_payload = apdu.hex().upper() + cmd = f'AT+CGLA={self.channel},{len(hex_payload)},"{hex_payload}"' + for query_attempt in range(2): + for line in self.query(cmd): + if line.startswith("+CGLA:"): + parts = line.split(":", 1)[1].split(",", 1) + if len(parts) == 2: + data = _decode_apdu_response_blob(parts[1].strip().strip('"'), "+CGLA") + if len(data) >= 2: + return data[:-2], data[-2], data[-1] + if query_attempt == 0: + time.sleep(0.2) + raise RuntimeError("Missing +CGLA response") + except RuntimeError: + self.channel = None + self._use_csim = False + if attempt == 2: + raise + time.sleep(1 + attempt) + raise RuntimeError("send_apdu failed") + + def _send_csim_apdu(self, apdu: bytes) -> tuple[bytes, int, int]: + hex_payload = apdu.hex().upper() + cmd = f'AT+CSIM={len(hex_payload)},"{hex_payload}"' + for attempt in range(2): + for line in self.query(cmd): + if line.startswith("+CSIM:"): + parts = line.split(":", 1)[1].split(",", 1) + if len(parts) == 2: + data = _decode_apdu_response_blob(parts[1].strip().strip('"'), "+CSIM") + if len(data) >= 2: + return data[:-2], data[-2], data[-1] + if attempt == 0: + time.sleep(0.2) + raise RuntimeError("Missing +CSIM response") + + +# --- TLV utilities --- + +def iter_tlv(data: bytes, with_positions: bool = False) -> Generator: + idx, length = 0, len(data) + while idx < length: + start_pos = idx + tag = data[idx] + idx += 1 + if tag & 0x1F == 0x1F: # Multi-byte tag + tag_value = tag + while idx < length: + next_byte = data[idx] + idx += 1 + tag_value = (tag_value << 8) | next_byte + if not (next_byte & 0x80): + break + else: + tag_value = tag + if idx >= length: + break + size = data[idx] + idx += 1 + if size & 0x80: # Multi-byte length + num_bytes = size & 0x7F + if idx + num_bytes > length: + break + size = int.from_bytes(data[idx : idx + num_bytes], "big") + idx += num_bytes + if idx + size > length: + break + value = data[idx : idx + size] + idx += size + yield (tag_value, value, start_pos, idx) if with_positions else (tag_value, value) + + +def find_tag(data: bytes, target: int) -> bytes | None: + return next((v for t, v in iter_tlv(data) if t == target), None) + + +def encode_tlv(tag: int, value: bytes) -> bytes: + tag_bytes = bytes([(tag >> 8) & 0xFF, tag & 0xFF]) if tag > 255 else bytes([tag]) + vlen = len(value) + if vlen <= 127: + return tag_bytes + bytes([vlen]) + value + length_bytes = vlen.to_bytes((vlen.bit_length() + 7) // 8, "big") + return tag_bytes + bytes([0x80 | len(length_bytes)]) + length_bytes + value + + +def tbcd_to_string(raw: bytes) -> str: + return "".join(str(n) for b in raw for n in (b & 0x0F, b >> 4) if n <= 9) + + +def string_to_tbcd(s: str) -> bytes: + digits = [int(c) for c in s if c.isdigit()] + return bytes(digits[i] | ((digits[i + 1] if i + 1 < len(digits) else 0xF) << 4) for i in range(0, len(digits), 2)) + + +def base64_trim(s: str) -> str: + return "".join(c for c in s if c not in "\n\r \t") + + +# --- Shared helpers --- + +def _int_bytes(n: int) -> bytes: + """Encode a positive integer as minimal big-endian bytes (at least 1 byte).""" + return n.to_bytes((n.bit_length() + 7) // 8 or 1, "big") + + +def _extract_status(response: bytes, tag: int, name: str) -> int: + """Extract the status byte from a tagged ES10x response.""" + root = find_tag(response, tag) + if root is None: + raise RuntimeError(f"Missing {name}Response") + status = find_tag(root, TAG_STATUS) + if status is None: + raise RuntimeError(f"Missing status in {name}Response") + return status[0] + + +# Profile field decoders: TLV tag -> (field_name, decoder) +_PROFILE_FIELDS = { + TAG_ICCID: ("iccid", tbcd_to_string), + 0x4F: ("isdpAid", lambda v: v.hex().upper()), + 0x9F70: ("profileState", lambda v: STATE_LABELS.get(v[0], "unknown")), + 0x90: ("profileNickname", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x91: ("serviceProviderName", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x92: ("profileName", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x93: ("iconType", lambda v: ICON_LABELS.get(v[0], "unknown")), + 0x94: ("icon", b64e), + 0x95: ("profileClass", lambda v: CLASS_LABELS.get(v[0], "unknown")), +} + + +def _decode_profile_fields(data: bytes) -> dict: + """Parse known profile metadata TLV fields into a dict.""" + result = {} + for tag, value in iter_tlv(data): + if (field := _PROFILE_FIELDS.get(tag)): + result[field[0]] = field[1](value) + return result + + +# Notification field decoders: TLV tag -> (field_name, decoder) +_NOTIF_FIELDS = { + TAG_STATUS: ("seqNumber", lambda v: int.from_bytes(v, "big")), + 0x81: ("profileManagementOperation", lambda v: next((m for m in [0x80, 0x40, 0x20, 0x10] if len(v) >= 2 and v[1] & m), 0xFF)), + 0x0C: ("notificationAddress", lambda v: v.decode("utf-8", errors="ignore")), + TAG_ICCID: ("iccid", tbcd_to_string), +} + + +# --- ES10x command transport --- + +def es10x_command(client: AtClient, data: bytes) -> bytes: + response = bytearray() + sequence = 0 + offset = 0 + while offset < len(data): + chunk = data[offset : offset + ES10X_MSS] + offset += len(chunk) + is_last = offset == len(data) + apdu = bytes([0x80, 0xE2, 0x91 if is_last else 0x11, sequence & 0xFF, len(chunk)]) + chunk + segment, sw1, sw2 = client.send_apdu(apdu) + response.extend(segment) + while True: + if sw1 == 0x61: # More data available + segment, sw1, sw2 = client.send_apdu(bytes([0x80, 0xC0, 0x00, 0x00, sw2 or 0])) + response.extend(segment) + continue + if (sw1 & 0xF0) == 0x90: + break + raise RuntimeError(f"APDU failed with SW={sw1:02X}{sw2:02X}") + sequence += 1 + return bytes(response) + + +# --- ES9P HTTP --- + +def es9p_request(smdp_address: str, endpoint: str, payload: dict, error_prefix: str = "Request") -> dict: + if not system_time_valid(): + raise RuntimeError("System time is not set; TLS certificate validation requires a valid clock") + url = f"https://{smdp_address}/gsma/rsp2/es9plus/{endpoint}" + headers = {"User-Agent": "gsma-rsp-lpad", "X-Admin-Protocol": "gsma/rsp/v2.3.0", "Content-Type": "application/json"} + resp = requests.post(url, json=payload, headers=headers, timeout=30, verify=_tls_verify_option()) + resp.raise_for_status() + if not resp.content: + return {} + data = resp.json() + if "header" in data and "functionExecutionStatus" in data["header"]: + status = data["header"]["functionExecutionStatus"] + if status.get("status") == "Failed": + sd = status.get("statusCodeData", {}) + raise RuntimeError(f"{error_prefix} failed: {sd.get('reasonCode', 'unknown')}/{sd.get('subjectCode', 'unknown')} - {sd.get('message', 'unknown')}") + return data + + +# --- Profile operations --- + +def decode_profiles(blob: bytes) -> list[dict]: + root = find_tag(blob, TAG_PROFILE_INFO_LIST) + if root is None: + raise RuntimeError("Missing ProfileInfoList") + list_ok = find_tag(root, 0xA0) + if list_ok is None: + return [] + defaults = {name: None for name, _ in _PROFILE_FIELDS.values()} + return [{**defaults, **_decode_profile_fields(value)} for tag, value in iter_tlv(list_ok) if tag == 0xE3] + + +def list_profiles(client: AtClient) -> list[dict]: + return decode_profiles(es10x_command(client, bytes.fromhex("BF2D00"))) + + +def _profile_op(client: AtClient, tag: int, iccid: str, refresh: bool, action: str) -> None: + inner = encode_tlv(TAG_ICCID, string_to_tbcd(iccid)) + if tag != TAG_DELETE_PROFILE: + if not refresh: + inner += encode_tlv(0x81, b'\x00') + inner = encode_tlv(0xA0, inner) + code = _extract_status(es10x_command(client, encode_tlv(tag, inner)), tag, f"{action.capitalize()}Profile") + if code == 0x00: + return + if code == 0x02 and tag != TAG_DELETE_PROFILE: + print(f"profile {iccid} already {action}d") + return + raise RuntimeError(f"{action.capitalize()}Profile failed: {PROFILE_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})") + + +def enable_profile(client: AtClient, iccid: str, refresh: bool = True) -> None: + _profile_op(client, TAG_ENABLE_PROFILE, iccid, refresh, "enable") + + +def disable_profile(client: AtClient, iccid: str, refresh: bool = True) -> None: + _profile_op(client, TAG_DISABLE_PROFILE, iccid, refresh, "disable") + + +def delete_profile(client: AtClient, iccid: str) -> None: + _profile_op(client, TAG_DELETE_PROFILE, iccid, True, "delete") + + +def set_profile_nickname(client: AtClient, iccid: str, nickname: str) -> None: + nickname_bytes = nickname.encode("utf-8") + if len(nickname_bytes) > 64: + raise ValueError("Profile nickname must be 64 bytes or less") + content = encode_tlv(TAG_ICCID, string_to_tbcd(iccid)) + encode_tlv(0x90, nickname_bytes) + code = _extract_status(es10x_command(client, encode_tlv(TAG_SET_NICKNAME, content)), TAG_SET_NICKNAME, "SetNickname") + if code == 0x01: + raise RuntimeError(f"profile {iccid} not found") + if code != 0x00: + raise RuntimeError(f"SetNickname failed with status 0x{code:02X}") + + +# --- Notifications --- + +def list_notifications(client: AtClient) -> list[dict]: + response = es10x_command(client, encode_tlv(TAG_LIST_NOTIFICATION, b"")) + root = find_tag(response, TAG_LIST_NOTIFICATION) + if root is None: + raise RuntimeError("Missing ListNotificationResponse") + metadata_list = find_tag(root, 0xA0) + if metadata_list is None: + return [] + notifications: list[dict] = [] + for tag, value in iter_tlv(metadata_list): + if tag != TAG_NOTIFICATION_METADATA: + continue + notification = {"seqNumber": None, "profileManagementOperation": None, "notificationAddress": None, "iccid": None} + for t, v in iter_tlv(value): + if (field := _NOTIF_FIELDS.get(t)): + notification[field[0]] = field[1](v) + if notification["seqNumber"] is not None and notification["profileManagementOperation"] is not None and notification["notificationAddress"]: + notifications.append(notification) + return notifications + + +def retrieve_notification(client: AtClient, seq_number: int) -> dict: + request = encode_tlv(TAG_RETRIEVE_NOTIFICATION, encode_tlv(0xA0, encode_tlv(TAG_STATUS, _int_bytes(seq_number)))) + response = es10x_command(client, request) + root = find_tag(response, TAG_RETRIEVE_NOTIFICATION) + if root is None: + raise RuntimeError("Invalid RetrieveNotificationsListResponse") + a0_content = find_tag(root, 0xA0) + if a0_content is None: + raise RuntimeError("Invalid RetrieveNotificationsListResponse") + pending_notif, pending_tag = None, None + for tag, value in iter_tlv(a0_content): + if tag in (TAG_PROFILE_INSTALL_RESULT, 0x30): + pending_notif, pending_tag = value, tag + break + if pending_notif is None: + raise RuntimeError("Missing PendingNotification") + if pending_tag == TAG_PROFILE_INSTALL_RESULT: + result_data = find_tag(pending_notif, 0xBF27) + notif_meta = find_tag(result_data, TAG_NOTIFICATION_METADATA) if result_data else None + else: + notif_meta = find_tag(pending_notif, TAG_NOTIFICATION_METADATA) + if notif_meta is None: + raise RuntimeError("Missing NotificationMetadata") + addr = find_tag(notif_meta, 0x0C) + if addr is None: + raise RuntimeError("Missing notificationAddress") + return {"notificationAddress": addr.decode("utf-8", errors="ignore"), "b64_PendingNotification": b64e(pending_notif)} + + +def remove_notification(client: AtClient, seq_number: int) -> None: + response = es10x_command(client, encode_tlv(TAG_NOTIFICATION_SENT, encode_tlv(TAG_STATUS, _int_bytes(seq_number)))) + root = find_tag(response, TAG_NOTIFICATION_SENT) + if root is None: + raise RuntimeError("Invalid NotificationSentResponse") + status = find_tag(root, TAG_STATUS) + if status is None or int.from_bytes(status, "big") != 0: + raise RuntimeError("RemoveNotificationFromList failed") + + +def process_notifications(client: AtClient) -> None: + for notification in list_notifications(client): + seq_number, smdp_address = notification["seqNumber"], notification["notificationAddress"] + if not seq_number or not smdp_address: + continue + try: + notif_data = retrieve_notification(client, seq_number) + es9p_request(smdp_address, "handleNotification", {"pendingNotification": notif_data["b64_PendingNotification"]}, "HandleNotification") + remove_notification(client, seq_number) + except Exception: + pass + + +# --- Authentication & Download --- + +def get_challenge_and_info(client: AtClient) -> tuple[bytes, bytes]: + challenge_resp = es10x_command(client, encode_tlv(TAG_EUICC_CHALLENGE, b"")) + root = find_tag(challenge_resp, TAG_EUICC_CHALLENGE) + if root is None: + raise RuntimeError("Missing GetEuiccDataResponse") + challenge = find_tag(root, TAG_STATUS) + if challenge is None: + raise RuntimeError("Missing challenge in response") + info_resp = es10x_command(client, encode_tlv(TAG_EUICC_INFO, b"")) + if not info_resp.startswith(bytes([0xBF, 0x20])): + raise RuntimeError("Missing GetEuiccInfo1Response") + return challenge, info_resp + + +def authenticate_server(client: AtClient, b64_signed1: str, b64_sig1: str, b64_pk_id: str, b64_cert: str, matching_id: str | None = None) -> str: + # Build request + tac = bytes([0x35, 0x29, 0x06, 0x11]) + device_info = encode_tlv(TAG_STATUS, tac) + encode_tlv(0xA1, b"") + ctx_inner = b"" + if matching_id: + ctx_inner += encode_tlv(TAG_STATUS, matching_id.encode("utf-8")) + ctx_inner += encode_tlv(0xA1, device_info) + content = base64.b64decode(b64_signed1) + base64.b64decode(b64_sig1) + base64.b64decode(b64_pk_id) + base64.b64decode(b64_cert) + encode_tlv(0xA0, ctx_inner) + request = encode_tlv(TAG_AUTH_SERVER, content) + + response = es10x_command(client, request) + if not response.startswith(bytes([0xBF, 0x38])): + raise RuntimeError("Invalid AuthenticateServerResponse") + + # Check for eUICC-side errors + root = find_tag(response, TAG_AUTH_SERVER) + if root is not None: + error_tag = find_tag(root, 0xA1) + if error_tag is not None: + code = int.from_bytes(error_tag, "big") if error_tag else 0 + desc = AUTH_SERVER_ERROR_CODES.get(code, "unknown") + raise RuntimeError(f"AuthenticateServer rejected by eUICC: {desc} (0x{code:02X})") + + return b64e(response) + + +def prepare_download(client: AtClient, b64_signed2: str, b64_sig2: str, b64_cert: str, cc: str | None = None) -> str: + smdp_signed2 = base64.b64decode(b64_signed2) + smdp_signature2 = base64.b64decode(b64_sig2) + smdp_certificate = base64.b64decode(b64_cert) + smdp_signed2_root = find_tag(smdp_signed2, 0x30) + if smdp_signed2_root is None: + raise RuntimeError("Invalid smdpSigned2") + transaction_id = find_tag(smdp_signed2_root, TAG_STATUS) + cc_required_flag = find_tag(smdp_signed2_root, 0x01) + if transaction_id is None or cc_required_flag is None: + raise RuntimeError("Invalid smdpSigned2") + content = smdp_signed2 + smdp_signature2 + if int.from_bytes(cc_required_flag, "big") != 0: + if not cc: + raise RuntimeError("Confirmation code required but not provided") + content += encode_tlv(0x04, hashlib.sha256(hashlib.sha256(cc.encode("utf-8")).digest() + transaction_id).digest()) + content += smdp_certificate + response = es10x_command(client, encode_tlv(TAG_PREPARE_DOWNLOAD, content)) + if not response.startswith(bytes([0xBF, 0x21])): + raise RuntimeError("Invalid PrepareDownloadResponse") + return b64e(response) + + +def _parse_tlv_header_len(data: bytes) -> int: + """Return the combined tag + length header size for a TLV element.""" + tag_len = 2 if data[0] & 0x1F == 0x1F else 1 + length_byte = data[tag_len] + return tag_len + (1 + (length_byte & 0x7F) if length_byte & 0x80 else 1) + + +def load_bpp(client: AtClient, b64_bpp: str) -> dict: + bpp = b64d(b64_bpp) + if not bpp.startswith(bytes([0xBF, 0x36])): + raise RuntimeError("Invalid BoundProfilePackage") + + bpp_root_value = None + for tag, value, start, end in iter_tlv(bpp, with_positions=True): + if tag == TAG_BPP: + bpp_root_value = value + bpp_value_start = start + _parse_tlv_header_len(bpp[start:end]) + break + if bpp_root_value is None: + raise RuntimeError("Invalid BoundProfilePackage") + + chunks = [] + for tag, value, start, end in iter_tlv(bpp_root_value, with_positions=True): + if tag == 0xBF23: + chunks.append(bpp[0 : bpp_value_start + end]) + elif tag == 0xA0: + chunks.append(bpp[bpp_value_start + start : bpp_value_start + end]) + elif tag in (0xA1, 0xA3): + hdr_len = _parse_tlv_header_len(bpp_root_value[start:end]) + chunks.append(bpp[bpp_value_start + start : bpp_value_start + start + hdr_len]) + for _, _, child_start, child_end in iter_tlv(value, with_positions=True): + chunks.append(value[child_start:child_end]) + elif tag == 0xA2: + chunks.append(bpp[bpp_value_start + start : bpp_value_start + end]) + + result = {"seqNumber": 0, "success": False, "bppCommandId": None, "errorReason": None} + for chunk in chunks: + response = es10x_command(client, chunk) + if not response: + continue + root = find_tag(response, TAG_PROFILE_INSTALL_RESULT) + if not root: + continue + result_data = find_tag(root, 0xBF27) + if not result_data: + break + notif_meta = find_tag(result_data, TAG_NOTIFICATION_METADATA) + if notif_meta: + seq_num = find_tag(notif_meta, TAG_STATUS) + if seq_num: + result["seqNumber"] = int.from_bytes(seq_num, "big") + final_result = find_tag(result_data, 0xA2) + if final_result: + for tag, value in iter_tlv(final_result): + if tag == 0xA0: + result["success"] = True + elif tag == 0xA1: + bpp_cmd = find_tag(value, TAG_STATUS) + if bpp_cmd: + result["bppCommandId"] = int.from_bytes(bpp_cmd, "big") + err = find_tag(value, 0x81) + if err: + result["errorReason"] = int.from_bytes(err, "big") + break # ProfileInstallResult received — eUICC session is finalized + if not result["success"] and result["errorReason"] is not None: + cmd_name = BPP_COMMAND_NAMES.get(result["bppCommandId"], f"unknown({result['bppCommandId']})") + err_name = BPP_ERROR_REASONS.get(result["errorReason"], f"unknown({result['errorReason']})") + raise RuntimeError(f"Profile installation failed at {cmd_name}: {err_name} (bppCommandId={result['bppCommandId']}, errorReason={result['errorReason']})") + return result + + +def parse_metadata(b64_metadata: str) -> dict: + root = find_tag(b64d(b64_metadata), 0xBF25) + if root is None: + raise RuntimeError("Invalid profileMetadata") + defaults = {"iccid": None, "serviceProviderName": None, "profileName": None, "iconType": None, "icon": None, "profileClass": None} + return {**defaults, **_decode_profile_fields(root)} + + +def cancel_session(client: AtClient, transaction_id: bytes, reason: int = 127) -> str: + content = encode_tlv(0x80, transaction_id) + encode_tlv(0x81, bytes([reason])) + response = es10x_command(client, encode_tlv(TAG_CANCEL_SESSION, content)) + return b64e(response) + + +def parse_lpa_activation_code(activation_code: str) -> tuple[str, str, str]: + if not activation_code.startswith("LPA:"): + raise ValueError("Invalid activation code format") + + parts = activation_code[4:].split("$") + if len(parts) != 3: + raise ValueError("Invalid activation code format") + + version, smdp, matching_id = [p.strip() for p in parts] + if version != "1": + raise ValueError("Unsupported activation code version") + if len(smdp) == 0 or "." not in smdp: + raise ValueError("Invalid SM-DP+ address in activation code") + if len(matching_id) == 0: + raise ValueError("Missing matching ID in activation code") + return version, smdp, matching_id + + +def download_profile(client: AtClient, activation_code: str) -> str: + _, smdp, matching_id = parse_lpa_activation_code(activation_code) + + challenge, euicc_info = get_challenge_and_info(client) + + payload = {"smdpAddress": smdp, "euiccChallenge": b64e(challenge), "euiccInfo1": b64e(euicc_info)} + if matching_id: + payload["matchingId"] = matching_id + auth = es9p_request(smdp, "initiateAuthentication", payload, "Authentication") + tx_id = base64_trim(auth.get("transactionId", "")) + tx_id_bytes = base64.b64decode(tx_id) if tx_id else b"" + + try: + b64_auth_resp = authenticate_server( + client, base64_trim(auth.get("serverSigned1", "")), base64_trim(auth.get("serverSignature1", "")), + base64_trim(auth.get("euiccCiPKIdToBeUsed", "")), base64_trim(auth.get("serverCertificate", "")), + matching_id=matching_id) + + cli = es9p_request(smdp, "authenticateClient", {"transactionId": tx_id, "authenticateServerResponse": b64_auth_resp}, "Authentication") + metadata = parse_metadata(base64_trim(cli.get("profileMetadata", ""))) + iccid = str(metadata.get("iccid") or "") + print(f'Downloading profile: {iccid} - {metadata["serviceProviderName"]} - {metadata["profileName"]}') + + b64_prep = prepare_download( + client, base64_trim(cli.get("smdpSigned2", "")), base64_trim(cli.get("smdpSignature2", "")), base64_trim(cli.get("smdpCertificate", ""))) + + bpp = es9p_request(smdp, "getBoundProfilePackage", {"transactionId": tx_id, "prepareDownloadResponse": b64_prep}, "GetBoundProfilePackage") + + result = load_bpp(client, base64_trim(bpp.get("boundProfilePackage", ""))) + if result["success"]: + print(f"Profile installed successfully (seqNumber: {result['seqNumber']})") + return iccid + else: + raise RuntimeError(f"Profile installation failed: {result}") + except Exception: + if tx_id_bytes: + b64_cancel_resp = "" + try: + b64_cancel_resp = cancel_session(client, tx_id_bytes) + except Exception: + pass + try: + es9p_request(smdp, "cancelSession", { + "transactionId": tx_id, + "cancelSessionResponse": b64_cancel_resp, + }, "CancelSession") + except Exception: + pass + raise + + +class TiciLPA(LPABase): + handles_modem_restart = True + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, interface: Literal["qmi", "at"] = "at", device: str = DEFAULT_DEVICE, baud: int = DEFAULT_BAUD, + timeout: float = DEFAULT_TIMEOUT, verbose: bool = False): + if getattr(self, "_initialized", False): + self.verbose = verbose or self.verbose + return + # This backend is AT-only. Keep interface arg for compatibility with existing CLI/call sites. + self.interface = interface + self.device = device + self.baud = baud + self.timeout = timeout + self.verbose = verbose + self._last_good_device = device + self._euicc_present_cache: bool | None = None + from iqpilot.system.hardware.tici.hardware import get_device_type + self._is_eg25 = get_device_type() in ("tizi",) + self._client: AtClient | None = None + self._initialized = True + # _client is opened lazily via _ensure_client() on first actual eSIM operation. + # Do NOT call _open_ready_client() here: on pSIM devices (no eUICC) it retries + # for 10–30 s against the AT port, blocking hw_state_thread and starving the + # network-type polling loop that drives the WiFi/LTE sidebar indicator. + atexit.register(self.close) + + def close(self) -> None: + if self._client is not None: + try: + self._client.close() + finally: + self._client = None + + @staticmethod + def _set_modemmanager_running(running: bool) -> None: + action = "start" if running else "stop" + subprocess.check_call(["sudo", "systemctl", action, "ModemManager"]) + + @staticmethod + def _is_modemmanager_running() -> bool: + res = subprocess.run(["systemctl", "is-active", "ModemManager"], + capture_output=True, text=True, check=False) + return res.stdout.strip() == "active" + + def _probe_euicc_present(self) -> bool | None: + if self._euicc_present_cache is not None: + return self._euicc_present_cache + + if self._is_modemmanager_running(): + return None + + restarted_modemmanager = False + try: + res = subprocess.run( + ["sudo", "qmicli", "-p", "-d", "/dev/cdc-wdm0", "--uim-get-slot-status"], + capture_output=True, text=True, check=False, timeout=12, + ) + output = f"{res.stdout}\n{res.stderr}" + if "Is eUICC: yes" in output: + self._euicc_present_cache = True + elif "Is eUICC: no" in output: + self._euicc_present_cache = False + else: + self._euicc_present_cache = None + except Exception: + self._euicc_present_cache = None + finally: + if restarted_modemmanager: + self._set_modemmanager_running(True) + time.sleep(2.0) + + return self._euicc_present_cache + + @staticmethod + def _ttyusb2_mm_ignored() -> bool: + # AGNOS PR 521 marks ttyUSB2 with ID_MM_PORT_IGNORE=1 for EG25. + # If missing, ModemManager may contend with direct AT eSIM operations. + if not os.path.exists("/dev/ttyUSB2"): + return False + try: + res = subprocess.run( + ["udevadm", "info", "-q", "property", "-n", "/dev/ttyUSB2"], + capture_output=True, text=True, check=False, + ) + if res.returncode != 0: + return False + return "ID_MM_PORT_IGNORE=1" in res.stdout + except Exception: + return False + + def _ensure_client(self) -> AtClient: + if self._client is None: + self._client = self._open_ready_client() + return self._client + + @contextmanager + def _open_client(self): + yield self._ensure_client() + + def _open_ready_client(self) -> AtClient: + euicc_present = self._probe_euicc_present() + if euicc_present is False: + raise LPAError("Modem reports no eUICC support on this unit (QMI: Is eUICC: no)") + + last_error: Exception | None = None + recovery_attempted = False + for attempt in range(1, 6): + for device in self._candidate_devices(): + client: AtClient | None = None + try: + if self.verbose: + print(f"!! trying modem AT device: {device} (attempt {attempt})", file=sys.stderr) + client = AtClient(device, self.baud, self.timeout, self.verbose) + client.ensure_capabilities() + client.open_isdr() + self._last_good_device = device + if self.verbose: + print(f"!! selected modem AT device: {device}", file=sys.stderr) + return client + except Exception as e: + if self.verbose: + print(f"!! modem AT device failed: {device}: {e}", file=sys.stderr) + last_error = e + if client is not None: + try: + client.close() + except Exception: + pass + if not recovery_attempted and last_error is not None and self._should_recover_isdr(last_error): + recovery_attempted = self._recover_isdr_access() + if recovery_attempted: + continue + # Give modem a moment to finish startup / channel release. + time.sleep(min(0.4 * attempt, 1.5)) + assert last_error is not None + raise last_error + + def _candidate_devices(self) -> list[str]: + if self.device and not os.path.exists(self.device): + return [self.device] + + candidates = [ + self._last_good_device, + self.device, + "/dev/modem_at0", + "/dev/ttyUSB3", + "/dev/ttyUSB2", + "/dev/ttyUSB1", + "/dev/ttyUSB0", + ] + ordered: list[str] = [] + for dev in candidates: + should_include = dev in (self.device, "/dev/modem_at0") or os.path.exists(dev) + if dev and dev not in ordered and should_include: + ordered.append(dev) + return ordered if ordered else [self.device] + + @staticmethod + def _to_profile(data: dict) -> Profile: + return Profile( + iccid=str(data.get("iccid") or ""), + nickname=str(data.get("profileNickname") or ""), + enabled=str(data.get("profileState") or "") == "enabled", + provider=str(data.get("serviceProviderName") or ""), + ) + + def _clear_cat_busy(self) -> None: + time.sleep(3) + try: + self._ensure_client().query('AT+QSTKRSP=254') + except Exception: + pass + time.sleep(1) + try: + self._ensure_client().query('AT+CFUN=4') + except Exception: + pass + time.sleep(2) + try: + self._ensure_client().query('AT+CFUN=1') + except Exception: + pass + self._ensure_client().channel = None + time.sleep(5) + + def _wait_for_modem(self, timeout: float = 45.0) -> None: + start = time.monotonic() + last_error: Exception | None = None + while time.monotonic() - start < timeout: + try: + self.close() + self._client = self._open_ready_client() + return + except Exception as e: + last_error = e + time.sleep(2) + if last_error is not None: + raise RuntimeError("Modem did not recover after reboot") from last_error + raise RuntimeError("Modem did not recover after reboot") + + def _restart_modem(self) -> None: + client = self._ensure_client() + for cmd in ('AT+CFUN=0', 'AT+CFUN=1,1'): + try: + client.query(cmd) + except Exception: + pass + client.channel = None + client._use_csim = False + self._wait_for_modem() + + @staticmethod + def _should_recover_isdr(error: Exception) -> bool: + message = str(error).lower() + markers = ( + "failed to open isd-r", + "failed to access euicc isd-r", + "simfilenotfound", + "at+ccho", + "at+cgla", + ) + return any(marker in message for marker in markers) + + def _recover_isdr_access(self) -> bool: + was_modemmanager_running = self._is_modemmanager_running() + self.close() + try: + if was_modemmanager_running: + self._set_modemmanager_running(False) + time.sleep(2.0) + + for cmd in ( + ["sudo", "qmicli", "-p", "-d", "/dev/cdc-wdm0", "--uim-reset"], + ["sudo", "qmicli", "-p", "-d", "/dev/cdc-wdm0", "--uim-sim-power-off=1"], + ["sudo", "qmicli", "-p", "-d", "/dev/cdc-wdm0", "--uim-sim-power-on=1"], + ): + subprocess.check_call(cmd) + time.sleep(2.0) + + self._euicc_present_cache = None + return True + except Exception as e: + if self.verbose: + print(f"!! ISD-R recovery failed: {e}", file=sys.stderr) + return False + finally: + if was_modemmanager_running: + self._set_modemmanager_running(True) + time.sleep(2.0) + + def _with_lpa_error(self, fn): + last_error: Exception | None = None + recovery_attempted = False + for attempt in range(3): + try: + return fn() + except LPAError: + raise + except RuntimeError as e: + last_error = e + if "does not provide usable esim functionality" in str(e).lower(): + raise LPAError(str(e)) from e + if not recovery_attempted and self._should_recover_isdr(e): + recovery_attempted = self._recover_isdr_access() + if recovery_attempted: + continue + if self._is_transient_transport_error(str(e)) and attempt == 0: + self._wait_for_modem() + continue + raise LPAError(str(e)) from e + except (OSError, serial.SerialException) as e: + last_error = e + if not recovery_attempted and self._should_recover_isdr(e): + recovery_attempted = self._recover_isdr_access() + if recovery_attempted: + continue + if self._is_transient_transport_error(str(e)) and attempt == 0: + self._wait_for_modem() + continue + raise LPAError(f"AT transport error: {e}") from e + except TimeoutError as e: + raise LPAError(f"AT modem timeout: {e}") from e + except ValueError as e: + raise LPAError(str(e)) from e + assert last_error is not None + raise LPAError(str(last_error)) + + @staticmethod + def _is_transient_transport_error(message: str) -> bool: + lower = message.lower() + transient_markers = ( + "broken pipe", + "resource busy", + "input/output error", + "device disconnected", + "device reports readiness to read but returned no data", + "transport error", + "missing +cgla response", + "missing +csim response", + "malformed +cgla response payload", + "malformed +csim response payload", + "non-hexadecimal number found in fromhex", + ) + return any(m in lower for m in transient_markers) + + def list_profiles(self) -> list[Profile]: + def _run() -> list[Profile]: + return [self._to_profile(p) for p in list_profiles(self._ensure_client())] + return self._with_lpa_error(_run) + + def get_active_profile(self) -> Profile | None: + return next((p for p in self.list_profiles() if p.enabled), None) + + def _validate_profile_exists(self, iccid: str) -> None: + if not any(p.iccid == iccid for p in self.list_profiles()): + raise LPAProfileNotFoundError(f"profile {iccid} does not exist") + + def _is_bootstrapped(self) -> bool: + return not any(self.is_comma_profile(p.iccid) for p in self.list_profiles()) + + def _ensure_switchable_profile(self, iccid: str) -> None: + if self.is_comma_profile(iccid): + return + + if not self._is_bootstrapped(): + raise LPAError( + "Delete the existing Comma pSIM profile before activating a user eSIM profile" + ) + + def _process_notifications_after_delete(self, client: AtClient, iccid: str) -> None: + try: + process_notifications(client) + return + except Exception as error: + notification_error = error + + self._restart_modem() + + try: + deleted = not any(p.iccid == iccid for p in self.list_profiles()) + except Exception: + raise RuntimeError( + "Profile delete may have succeeded, but modem notification cleanup failed; " + "refresh profiles and retry if needed" + ) from notification_error + + if deleted: + return + + raise RuntimeError( + "Profile delete did not finish cleanly; refresh profiles and retry" + ) from notification_error + + def _process_notifications_after_state_change( + self, + validator, + recovery_message: str, + failure_message: str, + ) -> None: + try: + process_notifications(self._ensure_client()) + return + except Exception as notification_error: + pass + + self._wait_for_modem() + + try: + if validator(): + return + except Exception: + raise RuntimeError(recovery_message) from notification_error + + raise RuntimeError(failure_message) from notification_error + + def delete_profile(self, iccid: str) -> None: + self._validate_profile_exists(iccid) + active = self.get_active_profile() + if active is not None and active.iccid == iccid: + raise LPAError("cannot delete active profile, switch to another profile first") + + def _run() -> None: + client = self._ensure_client() + try: + delete_profile(client, iccid) + except RuntimeError as error: + if "catbusy" in str(error).lower(): + self._clear_cat_busy() + delete_profile(client, iccid) + else: + raise + self._process_notifications_after_delete(client, iccid) + + self._with_lpa_error(_run) + + def bootstrap(self) -> None: + comma_profiles = [p for p in self.list_profiles() if self.is_comma_profile(p.iccid)] + if not comma_profiles: + return + + def _run() -> None: + client = self._ensure_client() + for profile in comma_profiles: + if profile.enabled: + disable_profile(client, profile.iccid, refresh=True) + process_notifications(client) + delete_profile(client, profile.iccid) + process_notifications(client) + self._restart_modem() + + self._with_lpa_error(_run) + + def download_profile(self, qr: str, nickname: str | None = None) -> None: + # Strict upfront validation to provide immediate user feedback. + parse_lpa_activation_code(qr) + before = {p.iccid for p in self.list_profiles()} + + def _run() -> None: + client = self._ensure_client() + iccid = download_profile(client, qr) + self._process_notifications_after_state_change( + lambda: any(profile.iccid == iccid for profile in self.list_profiles()), + "Profile add may have succeeded, but modem notification cleanup failed; refresh profiles", + "Profile add did not finish cleanly; refresh profiles and retry", + ) + + self._with_lpa_error(_run) + + if nickname: + after = self.list_profiles() + new_profile = next((p for p in after if p.iccid not in before), None) + if new_profile is not None and new_profile.iccid: + self.nickname_profile(new_profile.iccid, nickname) + + def nickname_profile(self, iccid: str, nickname: str) -> None: + self._validate_profile_exists(iccid) + + def _run() -> None: + set_profile_nickname(self._ensure_client(), iccid, nickname) + self._with_lpa_error(_run) + + def switch_profile(self, iccid: str) -> None: + self._validate_profile_exists(iccid) + self._ensure_switchable_profile(iccid) + active = self.get_active_profile() + if active is not None and active.iccid == iccid: + return + + def _run() -> None: + client = self._ensure_client() + use_refresh = self._is_eg25 + try: + enable_profile(client, iccid, refresh=use_refresh) + except RuntimeError as error: + if "catbusy" in str(error).lower(): + self._clear_cat_busy() + enable_profile(client, iccid, refresh=use_refresh) + else: + raise + client.channel = None + client._use_csim = False + if use_refresh: + self._wait_for_modem() + else: + self._restart_modem() + self._process_notifications_after_state_change( + lambda: any(profile.iccid == iccid and profile.enabled for profile in self.list_profiles()), + "Profile switch may have succeeded, but modem notification cleanup failed; refresh profiles", + "Profile switch did not finish cleanly; refresh profiles and retry", + ) + self._with_lpa_error(_run) + + # Compatibility methods retained for old tests/scripts. + def process_notifications(self) -> None: + def _run() -> None: + process_notifications(self._ensure_client()) + self._with_lpa_error(_run) + + def enable_profile(self, iccid: str, refresh: bool = True) -> None: + self._validate_profile_exists(iccid) + + def _run() -> None: + client = self._ensure_client() + enable_profile(client, iccid, refresh=refresh) + client.channel = None + client._use_csim = False + self._wait_for_modem() + self._process_notifications_after_state_change( + lambda: any(profile.iccid == iccid and profile.enabled for profile in self.list_profiles()), + "Profile enable may have succeeded, but modem notification cleanup failed; refresh profiles", + "Profile enable did not finish cleanly; refresh profiles and retry", + ) + self._with_lpa_error(_run) + + def disable_profile(self, iccid: str, refresh: bool = True) -> None: + self._validate_profile_exists(iccid) + + def _run() -> None: + client = self._ensure_client() + disable_profile(client, iccid, refresh=refresh) + client.channel = None + client._use_csim = False + self._wait_for_modem() + self._process_notifications_after_state_change( + lambda: any(profile.iccid == iccid and not profile.enabled for profile in self.list_profiles()), + "Profile disable may have succeeded, but modem notification cleanup failed; refresh profiles", + "Profile disable did not finish cleanly; refresh profiles and retry", + ) + self._with_lpa_error(_run) + + +# --- CLI --- + +def main() -> None: + parser = argparse.ArgumentParser(description="Minimal AT-only LPA implementation") + parser.add_argument("--device", default=DEFAULT_DEVICE) + parser.add_argument("--baud", type=int, default=DEFAULT_BAUD) + parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT) + parser.add_argument("--verbose", action="store_true") + parser.add_argument("--enable", type=str) + parser.add_argument("--disable", type=str) + parser.add_argument("--delete", type=str, metavar="ICCID", help="Delete a disabled profile") + parser.add_argument("--no-refresh", action="store_true", help="Skip REFRESH after enable/disable") + parser.add_argument("--set-nickname", nargs=2, metavar=("ICCID", "NICKNAME")) + parser.add_argument("--list-notifications", action="store_true") + parser.add_argument("--process-notifications", action="store_true") + parser.add_argument("--download", type=str, metavar="CODE") + args = parser.parse_args() + + lpa = TiciLPA(device=args.device, baud=args.baud, timeout=args.timeout, verbose=args.verbose) + + if args.enable: + lpa.enable_profile(args.enable, refresh=not args.no_refresh) + elif args.disable: + lpa.disable_profile(args.disable, refresh=not args.no_refresh) + elif args.delete: + lpa.delete_profile(args.delete) + elif args.set_nickname: + lpa.nickname_profile(args.set_nickname[0], args.set_nickname[1]) + elif args.list_notifications: + with lpa._open_client() as client: + print(json.dumps(list_notifications(client), indent=2)) + return + elif args.process_notifications: + lpa.process_notifications() + return + elif args.download: + lpa.download_profile(args.download) + + profiles_out = [{ + "iccid": p.iccid, + "profileNickname": p.nickname, + "profileState": "enabled" if p.enabled else "disabled", + "serviceProviderName": p.provider, + } for p in lpa.list_profiles()] + print(json.dumps(profiles_out, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py new file mode 100644 index 000000000..d53183f7a --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py @@ -0,0 +1,30 @@ +# GPIO pin definitions +class GPIO: + # both GPIO_STM_RST_N and GPIO_LTE_RST_N are misnamed, they are high to reset + HUB_RST_N = 30 + UBLOX_RST_N = 32 + UBLOX_SAFEBOOT_N = 33 + GNSS_PWR_EN = 34 # SCHEMATIC LABEL: GPIO_UBLOX_PWR_EN + + STM_RST_N = 124 + STM_BOOT0 = 134 + STM_PWR_EN_N = 41 # because STM32H7 RST doesn't generate a full power-on-reset + + SIREN = 42 + SOM_ST_IO = 49 + + LTE_RST_N = 50 + LTE_PWRKEY = 116 + LTE_BOOT = 52 + + # GPIO_CAM0_DVDD_EN = /sys/kernel/debug/regulator/camera_rear_ldo + CAM0_AVDD_EN = 8 + CAM0_RSTN = 9 + CAM1_RSTN = 7 + CAM2_RSTN = 12 + + # Sensor interrupts + BMX055_ACCEL_INT = 21 + BMX055_GYRO_INT = 23 + BMX055_MAGN_INT = 87 + LSM_INT = 84 diff --git a/artifacts/iqpilot_navassist_private/python/iqpilot_private/__init__.py b/artifacts/iqpilot_navassist_private/python/iqpilot_private/__init__.py new file mode 100644 index 000000000..e69de29bb 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 new file mode 100755 index 000000000..7f3850256 Binary files /dev/null and b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/__init__.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/__init__.py b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/__init__.py new file mode 100644 index 000000000..69fde3183 --- /dev/null +++ b/artifacts/iqpilot_navassist_private/python/iqpilot_private/navassist/__init__.py @@ -0,0 +1 @@ +# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos 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 new file mode 100755 index 000000000..3d19590d4 Binary files /dev/null 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 new file mode 100755 index 000000000..8afc85567 Binary files /dev/null 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 new file mode 100755 index 000000000..113101776 Binary files /dev/null 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 new file mode 100755 index 000000000..097cac5e3 Binary files /dev/null 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 new file mode 100755 index 000000000..3ca2c9256 Binary files /dev/null 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 new file mode 100755 index 000000000..c462c067e Binary files /dev/null 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 146a9618d..a58269c43 100644 --- a/artifacts/iqpilot_navd_private/manifest.json +++ b/artifacts/iqpilot_navd_private/manifest.json @@ -186,7 +186,7 @@ }, "python/iqpilot_private/navd/__init__.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "10896807faa5044f6d42807fb7e5efefc6ef64243edea9da5639332964e1cb19", + "sha256": "68beafb13bbfbb47a690606c024441d913d91ef1ed3d9cbee386153ace26acfa", "size": 67568 }, "python/iqpilot_private/navd/__init__.py": { @@ -196,127 +196,127 @@ }, "python/iqpilot_private/navd/alpr_matcher.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "5faf834f012075671a390de2fe4ebb09f9c7d4c05406fa89407aa9e011e9d38c", + "sha256": "6d0ebc52d23e240fa3e25505892be041c7a585d1de20d32b965d795734be5afe", "size": 269896 }, "python/iqpilot_private/navd/amap_client.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "a8361fe53878b61d30d430d0b0488eaabe1641e11b7e584e1ad3566370e55085", + "sha256": "51ddca1a6f6c21c572c5c128e31b0897a13d960f02880c43a2dc8ecd2339ae57", "size": 202088 }, "python/iqpilot_private/navd/event_builder.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "47c673a898e6ceb321f01eff980250bbc85ddd058a8161098e8e91cb02f8f4a6", + "sha256": "940f72d2f887373cb57309002aae0966f1614a63375fc2ec17346aa3fb600a39", "size": 136928 }, "python/iqpilot_private/navd/helpers.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "55a8b9eb2e17f9baa1a851919d3ce908b69004bfbd09591985195af2da4cc40c", + "sha256": "306115c538bc64b20e9af74dde0f6f2c18ec43d71327f27fbdf6d23c6c77a853", "size": 135928 }, "python/iqpilot_private/navd/incident_feed.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "115b57594d411fde87f66d0d030cd5de043bf8d986274dfb8fa7007b8f752c87", + "sha256": "a92305a433ad021259f5b4c41b7d2a5d7f153bc7bae32c846f2da2aede624554", "size": 135496 }, "python/iqpilot_private/navd/iqmapd.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "90c8b38b5c0a328c9ccce27292f8399546a237cc72caf837ffa4c1a55a529c47", + "sha256": "e449066b14e6df8642d6698bdc3b0bd54640c84316241100e96c7a80b26d932f", "size": 135600 }, "python/iqpilot_private/navd/long_decel.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "6d80f03b5a4da8083d4b7f2d4b165748556230859bcdb96c2ff5164aa5500b6c", + "sha256": "19e25e5bf234362b49acb086943501b0ba92d4ea31a74b6daf51e92778d8a0c8", "size": 135256 }, "python/iqpilot_private/navd/mapbox_client.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "a6809b5c118c603706e6b709597a29daa2345fd53d736526f6a055a009ad70ac", + "sha256": "c41e63179df6208e5eb52db1c56a9977d9350fe54182229ea08c6d73933f7403", "size": 68128 }, "python/iqpilot_private/navd/nav_cameras.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "05f7b0433be88afdae23a68f7acd92ff0c8ce6b04d83fc46ad2233c3df51dea0", + "sha256": "6dd6a2ebf5484a1e402e00e89d590b154a56f251b03be980a4a13af711f87b5e", "size": 203024 }, "python/iqpilot_private/navd/nav_turn_signals.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "96414985cacbd85d02f69473b84ff8215fbf959996ca1f1f0dfdf7c77503c9cc", + "sha256": "4c56805bb05a3e50f83e9b754beec1c5a44ac08b4a3b7df7f94087c1a7175bf6", "size": 135536 }, "python/iqpilot_private/navd/navbridge_source.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "8bd53fba3f00e3f8320f873e72b3665f22b6065ee10425d270167b21d2348ad9", + "sha256": "99e82bafcd3b80e19e77065476dd5bd8444833f0c9589793c7051485a64b618d", "size": 136576 }, "python/iqpilot_private/navd/navd.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "d395b00b81da1acdab40d6033b5ccf084effcc6531978af63d568d4ba17ee894", + "sha256": "860867152a5282fc86a5b10f524430f0b4c0750d506c7eb24a9c5fd28b32c5e9", "size": 862920 }, "python/iqpilot_private/navd/navincidentd.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "43bdd7f93c317f26bfa9203953b3a35bf91bd0475dc3777aa5fc3e991216c3f4", - "size": 135464 + "sha256": "742355765901f26bf133d17d7a8d8dd0c21ea0a0da4e01c9a4b0d0066e8a3b14", + "size": 135272 }, "python/iqpilot_private/navd/navrenderd.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "4678f2e256e0fd2b01d4441af848056b173d7d2d6c8ba590ad32b8e23ee8591e", + "sha256": "1f1e7fb278183bb96e249257bf21194ba5cb8e7a8ecd1df07584a5b74e477a3a", "size": 135552 }, "python/iqpilot_private/navd/osm_cameras.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "52e4d03639c9d9be88209ab2e4a1f0f23c0adf81631983248f7d069d46cf61a5", + "sha256": "c934cf04ab106482177c548ead39b61b1f92d6f693e55fcbc968685e745fe01f", "size": 267936 }, "python/iqpilot_private/navd/reroute.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "cb066c1e5637b8a6b60b44753b6a0855a04ce7f5771464d71fc9da245de4a56e", + "sha256": "20144a661ca25686604c85c6d64f2ef517f73cdb46bfe07fcbe2dd69e002c39e", "size": 136224 }, "python/iqpilot_private/navd/road_incidents.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "2e226e29757151bc3cf350a933f6e8f402d7149b0b9e66b82ac9cb721cadef8e", + "sha256": "c8e10f4f2cd3eee95534b4e5b362337b162f9125c2b9d6d0c498d047f6f99a5b", "size": 203096 }, "python/iqpilot_private/navd/route_manager.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "2b12153db93198ec46c268384cfbd65a9ae526faa584274987ed8a289531547c", + "sha256": "64fd9294cb08ddc2268caa77b07a6e97ae2f6dc1d055f29d9bd117946be1d2ef", "size": 1009128 }, "python/iqpilot_private/navd/runtime_common.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "6ba9e9ae810a4e283a8ad544e1f42e899e2e98939754f9b88ffc21f320a8514f", + "sha256": "23f6847a4e2f28309fcf733dc5b8644cd85c0cc0820d77f46762314a3aceaad1", "size": 135752 }, "python/iqpilot_private/navd/traffic.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "0e233af11a3c07d6469e221b5f02cebee4addfe1706d635f8ed7624eb16ff908", + "sha256": "aaf80b19f6d181c1c7cba4a5ec244226b7cc1d3119818f26ddd134cbb2f6cdd0", "size": 69688 }, "python/iqpilot_private/navd/traffic_sources.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "8485347d40907cd0e654b93e1e872dffd156278c3868c640f7d2e51884d52a00", + "sha256": "bf832215e0b9567488ad9505faaf7c8c4b6128174f7023f29717398e3951147d", "size": 135944 }, "python/iqpilot_private/navd/traffic_writer.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "c8b907c51c69c7950bc99ff577ddf4473dbb42f90e53252e3dd325df2a4f9ed3", + "sha256": "b97b65d8ddfa544e14248297f2d000b20bf6f2cd74e560ed2d34c31c95d68f8a", "size": 136944 }, "python/iqpilot_private/navd/turn_desire.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "c13b5fdec4a8d4a33dbd7c25d9ffba30f6faffa1d286438d80c4d9a615b31145", + "sha256": "93ff254bb89bf423981f9aa1fc08168a4eae11851df00431e3a08fd330d4a0ef", "size": 135280 }, "python/iqpilot_private/navd/valhalla_process.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "695322ac198a75121f05989387bd32a733f18921f01f373d45e22f7ec839e8a5", + "sha256": "02869f86b96e880a41fc1e221957b2cbf3849967d741412d6fb631dc670f6394", "size": 204064 }, "python/iqpilot_private/navd/web_server.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "24d967c264e1b879b0517da9cd6fddefdc4787efe6a52adcd2429efaac2db3db", + "sha256": "da4d12c2905fb60fbae9b8c267cf008b2ed49ee7606a646eb3d7f4d80c022712", "size": 201368 }, "runtime": { @@ -356,31 +356,31 @@ } }, "signatures": { - "python/iqpilot_private/navd/__init__.cpython-312-aarch64-linux-gnu.so": "3zeWvLW0gfxiEeHYfOO7atzH0V9/YxbbPpLV6cdokoSP+UlxRJUrMTkJiNCiLBzXuhnNbEhj0Y6q9dLK8QNDDw==", - "python/iqpilot_private/navd/alpr_matcher.cpython-312-aarch64-linux-gnu.so": "1vJWjDPECvOrFe5UfWFN3rFeZSfO3Te9o3Xj0ue1Q+2Zt+gMkvaXgOZG3DNfbs7yaZ2yheabOhKRdwvwHGumCA==", - "python/iqpilot_private/navd/amap_client.cpython-312-aarch64-linux-gnu.so": "6IWRbqN7iDaCKDPQ3Ho6cEhWrP0Tp6f5nKoo5sXEEAtYfUJVZADBHXY7Qk5MszNoa13Sn+8H2eE0imUwjGUtDA==", - "python/iqpilot_private/navd/event_builder.cpython-312-aarch64-linux-gnu.so": "gakLE/auDWpkOm9vOzHryjZm5dAIln/GbASMoFl4iMiMRVmFmBjdV0uyY6bkVDKPVHX6FRr65f1yuJbF1d09DA==", - "python/iqpilot_private/navd/helpers.cpython-312-aarch64-linux-gnu.so": "yOqbMSdUVZSlHAiw08Yb+vSyEr2YoHBsSIWtFQa1RqoSmLbeLUeFpTiuczVaW/HtCNeZZU8mJYXWyTUM1N0xCQ==", - "python/iqpilot_private/navd/incident_feed.cpython-312-aarch64-linux-gnu.so": "8uEYXTWYXVzhDctzoEBbSc5wJ0mqX8qAe9DuXeKgfmpPsM6rR8jHpjHiyjkLAAPorWDRVN3a5W3iyH2u2tD6Cw==", - "python/iqpilot_private/navd/iqmapd.cpython-312-aarch64-linux-gnu.so": "+kTCQnh0TKGtTVl/XqtKJfrFDVJloVVhsUmt793WbvaULUK9aAJmtaEJRvTNV/DiOjYGdgL1texfnBw3x3CkDw==", - "python/iqpilot_private/navd/long_decel.cpython-312-aarch64-linux-gnu.so": "oFJS3s2xxAmMxBpLAiaFklhstR8719Ah/GtUy+YMPdaDZBP6TYLIe89aqmPjW7LUHagkmxsJogppJXl3YtFUAg==", - "python/iqpilot_private/navd/mapbox_client.cpython-312-aarch64-linux-gnu.so": "rsDD81jJXWuvS+IYSDwKVZy/C2lvnmmEh+wYucqLuUg64BIW5Q8YpdtRM9Z++fYpSut90FeYHtrIJsAavTmOAA==", - "python/iqpilot_private/navd/nav_cameras.cpython-312-aarch64-linux-gnu.so": "5CiNWqZqmmiNfnj1plhm67WEF5PXkPGr6dMsVui4VplsrVwaWCSQYJ4ywPHlsoUeX+c4FoCX7HJSNqwxjn5IBg==", - "python/iqpilot_private/navd/nav_turn_signals.cpython-312-aarch64-linux-gnu.so": "dkNKNQEHnq0Wzw/Usmwf/qNxN72cpBZBnPHRKN1ZEq5nExMqrrraTe6bn+EWM2yo1hMsXsep5JPpNGxpqqusBQ==", - "python/iqpilot_private/navd/navbridge_source.cpython-312-aarch64-linux-gnu.so": "0PDFbzaD2KoWZRAP6I38SKdUG0T11j86Ue27RgjziWLAPuxDSd6ogJPbTuPxKLV6pBIASN6y1KU3Cf4BLOkKBg==", - "python/iqpilot_private/navd/navd.cpython-312-aarch64-linux-gnu.so": "WikwGS5js59suyyzVj90X0dYz6I+wBb7N/KHRGkD4kYEX59jP8hAK+MlLsUAr9Vbmss131S8z5inlifsCjN4BA==", - "python/iqpilot_private/navd/navincidentd.cpython-312-aarch64-linux-gnu.so": "LOnh9gEJeT8leh4xo0gabomCwTOo1Xhp2oALjrsL0Ij1MCur9bLl5QE33iFVcomQr8jGH0iw7Vq1vXDMlIRuBg==", - "python/iqpilot_private/navd/navrenderd.cpython-312-aarch64-linux-gnu.so": "KthwWou6yvP+17m4g/3PGY7aPTKKjUqLT8KTtsiTr8/y3Le6YTntbn680C1hJU/xKm1j+IcnAomDw7wdma1PDA==", - "python/iqpilot_private/navd/osm_cameras.cpython-312-aarch64-linux-gnu.so": "cdd78rET10BlVNmGXsNJgYpbS9VCSNEB5HJQ9krzeMu05ikqVnI7vHA/zlxmuHHFiHnZyAN00c16mEmgf62dDw==", - "python/iqpilot_private/navd/reroute.cpython-312-aarch64-linux-gnu.so": "QyrJUPOGnI39p3n24ZCKdHG+MI/bqIvBwUdFyGO527eqHVVWJyinfneK+X7vARV+HLwxqO5bBcgwqHLdi9enAQ==", - "python/iqpilot_private/navd/road_incidents.cpython-312-aarch64-linux-gnu.so": "YkM9q3yJaDmYhExd3Te/7tpFQXXrt+/82M0QRZrVxjKRKOxrR7xu3jdjXQIT67I6WxpHzsNIFhnzX2Bl2/9eBQ==", - "python/iqpilot_private/navd/route_manager.cpython-312-aarch64-linux-gnu.so": "+Uu9KthRwOh5+YJaj1/SD/QuSOfEuT0d4CwO//wB7sCueUKsVx7+CcyAC5T2YB8bRN+42NJDn8pQeA9JcrPoDg==", - "python/iqpilot_private/navd/runtime_common.cpython-312-aarch64-linux-gnu.so": "BoQYpXsWgO+RdUhI5HL4WSk8La2HJ8V8DSFWvAylCaDNWJX3yaClso603Nf0CcKd1MDycb8J33hiHvJSd7R7AQ==", - "python/iqpilot_private/navd/traffic.cpython-312-aarch64-linux-gnu.so": "5+ip8rEk+Upsvyp2tlfdVhmIdQnlamloY1SCuwFzkSM39y+W9tvo4r7JTeb5pcMiUijmwK54m4Rvx0XWvxz5AA==", - "python/iqpilot_private/navd/traffic_sources.cpython-312-aarch64-linux-gnu.so": "WtPKZHGDG8iQj7q1UsXw6jTXxJsWLfNaw9kfJ3huWYjxWcHz0K7ibJ5JzEGSc5l5u3LdRkt6/aZHlBMrDPBaBA==", - "python/iqpilot_private/navd/traffic_writer.cpython-312-aarch64-linux-gnu.so": "VVFiWnkiwn7nwhq+6q3aYsnxMW1GLdfaGB/a/h8JV46OoSn0NOTVA72jPYlzs0d4bacEN+IgMAelCjnMlqiBBA==", - "python/iqpilot_private/navd/turn_desire.cpython-312-aarch64-linux-gnu.so": "upuJWa/0B6du9cu3JwdzK1cSqSAORKfvd6ir7sh/oWSRPEhkxyjPmo9/uoNw4Qeq2zoh3iKkV2udK5gue/PzCQ==", - "python/iqpilot_private/navd/valhalla_process.cpython-312-aarch64-linux-gnu.so": "DXSI5ljQTXPHV+QvQCQu1+oOfLKcPXMkx7qx6id0NTFPHI/M5KIooBYKgJjbN4abl+mzUXKrtOmGF/y3f0lwDw==", - "python/iqpilot_private/navd/web_server.cpython-312-aarch64-linux-gnu.so": "h4bQodknw1y0emCI3aJYviy3kzaQH8hElJMFmpS/sMvvC+sCHfOGUo+8BK+T7JGZ9/VzNXBzP8RKkHG5cAf0Cg==" + "python/iqpilot_private/navd/__init__.cpython-312-aarch64-linux-gnu.so": "HqvQHuszUk6/MEm2nHzmWFCj53k63QEJUa9FhP6n1wBQRhSvzLPPS5zYoQlwV9tguWcIa64N5IRd0oA8M3KgBg==", + "python/iqpilot_private/navd/alpr_matcher.cpython-312-aarch64-linux-gnu.so": "Akv5Etd1UVJ0+KWYfMfhqFVp/XT1WjPEEUKLX/ijn44W/plrVaw5VJphiXPW6TSmVixVz2itiKGaZ6cYTJMQAg==", + "python/iqpilot_private/navd/amap_client.cpython-312-aarch64-linux-gnu.so": "b8+ucEd/I1rmHWIFTfnNg7Il8p+MAtiuUqD8bQ4CJD8sApGGd0uF4xUkB/VYVfMiy6FLONIXY+SHxEr3RyX0Aw==", + "python/iqpilot_private/navd/event_builder.cpython-312-aarch64-linux-gnu.so": "Ep3uIUav6jaSUjwb1oR+A0Ss3Jpkiy0n77/GZqeSpTBTOwQpaysbfAsPAaQs/UfKyhggHPYavLiODxpDZLP6CQ==", + "python/iqpilot_private/navd/helpers.cpython-312-aarch64-linux-gnu.so": "wGejxqMf8IUMSgLIL5z+RYGwFZokGAmyQ/1Ev/W9kMMrsEkFL8/upFi36e3bLzF3JojFXv8t1SApDXTaNhNtAA==", + "python/iqpilot_private/navd/incident_feed.cpython-312-aarch64-linux-gnu.so": "yzc0IeL2KSVmquBnWfY9zzgVH0PM71JZIr0nWjCliC/jvX4hQFCaQx+8H8vrGhJ8s/5i+Potjq3nKUdMMpJpBQ==", + "python/iqpilot_private/navd/iqmapd.cpython-312-aarch64-linux-gnu.so": "DQuL1PP6QD34O73YvlbiV0bc2tfB0vkfsBc8dekjCGR5HuK6+Fs/z+JS7btjGPWnpkQwGgAQqVYUwIhYkO39Dw==", + "python/iqpilot_private/navd/long_decel.cpython-312-aarch64-linux-gnu.so": "4kjuCRTUQYO6iRNpVb9MSgJOcBXpY946PWgnCT78SkliIp7JPA8wmdaKnQEY8/79u5o5YpR9O5/TDriH30YWCg==", + "python/iqpilot_private/navd/mapbox_client.cpython-312-aarch64-linux-gnu.so": "GadPtY3B1UgUAngKJ2495TcBZDKoIJgmTC+LFlk7mgdWC/B491cag0c/NATgWQuZxGxSG8MoO+I2RQ2Xe6NTCA==", + "python/iqpilot_private/navd/nav_cameras.cpython-312-aarch64-linux-gnu.so": "1Maph+7ojm3ycePrVhb2qa/y34HkpCEEPGv/HpsnuwvgrwuFwqoaLFgkm4wk/XkFsLM44V2+dGQbv9LurWx2CQ==", + "python/iqpilot_private/navd/nav_turn_signals.cpython-312-aarch64-linux-gnu.so": "YrcE7SIleObN5ABgDWaFIKu+eLgqSDY5c8QcIYyaZxdwLwyYtiS9GnZh8ZXZr8oTXCzKDl7IzaWcaHHMITWQDQ==", + "python/iqpilot_private/navd/navbridge_source.cpython-312-aarch64-linux-gnu.so": "4ONoDnKgAD3EsLM5SxZyfCuQMJc/w0hKXRTtYlYzuRuTjZU3u1Myh72/FDt6/4s8mgFGiPUcGpxPi9H80yjDBA==", + "python/iqpilot_private/navd/navd.cpython-312-aarch64-linux-gnu.so": "sDHR5jK0jMRVk9Ms0DfxoH4ppJyO3of0tYV/SMxf/5lGPEhUrhiQ3Sxh7E8vRuQMjQMxcpK4xinVsEkAgo1zCg==", + "python/iqpilot_private/navd/navincidentd.cpython-312-aarch64-linux-gnu.so": "qcqd/kQQOsbm8pf4E/skWlf2SoAykc3Yz/sk/Ic4sg7EIoK6thwapk+3SP+OOyJOlyvv2nBM9CJZ/tmAJM5YDw==", + "python/iqpilot_private/navd/navrenderd.cpython-312-aarch64-linux-gnu.so": "nWBumz9rDCROfCQr/hzfaDOwlC8hHoOHXOl7UGMZREXA1BdShgxfwLC5Y89GpM+LqOi007lNfw8XA5E2xUxdBA==", + "python/iqpilot_private/navd/osm_cameras.cpython-312-aarch64-linux-gnu.so": "KZUxh2P4fL/cnLlD5hdRoBBRZERkKRksgaeE4g5xd0LCKWIyTa74emzbPWJM04Iax8bG5vG/8qr92v/uMGieAA==", + "python/iqpilot_private/navd/reroute.cpython-312-aarch64-linux-gnu.so": "uZxb9Gi1kYTrRFLxSQ7bFK/kfJXIv+uTQbNCQofM/f7p2QWqRVnrsccr2H4/UwzWkq6/oXvDYKl7kt8RYwqWCA==", + "python/iqpilot_private/navd/road_incidents.cpython-312-aarch64-linux-gnu.so": "B+0mOr/weMU0dYotSKJ+0fZAtl0EhJq5VLPQNWXDSRYxHydTNMlNFwJYfRVG+0W6crrMgCK1/FmfKdvg6wK5AA==", + "python/iqpilot_private/navd/route_manager.cpython-312-aarch64-linux-gnu.so": "llgYyFkE5NGwVzq6X2rF92Jt7jVCqoSKWfHDo8XAPU5uxf0WMlmgqqGvSTqxyef2PIA6/4xPsycmECPh2dbuCQ==", + "python/iqpilot_private/navd/runtime_common.cpython-312-aarch64-linux-gnu.so": "1IhEI94wbdRwcec3LSt1ed7O5ipXqUYqh/xdWPpdOjnsqoc1UxqL5k2aGvQsSw24ugYTtujL08WfZg5U5KKgAQ==", + "python/iqpilot_private/navd/traffic.cpython-312-aarch64-linux-gnu.so": "1BO+xJ81fxdFd4LX8Qjl8pTRE2J7uEa6nXAxe2RxLplThga3ygojXvOdc4fdsrJe6CeP06d++Q7Q+7twWjBVDg==", + "python/iqpilot_private/navd/traffic_sources.cpython-312-aarch64-linux-gnu.so": "uUU02Y2ATNN1II1yflbxoFolAbi7i82231ZzXKe0HXMEx4Ul69DtRbDPPDt3QSMPRuvq7MkMdfUg/vjsNtGbCA==", + "python/iqpilot_private/navd/traffic_writer.cpython-312-aarch64-linux-gnu.so": "3RyqM4h3zCP8+a+h23mLdNPipPxaHEGikeGTejsvovIAG+3e6vqXp1qJ5p9Iv6+cgkiOjwTi1tNsZL5sPfF4Aw==", + "python/iqpilot_private/navd/turn_desire.cpython-312-aarch64-linux-gnu.so": "AqDfZIJx5Q9qhsnKOe69YkpBZvFjSyFBwScG1U7VbGpqpqAsZQu2DisH6t9+b50J1vDMZV6O7dzeQ4O8uCl8Dg==", + "python/iqpilot_private/navd/valhalla_process.cpython-312-aarch64-linux-gnu.so": "SqWu7atpm0cv9Xrz+In6JLKjUubIRgmO1GiUTWRmxXe7BJKAGvqqOwmIl7CuQGiIBm7U9QEaXIoiUh50jl9QAg==", + "python/iqpilot_private/navd/web_server.cpython-312-aarch64-linux-gnu.so": "SIE6woZzt8xDyoFCbtRCUMRu1nRi3RY95KTh1LurBjdk0YB92sEsE/7/KgTpiuWIAMtzR6mGqXglIc5ATZMbDQ==" } } diff --git a/artifacts/iqpilot_navd_private/manifest.json.sig b/artifacts/iqpilot_navd_private/manifest.json.sig index 1cd57d4f4..51371690b 100644 --- a/artifacts/iqpilot_navd_private/manifest.json.sig +++ b/artifacts/iqpilot_navd_private/manifest.json.sig @@ -1 +1 @@ -kmjHw5sbr1uhbckWG6FoCjTR0idPL+RGlQ19Wri9phayqsni9P3V2rqLDUPeZ0ds+CRqMb378Jgghj6eJ6gxCQ== +895wuDUENqe6UUjsi9Cjy0ZHiFxVMYtjNXm8ngrGYhTxzA4PUr5j3EM2LLlqq1oqQmGBsLjsilpILKUt3o8aAw== 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 index 51a6fa753..3f847f2f5 100755 Binary files a/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/__init__.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_navd_private/python/iqpilot_private/navd/__init__.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 index 76cd3a52e..4c3a2af54 100755 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 index 538117876..a5336e755 100755 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 index 8cdf95127..317b41b17 100755 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 index 7ea4bb95a..baef642f3 100755 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 index ccd477f85..451945ad1 100755 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 index d895a49f9..0e31cf27a 100755 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 index 1b78dd316..8b6859d40 100755 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 index 797c8ed72..23780cbdd 100755 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/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 index efbaa657b..90613ded1 100755 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_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 index 743c91630..6e902d605 100755 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 index 950c6ffb5..0a99278a6 100755 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 index ddd9a89df..ac66026bf 100755 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 index d4cc31602..4e5a89f47 100755 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 index d8020b7d2..8d4614af9 100755 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 index c3104666e..a0414c897 100755 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 index 9f07771d3..b4efb9122 100755 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 index e0d587bb9..caabbd3ce 100755 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 index b522b7a1e..4e71bc905 100755 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 index 15a0c3a12..a80c21c08 100755 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 index 1995b9f60..41cb9b8c2 100755 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 index e1b385c44..8f0890cb3 100755 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 index b9c4bbe4c..1a22978dd 100755 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 index 11fccb337..1b7ea1e4f 100755 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 index 946608bb7..f8feb8dfd 100755 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 index 39a94a77a..fc720a60b 100755 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/conftest.py b/conftest.py index ba4b3fc4a..4652a1d90 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,8 @@ import contextlib import gc import os +import shutil +import uuid import pytest from iqpilot.common.prefix import OpenpilotPrefix @@ -17,6 +19,25 @@ collect_ignore_glob = [ ] +def _isolate_session_prefix() -> OpenpilotPrefix | None: + if os.environ.get("OPENPILOT_PREFIX"): + return None + prefix = OpenpilotPrefix(prefix="pytest" + uuid.uuid4().hex[:12], clean_dirs_on_exit=False) + prefix.__enter__() + return prefix + + +# msgq only reclaims dead-reader slots on Linux (/proc check), so shared /tmp/msgq_* files on the macOS runner fill up and ENOBUFS every later subscriber. +SESSION_PREFIX = _isolate_session_prefix() + + +def pytest_sessionfinish(session, exitstatus): + if SESSION_PREFIX is None or os.environ.get("PYTEST_XDIST_WORKER"): + return + SESSION_PREFIX.clean_dirs() + shutil.rmtree(SESSION_PREFIX.msgq_path, ignore_errors=True) + + def pytest_sessionstart(session): # TODO: fix tests and enable test order randomization if session.config.pluginmanager.hasplugin('randomly'): diff --git a/iqpilot/common/params_keys.h b/iqpilot/common/params_keys.h index 44ec73f94..27fd3a3da 100644 --- a/iqpilot/common/params_keys.h +++ b/iqpilot/common/params_keys.h @@ -369,6 +369,11 @@ inline static std::unordered_map keys = { // never meant to survive a manager restart or reach a log. {"IQAndroidNavEmail", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}}, {"IQAndroidNavPassword", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}}, + // Report-request flags: the UI writes 1 to ask navassist to file that report in the nav + // app; navassist sets it back to 0 once the app confirms it was sent, so 0 == done. + {"IQNavReportPolice", {PERSISTENT, BOOL, "0"}}, + {"IQNavReportCrash", {PERSISTENT, BOOL, "0"}}, + {"IQNavReportHazard", {PERSISTENT, BOOL, "0"}}, {"IQLiveSteerDelay", {PERSISTENT, BOOL, "1"}}, {"IQLateralAccelSlew", {PERSISTENT, BOOL, "0"}}, {"IQLateralCurvatureLookahead", {PERSISTENT, BOOL, "0"}}, diff --git a/iqpilot/selfdrive/iqmodeld/daemon.py b/iqpilot/selfdrive/iqmodeld/daemon.py index 97327992f..8828032d3 100755 --- a/iqpilot/selfdrive/iqmodeld/daemon.py +++ b/iqpilot/selfdrive/iqmodeld/daemon.py @@ -513,6 +513,9 @@ class InferenceDaemon: self._lat_smooth_extra_sec = 0.0 def _load_car_params(self, demo: bool): + if not demo and self._params.get_bool("IQBenchIgnition") and self._params.get("CarParams") is None: + cloudlog.warning("iqmodeld: bench ignition with no CarParams; running with the demo car") + demo = True car_params = get_demo_car_params() if demo else messaging.log_from_bytes( self._params.get("CarParams", block=True), car.CarParams) cloudlog.info("iqmodeld got CarParams: %s", car_params.brand) diff --git a/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py b/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py index 07e4886eb..03fe6a88d 100644 --- a/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py +++ b/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py @@ -87,8 +87,6 @@ def main() -> None: print(f"saved warp JIT to {out} ({os.path.getsize(out) / 1e6:.2f} MB)") -if __name__ == "__main__": - main() def selftest_inputs(cam_w: int, cam_h: int, nv12_size: int): @@ -117,3 +115,7 @@ def selftest_digest(compiled, cam_w: int, cam_h: int, nv12_size: int) -> str: frame=Tensor(frame, device=dev).realize(), big_frame=Tensor(big_frame, device=dev).realize()) return hashlib.sha256(out.numpy().astype(np.uint8).tobytes()).hexdigest() + + +if __name__ == "__main__": + main() diff --git a/iqpilot/selfdrive/ui/mici/layouts/settings/models.py b/iqpilot/selfdrive/ui/mici/layouts/settings/models.py index 5a3ba628a..f8c3623bd 100644 --- a/iqpilot/selfdrive/ui/mici/layouts/settings/models.py +++ b/iqpilot/selfdrive/ui/mici/layouts/settings/models.py @@ -552,7 +552,8 @@ class ModelsLayoutMici(NavScroller): def _update_steer_delay_subtext(self): if self._steer_delay._checked: try: - self._steer_delay.set_value(f"measured {ui_state.sm['lateralDelay'].lateralDelay:.3f} s") + measured = ui_state.measured_steer_delay() + self._steer_delay.set_value("calibrating" if measured is None else f"measured {measured:.3f} s") except Exception: self._steer_delay.set_value("") return diff --git a/iqpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/iqpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index 73802dfd9..d445a7e8b 100644 --- a/iqpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/iqpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -54,7 +54,9 @@ MICI_BORDER_THICKNESS = 50 MICI_BORDER_ROUNDNESS = 0.2 * 1.02 MICI_BORDER_BOTTOM_ONLY_HEIGHT = 95 MICI_EXPERIMENTAL_ICON_SIZE = 28 -MICI_EXPERIMENTAL_ICON_SPACING = 8 +MICI_EXPERIMENTAL_ICON_SLOT = 60 +MICI_EXPERIMENTAL_ICON_MARGIN_X = 16 +MICI_EXPERIMENTAL_ICON_MARGIN_Y = 10 class BookmarkIcon(Widget): @@ -274,7 +276,7 @@ class AugmentedRoadView(CameraView): # don't draw the experimental/IQ.Dynamic icon over alert text (it falls back to the # top-left alert anchor when the DMoji is hidden while disengaged) if alert_to_render is None: - self._draw_experimental_icon(should_draw_dmoji) + self._draw_experimental_icon() # End clipping region rl.end_scissor_mode() @@ -304,7 +306,7 @@ class AugmentedRoadView(CameraView): rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175)) self._offroad_label.render(self._content_rect) - def _draw_experimental_icon(self, draw_below_driver_state: bool) -> None: + def _draw_experimental_icon(self) -> None: if not ui_state.started: return @@ -316,13 +318,10 @@ class AugmentedRoadView(CameraView): else: icon = self._iqstandard_txt - if draw_below_driver_state: - pos_x = self._rect.x + 16 + (self._driver_state_renderer.rect.width - icon.width) / 2 - pos_y = self._rect.y + 10 + self._driver_state_renderer.rect.height + MICI_EXPERIMENTAL_ICON_SPACING - else: - pos_x = self._rect.x + 18 - pos_y = self._rect.y + 18 - + slot_x = self._content_rect.x + self._content_rect.width - MICI_EXPERIMENTAL_ICON_MARGIN_X - MICI_EXPERIMENTAL_ICON_SLOT + slot_y = self._content_rect.y + MICI_EXPERIMENTAL_ICON_MARGIN_Y + pos_x = slot_x + (MICI_EXPERIMENTAL_ICON_SLOT - icon.width) / 2 + pos_y = slot_y + (MICI_EXPERIMENTAL_ICON_SLOT - icon.height) / 2 rl.draw_texture(icon, int(pos_x), int(pos_y), rl.WHITE) def _draw_border(self): diff --git a/iqpilot/selfdrive/ui/mici/onroad/model_renderer.py b/iqpilot/selfdrive/ui/mici/onroad/model_renderer.py index b4169be20..a8ddd0ce9 100644 --- a/iqpilot/selfdrive/ui/mici/onroad/model_renderer.py +++ b/iqpilot/selfdrive/ui/mici/onroad/model_renderer.py @@ -14,6 +14,7 @@ from iqpilot.selfdrive.ui.mici.onroad import blend_colors from iqpilot.system.ui.lib.application import gui_app from iqpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient from iqpilot.system.ui.widgets import Widget +from iqpilot.ui.theme import NeonTheme CLIP_MARGIN = 500 MIN_DRAW_DISTANCE = 10.0 @@ -297,7 +298,10 @@ class ModelRenderer(Widget): def _get_ll_color(self, prob: float, adjacent: bool, left: bool): alpha = np.clip(prob, 0.0, 0.7) if adjacent: - _base_color = LANE_LINE_COLORS.get(ui_state.status, LANE_LINE_COLORS[UIStatus.DISENGAGED]) + if gui_app.iqpilot_ui() and ui_state.status == UIStatus.ENGAGED: + _base_color = NeonTheme.glow() + else: + _base_color = LANE_LINE_COLORS.get(ui_state.status, LANE_LINE_COLORS[UIStatus.DISENGAGED]) color = rl.Color(_base_color.r, _base_color.g, _base_color.b, int(alpha * 255)) # turn adjacent lls orange if torque is high diff --git a/iqpilot/selfdrive/ui/ui_state.py b/iqpilot/selfdrive/ui/ui_state.py index 07af7795b..7e19df516 100644 --- a/iqpilot/selfdrive/ui/ui_state.py +++ b/iqpilot/selfdrive/ui/ui_state.py @@ -424,6 +424,13 @@ class UIState(IQUIState): self._started_prev = self.started + def measured_steer_delay(self) -> float | None: + # estimatord is onroad-only, so offroad the lateralDelay socket reads back a default 0.0 + if self.sm.alive['lateralDelay']: + return float(self.sm['lateralDelay'].lateralDelay) + lag = log_param_from_bytes(self.params, "LiveDelay", log.Event) + return float(lag.lateralDelay.lateralDelay) if lag is not None else None + def update_params(self) -> None: CP = log_param_from_bytes(self.params, "CarParamsPersistent", car.CarParams) if CP is not None: diff --git a/iqpilot/system/android/androidd.py b/iqpilot/system/android/androidd.py deleted file mode 100644 index 404ced7c2..000000000 --- a/iqpilot/system/android/androidd.py +++ /dev/null @@ -1,200 +0,0 @@ -# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos -from __future__ import annotations - -import json -import os -import socket -import subprocess -import time - -from iqpilot.cereal import messaging -from iqpilot.common.params import Params -from iqpilot.common.realtime import Ratekeeper -from iqpilot.common.swaglog import cloudlog - -ANDROID_ROOT = "/data/android" -HEADLESS = f"{ANDROID_ROOT}/waydroid_headless.sh" -TRIM = f"{ANDROID_ROOT}/android_trim.sh" -GUARD = f"{ANDROID_ROOT}/android_guard.sh" -LXC_ATTACH = f"{ANDROID_ROOT}/root/usr/bin/lxc-attach" -LD = f"{ANDROID_ROOT}/root/usr/lib/aarch64-linux-gnu" - -# Waze's launcher is FreeMapAppActivity; there is no com.waze.MainActivity, and asking for -# one fails with "Activity class does not exist" rather than anything that names the problem. -NAV_APPS = { - "waze": ("com.waze", "com.waze/.FreeMapAppActivity"), - "maps": ("com.google.android.apps.maps", "com.google.android.apps.maps/com.google.android.maps.MapsActivity"), -} -BRIDGE_HOST = "192.168.240.112" -BRIDGE_PORT = 8099 -GPS_SOURCES = ["iqLiveLocation", "liveLocationKalman", "gpsLocationExternal", "gpsLocation"] -PROVIDERS = ("gps", "fused", "network") -GPS_INTERVAL_S = 0.5 - - -def attach(*args: str, timeout: float = 10.0) -> subprocess.CompletedProcess: - cmd = ["sudo", "-n", "env", f"LD_LIBRARY_PATH={LD}", LXC_ATTACH, - "-P", "/var/lib/waydroid/lxc", "-n", "waydroid", "--", *args] - return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False) - - -def container_booted() -> bool: - try: - return attach("/system/bin/getprop", "sys.boot_completed", timeout=8).stdout.strip() == "1" - except (subprocess.SubprocessError, OSError): - return False - - -def bring_up() -> None: - # AGNOS mounts / read-only, so no unit file can be installed; a transient unit is the only - # way the compositor and lxc-start survive this process exiting. - subprocess.run(["sudo", "-n", "systemd-run", "--unit=iq-android", "--service-type=oneshot", - "--remain-after-exit", HEADLESS, "up"], - capture_output=True, text=True, timeout=120, check=False) - - -def start_guard() -> None: - subprocess.run(["sudo", "-n", "systemd-run", "--unit=iq-android-guard", GUARD], - capture_output=True, text=True, timeout=30, check=False) - - -def trim(nav_app: str) -> None: - subprocess.run(["sudo", "-n", "env", f"ANDROID_NAV_APP={nav_app}", TRIM], - capture_output=True, timeout=240, check=False) - - -def enable_mock_location() -> None: - # Wiped by every container restart, and the package form does not cover uid 0: without the - # --uid form every injection fails with SecurityException and the apps just say "no GPS". - attach("/system/bin/cmd", "appops", "set", "--uid", "0", "android:mock_location", "allow") - for p in PROVIDERS: - attach("/system/bin/cmd", "location", "providers", "add-test-provider", p) - attach("/system/bin/cmd", "location", "providers", "set-test-provider-enabled", p, "true") - # The a11y tree is only updated while the display is awake; asleep it emits nothing at all. - attach("/system/bin/svc", "power", "stayon", "true") - - -def inject(lat: float, lon: float) -> None: - for p in PROVIDERS: - attach("/system/bin/cmd", "location", "providers", "set-test-provider-location", p, - "--location", f"{lat:.6f},{lon:.6f}", "--accuracy", "4", timeout=6) - - -def read_position(sm: messaging.SubMaster) -> tuple[float, float] | None: - for src in GPS_SOURCES: - if src not in sm.data or not sm.valid.get(src, False): - continue - msg = sm[src] - for lat_a, lon_a in (("latitude", "longitude"), ("lat", "lon")): - lat = getattr(msg, lat_a, None) - lon = getattr(msg, lon_a, None) - if lat is not None and lon is not None and (lat or lon): - return float(lat), float(lon) - return None - - -class AndroidNavDaemon: - def __init__(self) -> None: - self.params = Params() - self.sources = [s for s in GPS_SOURCES if s in messaging.SERVICE_LIST] - self.sm = messaging.SubMaster(self.sources) if self.sources else None - self.last_inject = 0.0 - self.up = False - - def status(self, text: str) -> None: - self.params.put("IQAndroidNavStatus", text) - - def sign_in(self) -> None: - email = self.params.get("IQAndroidNavEmail", encoding="utf8") - password = self.params.get("IQAndroidNavPassword", encoding="utf8") - if not email or not password: - return - try: - with socket.create_connection((BRIDGE_HOST, BRIDGE_PORT), timeout=10) as s: - s.sendall(json.dumps({"cmd": "signin", "email": email, "password": password}).encode() + b"\n") - self.status("sign-in requested") - except OSError: - cloudlog.exception("androidd: sign-in request failed") - self.status("sign-in failed") - finally: - # Never let the credential outlive the one use it was handed over for. - self.params.remove("IQAndroidNavEmail") - self.params.remove("IQAndroidNavPassword") - - def nav_app(self) -> str: - value = self.params.get("IQAndroidNavApp", encoding="utf8") or "waze" - return value if value in NAV_APPS else "waze" - - def enforce_single_app(self, nav_app: str) -> None: - for name, (pkg, _) in NAV_APPS.items(): - if name != nav_app: - attach("/system/bin/am", "force-stop", pkg) - - def ensure_app_running(self, nav_app: str) -> None: - pkg, component = NAV_APPS[nav_app] - if attach("/system/bin/pidof", pkg, timeout=8).stdout.strip(): - return - attach("/system/bin/am", "start", "-n", component, timeout=20) - - def ensure_up(self) -> None: - if container_booted(): - if not self.up: - nav_app = self.nav_app() - enable_mock_location() - trim(nav_app) - self.enforce_single_app(nav_app) - start_guard() - self.status(f"running:{nav_app}") - self.up = True - return - self.up = False - self.status("starting") - bring_up() - - def step(self) -> None: - if not self.params.get_bool("IQAndroidNav"): - if self.up: - subprocess.run(["sudo", "-n", "systemctl", "stop", "iq-android", "iq-android-guard"], - capture_output=True, timeout=60, check=False) - self.up = False - self.status("disabled") - return - - self.ensure_up() - if not self.up: - return - - self.sign_in() - self.ensure_app_running(self.nav_app()) - - if self.sm is None: - return - self.sm.update(0) - now = time.monotonic() - if now - self.last_inject < GPS_INTERVAL_S: - return - position = read_position(self.sm) - if position is not None: - self.last_inject = now - try: - inject(*position) - except subprocess.SubprocessError: - cloudlog.exception("androidd: location inject failed") - - -def main() -> None: - if not os.path.exists(HEADLESS): - cloudlog.warning("androidd: android stack not staged, exiting") - return - daemon = AndroidNavDaemon() - rk = Ratekeeper(2.0, print_delay_threshold=None) - while True: - try: - daemon.step() - except Exception: - cloudlog.exception("androidd: step failed") - rk.keep_time() - - -if __name__ == "__main__": - main() diff --git a/iqpilot/system/hardware/hardwared.py b/iqpilot/system/hardware/hardwared.py index 10fe93ed2..ad4a7ecbf 100755 --- a/iqpilot/system/hardware/hardwared.py +++ b/iqpilot/system/hardware/hardwared.py @@ -427,8 +427,6 @@ def hardware_thread(end_event, hw_queue) -> None: # Set ignition based on any panda connected onroad_conditions["ignition"] = any(ps.ignitionLine or ps.ignitionCan for ps in pandaStates if ps.pandaType != log.PandaState.PandaType.unknown) - if params.get_bool("IQBenchIgnition"): - onroad_conditions["ignition"] = True pandaState = pandaStates[0] @@ -439,6 +437,9 @@ def hardware_thread(end_event, hw_queue) -> None: onroad_conditions["ignition"] = False cloudlog.error("panda timed out onroad") + if params.get_bool("IQBenchIgnition"): + onroad_conditions["ignition"] = True + # Run at 2Hz, plus either edge of ignition ign_edge = (started_ts is not None) != all(onroad_conditions.values()) if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_HW) != 0) and not ign_edge: @@ -470,7 +471,7 @@ def hardware_thread(end_event, hw_queue) -> None: online_cpu_usage = [int(round(n)) for n in psutil.cpu_percent(percpu=True)] offline_cpu_usage = [0., ] * (len(msg.deviceState.cpuTempC) - len(online_cpu_usage)) msg.deviceState.cpuUsagePercent = online_cpu_usage + offline_cpu_usage - if msg.deviceState.memoryUsagePercent > 85: + if msg.deviceState.memoryUsagePercent > 95: avg_cpu_usage = int(round(sum(online_cpu_usage) / max(1, len(online_cpu_usage)))) perf.emit( "hardware_low_memory", diff --git a/iqpilot/system/manager/process_config.py b/iqpilot/system/manager/process_config.py index 3f84ddf00..f37008826 100644 --- a/iqpilot/system/manager/process_config.py +++ b/iqpilot/system/manager/process_config.py @@ -67,7 +67,7 @@ def qcomgps(started: bool, params: Params, CP: car.CarParams) -> bool: def always_run(started: bool, params: Params, CP: car.CarParams) -> bool: return True -def android_nav(started: bool, params: Params, CP: car.CarParams) -> bool: +def nav_assist(started: bool, params: Params, CP: car.CarParams) -> bool: return params.get_bool("IQAndroidNav") def only_onroad(started: bool, params: Params, CP: car.CarParams) -> bool: @@ -179,7 +179,7 @@ procs = [ PythonProcess("journald", "iqpilot.system.journald", only_onroad, platform.system() != "Darwin"), PythonProcess("micd", "iqpilot.system.micd", or_(iscar, livestream)), PythonProcess("timed", "iqpilot.system.timed", always_run, enabled=not PC), - PythonProcess("androidd", "iqpilot.system.android.androidd", android_nav, 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), diff --git a/iqpilot/ui/layouts/settings/iq_panels.py b/iqpilot/ui/layouts/settings/iq_panels.py index 859354dc2..e7bb65356 100644 --- a/iqpilot/ui/layouts/settings/iq_panels.py +++ b/iqpilot/ui/layouts/settings/iq_panels.py @@ -900,7 +900,11 @@ class SteeringLayout(Widget): delay_desc = tr("Let IQ.Pilot measure how long your steering takes to respond and keep that figure up to date. " "Switch it off to pin the timing yourself.") if live_delay: - delay_desc += f"
{tr('Measured:')} {ui_state.sm['lateralDelay'].lateralDelay:.3f} s" + measured = ui_state.measured_steer_delay() + if measured is None: + delay_desc += f"
{tr('Measured:')} {tr('not yet, drive to calibrate')}" + else: + delay_desc += f"
{tr('Measured:')} {measured:.3f} s" elif ui_state.CP: sw = float(ui_state.params.get("IQSoftwareSteerDelay", "0.2")) cp = ui_state.CP.steerActuatorDelay diff --git a/iqpilot/ui/mici/onroad/speed_limit_sign.py b/iqpilot/ui/mici/onroad/speed_limit_sign.py index 565f939b6..899d3c58d 100644 --- a/iqpilot/ui/mici/onroad/speed_limit_sign.py +++ b/iqpilot/ui/mici/onroad/speed_limit_sign.py @@ -9,10 +9,10 @@ from iqpilot.system.ui.iqwidgets.lib import canvas from iqpilot.system.ui.lib.application import gui_app from iqpilot.ui.onroad.hud_overlays import IQSpeedLimitOverlay, _SL_ASSIST, _SL_DARK, _dim -_SIGN_X = 16 -_SIGN_Y = 108 -_SIGN_W = 60 -_SIGN_H = 64 +_SIGN_X = 17 +_SIGN_Y = 86 +_SIGN_W = 58 +_SIGN_H = 74 _BADGE_SIDE = 24 @@ -47,8 +47,8 @@ class MiciSpeedLimitSign(IQSpeedLimitOverlay): (self._vienna if ui_state.is_metric else self._mutcd)(box, value, badge, tint, has_limit, alpha) def _vienna(self, rect, value, badge, tint, has_limit, alpha=1.0): - hub = canvas.Pt(rect.x + rect.width / 2, rect.y + rect.height / 2) radius = rect.width / 2 + hub = canvas.Pt(rect.x + radius, rect.y + rect.height / 2) canvas.disc_at(hub, radius, _dim(canvas.WHITE, alpha)) canvas.annulus(hub, radius * 0.78, radius, 0, 360, 36, _dim(canvas.RED, alpha)) canvas.glyphs_centered(self._bold, value, 22 if len(value) >= 3 else 28, hub, _dim(tint, alpha)) @@ -64,9 +64,9 @@ class MiciSpeedLimitSign(IQSpeedLimitOverlay): inner = canvas.Box(rect.x + 4, rect.y + 4, rect.width - 8, rect.height - 8) canvas.panel_outline(inner, 0.25, 8, 2, _dim(canvas.BLACK, alpha)) mid = rect.x + rect.width / 2 - canvas.glyphs_centered(self._demi, "SPEED", 12, canvas.Pt(mid, rect.y + 14), _dim(canvas.BLACK, alpha)) - canvas.glyphs_centered(self._demi, "LIMIT", 12, canvas.Pt(mid, rect.y + 25), _dim(canvas.BLACK, alpha)) - canvas.glyphs_centered(self._bold, value, 30 if len(value) <= 2 else 24, canvas.Pt(mid, rect.y + 45), _dim(tint, alpha)) + canvas.glyphs_centered(self._demi, "SPEED", 13, canvas.Pt(mid, rect.y + 16), _dim(canvas.BLACK, alpha)) + canvas.glyphs_centered(self._demi, "LIMIT", 13, canvas.Pt(mid, rect.y + 29), _dim(canvas.BLACK, alpha)) + canvas.glyphs_centered(self._bold, value, 34 if len(value) <= 2 else 27, canvas.Pt(mid, rect.y + 52), _dim(tint, alpha)) if badge: chip = canvas.Box(rect.x + rect.width - _BADGE_SIDE * 0.55, rect.y - _BADGE_SIDE * 0.55, _BADGE_SIDE, _BADGE_SIDE) canvas.panel(chip, 0.35, 8, _dim(canvas.BLACK, alpha))