IQ.Pilot Prebuilt Release @ 27f668a

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-03 18:23:24 -05:00
commit b073c5182b
2554 changed files with 679696 additions and 0 deletions

View File

@@ -0,0 +1,240 @@
{
"python/_iqclosure/_vendored.json": {
"mode": 420,
"sha256": "870f5bc6aa5d8b528dc8e54bbaf03088193a5e764da6d7eb59706e1535dfd766",
"size": 868
},
"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/api/__init__.py": {
"mode": 420,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0
},
"python/_iqclosure/iqpilot/common/api/base.py": {
"mode": 420,
"sha256": "be55c83869bad911ee0204e8779b4673653f7d29ae4f524c978a280bd84c7777",
"size": 3187
},
"python/_iqclosure/iqpilot/common/basedir.py": {
"mode": 420,
"sha256": "58cfa5d6405903ee1acecc4544463bb8f47a66921b120c382143aa80d8539b28",
"size": 106
},
"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/konn3kt/__init__.py": {
"mode": 420,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0
},
"python/_iqclosure/iqpilot/konn3kt/cloud_client.py": {
"mode": 420,
"sha256": "4d58e62f8d628a8570881f2b87c82c801b46928fbb84f8dab8a81a9b55ba4e15",
"size": 493
},
"python/_iqclosure/iqpilot/selfdrive/__init__.py": {
"mode": 420,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0
},
"python/_iqclosure/iqpilot/selfdrive/iqmodeld/__init__.py": {
"mode": 420,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0
},
"python/_iqclosure/iqpilot/selfdrive/iqmodeld/config.py": {
"mode": 420,
"sha256": "056c744bbd9f1fe1053555d9b1fc1257d53093d69ddc94be3f05d840eb70781c",
"size": 3590
},
"python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/__init__.py": {
"mode": 420,
"sha256": "5865243a95ca4557d2a960302124a9898545cc3a8387e88ef569308e3d0f7077",
"size": 119
},
"python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/fetcher.py": {
"mode": 420,
"sha256": "23622cdec9eed5649a05422ba19eae956ba16a63850927af4a222c3d9030a1bb",
"size": 399
},
"python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py": {
"mode": 420,
"sha256": "e4f524c7595754fd9db86724a7bd9b67524c630275ac78599ab6838d1aa7d809",
"size": 11037
},
"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/_iqclosure/iqpilot/system/version.py": {
"mode": 420,
"sha256": "3a6bc7520dae7944a1da6f911cd090704a463e011f39949ea6c9df51b0e8c616",
"size": 6033
},
"python/iqpilot_private/__init__.py": {
"mode": 420,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0
},
"python/iqpilot_private/models/__init__.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "665a6a5163899f37bb7e41fd42fe79ecd77e50496285b60c6555971b8337f463",
"size": 67568
},
"python/iqpilot_private/models/__init__.py": {
"mode": 420,
"sha256": "5128931b54d4f1ff131185e7711d56a788b5e2cb71e3c8154a2f79fef184b452",
"size": 118
},
"python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "8a63da11107132869d7ea4e391197ca8bde2d13d6d92b09a83484503910a75d5",
"size": 133856
},
"python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "7090be17ce2063905fc364ee925384708895150f4d6eef642a94b941d3c6c88d",
"size": 68096
},
"python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "c2691e7a3e77e78d20d21cbf0d0bc4d59fd294935c7ebd0cc7225b0d77619914",
"size": 134032
},
"python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "d085741bb6a7d2a94d616d53e78e0ae76d381ca926017646ec33931375fb5963",
"size": 136200
},
"python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "951ecc3665d6a2232f50ca3002f7f84753789ecd1adfc06cc85884b42c47e05c",
"size": 68032
},
"python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "8670d22574d59c9b2f43c0fa047967a3236fc4eb0ca315a61fe0c9f2636d886a",
"size": 203512
},
"python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "c0fcfd5193cd11e52f22d8c804ace3b436f49aaa6655689c2f1903931544a977",
"size": 339976
},
"runtime": {
"entries": {
"iqpilot_private.models.manager": {
"path": "python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so",
"type": "python-module"
}
}
},
"signatures": {
"python/iqpilot_private/models/__init__.cpython-312-aarch64-linux-gnu.so": "BaGn+mLXPAGLqnVJhDUt8C2ZEm1E9+MUCPoOnJhQGQybFOr36MuoubfpEgPoy1993Tm4IGD3pZASyFefJNXtBQ==",
"python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so": "DfeUTDggp3zRuVHoECcYdCliGhB1A/BdQoicWHddyTS4YptXxaow5Rads9qnmRIiBpSgR8d0vH9fwNxpQl0aBw==",
"python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so": "9sIFDj6iIVEUE+lxa3Ryk/djxgiPMu78MM/WQdjYfpVQNv2OQ7IkBu121iNa5XIv/CTFMl1G6KGTWO5/Qm9iBw==",
"python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so": "CjvP1I4QBJu6zRW75XHEUWX6F6Wks9xtnuu1hdjv/V2Vw11Vf+Wa8zCztpKBgMx3dzbQ/p++xpTf5p7TCoMyAA==",
"python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so": "WNq4sNwhPeu7NuW+hj1UOv1lAiQN2k8lPwImfeS7vFLY0G+pUBpnCVhD49SmjMmPyfCoxJkxD+wdhogPT2u6DQ==",
"python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so": "FJ7yptMwvQxz5kB6XLjNJKBDy3+jrwWsPe/KBa8ki/e6uNavH63Umu18z5e6aY4AYp9D8z0pBOt5lk1X+bFvBg==",
"python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so": "9sX2Peumx+BT3G+LRIbPtcs42mULnRiXJMbQq3esj7yfaGsbf3l5XQ/9BDr8U/A6aFWfatOYSwbFD37ZGdLxCg==",
"python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so": "uyUgRSqgfhMcboJeDLTF1vAuw6thTLubkKp4wsn3i3VFAGkLzE0fEkhzoRo1TSn8hkb787kNQs+Z/woCPxi5AQ=="
}
}

