IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
0
iqpilot/system/hardware/tici/__init__.py
Normal file
0
iqpilot/system/hardware/tici/__init__.py
Normal file
91
iqpilot/system/hardware/tici/agnos.json
Normal file
91
iqpilot/system/hardware/tici/agnos.json
Normal file
@@ -0,0 +1,91 @@
|
||||
[
|
||||
{
|
||||
"name": "xbl",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz",
|
||||
"hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
|
||||
"hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
|
||||
"size": 3282256,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "d47a08914d2376557b03f1231b7233508222c04b57d781f9daf77c63eab92c2e"
|
||||
},
|
||||
{
|
||||
"name": "xbl_config",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz",
|
||||
"hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
|
||||
"hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
|
||||
"size": 98124,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "e7d04d9f040c9c040cdf013335d0b6d6e9346311458baeb2461b193e954f5f1c"
|
||||
},
|
||||
{
|
||||
"name": "abl",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz",
|
||||
"hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
|
||||
"hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee"
|
||||
},
|
||||
{
|
||||
"name": "aop",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz",
|
||||
"hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
|
||||
"hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
|
||||
"size": 184364,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "3aa0a79149ec57f4bc8c38f7bbdf4f6630dd659e49a111ce6258d2d06a07c8e5"
|
||||
},
|
||||
{
|
||||
"name": "devcfg",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz",
|
||||
"hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
|
||||
"hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
|
||||
"size": 40336,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "3d7bb33588491a2a40091a7e1cf6cb65e6dd503f69b640aba484d723f1ad47e8"
|
||||
},
|
||||
{
|
||||
"name": "splash",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/splash-993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2.img.xz",
|
||||
"hash": "993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2",
|
||||
"hash_raw": "993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2",
|
||||
"size": 34226176,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2"
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/boot-595f6696c47c17d70a654d8ca04abca946a16dbb1da6250e464a23dff82258a4.img.xz",
|
||||
"hash": "595f6696c47c17d70a654d8ca04abca946a16dbb1da6250e464a23dff82258a4",
|
||||
"hash_raw": "595f6696c47c17d70a654d8ca04abca946a16dbb1da6250e464a23dff82258a4",
|
||||
"size": 18216960,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "b0ee082b2e63a49fcfd1ca2adfb49275e1bb567889cbb9985827fb1de50f943b"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa.img.xz",
|
||||
"hash": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa",
|
||||
"hash_raw": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa",
|
||||
"size": 6291456000,
|
||||
"sparse": false,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa",
|
||||
"url_parts": 10
|
||||
}
|
||||
]
|
||||
1
iqpilot/system/hardware/tici/agnos.json.sig
Normal file
1
iqpilot/system/hardware/tici/agnos.json.sig
Normal file
@@ -0,0 +1 @@
|
||||
nqwhbcjeRqyPEM2UWshbP4eCC8EZzDAptGOG0refjvhHlEh32UCAp2Vi/GEKCGOLC3peRW8dRUgwCOXwtEm8Cg==
|
||||
433
iqpilot/system/hardware/tici/agnos.py
Executable file
433
iqpilot/system/hardware/tici/agnos.py
Executable file
@@ -0,0 +1,433 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import lzma
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
|
||||
import sys
|
||||
_VENV_PY = "/usr/local/venv/bin/python3"
|
||||
if sys.executable != _VENV_PY and os.path.exists(_VENV_PY):
|
||||
try:
|
||||
import Crypto # noqa: F401
|
||||
except ImportError:
|
||||
os.execv(_VENV_PY, [_VENV_PY, os.path.abspath(__file__), *sys.argv[1:]])
|
||||
|
||||
import requests
|
||||
|
||||
import iqpilot.system.updated.casync.casync as casync
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
except Exception as exc:
|
||||
Ed25519PublicKey = None
|
||||
_CRYPTO_IMPORT_ERROR = exc
|
||||
else:
|
||||
_CRYPTO_IMPORT_ERROR = None
|
||||
|
||||
SPARSE_CHUNK_FMT = struct.Struct('H2xI4x')
|
||||
CAIBX_URL = "https://commadist.azureedge.net/agnosupdate/"
|
||||
IQPILOT_MANIFEST_PUBLIC_KEY = bytes.fromhex("40ae3f81b77506ecc4982a1ca37ba1d6f8765d2ae510eae9039577206c3e5732")
|
||||
|
||||
AGNOS_MANIFEST_FILE = "system/hardware/tici/agnos.json"
|
||||
|
||||
LFS_POINTER_MAGIC = b"version https://git-lfs"
|
||||
|
||||
def _image_auth_module():
|
||||
try:
|
||||
from iqpilot.system.proprietary_runtime._verified_import import import_verified_module
|
||||
return import_verified_module("iqpilot_updater_private", "iqpilot_private.updater.git_remote")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
|
||||
bundle_python = os.path.join(root, "artifacts", "iqpilot_updater_private", "python")
|
||||
if os.path.isdir(bundle_python):
|
||||
if bundle_python not in sys.path:
|
||||
sys.path.insert(0, bundle_python)
|
||||
import importlib
|
||||
return importlib.import_module("iqpilot_private.updater.git_remote")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _download_headers(url: str) -> dict:
|
||||
mod = _image_auth_module()
|
||||
if mod is not None:
|
||||
try:
|
||||
headers = mod.os_image_headers(url)
|
||||
if headers:
|
||||
return headers
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from iqpilot.common.git_creds import get_credentials
|
||||
creds = get_credentials()
|
||||
if creds and all(creds) and "/iq.lvbs/iqos" in url.lower():
|
||||
return {"Authorization": "Basic " + base64.b64encode(f"{creds[0]}:{creds[1]}".encode()).decode()}
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
def _open_image_response(url: str) -> requests.Response:
|
||||
auth = _download_headers(url)
|
||||
req = requests.get(url, stream=True, headers={'Accept-Encoding': None, **auth}, timeout=60)
|
||||
req.raise_for_status()
|
||||
if int(req.headers.get('content-length') or 0) >= 1024:
|
||||
return req
|
||||
|
||||
body = req.content
|
||||
if not body.startswith(LFS_POINTER_MAGIC):
|
||||
raise requests.exceptions.InvalidURL(f"unexpected tiny response ({len(body)} bytes) for {url}")
|
||||
meta = dict(line.split(" ", 1) for line in body.decode().strip().splitlines() if " " in line)
|
||||
oid = meta["oid"].split(":", 1)[1]
|
||||
size = int(meta["size"])
|
||||
lfs_base = url.split("/raw/", 1)[0] + ".git/info/lfs"
|
||||
|
||||
req = requests.get(f"{lfs_base}/objects/{oid}", stream=True,
|
||||
headers={'Accept-Encoding': None, 'Accept': 'application/vnd.git-lfs', **auth}, timeout=60)
|
||||
if req.status_code == 200:
|
||||
return req
|
||||
|
||||
batch = requests.post(f"{lfs_base}/objects/batch",
|
||||
data=json.dumps({"operation": "download", "transfers": ["basic"],
|
||||
"objects": [{"oid": oid, "size": size}]}),
|
||||
headers={"Content-Type": "application/vnd.git-lfs+json",
|
||||
"Accept": "application/vnd.git-lfs+json", **auth},
|
||||
timeout=60)
|
||||
batch.raise_for_status()
|
||||
action = batch.json()["objects"][0]["actions"]["download"]
|
||||
req = requests.get(action["href"], stream=True,
|
||||
headers={'Accept-Encoding': None, **action.get("header", {})}, timeout=60)
|
||||
req.raise_for_status()
|
||||
return req
|
||||
|
||||
|
||||
def verify_manifest_signature(manifest_path: str) -> None:
|
||||
sig_path = f"{manifest_path}.sig"
|
||||
if not os.path.exists(sig_path):
|
||||
raise RuntimeError(f"missing AGNOS manifest signature: {sig_path}")
|
||||
if Ed25519PublicKey is None:
|
||||
raise RuntimeError(f"cryptography import failed: {_CRYPTO_IMPORT_ERROR}")
|
||||
|
||||
manifest_bytes = open(manifest_path, "rb").read()
|
||||
signature = base64.b64decode(open(sig_path, "rb").read().strip())
|
||||
digest = hashlib.sha256(manifest_bytes).digest()
|
||||
public_key = Ed25519PublicKey.from_public_bytes(IQPILOT_MANIFEST_PUBLIC_KEY)
|
||||
public_key.verify(signature, digest)
|
||||
|
||||
class _ChainedParts:
|
||||
def __init__(self, urls: list[str]) -> None:
|
||||
self.urls = urls
|
||||
self.req: requests.Response | None = None
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
if self.req is not None:
|
||||
self.req.raise_for_status()
|
||||
|
||||
def iter_content(self, chunk_size: int) -> Generator[bytes, None, None]:
|
||||
for u in self.urls:
|
||||
self.req = _open_image_response(u)
|
||||
yield from self.req.iter_content(chunk_size=chunk_size)
|
||||
|
||||
class StreamingDecompressor:
|
||||
def __init__(self, url: str, parts: int = 0) -> None:
|
||||
self.buf = b""
|
||||
|
||||
if parts > 1:
|
||||
self.req = _ChainedParts([f"{url}.p{i:02d}" for i in range(parts)])
|
||||
else:
|
||||
self.req = _open_image_response(url)
|
||||
self.it = self.req.iter_content(chunk_size=1024 * 1024)
|
||||
self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO)
|
||||
self.eof = False
|
||||
self.sha256 = hashlib.sha256()
|
||||
|
||||
def read(self, length: int) -> bytes:
|
||||
while len(self.buf) < length and not self.eof:
|
||||
if self.decompressor.needs_input:
|
||||
self.req.raise_for_status()
|
||||
|
||||
try:
|
||||
compressed = next(self.it)
|
||||
except StopIteration:
|
||||
self.eof = True
|
||||
break
|
||||
else:
|
||||
compressed = b''
|
||||
|
||||
self.buf += self.decompressor.decompress(compressed, max_length=length)
|
||||
|
||||
if self.decompressor.eof:
|
||||
self.eof = True
|
||||
break
|
||||
|
||||
result = self.buf[:length]
|
||||
self.buf = self.buf[length:]
|
||||
|
||||
self.sha256.update(result)
|
||||
return result
|
||||
|
||||
def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]:
|
||||
magic = struct.unpack("I", f.read(4))[0]
|
||||
assert(magic == 0xed26ff3a)
|
||||
|
||||
major = struct.unpack("H", f.read(2))[0]
|
||||
minor = struct.unpack("H", f.read(2))[0]
|
||||
assert(major == 1 and minor == 0)
|
||||
|
||||
f.read(2)
|
||||
f.read(2)
|
||||
|
||||
block_sz = struct.unpack("I", f.read(4))[0]
|
||||
f.read(4)
|
||||
num_chunks = struct.unpack("I", f.read(4))[0]
|
||||
f.read(4)
|
||||
|
||||
for _ in range(num_chunks):
|
||||
chunk_type, out_blocks = SPARSE_CHUNK_FMT.unpack(f.read(12))
|
||||
|
||||
if chunk_type == 0xcac1:
|
||||
yield f.read(out_blocks * block_sz)
|
||||
elif chunk_type == 0xcac2:
|
||||
filler = f.read(4) * (block_sz // 4)
|
||||
for _ in range(out_blocks):
|
||||
yield filler
|
||||
elif chunk_type == 0xcac3:
|
||||
yield b""
|
||||
else:
|
||||
raise Exception("Unhandled sparse chunk type")
|
||||
|
||||
def noop(f: StreamingDecompressor) -> Generator[bytes, None, None]:
|
||||
while len(chunk := f.read(1024 * 1024)) > 0:
|
||||
yield chunk
|
||||
|
||||
def get_target_slot_number() -> int:
|
||||
current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip()
|
||||
return 1 if current_slot == "_a" else 0
|
||||
|
||||
def slot_number_to_suffix(slot_number: int) -> str:
|
||||
assert slot_number in (0, 1)
|
||||
return '_a' if slot_number == 0 else '_b'
|
||||
|
||||
def get_partition_path(target_slot_number: int, partition: dict) -> str:
|
||||
path = f"/dev/disk/by-partlabel/{partition['name']}"
|
||||
|
||||
if partition.get('has_ab', True):
|
||||
path += slot_number_to_suffix(target_slot_number)
|
||||
|
||||
return path
|
||||
|
||||
def get_raw_hash(path: str, partition_size: int) -> str:
|
||||
raw_hash = hashlib.sha256()
|
||||
pos, chunk_size = 0, 1024 * 1024
|
||||
|
||||
with open(path, 'rb+') as out:
|
||||
while pos < partition_size:
|
||||
n = min(chunk_size, partition_size - pos)
|
||||
raw_hash.update(out.read(n))
|
||||
pos += n
|
||||
|
||||
return raw_hash.hexdigest().lower()
|
||||
|
||||
def verify_partition(target_slot_number: int, partition: dict[str, str | int], force_full_check: bool = False) -> bool:
|
||||
full_check = partition['full_check'] or force_full_check
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
|
||||
if not isinstance(partition['size'], int):
|
||||
return False
|
||||
|
||||
partition_size: int = partition['size']
|
||||
|
||||
if not isinstance(partition['hash_raw'], str):
|
||||
return False
|
||||
|
||||
partition_hash: str = partition['hash_raw']
|
||||
|
||||
if full_check:
|
||||
return get_raw_hash(path, partition_size) == partition_hash.lower()
|
||||
else:
|
||||
with open(path, 'rb+') as out:
|
||||
out.seek(partition_size)
|
||||
return out.read(64) == partition_hash.lower().encode()
|
||||
|
||||
def clear_partition_hash(target_slot_number: int, partition: dict) -> None:
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
with open(path, 'wb+') as out:
|
||||
partition_size = partition['size']
|
||||
|
||||
out.seek(partition_size)
|
||||
out.write(b"\x00" * 64)
|
||||
os.sync()
|
||||
|
||||
def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog):
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
downloader = StreamingDecompressor(partition['url'], parts=int(partition.get('url_parts', 0)))
|
||||
|
||||
with open(path, 'wb+') as out:
|
||||
last_p = 0
|
||||
raw_hash = hashlib.sha256()
|
||||
f = unsparsify if partition['sparse'] else noop
|
||||
for chunk in f(downloader):
|
||||
raw_hash.update(chunk)
|
||||
out.write(chunk)
|
||||
p = int(out.tell() / partition['size'] * 100)
|
||||
if p != last_p:
|
||||
last_p = p
|
||||
print(f"Installing {partition['name']}: {p}", flush=True)
|
||||
|
||||
if raw_hash.hexdigest().lower() != partition['hash_raw'].lower():
|
||||
raise Exception(f"Raw hash mismatch '{raw_hash.hexdigest().lower()}'")
|
||||
|
||||
if downloader.sha256.hexdigest().lower() != partition['hash'].lower():
|
||||
raise Exception("Uncompressed hash mismatch")
|
||||
|
||||
if out.tell() != partition['size']:
|
||||
raise Exception("Uncompressed size mismatch")
|
||||
|
||||
os.sync()
|
||||
|
||||
def extract_casync_image(target_slot_number: int, partition: dict, cloudlog):
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
seed_path = path[:-1] + ('b' if path[-1] == 'a' else 'a')
|
||||
|
||||
target = casync.parse_caibx(partition['casync_caibx'])
|
||||
|
||||
sources: list[tuple[str, casync.ChunkReader, casync.ChunkDict]] = []
|
||||
|
||||
try:
|
||||
raw_hash = get_raw_hash(seed_path, partition['size'])
|
||||
caibx_url = f"{CAIBX_URL}{partition['name']}-{raw_hash}.caibx"
|
||||
|
||||
try:
|
||||
cloudlog.info(f"casync fetching {caibx_url}")
|
||||
sources += [('seed', casync.FileChunkReader(seed_path), casync.build_chunk_dict(casync.parse_caibx(caibx_url)))]
|
||||
except requests.RequestException:
|
||||
cloudlog.error(f"casync failed to load {caibx_url}")
|
||||
except Exception:
|
||||
cloudlog.exception("casync failed to hash seed partition")
|
||||
|
||||
sources += [('target', casync.FileChunkReader(path), casync.build_chunk_dict(target))]
|
||||
|
||||
sources += [('remote', casync.RemoteChunkReader(partition['casync_store']), casync.build_chunk_dict(target))]
|
||||
|
||||
last_p = 0
|
||||
|
||||
def progress(cur):
|
||||
nonlocal last_p
|
||||
p = int(cur / partition['size'] * 100)
|
||||
if p != last_p:
|
||||
last_p = p
|
||||
print(f"Installing {partition['name']}: {p}", flush=True)
|
||||
|
||||
stats = casync.extract(target, sources, path, progress)
|
||||
cloudlog.error(f'casync done {json.dumps(stats)}')
|
||||
|
||||
os.sync()
|
||||
if not verify_partition(target_slot_number, partition, force_full_check=True):
|
||||
raise Exception(f"Raw hash mismatch '{partition['hash_raw'].lower()}'")
|
||||
|
||||
def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalone=False):
|
||||
cloudlog.info(f"Downloading and writing {partition['name']}")
|
||||
|
||||
if verify_partition(target_slot_number, partition):
|
||||
cloudlog.info(f"Already flashed {partition['name']}")
|
||||
return
|
||||
|
||||
full_check = partition['full_check']
|
||||
if not full_check:
|
||||
clear_partition_hash(target_slot_number, partition)
|
||||
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
|
||||
if ('casync_caibx' in partition) and not standalone:
|
||||
extract_casync_image(target_slot_number, partition, cloudlog)
|
||||
else:
|
||||
extract_compressed_image(target_slot_number, partition, cloudlog)
|
||||
|
||||
if not full_check:
|
||||
with open(path, 'wb+') as out:
|
||||
out.seek(partition['size'])
|
||||
out.write(partition['hash_raw'].lower().encode())
|
||||
|
||||
def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None:
|
||||
verify_manifest_signature(manifest_path)
|
||||
update = json.load(open(manifest_path))
|
||||
for partition in update:
|
||||
if not partition.get('full_check', False):
|
||||
clear_partition_hash(target_slot_number, partition)
|
||||
|
||||
while True:
|
||||
out = subprocess.check_output(f"abctl --set_active {target_slot_number}", shell=True, stderr=subprocess.STDOUT, encoding='utf8')
|
||||
if ("No such file or directory" not in out) and ("lun as boot lun" in out):
|
||||
cloudlog.info(f"Swap successful {out}")
|
||||
break
|
||||
else:
|
||||
cloudlog.error(f"Swap failed {out}")
|
||||
|
||||
def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, standalone=False) -> None:
|
||||
verify_manifest_signature(manifest_path)
|
||||
update = json.load(open(manifest_path))
|
||||
|
||||
cloudlog.info(f"Target slot {target_slot_number}")
|
||||
|
||||
os.system(f"abctl --set_unbootable {target_slot_number}")
|
||||
|
||||
for partition in update:
|
||||
success = False
|
||||
|
||||
for retries in range(10):
|
||||
try:
|
||||
flash_partition(target_slot_number, partition, cloudlog, standalone)
|
||||
success = True
|
||||
break
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
cloudlog.exception("Failed")
|
||||
cloudlog.info(f"Failed to download {partition['name']}, retrying ({retries})")
|
||||
time.sleep(10)
|
||||
|
||||
if not success:
|
||||
cloudlog.info(f"Failed to flash {partition['name']}, aborting")
|
||||
raise Exception("Maximum retries exceeded")
|
||||
|
||||
cloudlog.info(f"AGNOS ready on slot {target_slot_number}")
|
||||
|
||||
def verify_agnos_update(manifest_path: str, target_slot_number: int) -> bool:
|
||||
verify_manifest_signature(manifest_path)
|
||||
update = json.load(open(manifest_path))
|
||||
return all(verify_partition(target_slot_number, partition) for partition in update)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
parser = argparse.ArgumentParser(description="Flash and verify AGNOS update",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
|
||||
parser.add_argument("--verify", action="store_true", help="Verify and perform swap if update ready")
|
||||
parser.add_argument("--swap", action="store_true", help="Verify and perform swap, downloads if necessary")
|
||||
parser.add_argument("manifest", help="Manifest json")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
target_slot_number = get_target_slot_number()
|
||||
if args.verify:
|
||||
if verify_agnos_update(args.manifest, target_slot_number):
|
||||
swap(args.manifest, target_slot_number, logging)
|
||||
exit(0)
|
||||
exit(1)
|
||||
elif args.swap:
|
||||
while not verify_agnos_update(args.manifest, target_slot_number):
|
||||
logging.error("Verification failed. Flashing AGNOS")
|
||||
flash_agnos_update(args.manifest, target_slot_number, logging, standalone=True)
|
||||
|
||||
logging.warning(f"Verification succeeded. Swapping to slot {target_slot_number}")
|
||||
swap(args.manifest, target_slot_number, logging)
|
||||
else:
|
||||
flash_agnos_update(args.manifest, target_slot_number, logging, standalone=True)
|
||||
80
iqpilot/system/hardware/tici/agnos_tici_15_1.json
Normal file
80
iqpilot/system/hardware/tici/agnos_tici_15_1.json
Normal file
@@ -0,0 +1,80 @@
|
||||
[
|
||||
{
|
||||
"name": "xbl",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz",
|
||||
"hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
|
||||
"hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
|
||||
"size": 3282256,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "d47a08914d2376557b03f1231b7233508222c04b57d781f9daf77c63eab92c2e"
|
||||
},
|
||||
{
|
||||
"name": "xbl_config",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz",
|
||||
"hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
|
||||
"hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
|
||||
"size": 98124,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "e7d04d9f040c9c040cdf013335d0b6d6e9346311458baeb2461b193e954f5f1c"
|
||||
},
|
||||
{
|
||||
"name": "abl",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz",
|
||||
"hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6"
|
||||
},
|
||||
{
|
||||
"name": "aop",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz",
|
||||
"hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
|
||||
"hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
|
||||
"size": 184364,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "3aa0a79149ec57f4bc8c38f7bbdf4f6630dd659e49a111ce6258d2d06a07c8e5"
|
||||
},
|
||||
{
|
||||
"name": "devcfg",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz",
|
||||
"hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
|
||||
"hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
|
||||
"size": 40336,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "3d7bb33588491a2a40091a7e1cf6cb65e6dd503f69b640aba484d723f1ad47e8"
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/boot-595f6696c47c17d70a654d8ca04abca946a16dbb1da6250e464a23dff82258a4.img.xz",
|
||||
"hash": "595f6696c47c17d70a654d8ca04abca946a16dbb1da6250e464a23dff82258a4",
|
||||
"hash_raw": "595f6696c47c17d70a654d8ca04abca946a16dbb1da6250e464a23dff82258a4",
|
||||
"size": 18216960,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "b0ee082b2e63a49fcfd1ca2adfb49275e1bb567889cbb9985827fb1de50f943b"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa.img.xz",
|
||||
"hash": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa",
|
||||
"hash_raw": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa",
|
||||
"size": 6291456000,
|
||||
"sparse": false,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa",
|
||||
"url_parts": 10
|
||||
}
|
||||
]
|
||||
1
iqpilot/system/hardware/tici/agnos_tici_15_1.json.sig
Normal file
1
iqpilot/system/hardware/tici/agnos_tici_15_1.json.sig
Normal file
@@ -0,0 +1 @@
|
||||
JqXKQi3b6oUtR8CYSq6qoeGjE4SRViVSqYcL8dbMgwXGBkEY2fVCYDrO3nhzhEu7yv8EWGuvK4WiMdGB52iJAQ==
|
||||
400
iqpilot/system/hardware/tici/all-partitions.json
Normal file
400
iqpilot/system/hardware/tici/all-partitions.json
Normal file
@@ -0,0 +1,400 @@
|
||||
[
|
||||
{
|
||||
"name": "gpt_main_0",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_0-8928a31fd9ee20f8703649f89833eba9b55e84b6415e67799c777b163c95a0bd.img.xz",
|
||||
"hash": "8928a31fd9ee20f8703649f89833eba9b55e84b6415e67799c777b163c95a0bd",
|
||||
"hash_raw": "8928a31fd9ee20f8703649f89833eba9b55e84b6415e67799c777b163c95a0bd",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "8928a31fd9ee20f8703649f89833eba9b55e84b6415e67799c777b163c95a0bd",
|
||||
"gpt": {
|
||||
"lun": 0,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gpt_main_1",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_1-fe8ef7653db588d7420a625920ca06927dfcb0ed8aff3e3a1c74a52a24398ba6.img.xz",
|
||||
"hash": "fe8ef7653db588d7420a625920ca06927dfcb0ed8aff3e3a1c74a52a24398ba6",
|
||||
"hash_raw": "fe8ef7653db588d7420a625920ca06927dfcb0ed8aff3e3a1c74a52a24398ba6",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "fe8ef7653db588d7420a625920ca06927dfcb0ed8aff3e3a1c74a52a24398ba6",
|
||||
"gpt": {
|
||||
"lun": 1,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gpt_main_2",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_2-5ccfc7240c8cbfa2f1a018a2e376cf274a6baf858c9bfe71951d8e28cab53c21.img.xz",
|
||||
"hash": "5ccfc7240c8cbfa2f1a018a2e376cf274a6baf858c9bfe71951d8e28cab53c21",
|
||||
"hash_raw": "5ccfc7240c8cbfa2f1a018a2e376cf274a6baf858c9bfe71951d8e28cab53c21",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "5ccfc7240c8cbfa2f1a018a2e376cf274a6baf858c9bfe71951d8e28cab53c21",
|
||||
"gpt": {
|
||||
"lun": 2,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gpt_main_3",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_3-c707979fa21e89519328f4f30c2b21c9c453401ca8303f914c1873d410a95159.img.xz",
|
||||
"hash": "c707979fa21e89519328f4f30c2b21c9c453401ca8303f914c1873d410a95159",
|
||||
"hash_raw": "c707979fa21e89519328f4f30c2b21c9c453401ca8303f914c1873d410a95159",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "c707979fa21e89519328f4f30c2b21c9c453401ca8303f914c1873d410a95159",
|
||||
"gpt": {
|
||||
"lun": 3,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gpt_main_4",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_4-e9405dcd785dbe79412184e1894a9c51ab7deb33bb612166c4c42a3d2bf42a0e.img.xz",
|
||||
"hash": "e9405dcd785dbe79412184e1894a9c51ab7deb33bb612166c4c42a3d2bf42a0e",
|
||||
"hash_raw": "e9405dcd785dbe79412184e1894a9c51ab7deb33bb612166c4c42a3d2bf42a0e",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "e9405dcd785dbe79412184e1894a9c51ab7deb33bb612166c4c42a3d2bf42a0e",
|
||||
"gpt": {
|
||||
"lun": 4,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gpt_main_5",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_5-21ae965f05b2fa8d02e04f1eb74718f9779864f6eacdeb859757d6435e8ccce3.img.xz",
|
||||
"hash": "21ae965f05b2fa8d02e04f1eb74718f9779864f6eacdeb859757d6435e8ccce3",
|
||||
"hash_raw": "21ae965f05b2fa8d02e04f1eb74718f9779864f6eacdeb859757d6435e8ccce3",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "21ae965f05b2fa8d02e04f1eb74718f9779864f6eacdeb859757d6435e8ccce3",
|
||||
"gpt": {
|
||||
"lun": 5,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "persist",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/persist-d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786.img.xz",
|
||||
"hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786",
|
||||
"hash_raw": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786",
|
||||
"size": 4096,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786"
|
||||
},
|
||||
{
|
||||
"name": "systemrw",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/systemrw-8ce150ca38ef64a0885fc2fe816e5b63bae8adb4df5d809c5b318e6996366c7e.img.xz",
|
||||
"hash": "8ce150ca38ef64a0885fc2fe816e5b63bae8adb4df5d809c5b318e6996366c7e",
|
||||
"hash_raw": "8ce150ca38ef64a0885fc2fe816e5b63bae8adb4df5d809c5b318e6996366c7e",
|
||||
"size": 16777216,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "8ce150ca38ef64a0885fc2fe816e5b63bae8adb4df5d809c5b318e6996366c7e"
|
||||
},
|
||||
{
|
||||
"name": "cache",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/cache-ebfbaaa2f96dc4e5fea4f126364e5bf5b3b44c12cbc753b62fdd8baab82f70b4.img.xz",
|
||||
"hash": "ebfbaaa2f96dc4e5fea4f126364e5bf5b3b44c12cbc753b62fdd8baab82f70b4",
|
||||
"hash_raw": "ebfbaaa2f96dc4e5fea4f126364e5bf5b3b44c12cbc753b62fdd8baab82f70b4",
|
||||
"size": 134217728,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "ebfbaaa2f96dc4e5fea4f126364e5bf5b3b44c12cbc753b62fdd8baab82f70b4"
|
||||
},
|
||||
{
|
||||
"name": "xbl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz",
|
||||
"hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
|
||||
"hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
|
||||
"size": 3282256,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "d47a08914d2376557b03f1231b7233508222c04b57d781f9daf77c63eab92c2e"
|
||||
},
|
||||
{
|
||||
"name": "xbl_config",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz",
|
||||
"hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
|
||||
"hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
|
||||
"size": 98124,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "e7d04d9f040c9c040cdf013335d0b6d6e9346311458baeb2461b193e954f5f1c"
|
||||
},
|
||||
{
|
||||
"name": "abl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz",
|
||||
"hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
|
||||
"hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee"
|
||||
},
|
||||
{
|
||||
"name": "aop",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz",
|
||||
"hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
|
||||
"hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
|
||||
"size": 184364,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "3aa0a79149ec57f4bc8c38f7bbdf4f6630dd659e49a111ce6258d2d06a07c8e5"
|
||||
},
|
||||
{
|
||||
"name": "bluetooth",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/bluetooth-9bb766d2d2ce0cc4491664b3010fe1ef62f8ffc1e362d55f78e48c4141f75533.img.xz",
|
||||
"hash": "9bb766d2d2ce0cc4491664b3010fe1ef62f8ffc1e362d55f78e48c4141f75533",
|
||||
"hash_raw": "9bb766d2d2ce0cc4491664b3010fe1ef62f8ffc1e362d55f78e48c4141f75533",
|
||||
"size": 1048576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "9bb766d2d2ce0cc4491664b3010fe1ef62f8ffc1e362d55f78e48c4141f75533"
|
||||
},
|
||||
{
|
||||
"name": "cmnlib64",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/cmnlib64-1a876bd151bb9635f18719c4a17f953079de6e11d3eaec800968fc75669e0dc3.img.xz",
|
||||
"hash": "1a876bd151bb9635f18719c4a17f953079de6e11d3eaec800968fc75669e0dc3",
|
||||
"hash_raw": "1a876bd151bb9635f18719c4a17f953079de6e11d3eaec800968fc75669e0dc3",
|
||||
"size": 524288,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "1a876bd151bb9635f18719c4a17f953079de6e11d3eaec800968fc75669e0dc3"
|
||||
},
|
||||
{
|
||||
"name": "cmnlib",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/cmnlib-63df823e8a5fae01d66cb2b8c20f0d2ddb5c5f2425e5d0992a64676273ba1c82.img.xz",
|
||||
"hash": "63df823e8a5fae01d66cb2b8c20f0d2ddb5c5f2425e5d0992a64676273ba1c82",
|
||||
"hash_raw": "63df823e8a5fae01d66cb2b8c20f0d2ddb5c5f2425e5d0992a64676273ba1c82",
|
||||
"size": 524288,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "63df823e8a5fae01d66cb2b8c20f0d2ddb5c5f2425e5d0992a64676273ba1c82"
|
||||
},
|
||||
{
|
||||
"name": "devcfg",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz",
|
||||
"hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
|
||||
"hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
|
||||
"size": 40336,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "3d7bb33588491a2a40091a7e1cf6cb65e6dd503f69b640aba484d723f1ad47e8"
|
||||
},
|
||||
{
|
||||
"name": "devinfo",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/devinfo-143869c499a7e878fbeab756e9c53074195770cc41d6d0d10e45c043141389a3.img.xz",
|
||||
"hash": "143869c499a7e878fbeab756e9c53074195770cc41d6d0d10e45c043141389a3",
|
||||
"hash_raw": "143869c499a7e878fbeab756e9c53074195770cc41d6d0d10e45c043141389a3",
|
||||
"size": 4096,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "143869c499a7e878fbeab756e9c53074195770cc41d6d0d10e45c043141389a3"
|
||||
},
|
||||
{
|
||||
"name": "dsp",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/dsp-4b15fbd2f45581f1553f33f01649e450b24aa19d5deff2ac7dcb16a534d9c248.img.xz",
|
||||
"hash": "4b15fbd2f45581f1553f33f01649e450b24aa19d5deff2ac7dcb16a534d9c248",
|
||||
"hash_raw": "4b15fbd2f45581f1553f33f01649e450b24aa19d5deff2ac7dcb16a534d9c248",
|
||||
"size": 33554432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "4b15fbd2f45581f1553f33f01649e450b24aa19d5deff2ac7dcb16a534d9c248"
|
||||
},
|
||||
{
|
||||
"name": "hyp",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/hyp-ff5ece6a4e3d2b4d898c77ffe193fc8bbc8acebe78263996ecf52373d8088927.img.xz",
|
||||
"hash": "ff5ece6a4e3d2b4d898c77ffe193fc8bbc8acebe78263996ecf52373d8088927",
|
||||
"hash_raw": "ff5ece6a4e3d2b4d898c77ffe193fc8bbc8acebe78263996ecf52373d8088927",
|
||||
"size": 524288,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "ff5ece6a4e3d2b4d898c77ffe193fc8bbc8acebe78263996ecf52373d8088927"
|
||||
},
|
||||
{
|
||||
"name": "keymaster",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/keymaster-5c968c76f29b9a4d66fbe57e639bac6b7a2c83b1758e25abbaf5d276b8a6af04.img.xz",
|
||||
"hash": "5c968c76f29b9a4d66fbe57e639bac6b7a2c83b1758e25abbaf5d276b8a6af04",
|
||||
"hash_raw": "5c968c76f29b9a4d66fbe57e639bac6b7a2c83b1758e25abbaf5d276b8a6af04",
|
||||
"size": 524288,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "5c968c76f29b9a4d66fbe57e639bac6b7a2c83b1758e25abbaf5d276b8a6af04"
|
||||
},
|
||||
{
|
||||
"name": "limits",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/limits-94951a0f7aa55fb6cb975535ce4ebbfe6d695f04cb5424677b01c10dfa2e94e1.img.xz",
|
||||
"hash": "94951a0f7aa55fb6cb975535ce4ebbfe6d695f04cb5424677b01c10dfa2e94e1",
|
||||
"hash_raw": "94951a0f7aa55fb6cb975535ce4ebbfe6d695f04cb5424677b01c10dfa2e94e1",
|
||||
"size": 4096,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "94951a0f7aa55fb6cb975535ce4ebbfe6d695f04cb5424677b01c10dfa2e94e1"
|
||||
},
|
||||
{
|
||||
"name": "logfs",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/logfs-b8b5ac87f3d954404fc7ecbdd9ee3b5b0cf5691e5006e6ec55db4c899ff61220.img.xz",
|
||||
"hash": "b8b5ac87f3d954404fc7ecbdd9ee3b5b0cf5691e5006e6ec55db4c899ff61220",
|
||||
"hash_raw": "b8b5ac87f3d954404fc7ecbdd9ee3b5b0cf5691e5006e6ec55db4c899ff61220",
|
||||
"size": 8388608,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "b8b5ac87f3d954404fc7ecbdd9ee3b5b0cf5691e5006e6ec55db4c899ff61220"
|
||||
},
|
||||
{
|
||||
"name": "modem",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/modem-a3d014f0896d77a2df7e5a80a70f43a51a047b9d03cfc675b6f0e31a6ecc4994.img.xz",
|
||||
"hash": "a3d014f0896d77a2df7e5a80a70f43a51a047b9d03cfc675b6f0e31a6ecc4994",
|
||||
"hash_raw": "a3d014f0896d77a2df7e5a80a70f43a51a047b9d03cfc675b6f0e31a6ecc4994",
|
||||
"size": 125829120,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "a3d014f0896d77a2df7e5a80a70f43a51a047b9d03cfc675b6f0e31a6ecc4994"
|
||||
},
|
||||
{
|
||||
"name": "qupfw",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/qupfw-64cc7c29d5d69b04267452b8b4ddba9f4809e68f476fc162ca283f58537afe4a.img.xz",
|
||||
"hash": "64cc7c29d5d69b04267452b8b4ddba9f4809e68f476fc162ca283f58537afe4a",
|
||||
"hash_raw": "64cc7c29d5d69b04267452b8b4ddba9f4809e68f476fc162ca283f58537afe4a",
|
||||
"size": 65536,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "64cc7c29d5d69b04267452b8b4ddba9f4809e68f476fc162ca283f58537afe4a"
|
||||
},
|
||||
{
|
||||
"name": "splash",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/splash-5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08.img.xz",
|
||||
"hash": "5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08",
|
||||
"hash_raw": "5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08",
|
||||
"size": 34226176,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08"
|
||||
},
|
||||
{
|
||||
"name": "storsec",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/storsec-4494d86f68b125fbf2c004c824b1c6dbe71e61a65d2a1cc7db13c553edcb3fce.img.xz",
|
||||
"hash": "4494d86f68b125fbf2c004c824b1c6dbe71e61a65d2a1cc7db13c553edcb3fce",
|
||||
"hash_raw": "4494d86f68b125fbf2c004c824b1c6dbe71e61a65d2a1cc7db13c553edcb3fce",
|
||||
"size": 131072,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "4494d86f68b125fbf2c004c824b1c6dbe71e61a65d2a1cc7db13c553edcb3fce"
|
||||
},
|
||||
{
|
||||
"name": "tz",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/tz-e9443bf187641661bfa6c96702b9ab0156e72fb7482500f8799ba9ee2503cb16.img.xz",
|
||||
"hash": "e9443bf187641661bfa6c96702b9ab0156e72fb7482500f8799ba9ee2503cb16",
|
||||
"hash_raw": "e9443bf187641661bfa6c96702b9ab0156e72fb7482500f8799ba9ee2503cb16",
|
||||
"size": 2097152,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "e9443bf187641661bfa6c96702b9ab0156e72fb7482500f8799ba9ee2503cb16"
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-a0185fa5ffc860de2179e4d0fec703fef6d560eacd730f79f60891ca79c72756.img.xz",
|
||||
"hash": "a0185fa5ffc860de2179e4d0fec703fef6d560eacd730f79f60891ca79c72756",
|
||||
"hash_raw": "a0185fa5ffc860de2179e4d0fec703fef6d560eacd730f79f60891ca79c72756",
|
||||
"size": 17496064,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "0ee1ab104bb46d0f72e7d0b7d3e94629a7644a368896c6d4c558554fb955a08a"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-0cf8cb01e40d05d6d325afe68b934a6c0dda3a56703b2ef3e3de637d754ae5dd.img.xz",
|
||||
"hash": "7c58308be461126677ba02e9c9739556520ee02958934733867d86ecfe2e58e9",
|
||||
"hash_raw": "0cf8cb01e40d05d6d325afe68b934a6c0dda3a56703b2ef3e3de637d754ae5dd",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "826790516410c325aa30265846946d06a556f0a7b23c957f65fd11c055a663da",
|
||||
"alt": {
|
||||
"hash": "0cf8cb01e40d05d6d325afe68b934a6c0dda3a56703b2ef3e3de637d754ae5dd",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-0cf8cb01e40d05d6d325afe68b934a6c0dda3a56703b2ef3e3de637d754ae5dd.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userdata_90",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-ec31b8116125a95755adb32853c401c462a14a74f538535532bf2c34d72c60eb.img.xz",
|
||||
"hash": "aa0f0fe32187493e6135aee9e984d3f9705fc58560d537b34687bb6b51a38428",
|
||||
"hash_raw": "ec31b8116125a95755adb32853c401c462a14a74f538535532bf2c34d72c60eb",
|
||||
"size": 96636764160,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "9c916b7d05543d4608b0401bc867639f44ce9671639a1a6da83b6d58b4eaa1b4"
|
||||
},
|
||||
{
|
||||
"name": "userdata_89",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-7f092cc841124c10300e43574e90e3367e983bfbe4faa0969024e79e5ce90b11.img.xz",
|
||||
"hash": "fa83d4b7096857136820b0b0a8785c90677256b054c5c14039cd7b9b1065a90b",
|
||||
"hash_raw": "7f092cc841124c10300e43574e90e3367e983bfbe4faa0969024e79e5ce90b11",
|
||||
"size": 95563022336,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "1699e38de769eb32c21dfa6a5ac21eb3ad620a362c7b8abf1a2c0afe0f717530"
|
||||
},
|
||||
{
|
||||
"name": "userdata_30",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_30-3df2dcd5e1f426c90b090fdbcd1a95b035d96a4bdaf88d5517245db5ee84f5ed.img.xz",
|
||||
"hash": "890910f20b1ad88a728ee822a47b1234eb3d70cab28ca8a935679c8c2d33cbe9",
|
||||
"hash_raw": "3df2dcd5e1f426c90b090fdbcd1a95b035d96a4bdaf88d5517245db5ee84f5ed",
|
||||
"size": 32212254720,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "8e7cb392dd6e49c7d59fa850be7d1f44901314c86ba9c88be5bb27a0cd1123c9"
|
||||
}
|
||||
]
|
||||
159
iqpilot/system/hardware/tici/amplifier.py
Normal file
159
iqpilot/system/hardware/tici/amplifier.py
Normal 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)
|
||||
30
iqpilot/system/hardware/tici/esim.nmconnection
Normal file
30
iqpilot/system/hardware/tici/esim.nmconnection
Normal file
@@ -0,0 +1,30 @@
|
||||
[connection]
|
||||
id=esim
|
||||
uuid=fff6553c-3284-4707-a6b1-acc021caaafb
|
||||
type=gsm
|
||||
permissions=
|
||||
autoconnect=true
|
||||
autoconnect-retries=100
|
||||
autoconnect-priority=2
|
||||
metered=1
|
||||
|
||||
[gsm]
|
||||
apn=
|
||||
home-only=false
|
||||
auto-config=true
|
||||
sim-id=
|
||||
|
||||
[ipv4]
|
||||
route-metric=1000
|
||||
dns-priority=1000
|
||||
dns-search=
|
||||
method=auto
|
||||
|
||||
[ipv6]
|
||||
ddr-gen-mode=stable-privacy
|
||||
dns-search=
|
||||
route-metric=1000
|
||||
dns-priority=1000
|
||||
method=auto
|
||||
|
||||
[proxy]
|
||||
290
iqpilot/system/hardware/tici/esim_manager.py
Normal file
290
iqpilot/system/hardware/tici/esim_manager.py
Normal file
@@ -0,0 +1,290 @@
|
||||
import threading
|
||||
import subprocess
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from queue import Queue, Empty
|
||||
from typing import Callable
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.system.hardware import HARDWARE
|
||||
from iqpilot.system.hardware.base import LPAError, LPAProfileNotFoundError, Profile
|
||||
|
||||
|
||||
class EsimOperationState(Enum):
|
||||
IDLE = "idle"
|
||||
SCANNING = "scanning"
|
||||
DOWNLOADING = "downloading"
|
||||
SWITCHING = "switching"
|
||||
RENAMING = "renaming"
|
||||
DELETING = "deleting"
|
||||
REBOOTING_MODEM = "rebooting modem"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@dataclass
|
||||
class EsimUiState:
|
||||
state: EsimOperationState = EsimOperationState.IDLE
|
||||
message: str = ""
|
||||
profiles: list[Profile] | None = None
|
||||
busy: bool = False
|
||||
|
||||
|
||||
class EsimManager:
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self._lock = threading.Lock()
|
||||
self._callbacks: list[Callable[[EsimUiState], None]] = []
|
||||
self._state = EsimUiState()
|
||||
self._support_cache: bool | None = None
|
||||
self._support_cache_ts = 0.0
|
||||
|
||||
self._ops: Queue[Callable[[], None]] = Queue()
|
||||
self._worker = threading.Thread(target=self._worker_loop, daemon=True)
|
||||
self._worker.start()
|
||||
|
||||
def is_supported(self) -> bool:
|
||||
raw_flag = self._params.get("EnableEsimProvisioning")
|
||||
enabled = True if raw_flag is None else self._params.get_bool("EnableEsimProvisioning")
|
||||
if not enabled:
|
||||
return False
|
||||
if HARDWARE.get_device_type() not in ("tici", "tizi", "mici"):
|
||||
return False
|
||||
return self._has_euicc()
|
||||
|
||||
def _has_euicc(self, force_refresh: bool = False) -> bool:
|
||||
now = time.monotonic()
|
||||
if not force_refresh and self._support_cache is not None and now - self._support_cache_ts < 5.0:
|
||||
return self._support_cache
|
||||
|
||||
supported = self._query_euicc_support()
|
||||
self._support_cache = supported
|
||||
self._support_cache_ts = now
|
||||
return supported
|
||||
|
||||
@staticmethod
|
||||
def _query_euicc_support() -> bool:
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["sudo", "qmicli", "-p", "-d", "/dev/cdc-wdm0", "--uim-get-slot-status"],
|
||||
capture_output=True, text=True, check=False, timeout=8,
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
output = f"{res.stdout}\n{res.stderr}"
|
||||
if "Is eUICC: yes" in output:
|
||||
return True
|
||||
if "Is eUICC: no" in output:
|
||||
return False
|
||||
return False
|
||||
|
||||
def add_callback(self, cb: Callable[[EsimUiState], None]) -> None:
|
||||
with self._lock:
|
||||
self._callbacks.append(cb)
|
||||
state = self._copy_state_locked()
|
||||
cb(state)
|
||||
|
||||
def remove_callback(self, cb: Callable[[EsimUiState], None]) -> None:
|
||||
with self._lock:
|
||||
self._callbacks = [c for c in self._callbacks if c is not cb]
|
||||
|
||||
def get_state(self) -> EsimUiState:
|
||||
with self._lock:
|
||||
return self._copy_state_locked()
|
||||
|
||||
def refresh_profiles(self) -> None:
|
||||
if not self._is_supported_for_operation():
|
||||
self._set_profiles([])
|
||||
self._set_state(EsimOperationState.IDLE, self._unavailable_message(), busy=False)
|
||||
return
|
||||
self._enqueue(self._refresh_profiles)
|
||||
|
||||
def is_comma_profile(self, iccid: str) -> bool:
|
||||
try:
|
||||
return HARDWARE.get_sim_lpa().is_comma_profile(iccid)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def add_profile(self, activation_code: str, nickname: str | None = None) -> None:
|
||||
def _op() -> None:
|
||||
self._set_state(EsimOperationState.DOWNLOADING, "Downloading profile...", busy=True)
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
lpa.download_profile(activation_code, nickname=nickname)
|
||||
self._set_state(EsimOperationState.REBOOTING_MODEM, "Reconnecting modem...", busy=True)
|
||||
self._refresh_profiles()
|
||||
self._set_state(EsimOperationState.COMPLETED, "Profile added", busy=False)
|
||||
self._enqueue(_op)
|
||||
|
||||
def switch_profile(self, iccid: str) -> None:
|
||||
def _op() -> None:
|
||||
self._set_state(EsimOperationState.SWITCHING, "Switching profile...", busy=True)
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
lpa.switch_profile(iccid)
|
||||
self._set_state(EsimOperationState.REBOOTING_MODEM, "Reconnecting modem...", busy=True)
|
||||
self._refresh_profiles()
|
||||
self._set_state(EsimOperationState.COMPLETED, "Profile switched", busy=False)
|
||||
self._enqueue(_op)
|
||||
|
||||
def rename_profile(self, iccid: str, nickname: str) -> None:
|
||||
def _op() -> None:
|
||||
self._set_state(EsimOperationState.RENAMING, "Renaming profile...", busy=True)
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
lpa.nickname_profile(iccid, nickname)
|
||||
self._refresh_profiles()
|
||||
self._set_state(EsimOperationState.COMPLETED, "Profile renamed", busy=False)
|
||||
self._enqueue(_op)
|
||||
|
||||
def delete_profile(self, iccid: str) -> None:
|
||||
def _op() -> None:
|
||||
self._set_state(EsimOperationState.DELETING, "Deleting profile...", busy=True)
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
lpa.delete_profile(iccid)
|
||||
self._set_state(EsimOperationState.REBOOTING_MODEM, "Reconnecting modem...", busy=True)
|
||||
self._refresh_profiles()
|
||||
self._set_state(EsimOperationState.COMPLETED, "Profile deleted", busy=False)
|
||||
self._enqueue(_op)
|
||||
|
||||
def bootstrap(self) -> None:
|
||||
def _op() -> None:
|
||||
self._set_state(EsimOperationState.DELETING, "Removing Comma pSIM...", busy=True)
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
lpa.bootstrap()
|
||||
self._set_state(EsimOperationState.REBOOTING_MODEM, "Reconnecting modem...", busy=True)
|
||||
self._refresh_profiles()
|
||||
self._set_state(EsimOperationState.COMPLETED, "Comma pSIM removed", busy=False)
|
||||
self._enqueue(_op)
|
||||
|
||||
def set_scanning_state(self, scanning: bool) -> None:
|
||||
if scanning:
|
||||
self._set_state(EsimOperationState.SCANNING, "Point camera at an eSIM QR code", busy=True)
|
||||
else:
|
||||
self._set_state(EsimOperationState.IDLE, "", busy=False)
|
||||
|
||||
def _enqueue(self, fn: Callable[[], None]) -> None:
|
||||
if not self._is_supported_for_operation():
|
||||
self._set_state(EsimOperationState.FAILED, self._unavailable_message(), busy=False)
|
||||
return
|
||||
self._ops.put(fn)
|
||||
|
||||
def _is_supported_for_operation(self) -> bool:
|
||||
raw_flag = self._params.get("EnableEsimProvisioning")
|
||||
enabled = True if raw_flag is None else self._params.get_bool("EnableEsimProvisioning")
|
||||
if not enabled:
|
||||
return False
|
||||
if HARDWARE.get_device_type() not in ("tici", "tizi", "mici"):
|
||||
return False
|
||||
return self._has_euicc(force_refresh=True)
|
||||
|
||||
def _unavailable_message(self) -> str:
|
||||
if HARDWARE.get_device_type() in ("tici", "tizi", "mici"):
|
||||
return "Insert the original comma SIM card that came with the device to use eSIM"
|
||||
return "eSIM provisioning is unavailable on this device"
|
||||
|
||||
def _worker_loop(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
op = self._ops.get(timeout=0.2)
|
||||
except Empty:
|
||||
continue
|
||||
try:
|
||||
op()
|
||||
except Exception as e:
|
||||
self._set_state(EsimOperationState.FAILED, self._map_error(e), busy=False)
|
||||
finally:
|
||||
self._ops.task_done()
|
||||
|
||||
def _refresh_profiles(self) -> None:
|
||||
if not self._is_supported_for_operation():
|
||||
self._set_profiles([])
|
||||
return
|
||||
profiles = HARDWARE.get_sim_lpa().list_profiles()
|
||||
self._set_profiles(profiles)
|
||||
|
||||
def _set_profiles(self, profiles: list[Profile]) -> None:
|
||||
with self._lock:
|
||||
self._state.profiles = profiles
|
||||
state = self._copy_state_locked()
|
||||
callbacks = list(self._callbacks)
|
||||
for cb in callbacks:
|
||||
cb(state)
|
||||
|
||||
def _set_state(self, state: EsimOperationState, message: str, busy: bool) -> None:
|
||||
with self._lock:
|
||||
self._state.state = state
|
||||
self._state.message = message
|
||||
self._state.busy = busy
|
||||
snapshot = self._copy_state_locked()
|
||||
callbacks = list(self._callbacks)
|
||||
for cb in callbacks:
|
||||
cb(snapshot)
|
||||
|
||||
def _copy_state_locked(self) -> EsimUiState:
|
||||
profiles = list(self._state.profiles) if self._state.profiles is not None else None
|
||||
return EsimUiState(
|
||||
state=self._state.state,
|
||||
message=self._state.message,
|
||||
profiles=profiles,
|
||||
busy=self._state.busy,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _map_error(error: Exception) -> str:
|
||||
if isinstance(error, LPAProfileNotFoundError):
|
||||
return "Profile not found"
|
||||
if isinstance(error, LPAError):
|
||||
message = str(error)
|
||||
lower = message.lower()
|
||||
if "is euicc: no" in lower or "reports no euicc support" in lower:
|
||||
return "Insert the original comma SIM to enable eSIM provisioning on this device"
|
||||
if "certificate verify failed" in lower or "ssl" in lower or "tls" in lower:
|
||||
return "TLS validation failed while contacting SM-DP+"
|
||||
if "system time is not set" in lower:
|
||||
return "Device time is invalid; connect to network and retry"
|
||||
if "returned no modems" in lower or "object does not exist at path" in lower:
|
||||
return "Modem is restarting; wait a moment and refresh profiles"
|
||||
if "timed out" in lower or "timeout" in lower:
|
||||
return "Modem timed out while provisioning eSIM"
|
||||
if "delete the existing comma psim profile" in lower:
|
||||
return "Delete the Comma pSIM profile before activating RedPocket"
|
||||
if "not bootstrapped" in lower:
|
||||
return "Delete the Comma pSIM profile before using user eSIM profiles"
|
||||
if "cannot delete active profile" in lower:
|
||||
return "Cannot delete active profile"
|
||||
if "profile delete may have succeeded" in lower:
|
||||
return "Profile may already be deleted; refresh profiles"
|
||||
if "profile delete did not finish cleanly" in lower:
|
||||
return "Profile delete did not complete; refresh profiles and retry"
|
||||
if "profile switch may have succeeded" in lower:
|
||||
return "Profile likely switched; refresh profiles"
|
||||
if "profile switch did not finish cleanly" in lower:
|
||||
return "Profile switch did not complete; refresh profiles and retry"
|
||||
if "profile add may have succeeded" in lower:
|
||||
return "Profile may already be added; refresh profiles"
|
||||
if "profile add did not finish cleanly" in lower:
|
||||
return "Profile add did not complete; refresh profiles and retry"
|
||||
if "profile enable may have succeeded" in lower:
|
||||
return "Profile may already be enabled; refresh profiles"
|
||||
if "profile enable did not finish cleanly" in lower:
|
||||
return "Profile enable did not complete; refresh profiles and retry"
|
||||
if "profile disable may have succeeded" in lower:
|
||||
return "Profile may already be disabled; refresh profiles"
|
||||
if "profile disable did not finish cleanly" in lower:
|
||||
return "Profile disable did not complete; refresh profiles and retry"
|
||||
if "bf2800" in lower or "listnotification" in lower:
|
||||
return "Modem notification cleanup failed; refresh profiles"
|
||||
return message
|
||||
return str(error)
|
||||
|
||||
|
||||
_ESIM_MANAGER: EsimManager | None = None
|
||||
_ESIM_MANAGER_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def get_esim_manager() -> EsimManager:
|
||||
global _ESIM_MANAGER
|
||||
with _ESIM_MANAGER_LOCK:
|
||||
if _ESIM_MANAGER is None:
|
||||
_ESIM_MANAGER = EsimManager()
|
||||
return _ESIM_MANAGER
|
||||
133
iqpilot/system/hardware/tici/gsma_ci_bundle.pem
Normal file
133
iqpilot/system/hardware/tici/gsma_ci_bundle.pem
Normal file
@@ -0,0 +1,133 @@
|
||||
# GSMA Certificate Issuer (CI) bundle for eSIM RSP
|
||||
# Source: https://euicc-manual.osmocom.org/docs/pki/ci/bundle.pem
|
||||
|
||||
issuer=
|
||||
countryName = CH
|
||||
organizationName = OISTE Foundation
|
||||
commonName = OISTE GSMA CI G1
|
||||
notBefore=2024-01-16 23:17:39Z
|
||||
notAfter=2059-01-07 23:17:38Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB9zCCAZ2gAwIBAgIUSpBSCCDYPOEG/IFHUCKpZ2pIAQMwCgYIKoZIzj0EAwIw
|
||||
QzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRpb24xGTAXBgNV
|
||||
BAMMEE9JU1RFIEdTTUEgQ0kgRzEwIBcNMjQwMTE2MjMxNzM5WhgPMjA1OTAxMDcy
|
||||
MzE3MzhaMEMxCzAJBgNVBAYTAkNIMRkwFwYDVQQKDBBPSVNURSBGb3VuZGF0aW9u
|
||||
MRkwFwYDVQQDDBBPSVNURSBHU01BIENJIEcxMFkwEwYHKoZIzj0CAQYIKoZIzj0D
|
||||
AQcDQgAEvZ3s3PFC4NgrCcCMmHJ6DJ66uzAHuLcvjJnOn+TtBNThS7YHLDyHCa2v
|
||||
7D+zTP+XTtgqgcLoB56Gha9EQQQ4xKNtMGswDwYDVR0TAQH/BAUwAwEB/zAQBgNV
|
||||
HREECTAHiAVghXQFDjAXBgNVHSABAf8EDTALMAkGB2eBEgECAQAwHQYDVR0OBBYE
|
||||
FEwnlnrSDBSzkelgHkHmBK1XwCIvMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQD
|
||||
AgNIADBFAiBVcywTj017jKpAQ+gwy4MqK2hQvzve6lkvQkgSP6ykHwIhAI0KFwCD
|
||||
jnPbmcJsG41hUrWNlf+IcrMvFuYii0DasBNi
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
organizationName = GSM Association
|
||||
commonName = GSM Association - RSP2 Root CI1
|
||||
notBefore=2017-02-22 00:00:00Z
|
||||
notAfter=2052-02-21 23:59:59Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICSTCCAe+gAwIBAgIQbmhWeneg7nyF7hg5Y9+qejAKBggqhkjOPQQDAjBEMRgw
|
||||
FgYDVQQKEw9HU00gQXNzb2NpYXRpb24xKDAmBgNVBAMTH0dTTSBBc3NvY2lhdGlv
|
||||
biAtIFJTUDIgUm9vdCBDSTEwIBcNMTcwMjIyMDAwMDAwWhgPMjA1MjAyMjEyMzU5
|
||||
NTlaMEQxGDAWBgNVBAoTD0dTTSBBc3NvY2lhdGlvbjEoMCYGA1UEAxMfR1NNIEFz
|
||||
c29jaWF0aW9uIC0gUlNQMiBSb290IENJMTBZMBMGByqGSM49AgEGCCqGSM49AwEH
|
||||
A0IABJ1qutL0HCMX52GJ6/jeibsAqZfULWj/X10p/Min6seZN+hf5llovbCNuB2n
|
||||
unLz+O8UD0SUCBUVo8e6n9X1TuajgcAwgb0wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
|
||||
EwEB/wQFMAMBAf8wEwYDVR0RBAwwCogIKwYBBAGC6WAwFwYDVR0gAQH/BA0wCzAJ
|
||||
BgdngRIBAgEAME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9nc21hLWNybC5zeW1h
|
||||
dXRoLmNvbS9vZmZsaW5lY2EvZ3NtYS1yc3AyLXJvb3QtY2kxLmNybDAdBgNVHQ4E
|
||||
FgQUgTcPUSXQsdQI1MOyMubSXnlb6/swCgYIKoZIzj0EAwIDSAAwRQIgIJdYsOMF
|
||||
WziPK7l8nh5mu0qiRiVf25oa9ullG/OIASwCIQDqCmDrYf+GziHXBOiwJwnBaeBO
|
||||
aFsiLzIEOaUuZwdNUw==
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
countryName = US
|
||||
organizationName = Entrust, Inc.
|
||||
organizationalUnitName = See www.entrust.net/legal-terms
|
||||
organizationalUnitName = (c) 2016 Entrust, Inc. - for authorized use only
|
||||
commonName = Entrust eSIM Certification Authority
|
||||
notBefore=2016-11-16 16:04:02Z
|
||||
notAfter=2051-10-16 16:34:02Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC6DCCAo2gAwIBAgIRAIy4GT7M5nHsAAAAAFgsinowCgYIKoZIzj0EAwIwgbkx
|
||||
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9T
|
||||
ZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAx
|
||||
NiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLTArBgNV
|
||||
BAMTJEVudHJ1c3QgZVNJTSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAgFw0xNjEx
|
||||
MTYxNjA0MDJaGA8yMDUxMTAxNjE2MzQwMlowgbkxCzAJBgNVBAYTAlVTMRYwFAYD
|
||||
VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0
|
||||
L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNiBFbnRydXN0LCBJbmMuIC0g
|
||||
Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLTArBgNVBAMTJEVudHJ1c3QgZVNJTSBD
|
||||
ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
|
||||
BAdzwGHeQ1Wb2f4DmHTByR5/IWL3JugQ1U3908a++bHdlt+TTA7K4c5cYZ+51Yz/
|
||||
hg/bacxguPDh9uQUK6Wg3a6jcjBwMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
|
||||
BAQDAgEGMBcGA1UdIAEB/wQNMAswCQYHZ4ESAQIBADAVBgNVHREEDjAMiApghkgB
|
||||
hvpsFAoAMB0GA1UdDgQWBBQWcEt/NR42B/GMS3AAXDoAPf1BSjAKBggqhkjOPQQD
|
||||
AgNJADBGAiEAspjXMvaBZyAg86Z0AAtT0yBRAi1EyaAfNz9kDJeAE04CIQC3efj8
|
||||
ATL7/tDBOhANy3cK8PS/1NIlu9vqMLCZsZvJ0Q==
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
countryName = FR
|
||||
organizationName = OBERTHUR TECHNOLOGIES
|
||||
organizationalUnitName = TELECOM
|
||||
commonName = MC4 OT ROOT CI v1
|
||||
notBefore=2016-11-15 00:00:01Z
|
||||
notAfter=2046-11-08 23:59:59Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICOjCCAeGgAwIBAgIBATAKBggqhkjOPQQDAjBbMQswCQYDVQQGEwJGUjEeMBwG
|
||||
A1UEChMVT0JFUlRIVVIgVEVDSE5PTE9HSUVTMRAwDgYDVQQLEwdURUxFQ09NMRow
|
||||
GAYDVQQDExFNQzQgT1QgUk9PVCBDSSB2MTAeFw0xNjExMTUwMDAwMDFaFw00NjEx
|
||||
MDgyMzU5NTlaMFsxCzAJBgNVBAYTAkZSMR4wHAYDVQQKExVPQkVSVEhVUiBURUNI
|
||||
Tk9MT0dJRVMxEDAOBgNVBAsTB1RFTEVDT00xGjAYBgNVBAMTEU1DNCBPVCBST09U
|
||||
IENJIHYxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHb/Gajt3OZxuaDSklBQE
|
||||
D4lOd6PGPLSvtfkM952ubdyy45tJwAeA0eEii0CLrFT6tcfXkW+H/5mQyMRXaAUk
|
||||
T6OBlTCBkjAfBgNVHSMEGDAWgBTNbmC3LXoGPLyEYluR6A/jBAbhPjAdBgNVHQ4E
|
||||
FgQUzW5gty16Bjy8hGJbkegP4wQG4T4wDgYDVR0PAQH/BAQDAgAGMBcGA1UdIAEB
|
||||
/wQNMAswCQYHZ4ESAQIBADAWBgNVHREEDzANiAsrBgEEAYHvb7OITTAPBgNVHRMB
|
||||
Af8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIEw4Nc7f2fDtoH+6ON/bknfDQxmT
|
||||
ikThXjhpLtSrSKN2AiAxHxgC87L0FDnH8dJNlkdGX9c0JIx6oLheIplfS6k+jg==
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
commonName = SubMan V4.2 CI Google Pixel
|
||||
organizationName = Giesecke and Devrient GmbH
|
||||
organizationalUnitName = Mobile Security
|
||||
countryName = DE
|
||||
notBefore=2017-05-10 00:00:00Z
|
||||
notAfter=2027-05-10 00:00:00Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICaTCCAg6gAwIBAgICASwwCgYIKoZIzj0EAwIwczElMCMGA1UEAxMcIFN1Yk1h
|
||||
biBWNC4yIENJIEdvb2dsZSBQaXhlbDEjMCEGA1UEChMaR2llc2Vja2UgYW5kIERl
|
||||
dnJpZW50IEdtYkgxGDAWBgNVBAsTD01vYmlsZSBTZWN1cml0eTELMAkGA1UEBhMC
|
||||
REUwHhcNMTcwNTEwMDAwMDAwWhcNMjcwNTEwMDAwMDAwWjBzMSUwIwYDVQQDExwg
|
||||
U3ViTWFuIFY0LjIgQ0kgR29vZ2xlIFBpeGVsMSMwIQYDVQQKExpHaWVzZWNrZSBh
|
||||
bmQgRGV2cmllbnQgR21iSDEYMBYGA1UECxMPTW9iaWxlIFNlY3VyaXR5MQswCQYD
|
||||
VQQGEwJERTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHNorfaJsGzqWNawyAhl
|
||||
IAv9QL2/+b9RsUoso06t/dKX1MRr5CUJ51acvv5TAFhQKIml+dwLbFnV5aO+8W6Z
|
||||
wxajgZEwgY4wHwYDVR0jBBgwFoAUtg8LiX/WMLiM/tYWH46oCMU4KsMwHQYDVR0O
|
||||
BBYEFLYPC4l/1jC4jP7WFh+OqAjFOCrDMA4GA1UdDwEB/wQEAwIBBjAXBgNVHSAB
|
||||
Af8EDTALMAkGB2eBEgECAQAwDwYDVR0TAQH/BAUwAwEB/zASBgNVHREECzAJiAcr
|
||||
BgEEAdwPMAoGCCqGSM49BAMCA0kAMEYCIQDpoZcuAQrjATW8U+AWqMUJ0dY6nWW1
|
||||
R1QmFzVZ1yMXSwIhALCvRqkCtgiavdeFeSgsSNbY5Fhd+QoCltuSh1U4TE7A
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
countryName = DE
|
||||
commonName = SubMan V4.2 CI
|
||||
organizationName = Giesecke and Devrient
|
||||
organizationalUnitName = Mobile Security
|
||||
notBefore=2016-08-12 13:51:48Z
|
||||
notAfter=2026-08-12 13:51:48Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICUjCCAfigAwIBAgIDQgAAMAoGCCqGSM49BAMCMGAxCzAJBgNVBAYTAkRFMRcw
|
||||
FQYDVQQDEw5TdWJNYW4gVjQuMiBDSTEeMBwGA1UEChMVR2llc2Vja2UgYW5kIERl
|
||||
dnJpZW50MRgwFgYDVQQLEw9Nb2JpbGUgU2VjdXJpdHkwHhcNMTYwODEyMTM1MTQ4
|
||||
WhcNMjYwODEyMTM1MTQ4WjBgMQswCQYDVQQGEwJERTEXMBUGA1UEAxMOU3ViTWFu
|
||||
IFY0LjIgQ0kxHjAcBgNVBAoTFUdpZXNlY2tlIGFuZCBEZXZyaWVudDEYMBYGA1UE
|
||||
CxMPTW9iaWxlIFNlY3VyaXR5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYIgl
|
||||
VQr9wbXOlwPp8qMg5Df08Cli9Mc+lpr3Lwa9PlVA3QWlLeX4GfD4H3phLBqVIa17
|
||||
yHttmtheTxi0KoEqhKOBoDCBnTAdBgNVHQ4EFgQU6lOt7zMpuVCa/XVf1Ei4LcG8
|
||||
7P8wDgYDVR0PAQH/BAQDAgEGMBcGA1UdIAEB/wQNMAswCQYHZ4ESAQIBADAPBgNV
|
||||
HRMBAf8EBTADAQH/MBIGA1UdEQQLMAmIBysGAQQB3A8wLgYDVR0fBCcwJTAjoCGg
|
||||
H4YdaHR0cDovL2dpLWRlLmNvbS90ZXN0LmNybC5wZW0wCgYIKoZIzj0EAwIDSAAw
|
||||
RQIhAMMx2L/VHDiOW+Fl/OuFmhCdizYM17Yn9zAVieKO2T0iAiANWtCMmY+DzkqK
|
||||
yHxBFX0U2tBd682zP4DpgRt8j3Ylew==
|
||||
-----END CERTIFICATE-----
|
||||
740
iqpilot/system/hardware/tici/hardware.py
Normal file
740
iqpilot/system/hardware/tici/hardware.py
Normal 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())
|
||||
28
iqpilot/system/hardware/tici/id_rsa
Normal file
28
iqpilot/system/hardware/tici/id_rsa
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+iXXq30Tq+J5N
|
||||
Kat3KWHCzcmwZ55nGh6WggAqECa5CasBlM9VeROpVu3beA+5h0MibRgbD4DMtVXB
|
||||
t6gEvZ8nd04E7eLA9LTZyFDZ7SkSOVj4oXOQsT0GnJmKrASW5KslTWqVzTfo2XCt
|
||||
Z+004ikLxmyFeBO8NOcErW1pa8gFdQDToH9FrA7kgysic/XVESTOoe7XlzRoe/eZ
|
||||
acEQ+jtnmFd21A4aEADkk00Ahjr0uKaJiLUAPatxs2icIXWpgYtfqqtaKF23wSt6
|
||||
1OTu6cAwXbOWr3m+IUSRUO0IRzEIQS3z1jfd1svgzSgSSwZ1Lhj4AoKxIEAIc8qJ
|
||||
rO4uymCJAgMBAAECggEBAISFevxHGdoL3Z5xkw6oO5SQKO2GxEeVhRzNgmu/HA+q
|
||||
x8OryqD6O1CWY4037kft6iWxlwiLOdwna2P25ueVM3LxqdQH2KS4DmlCx+kq6FwC
|
||||
gv063fQPMhC9LpWimvaQSPEC7VUPjQlo4tPY6sTTYBUOh0A1ihRm/x7juKuQCWix
|
||||
Cq8C/DVnB1X4mGj+W3nJc5TwVJtgJbbiBrq6PWrhvB/3qmkxHRL7dU2SBb2iNRF1
|
||||
LLY30dJx/cD73UDKNHrlrsjk3UJc29Mp4/MladKvUkRqNwlYxSuAtJV0nZ3+iFkL
|
||||
s3adSTHdJpClQer45R51rFDlVsDz2ZBpb/hRNRoGDuECgYEA6A1EixLq7QYOh3cb
|
||||
Xhyh3W4kpVvA/FPfKH1OMy3ONOD/Y9Oa+M/wthW1wSoRL2n+uuIW5OAhTIvIEivj
|
||||
6bAZsTT3twrvOrvYu9rx9aln4p8BhyvdjeW4kS7T8FP5ol6LoOt2sTP3T1LOuJPO
|
||||
uQvOjlKPKIMh3c3RFNWTnGzMPa0CgYEA0jNiPLxP3A2nrX0keKDI+VHuvOY88gdh
|
||||
0W5BuLMLovOIDk9aQFIbBbMuW1OTjHKv9NK+Lrw+YbCFqOGf1dU/UN5gSyE8lX/Q
|
||||
FsUGUqUZx574nJZnOIcy3ONOnQLcvHAQToLFAGUd7PWgP3CtHkt9hEv2koUwL4vo
|
||||
ikTP1u9Gkc0CgYEA2apoWxPZrY963XLKBxNQecYxNbLFaWq67t3rFnKm9E8BAICi
|
||||
4zUaE5J1tMVi7Vi9iks9Ml9SnNyZRQJKfQ+kaebHXbkyAaPmfv+26rqHKboA0uxA
|
||||
nDOZVwXX45zBkp6g1sdHxJx8JLoGEnkC9eyvSi0C//tRLx86OhLErXwYcNkCf1it
|
||||
VMRKrWYoXJTUNo6tRhvodM88UnnIo3u3CALjhgU4uC1RTMHV4ZCGBwiAOb8GozSl
|
||||
s5YD1E1iKwEULloHnK6BIh6P5v8q7J6uf/xdqoKMjlWBHgq6/roxKvkSPA1DOZ3l
|
||||
jTadcgKFnRUmc+JT9p/ZbCxkA/ALFg8++G+0ghECgYA8vG3M/utweLvq4RI7l7U7
|
||||
b+i2BajfK2OmzNi/xugfeLjY6k2tfQGRuv6ppTjehtji2uvgDWkgjJUgPfZpir3I
|
||||
RsVMUiFgloWGHETOy0Qvc5AwtqTJFLTD1Wza2uBilSVIEsg6Y83Gickh+ejOmEsY
|
||||
6co17RFaAZHwGfCFFjO76Q==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
35
iqpilot/system/hardware/tici/iwlist.py
Normal file
35
iqpilot/system/hardware/tici/iwlist.py
Normal 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
|
||||
1482
iqpilot/system/hardware/tici/lpa.py
Executable file
1482
iqpilot/system/hardware/tici/lpa.py
Executable file
File diff suppressed because it is too large
Load Diff
30
iqpilot/system/hardware/tici/pins.py
Normal file
30
iqpilot/system/hardware/tici/pins.py
Normal 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
|
||||
66
iqpilot/system/hardware/tici/power_monitor.py
Executable file
66
iqpilot/system/hardware/tici/power_monitor.py
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
import numpy as np
|
||||
from collections import deque
|
||||
|
||||
from iqpilot.common.realtime import Ratekeeper
|
||||
from iqpilot.common.filter_simple import FirstOrderFilter
|
||||
|
||||
|
||||
def read_power():
|
||||
with open("/sys/bus/i2c/devices/0-0040/hwmon/hwmon1/power1_input") as f:
|
||||
return int(f.read()) / 1e6
|
||||
|
||||
def sample_power(seconds=5) -> list[float]:
|
||||
rate = 123
|
||||
rk = Ratekeeper(rate, print_delay_threshold=None)
|
||||
|
||||
pwrs = []
|
||||
for _ in range(rate*seconds):
|
||||
pwrs.append(read_power())
|
||||
rk.keep_time()
|
||||
return pwrs
|
||||
|
||||
def get_power(seconds=5):
|
||||
pwrs = sample_power(seconds)
|
||||
return np.mean(pwrs)
|
||||
|
||||
def wait_for_power(min_pwr, max_pwr, min_secs_in_range, timeout):
|
||||
start_time = time.monotonic()
|
||||
pwrs = deque([min_pwr - 1.]*min_secs_in_range, maxlen=min_secs_in_range)
|
||||
while (time.monotonic() - start_time < timeout):
|
||||
pwrs.append(get_power(1))
|
||||
if all(min_pwr <= p <= max_pwr for p in pwrs):
|
||||
break
|
||||
return np.mean(pwrs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
duration = None
|
||||
if len(sys.argv) > 1:
|
||||
duration = int(sys.argv[1])
|
||||
|
||||
rate = 23
|
||||
rk = Ratekeeper(rate, print_delay_threshold=None)
|
||||
fltr = FirstOrderFilter(0, 5, 1. / rate, initialized=False)
|
||||
|
||||
measurements = []
|
||||
start_time = time.monotonic()
|
||||
|
||||
try:
|
||||
while duration is None or time.monotonic() - start_time < duration:
|
||||
fltr.update(read_power())
|
||||
if rk.frame % rate == 0:
|
||||
measurements.append(fltr.x)
|
||||
t = datetime.timedelta(seconds=time.monotonic() - start_time)
|
||||
avg = sum(measurements) / len(measurements)
|
||||
print(f"Now: {fltr.x:.2f} W, Avg: {avg:.2f} W over {t}")
|
||||
rk.keep_time()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
t = datetime.timedelta(seconds=time.monotonic() - start_time)
|
||||
avg = sum(measurements) / len(measurements)
|
||||
print(f"\nAverage power: {avg:.2f}W over {t}")
|
||||
145
iqpilot/system/hardware/tici/qr_decode.py
Normal file
145
iqpilot/system/hardware/tici/qr_decode.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import ctypes
|
||||
import hashlib
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from pyzbar.pyzbar import decode as _pyzbar_decode
|
||||
except Exception:
|
||||
_pyzbar_decode = None
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
QUIRC_LIB_DIR = ROOT / "third_party" / "quirc" / "lib"
|
||||
HELPER_C = Path(__file__).with_name("qr_decode_quirc.c")
|
||||
BUILD_DIR = ROOT / ".run" / "cache" / "esim_qr"
|
||||
SO_PATH = BUILD_DIR / "libiqpilot_quirc_decode.so"
|
||||
|
||||
_LIB: ctypes.CDLL | None = None
|
||||
|
||||
|
||||
def _build_decoder() -> bool:
|
||||
BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
cmd = [
|
||||
os.environ.get("CC", "cc"),
|
||||
"-O2",
|
||||
"-shared",
|
||||
"-fPIC",
|
||||
str(HELPER_C),
|
||||
str(QUIRC_LIB_DIR / "quirc.c"),
|
||||
str(QUIRC_LIB_DIR / "identify.c"),
|
||||
str(QUIRC_LIB_DIR / "decode.c"),
|
||||
str(QUIRC_LIB_DIR / "version_db.c"),
|
||||
"-I",
|
||||
str(QUIRC_LIB_DIR),
|
||||
"-o",
|
||||
str(SO_PATH),
|
||||
]
|
||||
try:
|
||||
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _load_decoder() -> ctypes.CDLL | None:
|
||||
global _LIB
|
||||
if _LIB is not None:
|
||||
return _LIB
|
||||
|
||||
if not SO_PATH.exists():
|
||||
if not _build_decoder():
|
||||
return None
|
||||
|
||||
try:
|
||||
lib = ctypes.CDLL(str(SO_PATH))
|
||||
lib.iqpilot_decode_qr_gray.argtypes = [
|
||||
ctypes.POINTER(ctypes.c_uint8),
|
||||
ctypes.c_int,
|
||||
ctypes.c_int,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_int,
|
||||
]
|
||||
lib.iqpilot_decode_qr_gray.restype = ctypes.c_int
|
||||
_LIB = lib
|
||||
return _LIB
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def decode_qr(image: bytes | np.ndarray, width: int | None = None, height: int | None = None) -> list[str]:
|
||||
"""
|
||||
Decode QR payloads from a grayscale image.
|
||||
Accepts:
|
||||
- ndarray shape (H, W), uint8
|
||||
- bytes + explicit width/height
|
||||
"""
|
||||
arr: np.ndarray
|
||||
if isinstance(image, np.ndarray):
|
||||
if image.ndim != 2:
|
||||
raise ValueError("decode_qr expects grayscale ndarray with shape (H, W)")
|
||||
arr = np.ascontiguousarray(image, dtype=np.uint8)
|
||||
h, w = arr.shape
|
||||
else:
|
||||
if width is None or height is None:
|
||||
raise ValueError("width and height are required when passing raw bytes")
|
||||
arr = np.frombuffer(image, dtype=np.uint8).reshape((height, width))
|
||||
arr = np.ascontiguousarray(arr)
|
||||
h, w = arr.shape
|
||||
|
||||
if _pyzbar_decode is not None:
|
||||
try:
|
||||
pyzbar_results = _pyzbar_decode(arr)
|
||||
payloads = []
|
||||
for result in pyzbar_results:
|
||||
payload = result.data.decode("utf-8", errors="ignore").strip()
|
||||
if payload:
|
||||
payloads.append(payload)
|
||||
if payloads:
|
||||
return payloads
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
lib = _load_decoder()
|
||||
if lib is None:
|
||||
return []
|
||||
|
||||
out_size = 8192
|
||||
out_buf = ctypes.create_string_buffer(out_size)
|
||||
count = lib.iqpilot_decode_qr_gray(
|
||||
arr.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
|
||||
int(w),
|
||||
int(h),
|
||||
out_buf,
|
||||
out_size,
|
||||
)
|
||||
if count <= 0:
|
||||
return []
|
||||
|
||||
raw = out_buf.value.decode("utf-8", errors="ignore")
|
||||
return [line.strip() for line in raw.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def validate_lpa_activation_code(payload: str) -> tuple[bool, str]:
|
||||
if not payload.startswith("LPA:"):
|
||||
return False, "QR does not contain an LPA activation code"
|
||||
|
||||
parts = payload[4:].split("$")
|
||||
if len(parts) != 3:
|
||||
return False, "Invalid LPA format"
|
||||
|
||||
version, smdp, matching = [p.strip() for p in parts]
|
||||
if version != "1":
|
||||
return False, "Unsupported LPA version"
|
||||
if len(smdp) == 0 or "." not in smdp:
|
||||
return False, "Invalid SM-DP+ address"
|
||||
if len(matching) == 0:
|
||||
return False, "Missing matching ID"
|
||||
return True, ""
|
||||
|
||||
|
||||
def stable_code_key(payload: str) -> str:
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
18
iqpilot/system/hardware/tici/restart_modem.sh
Executable file
18
iqpilot/system/hardware/tici/restart_modem.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#nmcli connection modify --temporary lte gsm.home-only yes
|
||||
#nmcli connection modify --temporary lte gsm.auto-config yes
|
||||
#nmcli connection modify --temporary lte connection.autoconnect-retries 20
|
||||
sudo nmcli connection reload
|
||||
|
||||
sudo systemctl stop ModemManager
|
||||
nmcli con down lte
|
||||
nmcli con down blue-prime
|
||||
|
||||
# power cycle modem
|
||||
/usr/comma/lte/lte.sh stop_blocking
|
||||
/usr/comma/lte/lte.sh start
|
||||
|
||||
sudo systemctl restart NetworkManager
|
||||
#sudo systemctl restart ModemManager
|
||||
sudo ModemManager --debug
|
||||
233
iqpilot/system/hardware/tici/set_usb_storage.sh
Executable file
233
iqpilot/system/hardware/tici/set_usb_storage.sh
Executable file
@@ -0,0 +1,233 @@
|
||||
#!/bin/bash
|
||||
# USB mass-storage gadget exposing a snapshot of /data/media/0/realdata (dashcam clips + logs)
|
||||
# over the same configfs gadget mechanism as /usr/comma/set_adb.sh. openpilot keeps running; the
|
||||
# export is a read-only snapshot built at enable time, not a live view of realdata.
|
||||
#
|
||||
# The device only has one physical USB controller (UDC), so ADB and USB storage must live in the
|
||||
# SAME composite gadget (/config/usb_gadget/g1) rather than each owning their own. Earlier versions
|
||||
# of this script called /usr/comma/set_adb.sh as a black box and then unbound/rebound around it,
|
||||
# but that intermediate bind/unbind churn made the *next* bind flaky (functionfs needs its
|
||||
# userspace side, adbd, settled before the gadget can (re)bind). So instead we replicate set_adb.sh's
|
||||
# handful of setup lines directly here and do exactly one bind at the end, covering whichever
|
||||
# functions (ADB, mass storage) are currently enabled.
|
||||
#
|
||||
# Without composing like this, comma's adb-param-watcher systemd unit (which fires whenever
|
||||
# /data/params/d/AdbEnabled is touched, even to the same value) would rebuild g1 with only its own
|
||||
# functions and silently drop ours.
|
||||
|
||||
set -e
|
||||
|
||||
# serialize invocations: rapid toggling can otherwise race on the same /config/usb_gadget/g1 tree
|
||||
# and leave it in a half-built state
|
||||
LOCKFILE="/tmp/set_usb_storage.lock"
|
||||
exec 9>"$LOCKFILE"
|
||||
flock 9
|
||||
|
||||
IMG="/data/media/0/usb_storage.img"
|
||||
LOOP_MNT="/tmp/usb_storage_mnt"
|
||||
REALDATA="/data/media/0/realdata"
|
||||
UDC_NAME="a600000.dwc3"
|
||||
GADGET="/config/usb_gadget/g1"
|
||||
SAFETY_MARGIN_KB=$((2 * 1024 * 1024)) # keep 2GB free on /data after the image
|
||||
CAP_KB=$((4 * 1024 * 1024)) # never build more than a 4GB snapshot (FAT32 + dir overhead eats into this)
|
||||
|
||||
build_image() {
|
||||
avail_kb=$(df --output=avail -k /data | tail -1)
|
||||
budget_kb=$((avail_kb - SAFETY_MARGIN_KB))
|
||||
if [ "$budget_kb" -gt "$CAP_KB" ]; then
|
||||
budget_kb=$CAP_KB
|
||||
fi
|
||||
if [ "$budget_kb" -lt $((512 * 1024)) ]; then
|
||||
echo "Not enough free space on /data to build a USB storage snapshot" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building ${budget_kb}KB FAT32 snapshot image at $IMG"
|
||||
sudo rm -f "$IMG"
|
||||
sudo fallocate -l "${budget_kb}K" "$IMG" || sudo dd if=/dev/zero of="$IMG" bs=1M count=$((budget_kb / 1024))
|
||||
sudo mkfs.vfat -F 32 -n IQPILOT "$IMG"
|
||||
|
||||
sudo mkdir -p "$LOOP_MNT"
|
||||
LOOP_DEV=$(sudo losetup -f)
|
||||
sudo losetup "$LOOP_DEV" "$IMG"
|
||||
sudo mount -t vfat "$LOOP_DEV" "$LOOP_MNT"
|
||||
|
||||
# select the most recent files up to budget, then copy them in one rsync
|
||||
# pass (this script already runs as root, and one process beats thousands
|
||||
# of per-file forked sudo/mkdir/cp calls, which was previously the actual
|
||||
# bottleneck, not disk throughput).
|
||||
copy_budget_kb=$((budget_kb * 90 / 100))
|
||||
filelist=$(mktemp)
|
||||
find "$REALDATA" -type f -printf '%T@ %s %P\n' 2>/dev/null | sort -rn | awk -v budget="$copy_budget_kb" '
|
||||
{ used += int(($2 + 1023) / 1024); if (used > budget) { exit } print $3 }
|
||||
' > "$filelist"
|
||||
mkdir -p "$LOOP_MNT/realdata"
|
||||
# FAT32 has no concept of unix owner/group/perms, so don't ask rsync to preserve them
|
||||
rsync -rt --files-from="$filelist" "$REALDATA/" "$LOOP_MNT/realdata/"
|
||||
echo "Copied $(wc -l < "$filelist") files into snapshot"
|
||||
rm -f "$filelist"
|
||||
|
||||
sudo umount "$LOOP_MNT"
|
||||
sudo losetup -d "$LOOP_DEV"
|
||||
}
|
||||
|
||||
unbind() {
|
||||
if [ -d "$GADGET" ]; then
|
||||
cd "$GADGET"
|
||||
echo "" | sudo tee UDC >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
set_attr() {
|
||||
[ "$(cat "$1" 2>/dev/null)" = "$2" ] && return 0
|
||||
echo "$2" | sudo tee "$1" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
ensure_base() {
|
||||
if ! mountpoint -q /config; then
|
||||
sudo mount -t configfs none /config
|
||||
fi
|
||||
sudo mkdir -p "$GADGET/strings/0x409" "$GADGET/configs/c.1/strings/0x409"
|
||||
cd "$GADGET"
|
||||
# `[ -s ]` never guards a configfs attribute: an unset idVendor still reads back
|
||||
# as "0x0000", so those writes were all skipped and the gadget stayed nameless
|
||||
set_attr idVendor 0x04D8
|
||||
set_attr idProduct 0x1235
|
||||
set_attr strings/0x409/serialnumber "$(sed -e 's/^.*androidboot.serialno=//' -e 's/ .*$//' /proc/cmdline)"
|
||||
set_attr strings/0x409/manufacturer "comma.ai"
|
||||
set_attr strings/0x409/product "IQ.Pilot"
|
||||
set_attr configs/c.1/MaxPower 250
|
||||
set_attr configs/c.1/strings/0x409/configuration "IQ.Pilot"
|
||||
}
|
||||
|
||||
add_adb() {
|
||||
# same rationale as add_mass_storage: start from a clean slate to avoid stale busy attributes
|
||||
remove_adb
|
||||
cd "$GADGET"
|
||||
sudo mkdir -p functions/ffs.adb
|
||||
sudo mkdir -p /dev/usb-ffs/adb
|
||||
if ! mountpoint -q /dev/usb-ffs/adb; then
|
||||
sudo mount -t functionfs adb /dev/usb-ffs/adb
|
||||
fi
|
||||
sudo rm -f configs/c.1/ffs.adb
|
||||
sudo ln -s functions/ffs.adb configs/c.1/
|
||||
setprop service.adb.tcp.port -1 2>/dev/null || true
|
||||
sudo systemctl start adbd
|
||||
# adbd needs a moment to open the ffs endpoint and negotiate descriptors before the gadget can bind
|
||||
sleep 1
|
||||
}
|
||||
|
||||
remove_adb() {
|
||||
sudo systemctl stop adbd || true
|
||||
if [ -d "$GADGET" ]; then
|
||||
cd "$GADGET"
|
||||
sudo rm -f configs/c.1/ffs.adb
|
||||
sudo umount /dev/usb-ffs/adb 2>/dev/null || true
|
||||
sudo rmdir functions/ffs.adb 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ncm carries the usb0 ethernet link. ADB needs it, but so does the Mac-backed
|
||||
# model worker with ADB off, so it is enabled independently of either.
|
||||
# the kernel randomises the ncm MACs every boot, so macOS sees a new adapter each
|
||||
# time and orphans the network service holding the link's static address
|
||||
ncm_id() {
|
||||
local id
|
||||
id=$(tr -dc '0-9a-f' < /data/params/d/DongleId 2>/dev/null | tail -c 6)
|
||||
[ ${#id} -eq 6 ] || id="000001"
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
add_ncm() {
|
||||
remove_ncm
|
||||
cd "$GADGET"
|
||||
sudo mkdir -p functions/ncm.0
|
||||
local id
|
||||
id=$(ncm_id)
|
||||
# best effort: some kernels create the ncm netdev lazily and fail these writes
|
||||
# with ENODEV, and a pinned MAC is never worth losing the whole gadget over
|
||||
echo "02:49:51:${id:0:2}:${id:2:2}:${id:4:2}" | sudo tee functions/ncm.0/host_addr >/dev/null 2>&1 || true
|
||||
echo "06:49:51:${id:0:2}:${id:2:2}:${id:4:2}" | sudo tee functions/ncm.0/dev_addr >/dev/null 2>&1 || true
|
||||
sudo rm -f configs/c.1/ncm.0
|
||||
sudo ln -s functions/ncm.0 configs/c.1/
|
||||
}
|
||||
|
||||
remove_ncm() {
|
||||
if [ -d "$GADGET" ]; then
|
||||
cd "$GADGET"
|
||||
sudo rm -f configs/c.1/ncm.0
|
||||
sudo rmdir functions/ncm.0 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
add_mass_storage() {
|
||||
# a function group that's ever been bound before can refuse attribute writes ("Device or
|
||||
# resource busy") until it's torn down and recreated fresh, so always start from a clean slate
|
||||
remove_mass_storage
|
||||
cd "$GADGET"
|
||||
sudo mkdir -p functions/mass_storage.0
|
||||
echo 1 | sudo tee functions/mass_storage.0/stall >/dev/null
|
||||
echo 1 | sudo tee functions/mass_storage.0/lun.0/removable >/dev/null
|
||||
echo 1 | sudo tee functions/mass_storage.0/lun.0/ro >/dev/null
|
||||
echo "$IMG" | sudo tee functions/mass_storage.0/lun.0/file >/dev/null
|
||||
sudo rm -f configs/c.1/mass_storage.0
|
||||
sudo ln -s functions/mass_storage.0 configs/c.1/
|
||||
}
|
||||
|
||||
remove_mass_storage() {
|
||||
if [ -d "$GADGET" ]; then
|
||||
cd "$GADGET"
|
||||
sudo rm -f configs/c.1/mass_storage.0
|
||||
sudo rmdir functions/mass_storage.0 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
bind() {
|
||||
cd "$GADGET"
|
||||
for attempt in $(seq 1 20); do
|
||||
if echo "$UDC_NAME" | sudo tee UDC >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
echo "$UDC_NAME" | sudo tee UDC
|
||||
}
|
||||
|
||||
read_bool_param() {
|
||||
[ -f "$1" ] && [ "$(< "$1")" == "1" ]
|
||||
}
|
||||
|
||||
USB_STORAGE_ENABLE=0
|
||||
read_bool_param "/data/params/d/UsbStorageEnabled" && USB_STORAGE_ENABLE=1
|
||||
ADB_ENABLE=0
|
||||
read_bool_param "/data/params/d/AdbEnabled" && ADB_ENABLE=1
|
||||
EMAC_ENABLE=0
|
||||
read_bool_param "/data/params/d/IQEmacEnabled" && EMAC_ENABLE=1
|
||||
|
||||
unbind
|
||||
ensure_base
|
||||
|
||||
if [ "$ADB_ENABLE" == "1" ] || [ "$EMAC_ENABLE" == "1" ]; then
|
||||
add_ncm
|
||||
else
|
||||
remove_ncm
|
||||
fi
|
||||
|
||||
if [ "$ADB_ENABLE" == "1" ]; then
|
||||
add_adb
|
||||
else
|
||||
remove_adb
|
||||
fi
|
||||
|
||||
if [ "$USB_STORAGE_ENABLE" == "1" ]; then
|
||||
echo "Enabling USB storage mode"
|
||||
if [ ! -f "$IMG" ] || [ "$1" == "--rebuild" ]; then
|
||||
build_image
|
||||
fi
|
||||
add_mass_storage
|
||||
else
|
||||
echo "Disabling USB storage mode"
|
||||
remove_mass_storage
|
||||
fi
|
||||
|
||||
bind
|
||||
0
iqpilot/system/hardware/tici/tests/__init__.py
Normal file
0
iqpilot/system/hardware/tici/tests/__init__.py
Normal file
73
iqpilot/system/hardware/tici/tests/compare_casync_manifest.py
Executable file
73
iqpilot/system/hardware/tici/tests/compare_casync_manifest.py
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import collections
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
import iqpilot.system.hardware.tici.casync as casync
|
||||
|
||||
|
||||
def get_chunk_download_size(chunk):
|
||||
sha = chunk.sha.hex()
|
||||
path = os.path.join(remote_url, sha[:4], sha + ".cacnk")
|
||||
if os.path.isfile(path):
|
||||
return os.path.getsize(path)
|
||||
else:
|
||||
r = requests.head(path, timeout=10)
|
||||
r.raise_for_status()
|
||||
return int(r.headers['content-length'])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description='Compute overlap between two casync manifests')
|
||||
parser.add_argument('frm')
|
||||
parser.add_argument('to')
|
||||
args = parser.parse_args()
|
||||
|
||||
frm = casync.parse_caibx(args.frm)
|
||||
to = casync.parse_caibx(args.to)
|
||||
remote_url = args.to.replace('.caibx', '')
|
||||
|
||||
most_common = collections.Counter(t.sha for t in to).most_common(1)[0][0]
|
||||
|
||||
frm_dict = casync.build_chunk_dict(frm)
|
||||
|
||||
# Get content-length for each chunk
|
||||
with multiprocessing.Pool() as pool:
|
||||
szs = list(tqdm(pool.imap(get_chunk_download_size, to), total=len(to)))
|
||||
chunk_sizes = {t.sha: sz for (t, sz) in zip(to, szs, strict=True)}
|
||||
|
||||
sources: dict[str, list[int]] = {
|
||||
'seed': [],
|
||||
'remote_uncompressed': [],
|
||||
'remote_compressed': [],
|
||||
}
|
||||
|
||||
for chunk in to:
|
||||
# Assume most common chunk is the zero chunk
|
||||
if chunk.sha == most_common:
|
||||
continue
|
||||
|
||||
if chunk.sha in frm_dict:
|
||||
sources['seed'].append(chunk.length)
|
||||
else:
|
||||
sources['remote_uncompressed'].append(chunk.length)
|
||||
sources['remote_compressed'].append(chunk_sizes[chunk.sha])
|
||||
|
||||
print()
|
||||
print("Update statistics (excluding zeros)")
|
||||
print()
|
||||
print("Download only with no seed:")
|
||||
print(f" Remote (uncompressed)\t\t{sum(sources['seed'] + sources['remote_uncompressed']) / 1000 / 1000:.2f} MB\tn = {len(to)}")
|
||||
print(f" Remote (compressed download)\t{sum(chunk_sizes.values()) / 1000 / 1000:.2f} MB\tn = {len(to)}")
|
||||
print()
|
||||
print("Upgrade with seed partition:")
|
||||
print(f" Seed (uncompressed)\t\t{sum(sources['seed']) / 1000 / 1000:.2f} MB\t\t\t\tn = {len(sources['seed'])}")
|
||||
sz, n = sum(sources['remote_uncompressed']), len(sources['remote_uncompressed'])
|
||||
print(f" Remote (uncompressed)\t\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}")
|
||||
sz, n = sum(sources['remote_compressed']), len(sources['remote_compressed'])
|
||||
print(f" Remote (compressed download)\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}")
|
||||
37
iqpilot/system/hardware/tici/tests/test_agnos_updater.py
Normal file
37
iqpilot/system/hardware/tici/tests/test_agnos_updater.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
|
||||
TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
||||
MANIFESTS = [
|
||||
os.path.join(TEST_DIR, "../agnos.json"),
|
||||
os.path.join(TEST_DIR, "../agnos_tici_15_1.json"),
|
||||
]
|
||||
|
||||
IMAGE_HOST = "git.konn3kt.com"
|
||||
|
||||
XZ_MAGIC = b"\xfd7zXZ\x00"
|
||||
LFS_POINTER_MAGIC = b"version https://git-lfs"
|
||||
|
||||
|
||||
class TestAgnosUpdater:
|
||||
|
||||
def test_manifest(self):
|
||||
for manifest in MANIFESTS:
|
||||
with open(manifest) as f:
|
||||
m = json.load(f)
|
||||
|
||||
for img in m:
|
||||
assert img['url'].split('/')[2] == IMAGE_HOST
|
||||
if not img['sparse']:
|
||||
assert img['hash'] == img['hash_raw']
|
||||
|
||||
s = requests.Session()
|
||||
s.trust_env = False
|
||||
r = s.get(img['url'], timeout=10, stream=True,
|
||||
headers={"User-Agent": "IQOS-Updater"})
|
||||
if r.status_code in (401, 403, 404):
|
||||
continue
|
||||
head = next(r.iter_content(chunk_size=256), b"") or b""
|
||||
assert not head.startswith(XZ_MAGIC), f"{img['name']}: anonymous request served image content"
|
||||
assert not head.startswith(LFS_POINTER_MAGIC), f"{img['name']}: anonymous request served the LFS pointer"
|
||||
66
iqpilot/system/hardware/tici/tests/test_amplifier.py
Normal file
66
iqpilot/system/hardware/tici/tests/test_amplifier.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import pytest
|
||||
import time
|
||||
import random
|
||||
import subprocess
|
||||
|
||||
from panda import Panda
|
||||
from iqpilot.system.hardware import HARDWARE
|
||||
from iqpilot.system.hardware.tici.hardware import Tici
|
||||
from iqpilot.system.hardware.tici.amplifier import Amplifier
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestAmplifier:
|
||||
|
||||
def setup_method(self):
|
||||
# clear dmesg
|
||||
subprocess.check_call("sudo dmesg -C", shell=True)
|
||||
|
||||
HARDWARE.reset_internal_panda()
|
||||
Panda.wait_for_panda(None, 30)
|
||||
self.panda = Panda()
|
||||
|
||||
def teardown_method(self):
|
||||
HARDWARE.reset_internal_panda()
|
||||
|
||||
def _check_for_i2c_errors(self, expected):
|
||||
dmesg = subprocess.check_output("dmesg", shell=True, encoding='utf8')
|
||||
i2c_lines = [l for l in dmesg.strip().splitlines() if 'i2c_geni a88000.i2c' in l]
|
||||
i2c_str = '\n'.join(i2c_lines)
|
||||
|
||||
if not expected:
|
||||
return len(i2c_lines) == 0
|
||||
else:
|
||||
return "i2c error :-107" in i2c_str or "Bus arbitration lost" in i2c_str
|
||||
|
||||
def test_init(self):
|
||||
amp = Amplifier(debug=True)
|
||||
r = amp.initialize_configuration(Tici().get_device_type())
|
||||
assert r
|
||||
assert self._check_for_i2c_errors(False)
|
||||
|
||||
def test_shutdown(self):
|
||||
amp = Amplifier(debug=True)
|
||||
for _ in range(10):
|
||||
r = amp.set_global_shutdown(True)
|
||||
r = amp.set_global_shutdown(False)
|
||||
# amp config should be successful, with no i2c errors
|
||||
assert r
|
||||
assert self._check_for_i2c_errors(False)
|
||||
|
||||
def test_init_while_siren_play(self):
|
||||
for _ in range(10):
|
||||
self.panda.set_siren(False)
|
||||
time.sleep(0.1)
|
||||
|
||||
self.panda.set_siren(True)
|
||||
time.sleep(random.randint(0, 5))
|
||||
|
||||
amp = Amplifier(debug=True)
|
||||
r = amp.initialize_configuration(Tici().get_device_type())
|
||||
assert r
|
||||
|
||||
if self._check_for_i2c_errors(True):
|
||||
break
|
||||
else:
|
||||
pytest.fail("didn't hit any i2c errors")
|
||||
238
iqpilot/system/hardware/tici/tests/test_esim.py
Normal file
238
iqpilot/system/hardware/tici/tests/test_esim.py
Normal file
@@ -0,0 +1,238 @@
|
||||
import pytest
|
||||
|
||||
from iqpilot.system.hardware import HARDWARE
|
||||
from iqpilot.system.hardware.base import LPAError, LPAProfileNotFoundError, Profile
|
||||
from iqpilot.system.hardware.tici import lpa as lpa_module
|
||||
from iqpilot.system.hardware.tici.esim_manager import EsimManager
|
||||
|
||||
# https://euicc-manual.osmocom.org/docs/rsp/known-test-profile
|
||||
# iccid is always the same for the given activation code
|
||||
TEST_ACTIVATION_CODE = 'LPA:1$rsp.truphone.com$QRF-BETTERROAMING-PMRDGIR2EARDEIT5'
|
||||
TEST_ICCID = '8944476500001944011'
|
||||
|
||||
TEST_NICKNAME = 'test_profile'
|
||||
|
||||
def cleanup():
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
try:
|
||||
lpa.delete_profile(TEST_ICCID)
|
||||
except LPAProfileNotFoundError:
|
||||
pass
|
||||
lpa.process_notifications()
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestEsim:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cleanup()
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
cleanup()
|
||||
|
||||
def test_provision_enable_disable(self):
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
current_active = lpa.get_active_profile()
|
||||
|
||||
lpa.download_profile(TEST_ACTIVATION_CODE, TEST_NICKNAME)
|
||||
assert any(p.iccid == TEST_ICCID and p.nickname == TEST_NICKNAME for p in lpa.list_profiles())
|
||||
|
||||
lpa.enable_profile(TEST_ICCID)
|
||||
new_active = lpa.get_active_profile()
|
||||
assert new_active is not None
|
||||
assert new_active.iccid == TEST_ICCID
|
||||
assert new_active.nickname == TEST_NICKNAME
|
||||
|
||||
lpa.disable_profile(TEST_ICCID)
|
||||
new_active = lpa.get_active_profile()
|
||||
assert new_active is None
|
||||
|
||||
if current_active:
|
||||
lpa.enable_profile(current_active.iccid)
|
||||
|
||||
|
||||
class TestEsimDeleteHandling:
|
||||
def test_delete_ignores_notification_cleanup_if_profile_is_gone(self, monkeypatch):
|
||||
target_iccid = "89012804332267989477"
|
||||
lpa = lpa_module.TiciLPA()
|
||||
|
||||
monkeypatch.setattr(lpa, "_validate_profile_exists", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "get_active_profile", lambda: Profile("8901240527117095243", "US Mobile", True, "Wireless"))
|
||||
monkeypatch.setattr(lpa, "_restart_modem", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
lpa,
|
||||
"list_profiles",
|
||||
lambda: [Profile("8901240527117095243", "US Mobile", True, "Wireless")],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(lpa, "_ensure_client", lambda: object())
|
||||
monkeypatch.setattr(lpa_module, "delete_profile", lambda client, iccid: None)
|
||||
|
||||
def fail_notifications(client):
|
||||
raise RuntimeError('AT command failed (AT+CGLA=2,16,"80E2910003BF2800"): AT command failed')
|
||||
|
||||
monkeypatch.setattr(lpa_module, "process_notifications", fail_notifications)
|
||||
|
||||
lpa.delete_profile(target_iccid)
|
||||
|
||||
def test_delete_raises_clear_error_if_profile_still_present_after_cleanup_failure(self, monkeypatch):
|
||||
target_iccid = "89012804332267989477"
|
||||
lpa = lpa_module.TiciLPA()
|
||||
|
||||
monkeypatch.setattr(lpa, "_validate_profile_exists", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "get_active_profile", lambda: Profile("8901240527117095243", "US Mobile", True, "Wireless"))
|
||||
monkeypatch.setattr(lpa, "_restart_modem", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
lpa,
|
||||
"list_profiles",
|
||||
lambda: [
|
||||
Profile("8901240527117095243", "US Mobile", True, "Wireless"),
|
||||
Profile(target_iccid, "RedPocket", False, "RedPocket"),
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(lpa, "_ensure_client", lambda: object())
|
||||
monkeypatch.setattr(lpa_module, "delete_profile", lambda client, iccid: None)
|
||||
|
||||
def fail_notifications(client):
|
||||
raise RuntimeError('AT command failed (AT+CGLA=2,16,"80E2910003BF2800"): AT command failed')
|
||||
|
||||
monkeypatch.setattr(lpa_module, "process_notifications", fail_notifications)
|
||||
|
||||
with pytest.raises(LPAError, match="Profile delete did not finish cleanly"):
|
||||
lpa.delete_profile(target_iccid)
|
||||
|
||||
def test_manager_maps_notification_cleanup_error(self):
|
||||
error = LPAError('AT command failed (AT+CGLA=2,16,"80E2910003BF2800"): AT command failed')
|
||||
assert EsimManager._map_error(error) == "Modem notification cleanup failed; refresh profiles"
|
||||
|
||||
|
||||
class TestEsimNotificationCleanupRecovery:
|
||||
def test_switch_ignores_notification_cleanup_if_target_is_enabled(self, monkeypatch):
|
||||
target_iccid = "8901240527117194095"
|
||||
lpa = lpa_module.TiciLPA()
|
||||
|
||||
monkeypatch.setattr(lpa, "_validate_profile_exists", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "_ensure_switchable_profile", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "get_active_profile", lambda: Profile("8901240527117113293", "US Mobile", True, "Wireless"))
|
||||
monkeypatch.setattr(lpa, "_wait_for_modem", lambda: None)
|
||||
monkeypatch.setattr(lpa, "_ensure_client", lambda: type("Client", (), {"channel": "2", "_use_csim": False})())
|
||||
monkeypatch.setattr(
|
||||
lpa,
|
||||
"list_profiles",
|
||||
lambda: [
|
||||
Profile("8901240527117113293", "US Mobile", False, "Wireless"),
|
||||
Profile(target_iccid, "T-Mobile", True, "Wireless"),
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(lpa_module, "enable_profile", lambda client, iccid, refresh=True: None)
|
||||
monkeypatch.setattr(
|
||||
lpa_module,
|
||||
"process_notifications",
|
||||
lambda client: (_ for _ in ()).throw(RuntimeError('AT command failed (AT+CGLA=2,16,"80E2910003BF2800"): AT command failed')),
|
||||
)
|
||||
|
||||
lpa.switch_profile(target_iccid)
|
||||
|
||||
@pytest.mark.parametrize(("is_eg25", "expected_refresh", "expected_waits", "expected_reboots"), [
|
||||
(True, True, 1, 0),
|
||||
(False, False, 0, 1),
|
||||
])
|
||||
def test_switch_profile_uses_modem_specific_refresh_behavior(self, monkeypatch, is_eg25, expected_refresh, expected_waits, expected_reboots):
|
||||
target_iccid = "8901240527117194095"
|
||||
lpa = object.__new__(lpa_module.TiciLPA)
|
||||
lpa._is_eg25 = is_eg25
|
||||
lpa.verbose = False
|
||||
|
||||
waits = []
|
||||
reboots = []
|
||||
refresh_values = []
|
||||
|
||||
monkeypatch.setattr(lpa, "_validate_profile_exists", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "_ensure_switchable_profile", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "get_active_profile", lambda: Profile("8901240527117113293", "US Mobile", True, "Wireless"))
|
||||
monkeypatch.setattr(lpa, "_wait_for_modem", lambda: waits.append(True))
|
||||
monkeypatch.setattr(lpa, "_restart_modem", lambda: reboots.append(True))
|
||||
monkeypatch.setattr(lpa, "_with_lpa_error", lambda fn: fn())
|
||||
monkeypatch.setattr(lpa, "_ensure_client", lambda: type("Client", (), {"channel": "2", "_use_csim": False})())
|
||||
monkeypatch.setattr(
|
||||
lpa,
|
||||
"_process_notifications_after_state_change",
|
||||
lambda validator, _recovery_message, _failure_message: validator(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
lpa,
|
||||
"list_profiles",
|
||||
lambda: [
|
||||
Profile("8901240527117113293", "US Mobile", False, "Wireless"),
|
||||
Profile(target_iccid, "T-Mobile", True, "Wireless"),
|
||||
],
|
||||
)
|
||||
|
||||
def fake_enable_profile(client, iccid, refresh=True):
|
||||
refresh_values.append(refresh)
|
||||
|
||||
monkeypatch.setattr(lpa_module, "enable_profile", fake_enable_profile)
|
||||
|
||||
lpa.switch_profile(target_iccid)
|
||||
|
||||
assert refresh_values == [expected_refresh]
|
||||
assert len(waits) == expected_waits
|
||||
assert len(reboots) == expected_reboots
|
||||
|
||||
def test_download_ignores_notification_cleanup_if_profile_exists(self, monkeypatch, mocker):
|
||||
target_iccid = "8901240527117194095"
|
||||
lpa = lpa_module.TiciLPA()
|
||||
profiles = [
|
||||
[Profile("8901240527117113293", "US Mobile", True, "Wireless")],
|
||||
[
|
||||
Profile("8901240527117113293", "US Mobile", True, "Wireless"),
|
||||
Profile(target_iccid, "T-Mobile", False, "Wireless"),
|
||||
],
|
||||
[
|
||||
Profile("8901240527117113293", "US Mobile", True, "Wireless"),
|
||||
Profile(target_iccid, "T-Mobile", False, "Wireless"),
|
||||
],
|
||||
]
|
||||
|
||||
monkeypatch.setattr(lpa, "_ensure_client", lambda: object())
|
||||
monkeypatch.setattr(lpa, "_wait_for_modem", lambda: None)
|
||||
profile_states = iter(profiles)
|
||||
current_profiles = profiles[-1]
|
||||
|
||||
def list_profiles():
|
||||
nonlocal current_profiles
|
||||
current_profiles = next(profile_states, current_profiles)
|
||||
return current_profiles
|
||||
|
||||
monkeypatch.setattr(lpa, "list_profiles", list_profiles)
|
||||
monkeypatch.setattr(lpa_module, "download_profile", lambda client, qr: target_iccid)
|
||||
set_nickname = mocker.MagicMock()
|
||||
monkeypatch.setattr(lpa_module, "set_profile_nickname", set_nickname)
|
||||
monkeypatch.setattr(
|
||||
lpa_module,
|
||||
"process_notifications",
|
||||
lambda client: (_ for _ in ()).throw(RuntimeError('AT command failed (AT+CGLA=2,16,"80E2910003BF2800"): AT command failed')),
|
||||
)
|
||||
|
||||
lpa.download_profile(TEST_ACTIVATION_CODE, "T-Mobile")
|
||||
|
||||
set_nickname.assert_called_once_with(mocker.ANY, target_iccid, "T-Mobile")
|
||||
|
||||
|
||||
class TestEsimManagerSupportGating:
|
||||
def test_refresh_profiles_does_not_touch_lpa_without_euicc(self, monkeypatch, mocker):
|
||||
manager = EsimManager()
|
||||
|
||||
monkeypatch.setattr(manager, "_query_euicc_support", lambda: False)
|
||||
manager._params = mocker.MagicMock()
|
||||
manager._params.get.return_value = None
|
||||
manager._params.get_bool.return_value = True
|
||||
monkeypatch.setattr(HARDWARE, "get_device_type", lambda: "tici")
|
||||
monkeypatch.setattr(HARDWARE, "get_sim_lpa", lambda: (_ for _ in ()).throw(AssertionError("LPA should not be touched")))
|
||||
|
||||
manager.refresh_profiles()
|
||||
|
||||
assert manager.get_state().profiles == []
|
||||
assert manager.get_state().message == "Insert the original comma SIM card that came with the device to use eSIM"
|
||||
102
iqpilot/system/hardware/tici/tests/test_hardware.py
Normal file
102
iqpilot/system/hardware/tici/tests/test_hardware.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from iqpilot.system.hardware.tici.hardware import (
|
||||
MM_MODEM_ACCESS_TECHNOLOGY_LTE,
|
||||
MM_MODEM_STATE,
|
||||
NMActiveConnectionState,
|
||||
Tici,
|
||||
)
|
||||
from iqpilot.cereal import log
|
||||
|
||||
|
||||
def _make_connection(mocker, connection_type: str, state: int):
|
||||
connection = mocker.MagicMock()
|
||||
|
||||
def get_side_effect(_iface, prop, **_kwargs):
|
||||
values = {
|
||||
"Type": connection_type,
|
||||
"State": state,
|
||||
}
|
||||
return values[prop]
|
||||
|
||||
connection.Get.side_effect = get_side_effect
|
||||
return connection
|
||||
|
||||
|
||||
def test_reboot_modem_falls_back_to_direct_at(monkeypatch, mocker):
|
||||
device = Tici()
|
||||
direct_runner = mocker.MagicMock()
|
||||
|
||||
monkeypatch.setattr(device, "get_modem", mocker.MagicMock(side_effect=ModuleNotFoundError("dbus")))
|
||||
monkeypatch.setattr(device, "_run_direct_modem_command", direct_runner)
|
||||
|
||||
device.reboot_modem()
|
||||
|
||||
assert direct_runner.call_args_list == [
|
||||
(("AT+CFUN=0",), {}),
|
||||
(("AT+CFUN=1",), {}),
|
||||
]
|
||||
|
||||
|
||||
def test_get_network_type_ignores_non_activated_cellular(mocker):
|
||||
device = Tici()
|
||||
primary = _make_connection(mocker, "gsm", NMActiveConnectionState.ACTIVATING)
|
||||
bus = mocker.MagicMock()
|
||||
bus.get_object.return_value = primary
|
||||
nm = mocker.MagicMock()
|
||||
nm.Get.return_value = "/primary"
|
||||
|
||||
device.__dict__["bus"] = bus
|
||||
device.__dict__["nm"] = nm
|
||||
|
||||
assert device.get_network_type() == log.DeviceState.NetworkType.none
|
||||
|
||||
|
||||
def test_get_network_type_requires_registered_modem(monkeypatch, mocker):
|
||||
device = Tici()
|
||||
primary = _make_connection(mocker, "gsm", NMActiveConnectionState.ACTIVATED)
|
||||
cellular = _make_connection(mocker, "gsm", NMActiveConnectionState.ACTIVATED)
|
||||
bus = mocker.MagicMock()
|
||||
bus.get_object.side_effect = [primary, cellular]
|
||||
nm = mocker.MagicMock()
|
||||
nm.Get.side_effect = ["/primary", ["/cellular"]]
|
||||
modem = mocker.MagicMock()
|
||||
|
||||
def modem_get_side_effect(_iface, prop, **_kwargs):
|
||||
values = {
|
||||
"State": MM_MODEM_STATE.SEARCHING,
|
||||
"AccessTechnologies": MM_MODEM_ACCESS_TECHNOLOGY_LTE,
|
||||
}
|
||||
return values[prop]
|
||||
|
||||
modem.Get.side_effect = modem_get_side_effect
|
||||
|
||||
device.__dict__["bus"] = bus
|
||||
device.__dict__["nm"] = nm
|
||||
monkeypatch.setattr(device, "get_modem", mocker.MagicMock(return_value=modem))
|
||||
|
||||
assert device.get_network_type() == log.DeviceState.NetworkType.none
|
||||
|
||||
|
||||
def test_get_network_type_reports_lte_for_registered_modem(monkeypatch, mocker):
|
||||
device = Tici()
|
||||
primary = _make_connection(mocker, "gsm", NMActiveConnectionState.ACTIVATED)
|
||||
cellular = _make_connection(mocker, "gsm", NMActiveConnectionState.ACTIVATED)
|
||||
bus = mocker.MagicMock()
|
||||
bus.get_object.side_effect = [primary, cellular]
|
||||
nm = mocker.MagicMock()
|
||||
nm.Get.side_effect = ["/primary", ["/cellular"]]
|
||||
modem = mocker.MagicMock()
|
||||
|
||||
def modem_get_side_effect(_iface, prop, **_kwargs):
|
||||
values = {
|
||||
"State": MM_MODEM_STATE.CONNECTED,
|
||||
"AccessTechnologies": MM_MODEM_ACCESS_TECHNOLOGY_LTE,
|
||||
}
|
||||
return values[prop]
|
||||
|
||||
modem.Get.side_effect = modem_get_side_effect
|
||||
|
||||
device.__dict__["bus"] = bus
|
||||
device.__dict__["nm"] = nm
|
||||
monkeypatch.setattr(device, "get_modem", mocker.MagicMock(return_value=modem))
|
||||
|
||||
assert device.get_network_type() == log.DeviceState.NetworkType.cell4G
|
||||
53
iqpilot/system/hardware/tici/tests/test_lpa_activation.py
Normal file
53
iqpilot/system/hardware/tici/tests/test_lpa_activation.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
from iqpilot.system.hardware.tici import qr_decode as qr_decode_module
|
||||
from iqpilot.system.hardware.tici.lpa import parse_lpa_activation_code
|
||||
from iqpilot.system.hardware.tici.qr_decode import validate_lpa_activation_code
|
||||
|
||||
|
||||
def test_parse_valid_activation_code():
|
||||
version, smdp, matching = parse_lpa_activation_code("LPA:1$rsp.truphone.com$QRF-BETTERROAMING")
|
||||
assert version == "1"
|
||||
assert smdp == "rsp.truphone.com"
|
||||
assert matching == "QRF-BETTERROAMING"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [
|
||||
"",
|
||||
"foo",
|
||||
"LPA:2$rsp.truphone.com$abc",
|
||||
"LPA:1$$abc",
|
||||
"LPA:1$rsp.truphone.com$",
|
||||
"LPA:1$rsp.truphone.com",
|
||||
])
|
||||
def test_parse_invalid_activation_code(code):
|
||||
with pytest.raises(ValueError):
|
||||
parse_lpa_activation_code(code)
|
||||
|
||||
|
||||
def test_qr_validator_valid():
|
||||
valid, reason = validate_lpa_activation_code("LPA:1$rsp.truphone.com$QRF-123")
|
||||
assert valid
|
||||
assert reason == ""
|
||||
|
||||
|
||||
def test_qr_validator_invalid():
|
||||
valid, reason = validate_lpa_activation_code("https://example.com")
|
||||
assert not valid
|
||||
assert reason
|
||||
|
||||
|
||||
def test_decode_qr_prefers_pyzbar(monkeypatch):
|
||||
class FakeResult:
|
||||
data = b"LPA:1$rsp.truphone.com$QRF-123"
|
||||
|
||||
monkeypatch.setattr(qr_decode_module, "_pyzbar_decode", lambda arr: [FakeResult()])
|
||||
|
||||
def fail_load_decoder():
|
||||
raise AssertionError("quirc fallback should not be used when pyzbar succeeds")
|
||||
|
||||
monkeypatch.setattr(qr_decode_module, "_load_decoder", fail_load_decoder)
|
||||
|
||||
payloads = qr_decode_module.decode_qr(np.zeros((4, 4), dtype=np.uint8))
|
||||
assert payloads == ["LPA:1$rsp.truphone.com$QRF-123"]
|
||||
128
iqpilot/system/hardware/tici/tests/test_power_draw.py
Normal file
128
iqpilot/system/hardware/tici/tests/test_power_draw.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from collections import defaultdict, deque
|
||||
import pytest
|
||||
import time
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from tabulate import tabulate
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.cereal.services import SERVICE_LIST
|
||||
from iqdbc.car.car_helpers import get_demo_car_params
|
||||
from iqpilot.common.mock import mock_messages
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.system.hardware.tici.power_monitor import get_power
|
||||
from iqpilot.system.manager.process_config import managed_processes
|
||||
from iqpilot.system.manager.manager import manager_cleanup
|
||||
|
||||
SAMPLE_TIME = 8 # seconds to sample power
|
||||
MAX_WARMUP_TIME = 30 # seconds to wait for SAMPLE_TIME consecutive valid samples
|
||||
|
||||
@dataclass
|
||||
class Proc:
|
||||
procs: list[str]
|
||||
power: float
|
||||
msgs: list[str]
|
||||
rtol: float = 0.05
|
||||
atol: float = 0.12
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return '+'.join(self.procs)
|
||||
|
||||
|
||||
PROCS = [
|
||||
Proc(['camerad'], 1.65, atol=0.4, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']),
|
||||
Proc(['modeld'], 1.24, atol=0.2, msgs=['modelV2']),
|
||||
Proc(['dmonitoringmodeld'], 0.65, atol=0.35, msgs=['driverStateV2']),
|
||||
Proc(['encoderd'], 0.23, msgs=[]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestPowerDraw:
|
||||
|
||||
def setup_method(self):
|
||||
Params().put("CarParams", get_demo_car_params().to_bytes())
|
||||
|
||||
# wait a bit for power save to disable
|
||||
time.sleep(5)
|
||||
|
||||
def teardown_method(self):
|
||||
manager_cleanup()
|
||||
|
||||
def get_expected_messages(self, proc):
|
||||
return int(sum(SAMPLE_TIME * SERVICE_LIST[msg].frequency for msg in proc.msgs))
|
||||
|
||||
def valid_msg_count(self, proc, msg_counts):
|
||||
msgs_received = sum(msg_counts[msg] for msg in proc.msgs)
|
||||
msgs_expected = self.get_expected_messages(proc)
|
||||
return np.isclose(msgs_expected, msgs_received, rtol=.02, atol=2)
|
||||
|
||||
def valid_power_draw(self, proc, used):
|
||||
return np.isclose(used, proc.power, rtol=proc.rtol, atol=proc.atol)
|
||||
|
||||
def tabulate_msg_counts(self, msgs_and_power):
|
||||
msg_counts = defaultdict(int)
|
||||
for _, counts in msgs_and_power:
|
||||
for msg, count in counts.items():
|
||||
msg_counts[msg] += count
|
||||
return msg_counts
|
||||
|
||||
def get_power_with_warmup_for_target(self, proc, prev):
|
||||
socks = {msg: messaging.sub_sock(msg) for msg in proc.msgs}
|
||||
for sock in socks.values():
|
||||
messaging.drain_sock_raw(sock)
|
||||
|
||||
msgs_and_power = deque([], maxlen=SAMPLE_TIME)
|
||||
|
||||
start_time = time.monotonic()
|
||||
|
||||
while (time.monotonic() - start_time) < MAX_WARMUP_TIME:
|
||||
power = get_power(1)
|
||||
iteration_msg_counts = {}
|
||||
for msg,sock in socks.items():
|
||||
iteration_msg_counts[msg] = len(messaging.drain_sock_raw(sock))
|
||||
msgs_and_power.append((power, iteration_msg_counts))
|
||||
|
||||
if len(msgs_and_power) < SAMPLE_TIME:
|
||||
continue
|
||||
|
||||
msg_counts = self.tabulate_msg_counts(msgs_and_power)
|
||||
now = np.mean([m[0] for m in msgs_and_power])
|
||||
|
||||
if self.valid_msg_count(proc, msg_counts) and self.valid_power_draw(proc, now - prev):
|
||||
break
|
||||
|
||||
return now, msg_counts, time.monotonic() - start_time - SAMPLE_TIME
|
||||
|
||||
@mock_messages(['deviceMotion'])
|
||||
def test_camera_procs(self, subtests):
|
||||
baseline = get_power()
|
||||
|
||||
prev = baseline
|
||||
used = {}
|
||||
warmup_time = {}
|
||||
msg_counts = {}
|
||||
|
||||
for proc in PROCS:
|
||||
for p in proc.procs:
|
||||
managed_processes[p].start()
|
||||
now, local_msg_counts, warmup_time[proc.name] = self.get_power_with_warmup_for_target(proc, prev)
|
||||
msg_counts.update(local_msg_counts)
|
||||
|
||||
used[proc.name] = now - prev
|
||||
prev = now
|
||||
|
||||
manager_cleanup()
|
||||
|
||||
tab = [['process', 'expected (W)', 'measured (W)', '# msgs expected', '# msgs received', "warmup time (s)"]]
|
||||
for proc in PROCS:
|
||||
cur = used[proc.name]
|
||||
expected = proc.power
|
||||
msgs_received = sum(msg_counts[msg] for msg in proc.msgs)
|
||||
tab.append([proc.name, round(expected, 2), round(cur, 2), self.get_expected_messages(proc), msgs_received, round(warmup_time[proc.name], 2)])
|
||||
with subtests.test(proc=proc.name):
|
||||
assert self.valid_msg_count(proc, msg_counts), f"expected {self.get_expected_messages(proc)} msgs, got {msgs_received} msgs"
|
||||
assert self.valid_power_draw(proc, cur), f"expected {expected:.2f}W, got {cur:.2f}W"
|
||||
print(tabulate(tab))
|
||||
print(f"Baseline {baseline:.2f}W\n")
|
||||
17
iqpilot/system/hardware/tici/updater
Executable file
17
iqpilot/system/hardware/tici/updater
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
|
||||
AGNOS_PY=$1
|
||||
MANIFEST=$2
|
||||
|
||||
if [[ ! -f "$AGNOS_PY" || ! -f "$MANIFEST" ]]; then
|
||||
echo "invalid args"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet weston-ready; then
|
||||
$DIR/updater_weston $AGNOS_PY $MANIFEST
|
||||
else
|
||||
$DIR/updater_magic $AGNOS_PY $MANIFEST
|
||||
fi
|
||||
BIN
iqpilot/system/hardware/tici/updater_magic
Executable file
BIN
iqpilot/system/hardware/tici/updater_magic
Executable file
Binary file not shown.
BIN
iqpilot/system/hardware/tici/updater_weston
Executable file
BIN
iqpilot/system/hardware/tici/updater_weston
Executable file
Binary file not shown.
71
iqpilot/system/hardware/tici/usb_storage.py
Normal file
71
iqpilot/system/hardware/tici/usb_storage.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
|
||||
SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "set_usb_storage.sh")
|
||||
|
||||
|
||||
def apply_usb_storage_state(state: bool):
|
||||
Params().put_bool("UsbStorageEnabled", state)
|
||||
try:
|
||||
args = ["sudo", SCRIPT_PATH]
|
||||
if state:
|
||||
args.append("--rebuild")
|
||||
subprocess.Popen(args)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
NCM_TRIED_MARKER = "/tmp/.iqemac_ncm_provisioned"
|
||||
MAX_NCM_ATTEMPTS = 3
|
||||
|
||||
|
||||
def _ncm_attempts() -> int:
|
||||
try:
|
||||
with open(NCM_TRIED_MARKER) as f:
|
||||
return int(f.read().strip() or 0)
|
||||
except (OSError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def ensure_ncm_gadget() -> bool:
|
||||
# configfs is RAM backed, so the gadget must be rebuilt every boot; binding it
|
||||
# enumerates on the host, which must not happen mid-drive
|
||||
if os.path.isdir("/sys/class/net/usb0"):
|
||||
return True
|
||||
params = Params()
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected
|
||||
if not (params.get_bool("IQEmacEnabled") or egpu_selected(params)):
|
||||
return False
|
||||
attempts = _ncm_attempts()
|
||||
if attempts >= MAX_NCM_ATTEMPTS:
|
||||
return False
|
||||
try:
|
||||
# stamped before the run: the gadget build can wedge configfs in
|
||||
# uninterruptible D state, and retrying that forever helps nobody
|
||||
with open(NCM_TRIED_MARKER, "w") as f:
|
||||
f.write(str(attempts + 1))
|
||||
subprocess.run(["sudo", "-n", SCRIPT_PATH], timeout=120, check=False)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
return os.path.isdir("/sys/class/net/usb0")
|
||||
|
||||
|
||||
INPUT_SUSPEND = "/sys/class/power_supply/battery/input_suspend"
|
||||
|
||||
|
||||
def suspend_usb_input(suspend: bool = True) -> bool:
|
||||
# a host on the data port makes the PMIC sink USB-PD while OBD-C feeds the same
|
||||
# rail; the SOM browns out. This closes the charge path only, data is untouched
|
||||
if not os.path.exists(INPUT_SUSPEND):
|
||||
return False
|
||||
try:
|
||||
with open(INPUT_SUSPEND) as f:
|
||||
if f.read().strip() == ("1" if suspend else "0"):
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
rc = subprocess.run(["sudo", "-n", "sh", "-c", f"echo {int(suspend)} > {INPUT_SUSPEND}"],
|
||||
check=False, capture_output=True)
|
||||
return rc.returncode == 0
|
||||
19
iqpilot/system/hardware/tici/zram_setup.sh
Executable file
19
iqpilot/system/hardware/tici/zram_setup.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# RT control procs (controlsd/card) mlockall their pages, so they are never swapped.
|
||||
set -e
|
||||
|
||||
[ -e /sys/class/zram-control ] || exit 0
|
||||
grep -q "zram0" /proc/swaps 2>/dev/null && exit 0
|
||||
|
||||
DISKSIZE="${ZRAM_DISKSIZE:-2G}"
|
||||
|
||||
echo lzo > /sys/block/zram0/comp_algorithm 2>/dev/null || true
|
||||
echo "$DISKSIZE" > /sys/block/zram0/disksize
|
||||
|
||||
mkswap /dev/zram0 >/dev/null 2>&1
|
||||
swapon -p 100 /dev/zram0
|
||||
|
||||
sysctl -q vm.swappiness=100 2>/dev/null || true
|
||||
sysctl -q vm.page-cluster=0 2>/dev/null || true
|
||||
|
||||
echo "zram: $(free -m | awk '/Swap/{print $2}')MB compressed swap active"
|
||||
Reference in New Issue
Block a user