Files
sunnypilot/selfdrive/ui/lib/api_helpers.py
Shane Smiskol 26261387f8 Fix raylib ui spamming API calls (#36700)
* intern

* start

* move

* common caching

* use constant for slep

* works

* add gating back

* clean up

* more

* match cache logic

* hate this circular

* not needed since sync

* no need for lock?

* even qt had something like _load_initial_state for tests, keep

* clean up

* clean up

* clean up

* loading json as string works, else it will fail to parse json, catch that and log, and next api call will overwrite

* move over firehose

* clean up

* fix test

* no

* flip

* more

* match qt

* consistent

* clean up

* cmt

* fix test!
2025-11-27 01:37:16 -08:00

110 lines
3.5 KiB
Python

import time
import threading
from collections.abc import Callable
from functools import lru_cache
from openpilot.common.api import Api, api_get
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.common.time_helpers import system_time_valid
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
TOKEN_EXPIRY_HOURS = 2
@lru_cache(maxsize=1)
def _get_token(dongle_id: str, t: int):
if not system_time_valid():
raise RuntimeError("System time is not valid, cannot generate token")
return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS)
def get_token(dongle_id: str):
return _get_token(dongle_id, int(time.monotonic() / (TOKEN_EXPIRY_HOURS / 2 * 60 * 60)))
class RequestRepeater:
API_TIMEOUT = 10.0 # seconds for API requests
SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread
def __init__(self, dongle_id: str, request_route: str, period: int, cache_key: str | None = None):
self._dongle_id = dongle_id
self._request_route = request_route
self._period = period # seconds
self._cache_key = cache_key
self._request_done_callbacks: list[Callable[[str, bool], None]] = []
self._prev_response_text = None
self._running = False
self._thread = None
self._params = Params()
if self._cache_key is not None:
# Cache successful responses to params
def cache_response(response: str, success: bool):
if success and response != self._prev_response_text:
self._params.put(self._cache_key, response)
self._prev_response_text = response
self.add_request_done_callback(cache_response)
def add_request_done_callback(self, callback: Callable[[str, bool], None]):
self._request_done_callbacks.append(callback)
def _do_callbacks(self, response_text: str, success: bool):
for callback in self._request_done_callbacks:
try:
callback(response_text, success)
except Exception as e:
cloudlog.error(f"RequestRepeater callback error: {e}")
def load_cache(self):
# call callbacks with cached response
if self._cache_key is not None:
self._prev_response_text = self._params.get(self._cache_key)
if self._prev_response_text:
self._do_callbacks(self._prev_response_text, True)
def start(self):
if self._thread and self._thread.is_alive():
return
self._running = True
self._thread = threading.Thread(target=self._worker_thread, daemon=True)
self._thread.start()
def stop(self):
self._running = False
if self._thread and self._thread.is_alive():
self._thread.join(timeout=1.0)
def _worker_thread(self):
# Avoid circular imports
from openpilot.selfdrive.ui.ui_state import ui_state, device
while self._running:
# Don't run when device is asleep or onroad
if not ui_state.started and device.awake:
self._send_request()
for _ in range(int(self._period / self.SLEEP_INTERVAL)):
if not self._running:
break
time.sleep(self.SLEEP_INTERVAL)
def _send_request(self):
if not self._dongle_id or self._dongle_id == UNREGISTERED_DONGLE_ID:
return
try:
identity_token = get_token(self._dongle_id)
response = api_get(self._request_route, timeout=self.API_TIMEOUT, access_token=identity_token)
self._do_callbacks(response.text, 200 <= response.status_code < 300)
except Exception as e:
cloudlog.error(f"Failed to send request to {self._request_route}: {e}")
self._do_callbacks("", False)
def __del__(self):
self.stop()