mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-07 04:53:47 +08:00
179718dcb0
* esim: MICI profile download UI * Show temporary feedback for non-LPA QR codes * Scan eSIM QR codes every 250 ms * Shrink eSIM error text and log displayed errors * Activate downloaded eSIM through the profile tap flow * Let the eSIM connectivity error wrap naturally * Release add-profile button feedback immediately * Check internet before opening the eSIM QR scanner * Use UI network state and standard connectivity dialog for eSIM * Drop LPA prime settings change from eSIM download UI * Scope eSIM UI constants to their owning classes * Remove unused ot codespell exception * esim: simplify profile download operation and UI state * esim: share activation code validation with QR scanner * esim: match QR scanner video to cabin preview * esim: show errors in a scrollable text dialog * esim: require nonblank profile nicknames
158 lines
5.1 KiB
Python
158 lines
5.1 KiB
Python
import time
|
|
import threading
|
|
from collections.abc import Callable
|
|
from dataclasses import replace
|
|
|
|
from openpilot.common.hardware import HARDWARE
|
|
from openpilot.common.swaglog import cloudlog
|
|
from openpilot.common.esim.base import LPABase, Profile
|
|
from openpilot.common.esim.esim import execute_and_process_notifications
|
|
|
|
|
|
class CellularManager:
|
|
PROFILE_POLL_INTERVAL_S = 5.0
|
|
|
|
def __init__(self):
|
|
self._lpa: LPABase | None = None
|
|
self._profiles: list[Profile] = []
|
|
self._busy: bool = False
|
|
# re-probed every poll; SIM may be swapped at runtime on tray-accessible devices
|
|
self._is_euicc: bool | None = None
|
|
self._modem_state: dict = {}
|
|
|
|
self._lock = threading.Lock()
|
|
self._callback_lock = threading.Lock()
|
|
self._callback_queue: list[Callable] = []
|
|
|
|
self.on_profiles_updated: Callable[[], None] | None = None
|
|
self.on_operation_error: Callable[[str], None] | None = None
|
|
|
|
self._last_profile_poll: float = 0.0
|
|
self._polling: bool = False
|
|
|
|
@property
|
|
def modem_state(self) -> dict:
|
|
return self._modem_state
|
|
|
|
def process_callbacks(self):
|
|
with self._callback_lock:
|
|
to_run, self._callback_queue = self._callback_queue, []
|
|
for cb in to_run:
|
|
cb()
|
|
|
|
if not self._busy and not self._polling and time.monotonic() - self._last_profile_poll >= self.PROFILE_POLL_INTERVAL_S:
|
|
self._last_profile_poll = time.monotonic()
|
|
self._modem_state = HARDWARE.get_modem_state()
|
|
self._poll_profiles()
|
|
|
|
@property
|
|
def profiles(self) -> list[Profile]:
|
|
return self._profiles
|
|
|
|
@property
|
|
def active_profile(self) -> Profile | None:
|
|
return next((p for p in self._profiles if p.enabled), None)
|
|
|
|
@property
|
|
def busy(self) -> bool:
|
|
return self._busy
|
|
|
|
@property
|
|
def is_euicc(self) -> bool | None:
|
|
return self._is_euicc
|
|
|
|
def _ensure_lpa(self) -> LPABase:
|
|
if self._lpa is None:
|
|
self._lpa = HARDWARE.get_sim_lpa()
|
|
return self._lpa
|
|
|
|
def _enqueue(self, cb: Callable):
|
|
with self._callback_lock:
|
|
self._callback_queue.append(cb)
|
|
|
|
def _stop_polling(self):
|
|
self._polling = False
|
|
|
|
def _set_profiles(self, profiles: list[Profile]):
|
|
self._profiles = profiles
|
|
if self.on_profiles_updated:
|
|
self.on_profiles_updated()
|
|
|
|
def _finish(self, profiles: list[Profile] | None = None, error: str | None = None):
|
|
self._busy = False
|
|
# defer the next poll a full interval; the eUICC can briefly report stale state after an operation
|
|
self._last_profile_poll = time.monotonic()
|
|
if profiles is not None:
|
|
self._set_profiles(profiles)
|
|
if error is not None:
|
|
self.refresh_profiles()
|
|
if self.on_operation_error:
|
|
self.on_operation_error(error)
|
|
|
|
def _run_operation(self, fn: Callable[[LPABase], None], error_msg: str, relist: bool = True):
|
|
self._busy = True
|
|
|
|
def worker():
|
|
try:
|
|
with self._lock:
|
|
lpa = self._ensure_lpa()
|
|
fn(lpa)
|
|
profiles = lpa.list_profiles() if relist else None
|
|
self._enqueue(lambda: self._finish(profiles=profiles))
|
|
except Exception as e:
|
|
cloudlog.exception(error_msg)
|
|
err = str(e)
|
|
self._enqueue(lambda: self._finish(error=err))
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def refresh_profiles(self):
|
|
# next process_callbacks tick polls, respecting busy/polling guards
|
|
self._last_profile_poll = 0.0
|
|
|
|
def _poll_profiles(self):
|
|
self._polling = True
|
|
def worker():
|
|
try:
|
|
with self._lock:
|
|
lpa = self._ensure_lpa()
|
|
is_euicc = lpa.is_euicc()
|
|
profiles = lpa.list_profiles() if is_euicc else []
|
|
self._enqueue(lambda: self._finish_poll(is_euicc, profiles))
|
|
except Exception:
|
|
cloudlog.exception("Failed to poll eSIM profiles")
|
|
self._enqueue(self._stop_polling)
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def _finish_poll(self, is_euicc: bool, profiles: list[Profile]):
|
|
self._polling = False
|
|
if self._busy:
|
|
return
|
|
if not is_euicc and self._is_euicc:
|
|
# is_euicc() is False on any AT error (e.g. SIM busy during a profile refresh); confirm on the next poll
|
|
self._is_euicc = None
|
|
return
|
|
self._is_euicc = is_euicc
|
|
self._set_profiles(profiles)
|
|
|
|
def switch_profile(self, iccid: str):
|
|
def switch(lpa: LPABase):
|
|
execute_and_process_notifications(lpa, lambda: lpa.switch_profile(iccid))
|
|
|
|
# optimistic: list_profiles() can briefly return stale enabled state after a switch
|
|
self._set_profiles([replace(p, enabled=(p.iccid == iccid)) for p in self._profiles])
|
|
self._run_operation(switch, "Failed to switch eSIM profile", relist=False)
|
|
|
|
def delete_profile(self, iccid: str):
|
|
def delete(lpa: LPABase):
|
|
execute_and_process_notifications(lpa, lambda: lpa.delete_profile(iccid))
|
|
|
|
self._run_operation(delete, "Failed to delete eSIM profile")
|
|
|
|
def download_profile(self, qr: str, nickname: str | None = None):
|
|
self._run_operation(lambda lpa: lpa.download_profile(qr, nickname), "Failed to download eSIM profile")
|
|
|
|
def nickname_profile(self, iccid: str, nickname: str):
|
|
self._run_operation(lambda lpa: lpa.nickname_profile(iccid, nickname), "Failed to update eSIM profile nickname")
|