# Triumph Tiger 900 — Device Specification
# Status: In Progress — the service interval reset message and most of the surrounding
#         diagnostic surface were recovered by static analysis of TigerTool V3.51
#         (freeware, SHA-256 3c7270ef1bf0ab1f70920dc60baf48883907079fbeecc620e77eb08cd07b3d79)
#         and independently confirmed by decompiling TuneECU 23
#         (SHA-256 f724294669a3bc008d81dbb590a8c0bfa1b4ac4a223d524e040c85bc885408eb).
#         Two unrelated codebases build the same command with the same scaling.
#         Derived from tool binaries, NOT yet reproduced on a bike, so these carry
#         `verification: reported` rather than `confirmed`.
# Copyright 2026 Pigs Can Fly Labs LLC
# SPDX-License-Identifier: Apache-2.0

device:
  name: "Triumph Tiger 900"
  manufacturer: "Triumph Motorcycles Ltd."
  manufacturer_status: "active"
  protocol: "obd2"
  notes: >
    The Tiger 900's spanner/wrench service reminder cannot be cleared from the bike's own
    controls; it needs a dealer or a paid third-party tool (TuneECU, TigerTool, DealerTool,
    HealTech Maintenance Mate) plus, on many Triumph models, an STN-chipset OBD adapter.

    The transport is open — ISO 15765-4 CAN on a standard diagnostic connector — and the
    gate is a manufacturer-specific application-layer message. That message is the open
    question this spec exists to close.

    The reminder has two independent components. The service-due DISTANCE is reset over
    the diagnostic connector. The service-due DATE, on calendar-equipped models, is reset
    from the bike's own instrument menu and is explicitly not touched by TigerTool. Both
    the odometer and the service values are stored in the instrument cluster in kilometres,
    which is why the owning ECU still has to be established.

    The reset is NOT a UDS request to the engine ECU. It goes to the instrument cluster on
    CAN 0x701 as a two-byte proprietary frame, 33 <km/100> or 34 <miles/100> — which is
    why every tool's UI only offers intervals in multiples of 100: the wire format cannot
    express anything else. Every reply on this stack is the request opcode with bit 7 set.

    Four separate stacks are in play. Engine ECU speaks UDS over 29-bit CAN (18 DA D5 F1).
    Instrument cluster and immobiliser speak a proprietary raw-frame protocol on 11-bit
    0x701/0x704 and 0x604/0x602 with ISO-TP formatting switched OFF. ABS speaks KWP2000
    over K-line with ISO init address 0x43.

    CONFIDENCE: everything is derived from TigerTool V3.51's own code paths (verification:
    reported). Nothing here has been executed against a bike, and the reply field layouts
    for the SIA query opcodes are not yet decoded.

    SCOPE: this exists for repair-café and owner maintenance. The service reset is a write
    and is meant to be used — stationary bike, engine off, owner's consent, current values
    read back first. Functions needing expertise (ABS bleed, immobiliser and TPMS writes)
    carry `advanced: true`: they are documented because an old bike needs them, and the
    flag tells a consumer to warn and confirm. ABS bleed continues in the service manual.
  variants:
    - model: "Tiger 900 GT / GT Pro / Rally / Rally Pro (2020–2023)"
      identification:
        connector: "sae-j1962"
    - model: "Tiger 900 / 850 Gen 2 (MY2024+)"
      identification:
        connector: "iso-19689"
      notes: >
        Treat as a separate target. TigerTool does not connect to Gen 2 bikes because
        Triumph relocated ECU data, and HealTech's Maintenance Mate did not initially
        support MY2024 — so the 2020–2023 findings must not be assumed to carry over.

  setup:
    required: false
    confidence: "high"
    notes: >
      Nothing to provision. The bike is reached by plugging an adapter into its
      diagnostic connector; the setup that matters is the adapter's, not the
      vehicle's. Recorded so the absence is explicit rather than an omission.
    methods:
      - type: "wired"
        verified: false
        description: >
          Plug an adapter into the diagnostic connector and power the ignition.
          There is no provisioning, no credential exchange and no pairing on
          the vehicle side — whatever pairing is needed belongs to the adapter
          (see device-specs/devices/obd2-bluetooth-adapter.yaml).
        steps:
          - action: "Locate the diagnostic connector — see obd.connector for its type and position."
            actor: "user"
          - action: "Plug in the adapter and switch the ignition on without starting the engine, unless a procedure says otherwise."
            actor: "user"
            expect: "The adapter powers up from the connector."
          - action: "Initialise the adapter for this vehicle's CAN parameters, per obd.transport."
            actor: "client"
            notes: >
              The AT/ST init sequence is vehicle-specific and is recorded in
              obd.transport.notes; it is not a generic OBD-II session.
    factory_reset:
      applicable: false
      effect: >
        Not applicable. A vehicle holds no credentials or pairing state a
        client can clear, so there is nothing a reset would undo. Adaptation
        writes, coding and flashing are documented elsewhere in this spec and
        are in scope — see the advanced request definitions and the risk tiers
        in docs/protocols/obd2-common.md — but they are repair operations with
        their own recovery paths, not a way to return the vehicle to an
        unprovisioned state. Do not present them as a setup step.
    rejoin:
      in_place_supported: true
      requires_factory_reset: false
      notes: >
        Moving to a different adapter or host is a physical swap. The vehicle
        keeps no record of which tool last connected.
    credentials:
      wifi_passphrase_protection: "not_applicable"
      stored_on_device: []
      issued_to_client: []
      notes: >
        Access control is physical: whoever can reach the connector can talk to
        the vehicle. Some functions are additionally gated by a security-access
        seed/key exchange — see the request definitions.

