# OBD-II Bluetooth Adapter (ELM327 / STN) — Device Specification
# Status: Complete for the adapter link layer. The three GATT families below cover
#         most of the market; per-model coverage is open for contribution.
# Copyright 2026 Pigs Can Fly Labs LLC
# SPDX-License-Identifier: Apache-2.0

device:
  name: "OBD-II Bluetooth Adapter (ELM327 / STN)"
  manufacturer: "Generic (ELM327 clones), ScanTool.net, Vgate, LELink"
  manufacturer_status: "unsupported"
  protocol: "ble"
  notes: >
    The dongle every vehicle target in this repo is reached through — the transport under
    the transport. Bluetooth side is a serial pipe (Classic SPP/RFCOMM or BLE GATT)
    carrying an ASCII command set; vehicle side is OBD-II/UDS (see the `obd` block).

    Because the payload is ASCII, a Bluetooth capture of a vendor app driving one of these
    adapters is a plain-text transcript of the vendor's diagnostic protocol. That makes
    this device the cheapest capture path for any OBD target.

    Commands are CR-terminated; replies are complete only when the '>' prompt arrives, so
    a client must buffer across notifications rather than parsing each one. BLE MTU is
    commonly 20 bytes, so multi-notification replies are the norm, not the exception.

    CONFIDENCE: GATT families A and B are cross-referenced against open-source BLE OBD
    clients (verification: confirmed by third-party implementations). Family C is the
    HM-10 pattern and is community-reported. The AT/ST command set is public
    documentation. No pairing, authentication or encryption is used by most BLE adapters.

    SAFETY: OBD pin 16 is usually unswitched — a dongle left plugged in will flatten a
    battery, quickly on a motorcycle. Unplug after each session.
  identification:
    local_name_prefix: "OBD"
    service_uuids:
      - "0000fff0-0000-1000-8000-00805f9b34fb"
      - "000018f0-0000-1000-8000-00805f9b34fb"
      - "0000ffe0-0000-1000-8000-00805f9b34fb"
  discovery:
    methods:
      - type: "ble_scan"
        ble:
          local_name:
            match: "contains"
            value: "OBD"
          service_uuids:
            - "0000fff0-0000-1000-8000-00805f9b34fb"
            - "000018f0-0000-1000-8000-00805f9b34fb"
            - "0000ffe0-0000-1000-8000-00805f9b34fb"
    identity:
      stable_keys: ["address"]
      display: "local_name"
  variants:
    - model: "GATT family A (FFF0/FFF1/FFF2)"
      identification:
        service_uuids: ["0000fff0-0000-1000-8000-00805f9b34fb"]
      notes: "OBDLink CX and a large share of generic adapters. Separate notify and write characteristics."
    - model: "GATT family B (18F0/2AF0/2AF1)"
      identification:
        service_uuids: ["000018f0-0000-1000-8000-00805f9b34fb"]
      notes: "Vgate iCar Pro 2S BLE, LELink2. Separate notify and write characteristics."
    - model: "GATT family C (FFE0/FFE1)"
      identification:
        service_uuids: ["0000ffe0-0000-1000-8000-00805f9b34fb"]
      notes: >
        HM-10 module clones. Single bidirectional characteristic — the same pattern as the
        MoTool Slacker. Community-reported.
    - model: "UniCarScan UCSI-2100 (WGSoft)"
      identification:
        local_name_prefix: "UniCarScan"
      notes: >
        ARM Cortex-M0, Bluetooth 4.0 (BLE), works from iOS, Android and Windows. ELM327
        command-set compatible but accepts messages up to 255 bytes where a stock ELM327
        stops at 8 — long diagnostic writes become one command instead of a flow-control
        negotiation. Protocols: ISO9141-2, KWP2000 slow and fast init, CAN 11-bit and
        29-bit at 250 and 500 kbit/s, plus the BMW OEM protocols KWP2000, KWP2000*, D-CAN
        and UDS. Field-updatable firmware. The vendor's reference adapter for MotoScan,
        and named by TuneECU and BimmerCode.
    - model: "OBDLink MX Bluetooth"
      identification:
        local_name_prefix: "OBDLink MX"
      notes: >
        STN1170 (reported). Bluetooth Classic v3.0 SPP — NO iOS support. First Bluetooth
        adapter to reach GM SW-CAN and Ford MS-CAN alongside the legislated protocols.
        Backwards compatible with the ELM327 command set plus the ST extensions.
    - model: "OBDLink MX+"
      identification:
        local_name_prefix: "OBDLink MX+"
      notes: >
        STN2255 (reported). Bluetooth Classic v3.0 with MFi, so iOS, Android and Windows
        all work. MS-CAN and SW-CAN as well as every legislated protocol — the only
        wireless iOS adapter that reaches those manufacturer buses.
    - model: "Bluetooth Classic SPP (no GATT)"
      notes: >
        RFCOMM, usually channel 1, PIN 1234 / 0000 / 6789. Names OBDII, OBDII-BT, V-LINK,
        Vgate, OBDLink LX/MX+. Not reachable from iOS unless MFi-certified; not reachable
        from a BLE-only client at all. Linux: `rfcomm bind 0 <mac> 1` -> /dev/rfcomm0.

  setup:
    required: true
    confidence: "medium"
    notes: >
      The adapter is the one part of an OBD-II stack that does need pairing.
      Bluetooth Classic variants ask for a PIN; BLE variants generally do not.
      Which you have is not always printed on the dongle, and the two are
      frequently sold under the same listing.
    methods:
      - type: "ble_direct"
        verified: false
        description: >
          BLE variants advertise as soon as they are powered from the connector
          and accept a connection with no pairing. Identify by the advertised
          name prefix plus one of the serial-service UUIDs.
        ble:
          advertised_name: "OBD"
          pairing_required: false
        steps:
          - action: "Plug the adapter into the diagnostic connector and switch on the ignition."
            actor: "user"
            expect: "Adapter LEDs light and it begins advertising."
          - action: "Scan for the advertised name prefix and a serial-service UUID, then connect."
            actor: "client"
            request:
              protocol: "ble_gatt"
          - action: "Confirm the command interface with ATI and record the reported firmware string."
            actor: "client"
            expect: >
              An ELM327 identification string. Cloned firmware reports a
              version it does not implement, so match the adapter_profile class
              rather than trusting this string.
      - type: "button_pairing"
        verified: false
        description: >
          Bluetooth Classic (SPP/RFCOMM) variants pair with the host operating
          system first, using a fixed PIN. There is no button; the PIN is the
          proof of possession, and it is not a secret.
        steps:
          - action: "Plug the adapter in and power the ignition."
            actor: "user"
          - action: "Pair from the host's Bluetooth settings, entering the adapter's PIN."
            actor: "user"
            notes: >
              Commonly 1234 or 0000, printed in the manual. Treat it as a
              default rather than a credential — it is identical across
              enormous numbers of dongles.
          - action: "Open the resulting serial port and confirm with ATI."
            actor: "client"
    factory_reset:
      confidence: "medium"
      effect: >
        Clears the adapter's own configuration — protocol selection, echo,
        headers, timeouts and any saved defaults — and returns it to power-on
        state. Nothing about the vehicle is touched.
      procedures:
        - name: "ATZ software reset"
          indicator: "The adapter re-emits its identification string."
          steps:
            - action: "Send ATZ on the command interface."
              actor: "client"
              expect: "Adapter resets and reports its firmware string."
            - action: "Send ATD to restore defaults if settings persist across the reset."
              actor: "client"
        - name: "Power cycle"
          steps:
            - action: "Unplug the adapter from the connector for a few seconds and reinsert it."
              actor: "user"
              notes: >
                Also the remedy when the adapter is wedged, which cloned
                firmware does under multi-frame load.
    rejoin:
      in_place_supported: true
      requires_factory_reset: false
      notes: >
        Pairing with a different host is just pairing again. Remove the old
        bond from the previous host as well — Classic adapters accept one
        connection at a time, and a phone reconnecting in the background is the
        usual cause of an adapter that "stopped working".
    credentials:
      wifi_passphrase_protection: "not_applicable"
      stored_on_device:
        - "Bluetooth bond with the paired host (Classic variants)"
      issued_to_client: []
      notes: >
        The pairing PIN is a fixed default shared across a whole product
        category, so it identifies nothing and protects nothing. Anyone within
        radio range of a plugged-in adapter can reach the vehicle bus through
        it — worth knowing before leaving one in the connector.

