commaai/openpilot · error · RuntimeError

AT command failed: {line}

Error message

AT command failed: {line}

What it means

While collecting the response to an AT command, _expect() received a line equal to 'ERROR' or starting with '+CME ERROR' — the modem explicitly rejected the command. The full offending line is embedded in the RuntimeError so the CMS/CME error code is visible.

Source

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

    if DEBUG:
      print(f"SER >> {cmd}", file=sys.stderr)
    self._serial.write((cmd + "\r").encode("ascii"))

  def _expect(self) -> list[str]:
    lines: list[str] = []
    while True:
      raw = self._serial.readline()
      if not raw:
        raise TimeoutError("AT command timed out")
      line = raw.decode(errors="ignore").strip()
      if not line:
        continue
      if DEBUG:
        print(f"SER << {line}", file=sys.stderr)
      if line == "OK":
        return lines
      if line == "ERROR" or line.startswith("+CME ERROR"):
        raise RuntimeError(f"AT command failed: {line}")
      lines.append(line)

  def _ensure_serial(self, reconnect: bool = False) -> None:
    if reconnect:
      self.channel = None
      try:
        if self._serial:
          self._serial.close()
      except Exception:
        pass
      self._serial = None
    if self._serial is None:
      self._serial = Serial(self._device, baudrate=self._baud, timeout=self._timeout)

  def query(self, cmd: str) -> list[str]:
    self._ensure_serial()
    try:
      self._send(cmd)

View on GitHub (pinned to 516ec1e682)

Solutions

  1. Read the CME error number in the message and map it (e.g. 10 = SIM not inserted, 13 = SIM failure, 100 = unknown) to the actual condition
  2. Reset the channel state and reopen: set client.channel = None and call open_isdr() again — the library already does this in send_apdu's retry loop
  3. If SIM is PIN-locked or missing, resolve that at the modem level (check with AT+CPIN?) before eSIM operations
  4. Reset the modem via lte.sh and retry once from a clean state if errors persist

Example fix

# before
resp = client.query(f'AT+CGLA={client.channel},{len(hex_payload)},"{hex_payload}"')
# RuntimeError: AT command failed: +CME ERROR: 100

# after — drop the stale channel and reopen the ISD-R before retrying
client.channel = None
client.open_isdr()
resp = client.query(f'AT+CGLA={client.channel},{len(hex_payload)},"{hex_payload}"')
Defensive patterns

Strategy: retry

Validate before calling

status = client.query('AT+CPIN?')
if any('SIM not ready' in l or 'ERROR' in l for l in status):
  raise SystemExit('SIM/modem not ready; resolve before eSIM ops')

Try / catch

try:
  client.query('AT+CGLA=...')
except RuntimeError as e:
  if 'CME ERROR' in str(e) or str(e).endswith('failed: ERROR'):
    client.channel = None
    client.open_isdr()  # re-establish channel, then retry once
    client.query('AT+CGLA=...')
  else:
    raise

Prevention

When it happens

Trigger: Malformed or unsupported AT commands (wrong parameter count for AT+CGLA, invalid channel/length), modem not registered/not ready for the requested operation, SIM PIN locked, or channel state lost so CGLA references a stale <channel>. '+CME ERROR: <n>' carries the specific mobile-equipment error code.

Common situations: The ISD-R logical channel closed underneath the client (modem reset) so subsequent AT+CGLA=<channel>,... fails; issuing AT commands before the modem finished booting; SIM locked or absent; modem firmware that doesn't support the requested command.

Related errors


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