View File

@@ -0,0 +1 @@
aZ2PpZxlTDJ1Nffu+hFtfKK0qyUkkllomgYgTX69J4YU4/aODiwBvJDwHZ6+1nlvv4fDA10s7SPNxwwP9j0OBA==

View File

@@ -0,0 +1,27 @@
[
"iqpilot.common.api.base",
"iqpilot.common.basedir",
"iqpilot.common.gpio",
"iqpilot.common.i2c",
"iqpilot.common.logging_extra",
"iqpilot.common.realtime",
"iqpilot.common.swaglog",
"iqpilot.common.time_helpers",
"iqpilot.common.utils",
"iqpilot.konn3kt.cloud_client",
"iqpilot.selfdrive.iqmodeld.config",
"iqpilot.selfdrive.iqmodeld.models",
"iqpilot.selfdrive.iqmodeld.models.fetcher",
"iqpilot.selfdrive.iqmodeld.models.helpers",
"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",
"iqpilot.system.version"
]

View File

@@ -0,0 +1,84 @@
import jwt
import os
import requests
import unicodedata
from datetime import datetime, timedelta, UTC
from functools import lru_cache
from iqpilot.system.hardware.hw import Paths
from iqpilot.system.version import get_version
# name: jwt signature algorithm
KEYS = {"id_rsa": "RS256",
"id_ecdsa": "ES256"}
@lru_cache(maxsize=4)
def load_signing_key(private_key: str):
# PyJWT re-parses a PEM string on every encode; an RSA parse is ~40ms, so cache the key object
try:
from cryptography.hazmat.primitives.serialization import load_pem_private_key
return load_pem_private_key(private_key.encode(), password=None)
except Exception:
return private_key
class BaseApi:
def __init__(self, dongle_id, api_host, user_agent="openpilot-"):
self.dongle_id = dongle_id
self.api_host = api_host
self.user_agent = user_agent
self.jwt_algorithm, self.private_key, _ = self.get_key_pair()
def get(self, *args, **kwargs):
return self.request('GET', *args, **kwargs)
def post(self, *args, **kwargs):
return self.request('POST', *args, **kwargs)
def request(self, method, endpoint, timeout=None, access_token=None, **params):
return self.api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params)
def _get_token(self, payload_extra=None, expiry_hours=1, **extra_payload):
now = datetime.now(UTC).replace(tzinfo=None)
payload = {
'identity': self.dongle_id,
'nbf': now,
'iat': now,
'exp': now + timedelta(hours=expiry_hours),
**extra_payload
}
if payload_extra is not None:
payload.update(payload_extra)
key = load_signing_key(self.private_key) if self.private_key else self.private_key
token = jwt.encode(payload, key, algorithm=self.jwt_algorithm)
if isinstance(token, bytes):
token = token.decode('utf8')
return token
def get_token(self, payload_extra=None, expiry_hours=1):
return self._get_token(payload_extra, expiry_hours)
def remove_non_ascii_chars(self, text):
normalized_text = unicodedata.normalize('NFD', text)
ascii_encoded_text = normalized_text.encode('ascii', 'ignore')
return ascii_encoded_text.decode()
def api_get(self, endpoint, method='GET', timeout=None, access_token=None, session=None, json=None, **params):
headers = {}
if access_token is not None:
headers['Authorization'] = "JWT " + access_token
version = self.remove_non_ascii_chars(get_version())
headers['User-Agent'] = self.user_agent + version
# TODO: add session to Api
req = requests if session is None else session
return req.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params)
@staticmethod
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
for key in KEYS:
if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'):
with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public:
return KEYS[key], private.read(), public.read()
return None, None, None