services:
  - uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
    name: "Serial Pipe (family A)"
    characteristics:
      - uuid: "0000fff1-0000-1000-8000-00805f9b34fb"
        name: "Adapter to Host (notify)"
        properties: ["notify"]
      - uuid: "0000fff2-0000-1000-8000-00805f9b34fb"
        name: "Host to Adapter (write)"
        properties: ["write", "write_without_response"]
        commands:
          reset:
            description: "ATZ — full reset; returns the firmware banner. Claimed version is not proof of chipset."
            encoding: "bytes"
            value: [0x41, 0x54, 0x5A, 0x0D]
          identify:
            description: "ATI — identify without resetting."
            encoding: "bytes"
            value: [0x41, 0x54, 0x49, 0x0D]
          echo_off:
            description: "ATE0 — echo off; send before scripting anything."
            encoding: "bytes"
            value: [0x41, 0x54, 0x45, 0x30, 0x0D]
          read_voltage:
            description: "ATRV — adapter's estimate of battery voltage (approximate, not from the ECU)."
            encoding: "bytes"
            value: [0x41, 0x54, 0x52, 0x56, 0x0D]
          select_can_500k:
            description: "ATSP6 — select ISO 15765-4 CAN, 11-bit, 500 kbit/s."
            encoding: "bytes"
            value: [0x41, 0x54, 0x53, 0x50, 0x36, 0x0D]
          can_formatting_off:
            description: "ATCAF0 — raw frames; ISO-TP segmentation becomes the client's job. Clone firmware sometimes refuses."
            encoding: "bytes"
            value: [0x41, 0x54, 0x43, 0x41, 0x46, 0x30, 0x0D]
          flow_control_mode_1:
            description: "ATFCSM1 — use client-supplied flow control. If this fails, multi-frame transmit will fail."
            encoding: "bytes"
            value: [0x41, 0x54, 0x46, 0x43, 0x53, 0x4D, 0x31, 0x0D]

  - uuid: "000018f0-0000-1000-8000-00805f9b34fb"
    name: "Serial Pipe (family B)"
    characteristics:
      - uuid: "00002af0-0000-1000-8000-00805f9b34fb"
        name: "Adapter to Host (notify)"
        properties: ["notify"]
      - uuid: "00002af1-0000-1000-8000-00805f9b34fb"
        name: "Host to Adapter (write)"
        properties: ["write", "write_without_response"]

  - uuid: "0000ffe0-0000-1000-8000-00805f9b34fb"
    name: "Serial Pipe (family C, HM-10 pattern)"
    characteristics:
      - uuid: "0000ffe1-0000-1000-8000-00805f9b34fb"
        name: "Bidirectional Serial"
        properties: ["read", "write", "write_without_response", "notify"]

