openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

170 lines
6.2 KiB

#!/usr/bin/env python3
5 days ago
5 days ago
import argparse
6 days ago
import json
import os
import shutil
import subprocess
from dataclasses import dataclass
from typing import Literal
6 days ago
@dataclass
class Profile:
iccid: str
nickname: str
enabled: bool
provider: str
6 days ago
class LPAError(RuntimeError):
pass
6 days ago
class LPAProfileNotFoundError(LPAError):
6 days ago
pass
5 days ago
class LPA:
def __init__(self, interface: Literal['qmi', 'at'] = 'qmi'):
6 days ago
self.env = os.environ.copy()
self.env['LPAC_APDU'] = interface
6 days ago
self.env['QMI_DEVICE'] = '/dev/cdc-wdm0'
self.env['AT_DEVICE'] = '/dev/ttyUSB2'
6 days ago
self.timeout_sec = 45
6 days ago
if shutil.which('lpac') is None:
raise LPAError('lpac not found, must be installed!')
def list_profiles(self) -> list[Profile]:
msgs = self._invoke('profile', 'list')
5 days ago
self._validate_successful(msgs)
return [Profile(
iccid=p['iccid'],
nickname=p['profileNickname'],
enabled=p['profileState'] == 'enabled',
provider=p['serviceProviderName']
) for p in msgs[-1]['payload']['data']]
6 days ago
def get_active_profile(self) -> Profile | None:
return next((p for p in self.list_profiles() if p.enabled), None)
6 days ago
def enable_profile(self, iccid: str) -> None:
5 days ago
self._validate_profile_exists(iccid)
6 days ago
latest = self.get_active_profile()
5 days ago
if latest:
if latest.iccid == iccid:
return
self.disable_profile(latest.iccid)
5 days ago
self._validate_successful(self._invoke('profile', 'enable', iccid))
6 days ago
self.process_notifications()
def disable_profile(self, iccid: str) -> None:
5 days ago
self._validate_profile_exists(iccid)
6 days ago
latest = self.get_active_profile()
if latest is not None and latest.iccid != iccid:
5 days ago
return
self._validate_successful(self._invoke('profile', 'disable', iccid))
6 days ago
self.process_notifications()
def delete_profile(self, iccid: str) -> None:
5 days ago
self._validate_profile_exists(iccid)
latest = self.get_active_profile()
if latest is not None and latest.iccid == iccid:
self.disable_profile(iccid)
5 days ago
self._validate_successful(self._invoke('profile', 'delete', iccid))
6 days ago
self.process_notifications()
def download_profile(self, qr: str, nickname: str | None = None) -> None:
msgs = self._invoke('profile', 'download', '-a', qr)
5 days ago
self._validate_successful(msgs)
new_profile = next((m for m in msgs if m['payload']['message'] == 'es8p_meatadata_parse'), None)
if new_profile is None:
raise LPAError('no new profile found')
if nickname:
self.nickname_profile(new_profile['payload']['data']['iccid'], nickname)
6 days ago
self.process_notifications()
def nickname_profile(self, iccid: str, nickname: str) -> None:
5 days ago
self._validate_profile_exists(iccid)
self._validate_successful(self._invoke('profile', 'nickname', iccid, nickname))
5 days ago
def process_notifications(self) -> None:
"""
Process notifications stored on the eUICC, typically to activate/deactivate the profile with the carrier.
"""
5 days ago
self._validate_successful(self._invoke('notification', 'process', '-a', '-r'))
6 days ago
def _invoke(self, *cmd: str):
5 days ago
proc = subprocess.Popen(['sudo', '-E', 'lpac'] + list(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.env)
6 days ago
try:
5 days ago
out, err = proc.communicate(timeout=self.timeout_sec)
6 days ago
except subprocess.TimeoutExpired as e:
5 days ago
proc.kill()
6 days ago
raise LPAError(f"lpac {cmd} timed out after {self.timeout_sec} seconds") from e
messages = []
for line in out.decode().strip().splitlines():
if line.startswith('{'):
message = json.loads(line)
# lpac response format validations
assert 'type' in message, 'expected type in message'
assert message['type'] == 'lpa' or message['type'] == 'progress', 'expected lpa or progress message type'
assert 'payload' in message, 'expected payload in message'
assert 'code' in message['payload'], 'expected code in message payload'
5 days ago
assert 'data' in message['payload'], 'expected data in message payload'
6 days ago
5 days ago
msg_ret_code = message['payload']['code']
if msg_ret_code != 0:
raise LPAError(f"lpac {' '.join(cmd)} failed with code {msg_ret_code}: <{message['payload']['message']}> {message['payload']['data']}")
6 days ago
messages.append(message)
if len(messages) == 0:
raise LPAError(f"lpac {cmd} returned no messages")
return messages
5 days ago
def _validate_profile_exists(self, iccid: str) -> None:
if not any(p.iccid == iccid for p in self.list_profiles()):
5 days ago
raise LPAProfileNotFoundError(f'profile {iccid} does not exist')
def _validate_successful(self, msgs: list[dict]) -> None:
assert msgs[-1]['payload']['message'] == 'success', 'expected success notification'
if __name__ == "__main__":
5 days ago
parser = argparse.ArgumentParser(prog='esim.py', description='manage eSIM profiles on your comma device', epilog='comma.ai')
5 days ago
parser.add_argument('--enable', metavar='iccid', help='enable profile; will disable current profile')
parser.add_argument('--disable', metavar='iccid', help='disable profile')
parser.add_argument('--delete', metavar='iccid', help='delete profile (warning: this cannot be undone)')
parser.add_argument('--download', nargs=2, metavar=('qr', 'name'), help='download a profile using QR code')
parser.add_argument('--nickname', nargs=2, metavar=('iccid', 'name'), help='nickname for the downloaded profile')
5 days ago
args = parser.parse_args()
5 days ago
lpa = LPA()
5 days ago
if args.enable:
lpa.enable_profile(args.enable)
print('enabled profile, please restart device to apply changes')
5 days ago
elif args.disable:
lpa.disable_profile(args.disable)
print('disabled profile, please restart device to apply changes')
5 days ago
elif args.delete:
5 days ago
confirm = input('are you sure you want to delete this profile? (y/N) ')
if confirm == 'y':
lpa.delete_profile(args.delete)
print('deleted profile, please restart device to apply changes')
else:
print('cancelled')
exit(0)
5 days ago
elif args.download:
lpa.download_profile(args.download[0], args.download[1])
elif args.nickname:
lpa.nickname_profile(args.nickname[0], args.nickname[1])
5 days ago
else:
5 days ago
parser.print_help()
6 days ago
profiles = lpa.list_profiles()
5 days ago
print(f'\n{len(profiles)} profile{"s" if len(profiles) > 1 else ""}:')
for p in profiles:
5 days ago
print(f'- {p.iccid} (nickname: {p.nickname or "<none provided>"}) (provider: {p.provider}) - {"enabled" if p.enabled else "disabled"}')