sunnylink: Registration API

This commit is contained in:
DevTekVE
2024-03-13 18:29:58 +00:00
committed by Jason Wen
parent e2d6a9ab3e
commit 28efb5388b
10 changed files with 222 additions and 38 deletions
+17 -36
View File
@@ -1,46 +1,27 @@
import jwt
import os
import requests
from datetime import datetime, timedelta
from openpilot.system.hardware.hw import Paths
from openpilot.system.version import get_version
from .comma_connect import CommaConnectApi
from .sunnylink import SunnylinkApi
API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com')
class Api():
def __init__(self, dongle_id):
self.dongle_id = dongle_id
with open(Paths.persist_root()+'/comma/id_rsa') as f:
self.private_key = f.read()
class Api:
def __init__(self, dongle_id, use_sunnylink=False):
if use_sunnylink:
self.service = SunnylinkApi(dongle_id)
else:
self.service = CommaConnectApi(dongle_id)
def request(self, method, endpoint, **params):
return self.service.request(method, endpoint, **params)
def get(self, *args, **kwargs):
return self.request('GET', *args, **kwargs)
return self.service.get(*args, **kwargs)
def post(self, *args, **kwargs):
return self.request('POST', *args, **kwargs)
def request(self, method, endpoint, timeout=None, access_token=None, **params):
return api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params)
return self.service.post(*args, **kwargs)
def get_token(self, expiry_hours=1):
now = datetime.utcnow()
payload = {
'identity': self.dongle_id,
'nbf': now,
'iat': now,
'exp': now + timedelta(hours=expiry_hours)
}
token = jwt.encode(payload, self.private_key, algorithm='RS256')
if isinstance(token, bytes):
token = token.decode('utf8')
return token
return self.service.get_token(expiry_hours)
def api_get(endpoint, method='GET', timeout=None, access_token=None, **params):
headers = {}
if access_token is not None:
headers['Authorization'] = "JWT " + access_token
headers['User-Agent'] = "openpilot-" + get_version()
return requests.request(method, API_HOST + "/" + endpoint, timeout=timeout, headers=headers, params=params)
def api_get(endpoint, method='GET', timeout=None, access_token=None, use_sunnylink=False, **params):
return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params) if not use_sunnylink else \
SunnylinkApi(None).api_get(endpoint, method, timeout, access_token, **params)
+49
View File
@@ -0,0 +1,49 @@
import jwt
import requests
from datetime import datetime, timedelta
from openpilot.system.hardware.hw import Paths
from openpilot.system.version import get_version
class BaseApi:
def __init__(self, dongle_id, api_host, user_agent="openpilot-"):
self.dongle_id = dongle_id
self.api_host = api_host
self.user_agent = user_agent
with open(Paths.persist_root()+'/comma/id_rsa') as f:
self.private_key = f.read()
def get(self, *args, **kwargs):
return self.request('GET', *args, **kwargs)
def post(self, *args, **kwargs):
return self.request('POST', *args, **kwargs)
def request(self, method, endpoint, timeout=None, access_token=None, **params):
return self.api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params)
def _get_token(self, expiry_hours=1, **extra_payload):
now = datetime.utcnow()
payload = {
'identity': self.dongle_id,
'nbf': now,
'iat': now,
'exp': now + timedelta(hours=expiry_hours),
**extra_payload
}
token = jwt.encode(payload, self.private_key, algorithm='RS256')
if isinstance(token, bytes):
token = token.decode('utf8')
return token
def get_token(self, expiry_hours=1):
return self._get_token(expiry_hours)
def api_get(self, endpoint, method='GET', timeout=None, access_token=None, **params):
headers = {}
if access_token is not None:
headers['Authorization'] = "JWT " + access_token
headers['User-Agent'] = self.user_agent + get_version()
return requests.request(method, self.api_host + "/" + endpoint, timeout=timeout, headers=headers, params=params)
+11
View File
@@ -0,0 +1,11 @@
import os
from openpilot.common.api.base import BaseApi
API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com')
class CommaConnectApi(BaseApi):
def __init__(self, dongle_id):
super().__init__(dongle_id, API_HOST)
self.user_agent = "openpilot-"
+118
View File
@@ -0,0 +1,118 @@
import os
import time
import jwt
import json
from pathlib import Path
from datetime import datetime, timedelta
from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE
from openpilot.system.hardware.hw import Paths
from openpilot.common.api.base import BaseApi
API_HOST = os.getenv('SUNNYLINK_API_HOST', 'https://stg.api.sunnypilot.ai')
UNREGISTERED_SUNNYLINK_DONGLE_ID = "UnregisteredDevice"
MAX_RETRIES = 6
class SunnylinkApi(BaseApi):
def __init__(self, dongle_id):
super().__init__(dongle_id, API_HOST)
self.user_agent = "sunnypilot-"
self.spinner = None
def get_token(self, expiry_hours=1):
# Add your additional data here
additional_data = {}
return super()._get_token(expiry_hours, **additional_data)
def _status_update(self, message):
print(message)
if self.spinner:
self.spinner.update(message)
time.sleep(0.5)
def _resolve_dongle_ids(self, params):
sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8')
comma_dongle_id = self.dongle_id or params.get("DongleId", encoding='utf-8')
return sunnylink_dongle_id, comma_dongle_id
def _resolve_imeis(self, params):
imei1, imei2 = None, None
imei_try = 0
while imei1 is None and imei2 is None and imei_try < MAX_RETRIES:
try:
imei1, imei2 = params.get("IMEI", encoding='utf8') or HARDWARE.get_imei(0), HARDWARE.get_imei(1)
except Exception:
self._status_update(f"Error getting imei, trying again... [{imei_try+1}/{MAX_RETRIES}]")
time.sleep(1)
imei_try += 1
return imei1, imei2
def _resolve_serial(self, params):
serial = params.get("HardwareSerial", encoding='utf8') or HARDWARE.get_serial()
return serial
def register_device(self, spinner=None, timeout=60, verbose=False):
self.spinner = spinner
params = Params()
sunnylink_dongle_id, comma_dongle_id = self._resolve_dongle_ids(params)
if comma_dongle_id is None:
self._status_update("Comma dongle ID not found, deferring sunnylink's registration to comma's registration process.")
return None
imei1, imei2 = self._resolve_imeis(params)
serial = self._resolve_serial(params)
if sunnylink_dongle_id not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID):
return sunnylink_dongle_id
privkey_path = Path(Paths.persist_root()+"/comma/id_rsa")
pubkey_path = Path(Paths.persist_root()+"/comma/id_rsa.pub")
if not pubkey_path.is_file():
sunnylink_dongle_id = UNREGISTERED_SUNNYLINK_DONGLE_ID
self._status_update("Public key not found, setting dongle ID to unregistered.")
else:
with pubkey_path.open() as f1, privkey_path.open() as f2:
public_key = f1.read()
private_key = f2.read()
start_time = time.monotonic()
backoff = 1
while True:
register_token = jwt.encode({'register': True, 'exp': datetime.utcnow() + timedelta(hours=1)}, private_key, algorithm='RS256')
try:
if verbose or time.monotonic() - start_time < timeout / 2:
self._status_update("Registering device to sunnylink...")
elif time.monotonic() - start_time >= timeout / 2:
self._status_update("Still registering device to sunnylink...")
resp = self.api_get("v2/pilotauth/", method='POST', timeout=15, imei=imei1, imei2=imei2, serial=serial, comma_dongle_id=comma_dongle_id, public_key=public_key, register_token=register_token)
if resp.status_code != 200:
raise Exception(f"Failed to register with sunnylink. Status code: {resp.status_code}")
else:
dongleauth = json.loads(resp.text)
sunnylink_dongle_id = dongleauth["dongle_id"]
if sunnylink_dongle_id:
self._status_update("Device registered successfully.")
break
except Exception as e:
if verbose:
self._status_update(f"Waiting {backoff}s before retry, Exception occurred during registration: [{str(e)}]")
backoff = min(backoff * 2, 60)
time.sleep(backoff)
if time.monotonic() - start_time > timeout:
self._status_update(f"Giving up on sunnylink's registration after {timeout}s. Will retry on next boot.")
time.sleep(3)
break
if sunnylink_dongle_id:
params.put("SunnylinkDongleId", sunnylink_dongle_id)
self.spinner = None
return sunnylink_dongle_id
+1
View File
@@ -305,6 +305,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"StandStillTimer", PERSISTENT | BACKUP},
{"StockLongToyota", PERSISTENT | BACKUP},
{"SubaruManualParkingBrakeSng", PERSISTENT | BACKUP},
{"SunnylinkDongleId", PERSISTENT},
{"TorqueDeadzoneDeg", PERSISTENT | BACKUP},
{"TorqueFriction", PERSISTENT | BACKUP},
{"TorqueMaxLatAccel", PERSISTENT | BACKUP},
+2 -1
View File
@@ -86,7 +86,8 @@ function launch {
if [ ! -f $DIR/prebuilt ]; then
./build.py
fi
./mapd_installer.py && ./manager.py
./sunnylink.py; ./mapd_installer.py; ./manager.py
# if broken, keep on screen error
while true; do sleep 1; done
+4
View File
@@ -44,6 +44,9 @@ common/transformations/transformations.pxd
common/transformations/transformations.pyx
common/api/__init__.py
common/api/base.py
common/api/comma_connect.py
common/api/sunnylink.py
release/*
@@ -329,6 +332,7 @@ selfdrive/manager/__init__.py
selfdrive/manager/build.py
selfdrive/manager/helpers.py
selfdrive/manager/mapd_installer.py
selfdrive/manager/sunnylink.py
selfdrive/manager/manager.py
selfdrive/manager/process_config.py
selfdrive/manager/process.py
+3
View File
@@ -6,6 +6,7 @@ from pathlib import Path
from datetime import datetime, timedelta
from openpilot.common.api import api_get
from openpilot.common.api.sunnylink import SunnylinkApi
from openpilot.common.params import Params
from openpilot.common.spinner import Spinner
from openpilot.selfdrive.controls.lib.alertmanager import set_offroad_alert
@@ -86,6 +87,8 @@ def register(show_spinner=False) -> str | None:
if time.monotonic() - start_time > 60 and show_spinner:
spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})")
SunnylinkApi(dongle_id).register_device(spinner if show_spinner else None)
if show_spinner:
spinner.close()
-1
View File
@@ -139,5 +139,4 @@ if __name__ == "__main__":
install_manager.update_installed_version(VERSION)
else:
spinner.update(f"Checking if mapd is installed and valid. Prebuilt [{is_prebuilt()}]")
time.sleep(1)
install_manager.non_prebuilt_install()
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
from openpilot.common.api.sunnylink import SunnylinkApi
from openpilot.common.spinner import Spinner
from openpilot.system.version import is_prebuilt
if __name__ == "__main__":
spinner = Spinner()
extra_args = {}
if not is_prebuilt():
extra_args = {
"verbose": True,
"timeout": 60
}
SunnylinkApi(None).register_device(spinner, **extra_args)
spinner.close()