View File

@@ -0,0 +1,4 @@
import os
BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../.."))

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,17 @@
"""
Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
import os
from iqpilot.common.api.base import BaseApi
API_HOST = os.getenv('KONN3KT_API_HOST', 'https://api-iqlabs.konn3kt.com')
class Konn3ktApi(BaseApi):
def __init__(self, dongle_id):
super().__init__(dongle_id, API_HOST)
self.user_agent = "konn3kt-device-"
def get_token(self, expiry_hours=1):
return super()._get_token(expiry_hours=expiry_hours)

View File

@@ -0,0 +1,133 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from __future__ import annotations
import numpy as np
def index_function(index: int, max_val: float = 192, max_idx: int = 32) -> float:
return max_val * ((index / max_idx) ** 2)
def _quadratic_series(limit: float, steps: int) -> list[float]:
return [index_function(index, max_val=limit, max_idx=steps - 1) for index in range(steps)]
def _probability_window(*values: float) -> np.ndarray:
return np.asarray(values, dtype=np.float32)
def _field_group(start: int, stop: int, stride: int) -> slice:
return slice(start, stop, stride)
_IDX_COUNT = 33
_T_AXIS = _quadratic_series(10.0, _IDX_COUNT)
_X_AXIS = _quadratic_series(192.0, _IDX_COUNT)
class ModelConstants:
IDX_N = _IDX_COUNT
T_IDXS = _T_AXIS
X_IDXS = _X_AXIS
LEAD_T_IDXS = [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
LEAD_T_OFFSETS = [0.0, 2.0, 4.0]
META_T_IDXS = [2.0, 4.0, 6.0, 8.0, 10.0]
MODEL_FREQ = 20
FEATURE_LEN = 512
FULL_HISTORY_BUFFER_LEN = 99
HISTORY_BUFFER_LEN = FULL_HISTORY_BUFFER_LEN
DESIRE_LEN = 8
TRAFFIC_CONVENTION_LEN = 2
NAV_FEATURE_LEN = 256
NAV_INSTRUCTION_LEN = 150
LAT_PLANNER_STATE_LEN = 4
LATERAL_CONTROL_PARAMS_LEN = 2
PREV_DESIRED_CURV_LEN = 1
FCW_THRESHOLDS_5MS2 = _probability_window(0.05, 0.05, 0.15, 0.15, 0.15)
FCW_THRESHOLDS_3MS2 = _probability_window(0.7, 0.7)
FCW_5MS2_PROBS_WIDTH = 5
FCW_3MS2_PROBS_WIDTH = 2
DISENGAGE_WIDTH = 5
POSE_WIDTH = 6
WIDE_FROM_DEVICE_WIDTH = 3
SIM_POSE_WIDTH = 6
LEAD_WIDTH = 4
LANE_LINES_WIDTH = 2
ROAD_EDGES_WIDTH = 2
PLAN_WIDTH = 15
DESIRE_PRED_WIDTH = 8
LAT_PLANNER_SOLUTION_WIDTH = 4
DESIRED_CURV_WIDTH = 1
NUM_LANE_LINES = 4
NUM_ROAD_EDGES = 2
LEAD_TRAJ_LEN = 6
DESIRE_PRED_LEN = 4
PLAN_MHP_N = 5
LEAD_MHP_N = 2
PLAN_MHP_SELECTION = 1
LEAD_MHP_SELECTION = 3
FCW_THRESHOLD_5MS2_HIGH = 0.15
FCW_THRESHOLD_5MS2_LOW = 0.05
FCW_THRESHOLD_3MS2 = 0.7
CONFIDENCE_BUFFER_LEN = 5
RYG_GREEN = 0.01165
RYG_YELLOW = 0.06157
POLY_PATH_DEGREE = 4
class Plan:
POSITION = slice(0, 3)
VELOCITY = slice(3, 6)
ACCELERATION = slice(6, 9)
T_FROM_CURRENT_EULER = slice(9, 12)
ORIENTATION_RATE = slice(12, 15)
class Meta:
ENGAGED = _field_group(0, 1, 1)
GAS_DISENGAGE = _field_group(1, 31, 6)
BRAKE_DISENGAGE = _field_group(2, 31, 6)
STEER_OVERRIDE = _field_group(3, 31, 6)
HARD_BRAKE_3 = _field_group(4, 31, 6)
HARD_BRAKE_4 = _field_group(5, 31, 6)
HARD_BRAKE_5 = _field_group(6, 31, 6)
GAS_PRESS = _field_group(31, 55, 4)
BRAKE_PRESS = _field_group(32, 55, 4)
LEFT_BLINKER = _field_group(33, 55, 4)
RIGHT_BLINKER = _field_group(34, 55, 4)
class MetaTombRaider:
ENGAGED = _field_group(0, 1, 1)
GAS_DISENGAGE = _field_group(1, 41, 8)
BRAKE_DISENGAGE = _field_group(2, 41, 8)
STEER_OVERRIDE = _field_group(3, 41, 8)
HARD_BRAKE_3 = _field_group(4, 41, 8)
HARD_BRAKE_4 = _field_group(5, 41, 8)
HARD_BRAKE_5 = _field_group(6, 41, 8)
GAS_PRESS = _field_group(7, 41, 8)
BRAKE_PRESS = _field_group(8, 41, 8)
LEFT_BLINKER = _field_group(41, 53, 2)
RIGHT_BLINKER = _field_group(42, 53, 2)
class MetaSimPose:
ENGAGED = _field_group(0, 1, 1)
GAS_DISENGAGE = _field_group(1, 36, 7)
BRAKE_DISENGAGE = _field_group(2, 36, 7)
STEER_OVERRIDE = _field_group(3, 36, 7)
HARD_BRAKE_3 = _field_group(4, 36, 7)
HARD_BRAKE_4 = _field_group(5, 36, 7)
HARD_BRAKE_5 = _field_group(6, 36, 7)
GAS_PRESS = _field_group(7, 36, 7)
LEFT_BLINKER = _field_group(36, 48, 2)
RIGHT_BLINKER = _field_group(37, 48, 2)

View File

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

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
try:
load_private_module(__name__, "iqpilot_private.models.fetcher")
except ProprietaryModuleMissing:
from iqpilot.models_private_src.fetcher import * # noqa: F403

View File

@@ -0,0 +1,313 @@
#!/usr/bin/env python3
"""
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
import json
import os
import shutil
from pathlib import Path
from iqpilot.cereal import custom
from iqpilot.common.params import Params
from iqpilot.common.swaglog import cloudlog
from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
from iqpilot.system.hardware.hw import Paths
try:
load_private_module(__name__, "iqpilot_private.models.helpers")
except ProprietaryModuleMissing:
try:
from iqpilot.models_private_src.helpers import * # noqa: F403
except ImportError:
pass
ModelBundle = custom.IQModelManager.ModelBundle
Runner = custom.IQModelManager.Runner
_MODEL_ROOT = Path(Paths.model_root())
_ACTIVE_BUNDLE_KEY = "ModelManager_ActiveBundle"
_MODELS_CACHE_KEY = "ModelManager_ModelsCache"
_RUNNER_CACHE_KEY = "ModelRunnerTypeCache"
_DOWNLOAD_INDEX_KEY = "ModelManager_DownloadIndex"
_PENDING_INDEX_KEY = "ModelManager_PendingIndex"
_PENDING_MODEL_RESTORE_FILE = "/data/k3_pending_model_restore"
_STOCK_RUNNER = int(Runner.stock)
_TINYGRAD_RUNNER = int(Runner.tinygrad)
_SNPE_RUNNER = int(Runner.snpe)
_DEFAULT_MODEL_DIR = Path(__file__).resolve().parents[1] / "default_model"
_DEFAULT_BUNDLE_JSON = _DEFAULT_MODEL_DIR / "bundle.json"
_DEFAULT_BUNDLE_REF = "default"
def get_default_model_bundle(_bundles):
return None
def _coerce_runner_value(value) -> int | None:
raw = getattr(value, "raw", value)
try:
return int(raw)
except (TypeError, ValueError):
return None
def _bundle_models(bundle) -> list:
models = getattr(bundle, "models", None)
return list(models) if models is not None else []
def _bundle_needs_runtime_upgrade(bundle) -> bool:
if bundle is None:
return False
if _coerce_runner_value(getattr(bundle, "runner", None)) == _SNPE_RUNNER:
return True
for model in _bundle_models(bundle):
file_name = getattr(getattr(model, "artifact", None), "fileName", "") or ""
if file_name.endswith(".thneed"):
return True
return False
def _load_cached_manifest_bundles(params: Params):
cached = params.get(_MODELS_CACHE_KEY) or {}
bundles = []
for raw_bundle in cached.get("bundles", []):
try:
min_selector_version = int(raw_bundle.get("minimumSelectorVersion", raw_bundle.get("minimum_selector_version", 0)))
compatibility_view = dict(raw_bundle)
compatibility_view["minimumSelectorVersion"] = min_selector_version
is_compatible = globals().get("is_bundle_version_compatible")
if is_compatible is not None and not is_compatible(compatibility_view):
continue
if "short_name" in raw_bundle:
from iqpilot.selfdrive.iqmodeld.models.fetcher import ManifestDecoder
bundles.append(ManifestDecoder._decode_bundle(raw_bundle))
continue
if "internalName" in raw_bundle:
bundles.append(ModelBundle(**raw_bundle))
continue
bundle = ModelBundle()
bundle.index = int(raw_bundle["index"])
bundle.internalName = raw_bundle.get("short_name")
bundle.displayName = raw_bundle.get("display_name")
bundle.status = 0
bundle.generation = int(raw_bundle["generation"])
bundle.environment = raw_bundle["environment"]
bundle.runner = raw_bundle.get("runner", Runner.tinygrad)
bundle.is20hz = raw_bundle.get("is_20hz", False)
bundle.minimumSelectorVersion = int(min_selector_version)
bundle.ref = raw_bundle.get("ref")
bundle.overrides = []
for key, value in raw_bundle.get("overrides", {}).items():
override = custom.IQModelManager.Override()
override.key = key
override.value = value
bundle.overrides.append(override)
bundle.models = []
for raw_model in raw_bundle.get("models", []):
model = custom.IQModelManager.Model()
model.type = raw_model.get("type")
for attr_name in ("artifact", "metadata"):
raw_artifact = raw_model.get(attr_name)
if not raw_artifact:
continue
artifact = custom.IQModelManager.Artifact()
artifact.fileName = raw_artifact.get("file_name")
download_uri = custom.IQModelManager.DownloadUri()
download_uri.uri = raw_artifact.get("download_uri", {}).get("url")
download_uri.sha256 = raw_artifact.get("download_uri", {}).get("sha256")
artifact.downloadUri = download_uri
setattr(model, attr_name, artifact)
bundle.models.append(model)
bundles.append(bundle)
except Exception:
continue
return bundles
def _bundle_match_key(bundle) -> tuple[str | None, str | None, str | None]:
return (
getattr(bundle, "ref", None),
getattr(bundle, "internalName", None),
getattr(bundle, "displayName", None),
)
def _find_runtime_upgrade(bundle, params: Params, available_bundles=None):
if not _bundle_needs_runtime_upgrade(bundle):
return bundle
candidate_bundles = available_bundles if available_bundles is not None else _load_cached_manifest_bundles(params)
ref, internal_name, display_name = _bundle_match_key(bundle)
for candidate in candidate_bundles:
if getattr(candidate, "ref", None) and getattr(candidate, "ref", None) == ref:
return candidate
for candidate in candidate_bundles:
if getattr(candidate, "internalName", None) == internal_name:
return candidate
for candidate in candidate_bundles:
if getattr(candidate, "displayName", None) == display_name:
return candidate
return None
def bundle_files_ready(bundle) -> bool:
if bundle is None:
return False
for model in _bundle_models(bundle):
artifact = getattr(model, "artifact", None)
metadata = getattr(model, "metadata", None)
for file_name in (getattr(metadata, "fileName", None), getattr(artifact, "fileName", None)):
if file_name and not (_MODEL_ROOT / file_name).is_file():
return False
return True
def persist_active_bundle(params: Params, bundle) -> None:
params.put(_ACTIVE_BUNDLE_KEY, bundle.to_dict())
params.remove(_RUNNER_CACHE_KEY)
def _load_default_bundle_dict() -> dict:
return json.loads(_DEFAULT_BUNDLE_JSON.read_text())
def _default_bundle_filenames(bundle_dict: dict) -> list[str]:
names = []
for model in bundle_dict.get("models", []):
for artifact in (model.get("metadata"), model.get("artifact")):
file_name = artifact.get("fileName", "") if isinstance(artifact, dict) else ""
if file_name:
names.append(file_name)
return names
def is_default_bundle(bundle) -> bool:
return bool(bundle is not None and getattr(bundle, "ref", None) == _DEFAULT_BUNDLE_REF)
def ensure_default_model_files(bundle_dict: dict = None) -> None:
bundle_dict = bundle_dict if bundle_dict is not None else _load_default_bundle_dict()
try:
_MODEL_ROOT.mkdir(parents=True, exist_ok=True)
except OSError as e:
cloudlog.exception(f"default_model: cannot create model root: {e}")
return
for file_name in _default_bundle_filenames(bundle_dict):
src = _DEFAULT_MODEL_DIR / file_name
dst = _MODEL_ROOT / file_name
if not src.is_file():
cloudlog.error(f"default_model: shipped asset missing {src}")
continue
if dst.is_file() and dst.stat().st_size == src.stat().st_size:
continue
try:
shutil.copy2(src, dst)
cloudlog.warning(f"default_model: staged {file_name} into model root")
except OSError as e:
cloudlog.exception(f"default_model: failed staging {file_name}: {e}")
def select_default_model(params: Params = None) -> None:
params = Params() if params is None else params
bundle_dict = _load_default_bundle_dict()
ensure_default_model_files(bundle_dict)
params.remove(_DOWNLOAD_INDEX_KEY)
params.remove(_PENDING_INDEX_KEY)
params.put(_ACTIVE_BUNDLE_KEY, bundle_dict)
params.remove(_RUNNER_CACHE_KEY)
params.put(_RUNNER_CACHE_KEY, _TINYGRAD_RUNNER)
try:
if os.path.isfile(_PENDING_MODEL_RESTORE_FILE):
os.remove(_PENDING_MODEL_RESTORE_FILE)
except OSError:
pass
def seed_default_bundle_if_unset(params: Params = None) -> None:
params = Params() if params is None else params
if params.get(_ACTIVE_BUNDLE_KEY):
return
queued_download = params.get(_DOWNLOAD_INDEX_KEY)
try:
select_default_model(params)
if queued_download is not None:
params.put(_DOWNLOAD_INDEX_KEY, queued_download)
cloudlog.warning("default_model: seeded Default (CD210) as active bundle")
except Exception as e:
cloudlog.exception(f"default_model: failed to seed default bundle: {e}")
def get_runtime_bundle_upgrade(bundle, params: Params = None, available_bundles=None):
params = Params() if params is None else params
return _find_runtime_upgrade(bundle, params, available_bundles)
def get_active_bundle(params: Params = None):
params = Params() if params is None else params
try:
active_bundle = params.get(_ACTIVE_BUNDLE_KEY) or {}
if not active_bundle:
return None
is_compatible = globals().get("is_bundle_version_compatible")
if is_compatible is not None and not is_compatible(active_bundle):
return None
bundle = ModelBundle(**active_bundle)
except Exception:
return None
replacement = _find_runtime_upgrade(bundle, params)
if replacement is not None and replacement is not bundle and bundle_files_ready(replacement):
persist_active_bundle(params, replacement)
return replacement
return bundle
def get_active_model_runner(params: Params = None, force_check=False):
params = Params() if params is None else params
active_bundle = get_active_bundle(params)
if not active_bundle:
seed_default_bundle_if_unset(params)
active_bundle = get_active_bundle(params)
if not active_bundle:
if params.get(_RUNNER_CACHE_KEY) != str(_TINYGRAD_RUNNER):
params.put(_RUNNER_CACHE_KEY, _TINYGRAD_RUNNER)
return _TINYGRAD_RUNNER
cached_runner_type = params.get(_RUNNER_CACHE_KEY)
if cached_runner_type and not force_check and isinstance(cached_runner_type, str) and cached_runner_type.isdigit():
return int(cached_runner_type)
runner_type = _coerce_runner_value(active_bundle.runner)
if runner_type == _SNPE_RUNNER:
replacement = _find_runtime_upgrade(active_bundle, params)
if replacement is not None and replacement is not active_bundle and bundle_files_ready(replacement):
persist_active_bundle(params, replacement)
runner_type = _coerce_runner_value(replacement.runner)
else:
if replacement is not None and getattr(replacement, "index", None) is not None and params.get(_DOWNLOAD_INDEX_KEY) is None:
params.put(_DOWNLOAD_INDEX_KEY, int(replacement.index))
cloudlog.warning(f"Queued tinygrad migration for retired bundle {getattr(active_bundle, 'internalName', '<unknown>')}")
runner_type = _TINYGRAD_RUNNER
if cached_runner_type != runner_type:
params.put(_RUNNER_CACHE_KEY, int(runner_type))
return runner_type

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,200 @@
#!/usr/bin/env python3
from dataclasses import dataclass
from functools import cache
import json
import os
import pathlib
import subprocess
from iqpilot.common.basedir import BASEDIR
from iqpilot.common.swaglog import cloudlog
from iqpilot.common.git import get_commit, get_origin, get_branch, get_short_branch, get_commit_date
RELEASE_IQ_BRANCHES = ['release', 'release-tici', 'release-new']
TESTED_BRANCHES = RELEASE_IQ_BRANCHES
IQ_BRANCH_MIGRATIONS: dict[tuple[str, str], str] = {}
BUILD_METADATA_FILENAME = "build.json"
training_version: str = "0.2.0"
terms_version: str = "2"
UNKNOWN_VERSION = "0.0.0"
@cache
def get_version(path: str = BASEDIR) -> str:
try:
with open(os.path.join(path, "iqpilot", "common", "version.h")) as _versionf:
return _versionf.read().split('"')[1]
except (OSError, IndexError):
return UNKNOWN_VERSION
def get_release_notes(path: str = BASEDIR) -> str:
for rel in (("iqpilot", "docs", "CHANGELOG.md"), ("docs", "CHANGELOG.md")):
try:
with open(os.path.join(path, *rel)) as f:
return f.read().split('\n\n', 1)[0]
except OSError:
continue
return ""
@cache
def is_prebuilt(path: str = BASEDIR) -> bool:
return os.path.exists(os.path.join(path, 'prebuilt'))
@cache
def is_dirty(cwd: str = BASEDIR) -> bool:
if not get_origin() or not get_short_branch():
return True
dirty = False
try:
# Actually check dirty files
if not is_prebuilt(cwd):
# This is needed otherwise touched files might show up as modified
try:
subprocess.check_call(["git", "update-index", "--refresh"], cwd=cwd)
except subprocess.CalledProcessError:
pass
branch = get_branch()
if not branch:
return True
dirty = (subprocess.call(["git", "diff-index", "--quiet", branch, "--"], cwd=cwd)) != 0
except subprocess.CalledProcessError:
cloudlog.exception("git subprocess failed while checking dirty")
dirty = True
return dirty
@dataclass
class OpenpilotMetadata:
version: str
release_notes: str
git_commit: str
git_origin: str
git_commit_date: str
build_style: str
is_dirty: bool # whether there are local changes
@property
def short_version(self) -> str:
return self.version.split('-')[0]
@property
def comma_remote(self) -> bool:
# note to fork maintainers, this is used for release metrics. please do not
# touch this to get rid of the orange startup alert. there's better ways to do that
return self.git_normalized_origin == "github.com/commaai/openpilot"
@property
def iqpilot_remote(self) -> bool:
return self.git_normalized_origin in ("github.com/iqpilot/iqpilot",
"github.com/iqpilot/openpilot")
@property
def git_normalized_origin(self) -> str:
return self.git_origin \
.replace("git@", "", 1) \
.replace(".git", "", 1) \
.replace("https://", "", 1) \
.replace(":", "/", 1)
@dataclass
class BuildMetadata:
channel: str
openpilot: OpenpilotMetadata
@property
def tested_channel(self) -> bool:
return self.channel in TESTED_BRANCHES
@property
def release_channel(self) -> bool:
return self.channel in RELEASE_IQ_BRANCHES
@property
def canonical(self) -> str:
return f"{self.openpilot.version}-{self.openpilot.git_commit}-{self.openpilot.build_style}"
@property
def ui_description(self) -> str:
return f"{self.openpilot.version} / {self.openpilot.git_commit[:6]} / {self.channel}"
@property
def master_channel(self) -> bool:
return self.channel in RELEASE_IQ_BRANCHES
@property
def development_channel(self) -> bool:
return self.channel == "dev" or self.channel.startswith("dev-") or self.channel.endswith("-prebuilt")
@property
def channel_type(self) -> str:
if "-tici" in self.channel or self.channel in ("release-new" or "master-mici"):
return "tici"
elif self.development_channel:
return "development"
elif self.tested_channel:
return "staging"
elif self.master_channel:
return "master"
elif self.release_channel:
return "release"
else:
return "feature"
def build_metadata_from_dict(build_metadata: dict) -> BuildMetadata:
channel = build_metadata.get("channel", "unknown")
openpilot_metadata = build_metadata.get("openpilot", {})
version = openpilot_metadata.get("version", "unknown")
release_notes = openpilot_metadata.get("release_notes", "unknown")
git_commit = openpilot_metadata.get("git_commit", "unknown")
git_origin = openpilot_metadata.get("git_origin", "unknown")
git_commit_date = openpilot_metadata.get("git_commit_date", "unknown")
build_style = openpilot_metadata.get("build_style", "unknown")
return BuildMetadata(channel,
OpenpilotMetadata(
version=version,
release_notes=release_notes,
git_commit=git_commit,
git_origin=git_origin,
git_commit_date=git_commit_date,
build_style=build_style,
is_dirty=False))
def get_build_metadata(path: str = BASEDIR) -> BuildMetadata:
build_metadata_path = pathlib.Path(path) / BUILD_METADATA_FILENAME
if build_metadata_path.exists():
build_metadata = json.loads(build_metadata_path.read_text())
return build_metadata_from_dict(build_metadata)
git_folder = pathlib.Path(path) / ".git"
if git_folder.exists():
return BuildMetadata(get_short_branch(path),
OpenpilotMetadata(
version=get_version(path),
release_notes=get_release_notes(path),
git_commit=get_commit(path),
git_origin=get_origin(path),
git_commit_date=get_commit_date(path),
build_style="unknown",
is_dirty=is_dirty(path)))
cloudlog.exception("unable to get build metadata")
raise Exception("invalid build metadata")
if __name__ == "__main__":
print(get_build_metadata())

View File

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