forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Prebuilt Release @ ab07000
This commit is contained in:
4
iqpilot/konn3kt/__init__.py
Normal file
4
iqpilot/konn3kt/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
0
iqpilot/konn3kt/canlive/__init__.py
Normal file
0
iqpilot/konn3kt/canlive/__init__.py
Normal file
108
iqpilot/konn3kt/canlive/canlived.py
Executable file
108
iqpilot/konn3kt/canlive/canlived.py
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
canlived — live CAN bridge to konn3kt.
|
||||
|
||||
Streams the device's live CAN bus to the konn3kt server so it can be viewed remotely
|
||||
in Cabana (via a local ZMQ proxy on the laptop). This is the remote analogue of running
|
||||
`./cereal/messaging/bridge` locally: instead of re-publishing CAN over a LAN ZMQ socket,
|
||||
canlived opens its OWN websocket to konn3kt and forwards the raw capnp `Event` frames.
|
||||
|
||||
It is deliberately a separate daemon (not part of hephaestusd's control websocket):
|
||||
* hephaestusd's send path fragments everything as TEXT frames through one queue, which
|
||||
cannot carry binary capnp and would head-of-line-block the control plane at 100Hz.
|
||||
* a dedicated socket means CAN traffic and control traffic never contend.
|
||||
|
||||
Lifecycle: the manager launches canlived only while the `CanLiveStreaming` param is set.
|
||||
hephaestusd sets/clears that param via startCanLive/stopCanLive, which the konn3kt server
|
||||
calls when the first viewer connects / the last viewer disconnects. So canlived runs only
|
||||
during an active debug session — no idle connections, no battery/data cost otherwise.
|
||||
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved.
|
||||
"""
|
||||
import os
|
||||
import threading
|
||||
|
||||
from websocket import ABNF, create_connection
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.common.api import Api
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
# Cabana's live "Device" stream subscribes only to "can", so that's all we forward to
|
||||
# match the local experience exactly. (sendcan/TX is not shown by the live device view.)
|
||||
CAN_SERVICES = ["can"]
|
||||
|
||||
# Reconnect backoff bounds (seconds).
|
||||
RECONNECT_MIN = 1.0
|
||||
RECONNECT_MAX = 10.0
|
||||
|
||||
|
||||
def _api_host() -> str:
|
||||
# Same host hephaestusd talks to; force the websocket scheme.
|
||||
host = os.getenv("HEPHAESTUS_HOST") or os.getenv("KONN3KT_API_HOST") or "wss://api-iqlabs.konn3kt.com"
|
||||
host = host.rstrip("/")
|
||||
if host.startswith("https://"):
|
||||
host = "wss://" + host[len("https://"):]
|
||||
elif host.startswith("http://"):
|
||||
host = "ws://" + host[len("http://"):]
|
||||
return host
|
||||
|
||||
|
||||
def _stream_once(dongle_id: str, ws_uri: str, token: str, exit_event: threading.Event) -> None:
|
||||
"""Open one websocket and pump CAN until it drops or we're asked to exit."""
|
||||
ws = create_connection(ws_uri, cookie="jwt=" + token, enable_multithread=True, timeout=30.0)
|
||||
cloudlog.info("canlived: connected to %s", ws_uri)
|
||||
try:
|
||||
# Blocking receive with a short timeout so we periodically re-check exit_event and
|
||||
# the socket stays responsive to shutdown even when the bus is quiet.
|
||||
socks = [messaging.sub_sock(s, conflate=False, timeout=100) for s in CAN_SERVICES]
|
||||
while not exit_event.is_set():
|
||||
got_any = False
|
||||
for sock in socks:
|
||||
while True:
|
||||
raw = sock.receive(non_blocking=True)
|
||||
if raw is None:
|
||||
break
|
||||
got_any = True
|
||||
# Forward the exact capnp Event bytes as a single binary frame. canlived owns
|
||||
# this socket, so there is no fragmentation/interleaving to worry about.
|
||||
ws.send_frame(ABNF.create_frame(raw, ABNF.OPCODE_BINARY, 1))
|
||||
if not got_any:
|
||||
# Nothing pending across any sub — yield briefly instead of busy-spinning.
|
||||
exit_event.wait(0.005)
|
||||
finally:
|
||||
try:
|
||||
ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main(exit_event: threading.Event | None = None) -> None:
|
||||
if exit_event is None:
|
||||
exit_event = threading.Event()
|
||||
|
||||
params = Params()
|
||||
dongle_id = params.get("DongleId", encoding="utf-8")
|
||||
if not dongle_id:
|
||||
cloudlog.error("canlived: no DongleId, cannot stream")
|
||||
return
|
||||
|
||||
api = Api(dongle_id)
|
||||
host = _api_host()
|
||||
ws_uri = f"{host}/ws/can/{dongle_id}"
|
||||
|
||||
backoff = RECONNECT_MIN
|
||||
while not exit_event.is_set():
|
||||
try:
|
||||
token = api.get_token(expiry_hours=1)
|
||||
_stream_once(dongle_id, ws_uri, token, exit_event)
|
||||
backoff = RECONNECT_MIN # clean disconnect, reset backoff
|
||||
except Exception as e:
|
||||
cloudlog.exception("canlived: stream error: %s", e)
|
||||
exit_event.wait(backoff)
|
||||
backoff = min(backoff * 2, RECONNECT_MAX)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
17
iqpilot/konn3kt/cloud_client.py
Normal file
17
iqpilot/konn3kt/cloud_client.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
from openpilot.common.api.base import BaseApi
|
||||
API_HOST = os.getenv('KONN3KT_API_HOST', 'https://api-iqlabs.konn3kt.com')
|
||||
|
||||
class Konn3ktApi(BaseApi):
|
||||
|
||||
def __init__(self, dongle_id):
|
||||
super().__init__(dongle_id, API_HOST)
|
||||
self.user_agent = "konn3kt-device-"
|
||||
|
||||
def get_token(self, expiry_hours=1):
|
||||
return super()._get_token(expiry_hours=expiry_hours)
|
||||
3
iqpilot/konn3kt/common/__init__.py
Normal file
3
iqpilot/konn3kt/common/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
53
iqpilot/konn3kt/common/param_codec.py
Normal file
53
iqpilot/konn3kt/common/param_codec.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
|
||||
Serialises openpilot params to/from their on-wire byte form for backup transport.
|
||||
The byte encoding is unchanged so archives round-trip: BYTES pass through, JSON is
|
||||
json-encoded, everything else is str()'d; decoding is typed per the param's key.
|
||||
"""
|
||||
import base64
|
||||
import gzip
|
||||
import json
|
||||
|
||||
from openpilot.common.params import Params, ParamKeyType
|
||||
|
||||
|
||||
def encode_param(name: str, params=None, use_default: bool = False) -> bytes | None:
|
||||
params = params or Params()
|
||||
raw = params.get_default_value(name) if use_default else params.get(name)
|
||||
if raw is None:
|
||||
return None
|
||||
|
||||
ktype = params.get_type(name)
|
||||
if ktype == ParamKeyType.BYTES:
|
||||
return bytes(raw)
|
||||
if ktype == ParamKeyType.JSON:
|
||||
return json.dumps(raw).encode("utf-8")
|
||||
return str(raw).encode("utf-8")
|
||||
|
||||
|
||||
# text-form decoders keyed by param type; anything unlisted is left as the raw string
|
||||
_FROM_TEXT = {
|
||||
ParamKeyType.STRING: lambda s: s,
|
||||
ParamKeyType.BOOL: lambda s: s.lower() in ("true", "1", "yes"),
|
||||
ParamKeyType.INT: int,
|
||||
ParamKeyType.FLOAT: float,
|
||||
ParamKeyType.TIME: str,
|
||||
ParamKeyType.JSON: json.loads,
|
||||
}
|
||||
|
||||
|
||||
def restore_param_from_base64(name: str, b64_data: str, compressed: bool = False) -> None:
|
||||
params = Params()
|
||||
ktype = params.get_type(name)
|
||||
|
||||
blob = base64.b64decode(b64_data)
|
||||
if compressed:
|
||||
blob = gzip.decompress(blob)
|
||||
|
||||
if ktype == ParamKeyType.BYTES:
|
||||
value = blob
|
||||
else:
|
||||
value = _FROM_TEXT.get(ktype, lambda s: s)(blob.decode("utf-8"))
|
||||
|
||||
params.put(name, value)
|
||||
4
iqpilot/konn3kt/hephaestus/__init__.py
Normal file
4
iqpilot/konn3kt/hephaestus/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
29
iqpilot/konn3kt/hephaestus/ble_transportd.py
Normal file
29
iqpilot/konn3kt/hephaestus/ble_transportd.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import logging
|
||||
|
||||
from openpilot.iqpilot._proprietary_loader import load_private_module
|
||||
|
||||
|
||||
_ORIGINAL_LOGGER_LOG = logging.Logger._log
|
||||
|
||||
|
||||
def _ble_transport_logger_shim(self, level, msg, args,
|
||||
exc_info=None, extra=None, stack_info=False, stacklevel=1, **kwargs):
|
||||
if kwargs:
|
||||
suffix = " ".join(f"{key}={value!r}" for key, value in sorted(kwargs.items()))
|
||||
msg = f"{msg} {suffix}".strip() if msg is not None else suffix
|
||||
return _ORIGINAL_LOGGER_LOG(
|
||||
self, level, msg, args,
|
||||
exc_info=exc_info, extra=extra, stack_info=stack_info, stacklevel=stacklevel,
|
||||
)
|
||||
|
||||
|
||||
logging.Logger._log = _ble_transport_logger_shim
|
||||
|
||||
load_private_module(__name__, "iqpilot_private.konn3kt.hephaestus.ble_transportd")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
10
iqpilot/konn3kt/hephaestus/hephaestusd.py
Normal file
10
iqpilot/konn3kt/hephaestus/hephaestusd.py
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
from openpilot.iqpilot._proprietary_loader import load_private_module
|
||||
|
||||
load_private_module(__name__, "iqpilot_private.konn3kt.hephaestus.hephaestusd")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
122
iqpilot/konn3kt/hephaestus/manage_hephaestusd.py
Normal file
122
iqpilot/konn3kt/hephaestus/manage_hephaestusd.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import importlib
|
||||
import os
|
||||
import time
|
||||
from multiprocessing import Process
|
||||
|
||||
HEPHAESTUS_MGR_PID_PARAM = "HephaestusdPid"
|
||||
|
||||
|
||||
def _cloudlog():
|
||||
try:
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
return cloudlog
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _log(level: str, msg: str) -> None:
|
||||
cl = _cloudlog()
|
||||
if cl is not None:
|
||||
try:
|
||||
getattr(cl, level)(msg)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
print(f"manage_hephaestusd[{level}]: {msg}", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _lightweight_launcher(proc: str, name: str) -> None:
|
||||
try:
|
||||
mod = importlib.import_module(proc)
|
||||
try:
|
||||
from setproctitle import setproctitle
|
||||
setproctitle(proc)
|
||||
except Exception:
|
||||
pass
|
||||
cl = _cloudlog()
|
||||
if cl is not None:
|
||||
try:
|
||||
cl.bind(daemon=name)
|
||||
except Exception:
|
||||
pass
|
||||
mod.main()
|
||||
except KeyboardInterrupt:
|
||||
_log("warning", f"child {proc} got SIGINT")
|
||||
except Exception:
|
||||
_log("exception", f"child {proc} exception")
|
||||
raise
|
||||
|
||||
|
||||
def _bind_global_best_effort(dongle_id_param: str) -> None:
|
||||
try:
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
except Exception:
|
||||
return
|
||||
try:
|
||||
dongle_id = Params().get(dongle_id_param)
|
||||
try:
|
||||
from openpilot.system.version import get_build_metadata
|
||||
build_metadata = get_build_metadata()
|
||||
cloudlog.bind_global(dongle_id=dongle_id,
|
||||
version=build_metadata.openpilot.version,
|
||||
origin=build_metadata.openpilot.git_normalized_origin,
|
||||
branch=build_metadata.channel,
|
||||
commit=build_metadata.openpilot.git_commit,
|
||||
dirty=build_metadata.openpilot.is_dirty,
|
||||
device=HARDWARE.get_device_type())
|
||||
except Exception:
|
||||
cloudlog.bind_global(dongle_id=dongle_id, device=HARDWARE.get_device_type())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _remove_pid_param(pid_param: str) -> None:
|
||||
try:
|
||||
from openpilot.common.params import Params
|
||||
Params().remove(pid_param)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def manage_hephaestusd(dongle_id_param: str, pid_param: str, process_name: str, target: str) -> None:
|
||||
_bind_global_best_effort(dongle_id_param)
|
||||
|
||||
try:
|
||||
while 1:
|
||||
_log("info", f"starting {process_name} daemon")
|
||||
proc = Process(name=process_name, target=_lightweight_launcher, args=(target, process_name))
|
||||
proc.start()
|
||||
# Lower priority so BLE stack doesn't compete with OP's Python processes
|
||||
# on an already heavily loaded system (RT processes like pandad/modeld are unaffected)
|
||||
if proc.pid is not None:
|
||||
try:
|
||||
os.setpriority(os.PRIO_PROCESS, proc.pid, 10)
|
||||
except OSError:
|
||||
pass
|
||||
proc.join()
|
||||
_log("info", f"{process_name} exited (exitcode={proc.exitcode})")
|
||||
if proc.exitcode == 174:
|
||||
time.sleep(30)
|
||||
else:
|
||||
time.sleep(5)
|
||||
except Exception:
|
||||
_log("exception", f"manage_{process_name}.exception")
|
||||
finally:
|
||||
_remove_pid_param(pid_param)
|
||||
|
||||
|
||||
def main():
|
||||
manage_hephaestusd(dongle_id_param="DongleId", pid_param=HEPHAESTUS_MGR_PID_PARAM, process_name="hephaestusd",
|
||||
target="iqpilot.konn3kt.hephaestus.hephaestusd")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
4
iqpilot/konn3kt/iqlvbs/__init__.py
Normal file
4
iqpilot/konn3kt/iqlvbs/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
7
iqpilot/konn3kt/iqlvbs/alc.py
Normal file
7
iqpilot/konn3kt/iqlvbs/alc.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
from openpilot.iqpilot._proprietary_loader import load_private_module
|
||||
|
||||
load_private_module(__name__, "iqpilot_private.konn3kt.iqlvbs.alc")
|
||||
19
iqpilot/konn3kt/iqlvbs/git_remote.py
Normal file
19
iqpilot/konn3kt/iqlvbs/git_remote.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
|
||||
Public shim for the proprietary IQ.Lvbs git read-auth helper.
|
||||
|
||||
The token + git config logic live in the standalone, signed bundle
|
||||
``iqpilot_private.updater.git_remote`` (artifact iqpilot_updater_private) -- a
|
||||
dedicated bundle so the read-only PAT can be rotated by rebuilding only that tiny
|
||||
bundle, never touching ALC. Never in the open tree.
|
||||
|
||||
The private module exports:
|
||||
configure(repo_dir: str) -> None
|
||||
Install the read-only token as an ephemeral http.<host>.extraHeader on
|
||||
repo_dir (the only auth method that survives the WAF's 403-to-anonymous).
|
||||
"""
|
||||
from openpilot.iqpilot._proprietary_loader import load_private_module
|
||||
|
||||
load_private_module(__name__, "iqpilot_private.updater.git_remote")
|
||||
236
iqpilot/konn3kt/registration.py
Executable file
236
iqpilot/konn3kt/registration.py
Executable file
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import jwt
|
||||
import re
|
||||
import secrets
|
||||
from typing import cast
|
||||
from pathlib import Path
|
||||
|
||||
from datetime import datetime, timedelta, UTC
|
||||
from openpilot.common.api import api_get, get_key_pair
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.spinner import Spinner
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
|
||||
UNREGISTERED_DONGLE_ID = "UnregisteredDevice"
|
||||
|
||||
_DONGLE_ID_RE = re.compile(r"^[a-fA-F0-9]{16}$")
|
||||
IMEI_WAIT_TIMEOUT = 15.0
|
||||
|
||||
|
||||
def _read_persist_dongle_id() -> str | None:
|
||||
p = Path(Paths.persist_root()) / "comma" / "dongle_id"
|
||||
try:
|
||||
if not p.is_file():
|
||||
return None
|
||||
s = p.read_text().strip()
|
||||
return s or None
|
||||
except Exception:
|
||||
cloudlog.exception("failed to read persist dongle_id")
|
||||
return None
|
||||
|
||||
|
||||
def get_cached_dongle_id(params: Params | None = None, prefer_readonly: bool = True) -> str | None:
|
||||
ro = _read_persist_dongle_id()
|
||||
if is_valid_dongle_id(ro):
|
||||
ro = ro.lower()
|
||||
if prefer_readonly and ro:
|
||||
return ro
|
||||
p = Params() if params is None else params
|
||||
v = p.get("DongleId")
|
||||
if v and v != UNREGISTERED_DONGLE_ID:
|
||||
return v.lower() if is_valid_dongle_id(v) else v
|
||||
return ro or None
|
||||
def is_valid_dongle_id(dongle_id: str | None) -> bool:
|
||||
return bool(dongle_id and _DONGLE_ID_RE.fullmatch(dongle_id))
|
||||
def get_or_create_dongle_id(params: Params | None = None, prefer_readonly: bool = True) -> str:
|
||||
p = Params() if params is None else params
|
||||
dongle_id = get_cached_dongle_id(p, prefer_readonly=prefer_readonly)
|
||||
if dongle_id and dongle_id != UNREGISTERED_DONGLE_ID:
|
||||
return dongle_id
|
||||
dongle_id = secrets.token_hex(8)
|
||||
p.put("DongleId", dongle_id)
|
||||
cloudlog.warning(f"generated new DongleId={dongle_id} (no readonly dongle_id found)")
|
||||
return dongle_id
|
||||
def ensure_dev_pairing_identity(params: Params | None = None, force_reset: bool = False) -> dict[str, str]:
|
||||
p = Params() if params is None else params
|
||||
|
||||
persist_dir = Path(Paths.persist_root()) / "comma"
|
||||
persist_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
dongle_path = persist_dir / "dongle_id"
|
||||
priv_path = persist_dir / "id_rsa"
|
||||
pub_path = persist_dir / "id_rsa.pub"
|
||||
|
||||
if force_reset:
|
||||
for fp in (dongle_path, priv_path, pub_path):
|
||||
try:
|
||||
fp.unlink(missing_ok=True)
|
||||
except Exception:
|
||||
cloudlog.exception(f"failed to remove {fp}")
|
||||
try:
|
||||
(persist_dir / "konn3kt_prime_type").unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
p.remove("PrimeType")
|
||||
except Exception:
|
||||
pass
|
||||
forced_dongle = os.getenv("KONN3KT_DEV_DONGLE_ID")
|
||||
dongle_id = forced_dongle.strip().lower() if forced_dongle else None
|
||||
if dongle_id and not is_valid_dongle_id(dongle_id):
|
||||
cloudlog.error("KONN3KT_DEV_DONGLE_ID must be 16 hex chars")
|
||||
dongle_id = None
|
||||
if dongle_id is None:
|
||||
existing = None
|
||||
try:
|
||||
existing = dongle_path.read_text().strip().lower() if dongle_path.is_file() else None
|
||||
except Exception:
|
||||
cloudlog.exception("failed reading existing dev dongle_id")
|
||||
dongle_id = existing if is_valid_dongle_id(existing) else secrets.token_hex(8)
|
||||
try:
|
||||
dongle_path.write_text(dongle_id)
|
||||
except Exception:
|
||||
cloudlog.exception("failed writing dev dongle_id")
|
||||
p.put("DongleId", dongle_id)
|
||||
p.put("HardwareSerial", p.get("HardwareSerial") or f"DEV-{dongle_id}")
|
||||
if force_reset or (not priv_path.is_file()) or (not pub_path.is_file()):
|
||||
try:
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
priv_bytes = key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
pub_bytes = key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
priv_path.write_bytes(priv_bytes)
|
||||
pub_path.write_bytes(pub_bytes)
|
||||
except Exception:
|
||||
cloudlog.exception("failed generating dev RSA keys")
|
||||
raise
|
||||
return {
|
||||
"dongle_id": dongle_id,
|
||||
"serial": p.get("HardwareSerial") or f"DEV-{dongle_id}",
|
||||
"persist_dir": str(persist_dir),
|
||||
}
|
||||
def is_registered_device() -> bool:
|
||||
dongle = Params().get("DongleId")
|
||||
return dongle not in (None, UNREGISTERED_DONGLE_ID)
|
||||
|
||||
|
||||
def _normalize_imei(value: str | None) -> str:
|
||||
return value or ""
|
||||
|
||||
|
||||
def get_registration_identifiers(wait_timeout: float = IMEI_WAIT_TIMEOUT, show_spinner: bool = False) -> tuple[str, str, str]:
|
||||
serial = HARDWARE.get_serial()
|
||||
spinner = Spinner() if show_spinner else None
|
||||
start_time = time.monotonic()
|
||||
imei1: str | None = None
|
||||
imei2: str | None = None
|
||||
|
||||
while time.monotonic() - start_time < wait_timeout:
|
||||
try:
|
||||
imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1)
|
||||
if imei1 or imei2:
|
||||
break
|
||||
except RuntimeError as e:
|
||||
if "no modems" in str(e).lower():
|
||||
cloudlog.warning("No cellular modem available, proceeding without IMEI")
|
||||
break
|
||||
cloudlog.exception("Error getting imei, trying again...")
|
||||
except Exception:
|
||||
cloudlog.exception("Error getting imei, trying again...")
|
||||
time.sleep(1)
|
||||
|
||||
imei1 = _normalize_imei(imei1)
|
||||
imei2 = _normalize_imei(imei2)
|
||||
|
||||
if not imei1 and not imei2:
|
||||
cloudlog.warning(f"proceeding with serial-only registration for serial={serial}")
|
||||
if spinner is not None:
|
||||
spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1 or None}, {imei2 or None})")
|
||||
spinner.close()
|
||||
|
||||
return serial, imei1, imei2
|
||||
|
||||
|
||||
def register(show_spinner=False) -> str | None:
|
||||
"""
|
||||
All devices built since March 2024 come with all
|
||||
info stored in /persist/. This is kept around
|
||||
only for devices built before then.
|
||||
|
||||
With a backend update to take serial number instead
|
||||
of dongle ID to some endpoints, this can be removed
|
||||
entirely.
|
||||
"""
|
||||
params = Params()
|
||||
|
||||
dongle_id: str | None = get_cached_dongle_id(params, prefer_readonly=True)
|
||||
if dongle_id in ("", UNREGISTERED_DONGLE_ID):
|
||||
dongle_id = None
|
||||
|
||||
# Create registration token, in the future, this key will make JWTs directly
|
||||
jwt_algo, private_key, public_key = get_key_pair()
|
||||
|
||||
if not public_key:
|
||||
dongle_id = UNREGISTERED_DONGLE_ID
|
||||
cloudlog.warning("missing public key")
|
||||
elif dongle_id is None:
|
||||
if show_spinner:
|
||||
spinner = Spinner()
|
||||
spinner.update("registering device")
|
||||
|
||||
serial, imei1, imei2 = get_registration_identifiers(wait_timeout=IMEI_WAIT_TIMEOUT, show_spinner=False)
|
||||
|
||||
backoff = 0
|
||||
start_time = time.monotonic()
|
||||
while True:
|
||||
try:
|
||||
register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)},
|
||||
cast(str, private_key), algorithm=jwt_algo)
|
||||
cloudlog.info("getting pilotauth")
|
||||
cloudlog.info("getting pilotauth")
|
||||
resp = api_get("v2/pilotauth/", method='POST', timeout=15,
|
||||
imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token)
|
||||
|
||||
if resp.status_code in (400, 402, 403):
|
||||
cloudlog.info(f"Unable to register device, got {resp.status_code}")
|
||||
dongle_id = UNREGISTERED_DONGLE_ID
|
||||
else:
|
||||
dongleauth = json.loads(resp.text)
|
||||
dongle_id = dongleauth["dongle_id"]
|
||||
break
|
||||
except Exception:
|
||||
cloudlog.exception("failed to authenticate")
|
||||
backoff = min(backoff + 1, 15)
|
||||
time.sleep(backoff)
|
||||
|
||||
if time.monotonic() - start_time > 60 and show_spinner:
|
||||
spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})")
|
||||
return UNREGISTERED_DONGLE_ID # hotfix to prevent an infinite wait for registration
|
||||
|
||||
if show_spinner:
|
||||
spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1 or None}, {imei2 or None})")
|
||||
spinner.close()
|
||||
|
||||
if dongle_id:
|
||||
params.put("DongleId", dongle_id)
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert # lazy: keeps registration import light for the setup zipapp
|
||||
set_offroad_alert("Offroad_UnregisteredHardware", False)
|
||||
return dongle_id
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(register())
|
||||
11
iqpilot/konn3kt/service_health.py
Normal file
11
iqpilot/konn3kt/service_health.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
|
||||
def hephaestus_ready(params=None) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def hephaestus_ready_shim():
|
||||
return hephaestus_ready()
|
||||
41
iqpilot/konn3kt/tests/test_registration.py
Normal file
41
iqpilot/konn3kt/tests/test_registration.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from openpilot.iqpilot.konn3kt import registration
|
||||
|
||||
|
||||
def test_get_registration_identifiers_uses_serial_without_imei():
|
||||
imei_calls = {"count": 0}
|
||||
|
||||
def fake_get_imei(slot: int) -> str | None:
|
||||
imei_calls["count"] += 1
|
||||
return None
|
||||
|
||||
monotonic_values = iter([0.0, 0.0, 1.0, 2.0])
|
||||
with patch.object(registration.HARDWARE, "get_serial", return_value="lite123"), \
|
||||
patch.object(registration.HARDWARE, "get_imei", side_effect=fake_get_imei), \
|
||||
patch.object(registration.time, "monotonic", side_effect=lambda: next(monotonic_values)), \
|
||||
patch.object(registration.time, "sleep", return_value=None):
|
||||
serial, imei1, imei2 = registration.get_registration_identifiers(wait_timeout=1.5, show_spinner=False)
|
||||
|
||||
assert serial == "lite123"
|
||||
assert imei1 == ""
|
||||
assert imei2 == ""
|
||||
assert imei_calls["count"] >= 2
|
||||
|
||||
|
||||
def test_get_registration_identifiers_returns_first_available_imei():
|
||||
imeis = [None, "123456789012345"]
|
||||
monotonic_values = iter([0.0, 0.0, 0.5, 0.5])
|
||||
|
||||
def fake_get_imei(slot: int) -> str | None:
|
||||
return imeis.pop(0) if slot == 0 else None
|
||||
|
||||
with patch.object(registration.HARDWARE, "get_serial", return_value="lite123"), \
|
||||
patch.object(registration.HARDWARE, "get_imei", side_effect=fake_get_imei), \
|
||||
patch.object(registration.time, "monotonic", side_effect=lambda: next(monotonic_values)), \
|
||||
patch.object(registration.time, "sleep", return_value=None):
|
||||
serial, imei1, imei2 = registration.get_registration_identifiers(wait_timeout=2.0, show_spinner=False)
|
||||
|
||||
assert serial == "lite123"
|
||||
assert imei1 == "123456789012345"
|
||||
assert imei2 == ""
|
||||
1
iqpilot/konn3kt/uploaderd/__init__.py
Normal file
1
iqpilot/konn3kt/uploaderd/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Public wrappers for proprietary uploader modules."""
|
||||
10
iqpilot/konn3kt/uploaderd/iquploaderd.py
Normal file
10
iqpilot/konn3kt/uploaderd/iquploaderd.py
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
from openpilot.iqpilot._proprietary_loader import load_private_module
|
||||
|
||||
load_private_module(__name__, "iqpilot_private.konn3kt.uploaderd.iquploaderd")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user