commaai/openpilot · error · RuntimeError

send_apdu failed

Error message

send_apdu failed

What it means

send_apdu() retries each APDU up to SEND_APDU_RETRIES=3 times on RuntimeError/ValueError (which includes missing +CGLA responses and bad hex parsing). This trailing raise is a defensive unreachable-in-practice branch: the last retry re-raises the original error, so 'send_apdu failed' would only appear if the loop exited without an exception on the final attempt.

Source

Thrown at openpilot/common/esim/lpa.py:257

  def send_apdu(self, apdu: bytes) -> tuple[bytes, int, int]:
    for attempt in range(SEND_APDU_RETRIES):
      try:
        if not self.channel:
          self.open_isdr()
        hex_payload = apdu.hex().upper()
        for line in self.query(f'AT+CGLA={self.channel},{len(hex_payload)},"{hex_payload}"'):
          if line.startswith("+CGLA:"):
            parts = line.split(":", 1)[1].split(",", 1)
            if len(parts) == 2:
              data = bytes.fromhex(parts[1].strip().strip('"'))
              if len(data) >= 2:
                return data[:-2], data[-2], data[-1]
        raise RuntimeError("Missing +CGLA response")
      except (RuntimeError, ValueError):
        self.channel = None
        if attempt == SEND_APDU_RETRIES - 1:
          raise
    raise RuntimeError("send_apdu failed")


# --- TLV utilities ---

def iter_tlv(data: bytes, with_positions: bool = False) -> Generator:
  idx, length = 0, len(data)
  while idx < length:
    start_pos = idx
    tag = data[idx]
    idx += 1
    if tag & 0x1F == 0x1F:  # Multi-byte tag
      tag_value = tag
      while idx < length:
        next_byte = data[idx]
        idx += 1
        tag_value = (tag_value << 8) | next_byte
        if not (next_byte & 0x80):
          break

View on GitHub (pinned to 516ec1e682)

Solutions

  1. If you see this message, inspect the real failure one frame up / enable DEBUG=1 for the raw AT transcript
  2. Check SEND_APDU_RETRIES was not configured to 0 or negative, which empties the retry loop and falls through to this raise
  3. In forks, ensure the last-attempt re-raise remains so callers get the root-cause exception
Defensive patterns

Strategy: try-catch

Try / catch

try:
  data, sw1, sw2 = client.send_apdu(apdu)
except RuntimeError as e:
  # the real cause is the original exception; 'send_apdu failed' indicates a modified/broken loop
  log.error('send_apdu exhausted retries: %s', e)
  raise

Prevention

When it happens

Trigger: Structurally unreachable in normal execution — the except block re-raises on the last attempt. Encountering this message would indicate control-flow tampering or an unexpected exception type escaping the loop; what users actually see after retry exhaustion is the underlying 'Missing +CGLA response', 'AT command failed', or 'AT command timed out' error.

Common situations: Essentially never in stock code; grep for it only if a fork modified the except clause or retry constants (e.g. SEND_APDU_RETRIES set to 0, making the loop body never run).

Related errors


AI-assisted analysis of commaai/openpilot@516ec1e682 (2026-08-15). Data as JSON: /api/errors/976fc672630bf109. Report an issue: GitHub.