IQ.Pilot Release Commit @ ba009d5

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-04 12:33:30 -05:00
parent 2e7a1d1fa5
commit fde770bf03
84 changed files with 4206 additions and 302 deletions

View File

@@ -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=="
}
}

View File

@@ -1 +1 @@
xwY/6EveM0uSrV6q37dNH9aAkFdYr5Wr1YSawQtQNWBSyzDwA5qacLqCqZFbLbLAx1A9d8wUSEDPeAESmMHMBA==
NIDyUgCyKnLKNpWgrqG5yIPAYBaD61T6Yv4KV4uzFhKpqo0OInBCXl1AcEGupEFLfTXdsrS5gDnm/4KdU2jiDw==

View File

@@ -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)

View File

@@ -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:

View File

@@ -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()

View File

@@ -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=="
}
}

View File

@@ -0,0 +1 @@
+k8uY8dYPLT5xuTlqHHgo9YUJkOQM6KQxhPn4leJUi2t9DsHDEMBWJwdtyqzQi5Zes9eRuG9GmF7Hircxj88DQ==

View File

@@ -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"
]

View File

@@ -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)

View File

@@ -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]]

View File

@@ -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")

View File

@@ -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

View File

@@ -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.*<zmq.*>")
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)

View File

@@ -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()

View File

@@ -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 = ["<table>"]
if hdrs:
parts.append("<thead>")
parts.append("<tr>" + "".join(f"<th>{h}</th>" for h in hdrs) + "</tr>")
parts.append("</thead>")
parts.append("<tbody>")
for row in formatted:
parts.append("<tr>" + "".join(f"<td>{c}</td>" for c in row) + "</tr>")
parts.append("</tbody>")
parts.append("</table>")
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

View File

@@ -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")

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

View File

@@ -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']
# <state>,"LTE",<is_tdd>,<mcc>,<mnc>,<cellid>,<pcid>,<earfcn>,<freq_band_ind>,
# <ul_bandwidth>,<dl_bandwidth>,<tac>,<rsrp>,<rsrq>,<rssi>,<sinr>,<srxlev>
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())

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos

View File

@@ -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=="
}
}

View File

@@ -1 +1 @@
kmjHw5sbr1uhbckWG6FoCjTR0idPL+RGlQ19Wri9phayqsni9P3V2rqLDUPeZ0ds+CRqMb378Jgghj6eJ6gxCQ==
895wuDUENqe6UUjsi9Cjy0ZHiFxVMYtjNXm8ngrGYhTxzA4PUr5j3EM2LLlqq1oqQmGBsLjsilpILKUt3o8aAw==

View File

@@ -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'):

View File

@@ -369,6 +369,11 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> 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"}},

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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",

View File

@@ -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),

View File

@@ -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"<br>{tr('Measured:')} {ui_state.sm['lateralDelay'].lateralDelay:.3f} s"
measured = ui_state.measured_steer_delay()
if measured is None:
delay_desc += f"<br>{tr('Measured:')} {tr('not yet, drive to calibrate')}"
else:
delay_desc += f"<br>{tr('Measured:')} {measured:.3f} s"
elif ui_state.CP:
sw = float(ui_state.params.get("IQSoftwareSteerDelay", "0.2"))
cp = ui_state.CP.steerActuatorDelay

View File

@@ -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))