obd:
  role: "adapter"

  # The market splits into four capability tiers. This spec documents the family, so the
  # profile below is the *baseline* every listed adapter meets; the variants above say
  # which physical dongles sit in which tier. A consumer should match a request's
  # `requires` list against the capabilities of the tier actually in hand.
  adapter_profile:
    class: "standards-elm327"
    capabilities:
      - "single_frame"
      - "multiframe_rx"
      - "custom_headers"
    # Per-model capabilities, since the named adapters differ in ways that decide whether
    # a given vendor function will run at all.
    models:
      - name: "UniCarScan UCSI-2100"
        class: "advanced-stn"
        capabilities:
          - "single_frame"
          - "multiframe_rx"
          - "multiframe_tx"
          - "flow_control"
          - "custom_headers"
          - "raw_frames"
        notes: "255-byte message support is how it satisfies multiframe_tx. No SW-CAN/MS-CAN."
        verification: "reported"
      - name: "OBDLink MX Bluetooth"
        class: "advanced-stn"
        capabilities:
          - "single_frame"
          - "multiframe_rx"
          - "multiframe_tx"
          - "flow_control"
          - "custom_headers"
          - "raw_frames"
          - "monitor_all"
          - "alt_can_bus"
        notes: "SW-CAN and MS-CAN capable. Bluetooth Classic only — not reachable from iOS."
        verification: "reported"
      - name: "OBDLink MX+"
        class: "advanced-stn"
        capabilities:
          - "single_frame"
          - "multiframe_rx"
          - "multiframe_tx"
          - "flow_control"
          - "custom_headers"
          - "raw_frames"
          - "monitor_all"
          - "alt_can_bus"
        notes: "As MX, plus MFi so iOS works. The only wireless iOS adapter reaching MS-CAN/SW-CAN."
        verification: "reported"
    known_limitations:
      - >
        basic-clone tier (cloned firmware sold as "ELM327 v2.1"): legislated single-frame
        reads only. Multi-frame transmit, client-supplied flow control and sometimes
        ATCAF0 fail or are silently wrong. TuneECU Lite's own listing states clone v2.1
        adapters do not work and a genuine v1.4/1.5 is required.
      - >
        standards-elm327 tier: reliable for reads and custom headers, but multi-frame
        transmit and tight timing are firmware-dependent — this is the tier where
        maintenance functions become a coin toss.
      - >
        advanced-stn tier (OBDLink LX/MX+/CX): adds dependable multiframe_tx,
        flow_control, raw_frames and the ST command set. This is what vendor tools name
        when a function must work.
      - >
        native-can tier (SocketCAN, PCAN, Kvaser): adds monitor_all and
        non_standard_bitrate. No AT layer, so nothing is hidden — the research tier.
      - >
        alt_can_bus (Ford MS-CAN, GM SW-CAN) cuts across the tiers: only the OBDLink MX,
        MX+ and EX provide it. An otherwise advanced-stn adapter without it cannot run
        FORScan's body and chassis module functions, because those buses sit on
        non-standard pins rather than behind a protocol setting.

  connector:
    standard: "sae-j1962"
    location: "vehicle diagnostic socket"
    pins:
      - pin: 6
        signal: "CAN-H"
        verification: "confirmed"
      - pin: 14
        signal: "CAN-L"
        verification: "confirmed"
      - pin: 7
        signal: "K-line"
        verification: "confirmed"
      - pin: 16
        signal: "battery +12V (usually unswitched)"
        verification: "confirmed"

  transport:
    standard: "iso15765-4"
    bitrate: 500000
    addressing: "11bit"
    request_id: "0x7E0"
    response_id: "0x7E8"
    functional_id: "0x7DF"
    verification: "confirmed"

  adapter_requirements:
    - "Single-frame reads (mode 01/03) work on essentially any adapter including clones."
    - >
      Multi-frame ISO-TP transmit and custom request headers need sound firmware
      — an STN
      chipset (OBDLink LX/MX+/CX) in practice. This is the dividing line behind
      vendor
      tools accepting any adapter for reads while naming specific ones for
      maintenance
      functions.

  requests:
    - name: "read_supported_pids"
      description: "Mode 01 PID 00 — bitmask of supported live-data PIDs; a safe first liveness check."
      command_class: "basic"
      requires: ["single_frame"]
      service: "01"
      request: "01 00"
      expected_response: "41 00 ?? ?? ?? ??"
      writes: false
      verification: "confirmed"
    - name: "read_dtcs"
      description: "Mode 03 — stored diagnostic trouble codes. Single frame, works on clones."
      command_class: "basic"
      requires: ["single_frame"]
      service: "03"
      request: "03"
      expected_response: "43 ??"
      writes: false
      verification: "confirmed"
    - name: "read_vin"
      description: >
        Mode 09 PID 02 — VIN. Legislated, but the reply is multi-frame, which makes it the
        cheapest test of whether an adapter can reassemble ISO-TP at all.
      command_class: "basic"
      requires: ["single_frame", "multiframe_rx"]
      service: "09"
      request: "09 02"
      expected_response: "49 02 ??"
      writes: false
      verification: "confirmed"
    - name: "read_vin_uds"
      description: "UDS ReadDataByIdentifier of the standard VIN DID — first step off the legislated path."
      command_class: "advanced"
      requires: ["custom_headers", "multiframe_rx"]
      service: "22"
      request: "22 F1 90"
      expected_response: "62 F1 90 ??"
      writes: false
      verification: "confirmed"

  data_identifiers:
    - did: "0xF190"
      name: "VIN (standard UDS DID)"
      type: "string"
      verification: "confirmed"
    - did: "0xF186"
      name: "Active diagnostic session (standard UDS DID)"
      type: "uint8"
      verification: "confirmed"