obd:
  role: "vehicle"

  connector:
    standard: "sae-j1962"
    location: "under the pillion seat"
    model_years: "2020–2023 (MY2024+ Gen 2 bikes use the 6-pin ISO 19689 connector and are NOT covered by these findings)"
    pins:
      - pin: 6
        signal: "CAN-H"
        verification: "reported"
      - pin: 14
        signal: "CAN-L"
        verification: "reported"
      - pin: 7
        signal: "K-line (ABS and engine-ECU ping stack)"
        verification: "reported"
      - pin: 16
        signal: "battery +12V"
        verification: "reported"
      - pin: 4
        signal: "chassis ground"
        verification: "reported"

  # The bike presents more than one stack. This block describes the cluster stack, where
  # the service interval lives; per-ECU addressing for the others is under `ecus`.
  transport:
    standard: "iso15765-4"
    bitrate: 500000
    addressing: "11bit"
    request_id: "0x701"
    response_id: "0x704"
    verification: "reported"
    notes: >
      ELM327 setup used by TigerTool for this stack: AT TP6, AT E0, AT H1, AT
      L0, AT CFC0,
      AT CAF0, AT SH701, AT CRA704, AT ST7F. CAF0 and CFC0 mean auto-formatting
      and flow
      control are OFF — frames are raw and short, first byte is an opcode, and
      there is no
      ISO-TP layer at all on this stack.

  adapter_requirements:
    - "Raw frame transmit (ATCAF0) and arbitrary headers/filters (ATSH/ATCRA) are mandatory — this is not legislated OBD-II."
    - "Session switches between 11-bit CAN, 29-bit CAN and K-line, so the adapter must handle protocol changes mid-session."
    - "K-line functions additionally need a custom ISO init address (ATIIA43)."
    - >
      This is the real reason clone ELM327 firmware fails while DTC reads
      succeed: it never
      reaches a payload, failing at the first ATCAF0 or ATCRA.

  ecus:
    - name: "Instrument cluster"
      request_id: "0x701"
      response_id: "0x704"
      owns:
        - "service interval (distance)"
        - "service date"
        - "odometer (kilometres)"
        - "instrument menu and unit settings"
      verification: "reported"
    - name: "Engine ECU (Keihin) — UDS"
      request_id: "0x18DAD5F1"
      response_id: "0x18DAF1D5"
      owns:
        - "ECU identity (tune, cal/build, serial, VIN)"
        - "diagnostic trouble codes"
        - "throttle balance MAP data"
      verification: "reported"
      notes: >
        29-bit addressing, target 0xD5, source (tester) 0xF1. Functional broadcast is
        0x18DB33F1. A second ECU answers on 0x18DAF1D6 (address 0xD6) during tune checks.
    - name: "Engine + ABS ECU — K-line (KWP2000)"
      owns:
        - "ECU ping / keepalive"
        - "security access (seed/key)"
        - "ABS DTCs"
        - "ABS brake bleed"
      verification: "reported"
      notes: >
        KWP2000 header 68 6A F1 (target 0x6A, source 0xF1) via AT SH 68 6A F1 and AT TP3.
        ABS additionally requires AT IIA43 (ISO init address 0x43).
    - name: "Engine-side ECU 0xC1"
      request_id: "0x18DAC1F1"
      owns: ["unknown — probed with UDS 10 03 extended session"]
      verification: "reported"
      notes: "Seen only in TuneECU. Confirms a UDS session exists on this platform, though not in front of the service reset."
    - name: "Engine-side ECU 0xC8"
      request_id: "0x18DAC8F1"
      owns: ["unknown — probed with UDS 10 03 extended session"]
      verification: "reported"
      notes: "Seen only in TuneECU."
    - name: "Node 0x780"
      request_id: "0x780"
      response_id: "0x781"
      owns: ["unknown — probed with UDS 10 03"]
      verification: "reported"
      notes: "Seen only in TuneECU, reached via the ELM327 user-defined protocol (ATPBE101 + ATSPB)."
    - name: "Immobiliser (also carries TPMS)"
      request_id: "0x604"
      response_id: "0x602"
      owns:
        - "immobiliser DTCs"
        - "TPMS enable state"
        - "TPMS sensor IDs"
        - "live TPMS data (broadcast on 0x600)"
      verification: "reported"

  requests:
    # ---- The service interval reset ----
    - name: "reset_service_interval_km"
      description: >
        Reset the service interval, distance in kilometres. The value byte is the interval
        DIVIDED BY 100 — the reason every tool UI only offers multiples of 100. 10000 km,
        the Tiger 800/900/Sport ceiling, is 100 = 0x64.
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "33 ??"
      expected_response: "B3"
      writes: true
      verification: "reported"
      notes: >
        Sent on CAN 0x701. Success is a frame on 0x704 beginning B3 (the request opcode with
        bit 7 set); a reply beginning 00 is the no-data/error case. No diagnostic session and
        no security access precede it. TuneECU builds the identical string:
        (units == km ? "33" : "34") + %02x(distance / 100).
    - name: "reset_service_interval_miles"
      description: "Reset the service interval, distance in miles. Value byte is miles/100 (6000 mi = 60 = 0x3C)."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "34 ??"
      expected_response: "B4"
      writes: true
      verification: "reported"
    - name: "reset_service_date"
      description: "Reset the service-due date. Two data bytes are a 16-bit date word plus one, split high/low; the epoch and the third byte are not yet decoded."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "5C ?? ?? ??"
      expected_response: "DC"
      writes: true
      verification: "reported"

    # ---- Reading service and odometer state ----
    - name: "sia_query_1"
      description: >
        Odometer read. Reply 704 8D 01 <b1> <b2> <b3>; the three bytes are a 24-bit
        big-endian value in KILOMETRES. Miles = km / 1.60934, or / 1.6099895 when the TFT
        flag from sia_query_3 is clear — two divisors in the same binary, which is the
        documented source of the ~1 mile tool-versus-dash discrepancy.
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "0D 01"
      expected_response: "8D 01 ??"
      writes: false
      verification: "reported"
    - name: "sia_query_2"
      description: "Second SIA/odometer record."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "47 01"
      writes: false
      verification: "reported"
    - name: "sia_query_3"
      description: "Cluster probe — TuneECU uses this as its connect handshake for this stack; reply begins DE."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "5E 01"
      expected_response: "DE ??"
      writes: false
      verification: "reported"
    - name: "sia_query_4"
      description: "Fourth SIA/odometer record."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "6E 76"
      writes: false
      verification: "reported"
    - name: "sia_query_5"
      description: "Fifth SIA/odometer record."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "6E 74"
      writes: false
      verification: "reported"

    # ---- Instrument menu / units ----
    - name: "set_odo_units"
      description: "Set odometer units (miles/km) in the instruments."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "30 ??"
      expected_response: "B0"
      writes: true
      verification: "reported"
    - name: "set_tpms_menu"
      description: "Enable or disable the TPMS menu item in the instruments."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "31 ??"
      expected_response: "B1"
      writes: true
      verification: "reported"
    - name: "set_instrument_option_40"
      description: "Further instrument option (TuneECU only). 40 00 / 40 01."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "40 ??"
      expected_response: "C0 ??"
      writes: true
      verification: "reported"
    - name: "set_instrument_option_41"
      description: "Further instrument option (TuneECU only). 41 00 / 41 01."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "41 ??"
      expected_response: "C1 ??"
      writes: true
      verification: "reported"
    - name: "set_abs_menu"
      description: "Enable or disable the ABS menu item in the instruments."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "32 ??"
      expected_response: "B2"
      writes: true
      verification: "reported"

    # ---- Engine ECU, UDS over 29-bit CAN ----
    - name: "read_ecu_identity"
      description: "ECU identity block read on connect: tune, tune count, cal/build, tune date, serial, VIN."
      command_class: "advanced"
      requires: ["custom_headers", "multiframe_rx", "flow_control"]
      service: "22"
      request: "03 22 F1 90"
      expected_response: "62 F1 90 ??"
      writes: false
      verification: "reported"
      notes: "Same shape for F1A0, F1A7, F1AE, F1A2, F199, F18C. See data_identifiers."
    - name: "read_calibration_id"
      description: "OBD-II mode 09 PID 04, sent functionally to 0x18DB33F1."
      command_class: "basic"
      requires: ["single_frame", "custom_headers"]
      service: "09"
      request: "02 09 04"
      writes: false
      verification: "reported"
    - name: "read_dtc_count"
      description: "UDS ReadDTCInformation, reportNumberOfDTCByStatusMask, mask 0x08 (confirmed)."
      command_class: "advanced"
      requires: ["custom_headers", "multiframe_rx"]
      service: "19"
      request: "03 19 01 08"
      expected_response: "59 01 ??"
      writes: false
      verification: "reported"
    - name: "read_dtc_list"
      description: "UDS ReadDTCInformation, reportDTCByStatusMask, mask 0x08."
      command_class: "advanced"
      requires: ["custom_headers", "multiframe_rx"]
      service: "19"
      request: "03 19 02 08"
      expected_response: "59 02 ??"
      writes: false
      verification: "reported"
    - name: "clear_dtcs"
      description: "UDS ClearDiagnosticInformation, all groups. Clears DTCs and the MIL."
      command_class: "advanced"
      requires: ["custom_headers", "multiframe_tx", "flow_control"]
      service: "14"
      request: "04 14 FF FF FF"
      expected_response: "44"
      writes: true
      verification: "reported"
      notes: "May answer 03 7F 14 78 (responsePending) first."
    - name: "security_access_seed"
      description: "SecurityAccess level 3 — request seed. Gates throttle-balance reads; NOT required for the service reset."
      command_class: "advanced"
      requires: ["custom_headers"]
      service: "27"
      request: "27 03"
      expected_response: "67 03 ??"
      writes: false
      verification: "reported"
    - name: "security_access_key"
      advanced: true
      description: "SecurityAccess level 4 — send key."
      command_class: "advanced"
      requires: ["custom_headers"]
      service: "27"
      request: "27 04 ??"
      expected_response: "67 04"
      writes: false
      verification: "reported"

    # ---- ABS, KWP2000 over K-line ----
    - name: "abs_read_identity"
      description: "ABS ECU identity."
      command_class: "advanced"
      requires: ["custom_headers", "raw_frames"]
      request: "A0"
      writes: false
      verification: "reported"
    - name: "abs_read_dtcs"
      description: "KWP2000 readDiagnosticTroubleCodesByStatus."
      command_class: "advanced"
      requires: ["custom_headers", "raw_frames"]
      service: "13"
      request: "13 40 FF"
      expected_response: "53 ??"
      writes: false
      verification: "reported"
    - name: "abs_clear_dtcs"
      advanced: true
      description: "KWP2000 clearDiagnosticInformation on the ABS modulator."
      command_class: "advanced"
      requires: ["custom_headers", "raw_frames"]
      service: "14"
      request: "14 00 00"
      expected_response: "54 00 00"
      writes: true
      verification: "reported"
    - name: "abs_bleed_start"
      advanced: true
      description: "Start the ABS modulator bleed cycle. Brake-system function — do not run outside an actual brake service."
      command_class: "advanced"
      requires: ["custom_headers", "raw_frames"]
      request: "A1 01 FF"
      expected_response: "E1 01 FF"
      writes: true
      verification: "reported"
    - name: "abs_bleed_status"
      advanced: true
      description: "Poll bleed state. E1 B0 00 = idle, E1 B0 01 = running."
      command_class: "advanced"
      requires: ["custom_headers", "raw_frames"]
      request: "A1 B0 FF"
      expected_response: "E1 B0 ??"
      writes: false
      verification: "reported"
    - name: "abs_bleed_stop"
      advanced: true
      description: "Stop the bleed cycle."
      command_class: "advanced"
      requires: ["custom_headers", "raw_frames"]
      request: "A1 01 00"
      expected_response: "E1 01 00"
      writes: true
      verification: "reported"

    # ---- Immobiliser / TPMS ----
    - name: "immobiliser_identify"
      description: "Identify the immobiliser; reply carries the type byte."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "00"
      expected_response: "0B ??"
      writes: false
      verification: "reported"
    - name: "immobiliser_access"
      description: "Immobiliser access request issued before DTC and TPMS operations."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "02 18 E2 31 02"
      writes: false
      verification: "reported"
    - name: "immobiliser_read_dtcs"
      description: "Read immobiliser/TPMS DTCs."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "40"
      expected_response: "40 00"
      writes: false
      verification: "reported"
    - name: "immobiliser_erase_dtcs"
      advanced: true
      description: "Erase immobiliser/TPMS DTCs."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "41 FF FF FF FF"
      expected_response: "41 00"
      writes: true
      verification: "reported"
    - name: "tpms_read_state"
      description: "Read TPMS enabled/disabled state."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "0A"
      expected_response: "0A ??"
      writes: false
      verification: "reported"
    - name: "tpms_set_state"
      advanced: true
      description: "Enable/disable TPMS. 03/04 opcodes with 00 or 10 as the value."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "03 ??"
      expected_response: "03 ??"
      writes: true
      verification: "reported"
    - name: "tpms_write_sensor_id"
      advanced: true
      description: "Program a TPMS sensor ID (8 hex digits) into the immobiliser."
      command_class: "advanced"
      requires: ["single_frame", "custom_headers", "raw_frames"]
      request: "09 00 00 ?? ?? ?? 01 19"
      expected_response: "09"
      writes: true
      verification: "reported"
    - name: "tpms_live_monitor"
      description: "Passive listen for live TPMS pressure/temperature broadcasts (ATCRA600 + ATMA)."
      command_class: "advanced"
      requires: ["custom_headers", "monitor_all"]
      writes: false
      verification: "reported"

  data_identifiers:
    - did: "0xF190"
      name: "VIN"
      type: "string"
      verification: "reported"
    - did: "0xF18C"
      name: "ECU serial number"
      type: "string"
      verification: "reported"
    - did: "0xF1A0"
      name: "Tune number"
      type: "bytes"
      verification: "reported"
    - did: "0xF1A7"
      name: "Tune number (second variant)"
      type: "bytes"
      verification: "reported"
    - did: "0xF1AE"
      name: "Tune count"
      type: "bytes"
      verification: "reported"
    - did: "0xF1A2"
      name: "Cal / build number"
      type: "bytes"
      verification: "reported"
    - did: "0xF199"
      name: "Tune date"
      type: "date"
      verification: "reported"
    - did: "0x0003"
      name: "Throttle balance MAP sensor (bank 0)"
      type: "bytes"
      verification: "reported"
    - did: "0x0017"
      name: "Throttle balance MAP sensor (bank 0)"
      type: "bytes"
      verification: "reported"
    - did: "0x0031"
      name: "Throttle balance MAP sensor (bank 0)"
      type: "bytes"
      verification: "reported"
    - did: "0x0033"
      name: "Throttle balance MAP sensor (bank 0)"
      type: "bytes"
      verification: "reported"
    - did: "0x0103"
      name: "Throttle balance MAP sensor (bank 1)"
      type: "bytes"
      verification: "reported"
    - did: "0x0117"
      name: "Throttle balance MAP sensor (bank 1)"
      type: "bytes"
      verification: "reported"
    - did: "0x0131"
      name: "Throttle balance MAP sensor (bank 1)"
      type: "bytes"
      verification: "reported"
    - did: "0x0133"
      name: "Throttle balance MAP sensor (bank 1)"
      type: "bytes"
      verification: "reported"
