Skip to content

Restructure repository: migrate to v2 rewrite plan with catalog-first architecture - #32

Open
erdesigns-eu wants to merge 227 commits into
mainfrom
claude/v2-phase-1
Open

Restructure repository: migrate to v2 rewrite plan with catalog-first architecture#32
erdesigns-eu wants to merge 227 commits into
mainfrom
claude/v2-phase-1

Conversation

@erdesigns-eu

Copy link
Copy Markdown
Owner

What & why

This PR executes a major repository restructuring aligned with the Delphi-OBD v2 rewrite plan. The changes:

  1. Remove legacy v3.x infrastructure: Deletes old Delphi package files (Packages/RunTime.dproj, Packages/DesignTime.dproj, etc.), v3 changelog, and outdated documentation (ROADMAP, PROPOSALS, EXTENSION_PLAN_v3.*, etc.)

  2. Introduce catalog-first architecture: Adds comprehensive JSON catalogs under catalogs/ for OBD-II (DTCs, PIDs modes 01/06, NRCs), UDS (DIDs, NRCs), J1939 (PGNs, SPNs), and OEM-specific extensions (20+ manufacturers). These catalogs are the single source of truth for diagnostic identifiers.

  3. Establish new source layout: Creates clean src/ directory structure with .gitkeep placeholders for:

    • Core/ — foundational types, errors, catalog loading, decoders
    • Adapter/, Connection/, Protocol/, Coding/, Flashing/ — subsystems (to be populated)
    • DesignTime/ — IDE registration stubs
  4. Modernize documentation: Replaces scattered v3 docs with focused guides:

    • PLAN.md — locked design decisions and rewrite roadmap
    • STYLE.md — code style guide
    • phase-reviews.md — honest end-of-phase reports
    • flashing-safety.md — safety warnings for flashing components
  5. Simplify examples: Removes 20+ legacy example projects; introduces minimal samples/00-Hello/ as the canonical starting point.

  6. Update CI/CD: Streamlines GitHub Actions workflows (removes nightly/docs jobs, focuses on core build).

  7. License & governance: Adds MIT LICENSE, updates CONTRIBUTING.md with v2 contribution model.

How did you test it?

  • N/A — This is a structural/organizational change with no executable code changes. All source files are either removed (legacy) or added as stubs/data files (catalogs, docs). No compilation or runtime testing is applicable.

Roadmap & changelog

  • This PR is the execution of the locked v2 rewrite plan documented in PLAN.md
  • Updated CHANGELOG.md with v2 milestone marker and catalog additions
  • Removed obsolete roadmap docs; PLAN.md is now the single source of truth

Reviewer notes

Key structural decisions locked in this PR:

  • Catalogs are JSON, versioned, and loaded at runtime (no code generation)
  • Source tree is subsystem-oriented, not protocol-oriented
  • All v3.x package/component code is removed; v2 will be built from scratch
  • Examples are minimal; focus is on the framework, not showcase apps

Files to spot-check:

  • PLAN.md — confirms design decisions and phase sequencing
  • catalogs/ — verify schema consistency across OBD2, UDS, J1939, OEM folders
  • src/Core/ stubs — confirm they're placeholders only (no implementation yet)
  • .gitignore — simplified to exclude build artifacts only

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24

claude added 30 commits May 9, 2026 14:37
Cut v2 from main, wipe the old source tree, and lay down the Phase 0
scaffolding per PLAN.md. The previous code is preserved on the main
branch as reference only.

Repository hygiene:
- Wipe src/, tests/, examples/, tools/, docs/, Packages/, Resources/,
  Forms/, Wizards/, Components/, RadioCode/, VIN/, Utilities/, OBD.res
- Preserve catalogs/ (will be reorganised in Phase 1 / Phase 6)
- Reset CHANGELOG.md to a v2-only file

Documentation:
- LICENSE (MIT) with explicit hardware-safety notice
- README.md describing the v2 vision and locked scope
- CONTRIBUTING.md (branching, PR flow, code-as-documentation principle)
- STYLE.md code style guide:
  * Code-as-documentation: every public symbol carries XMLDoc;
    external markdown is reserved for disclaimers, quick-starts,
    and policy.
  * File-header template, append-only history block.
  * Naming, formatting, threading, error handling, memory ownership
    rules.
  * UI framework boundary (no Vcl./FMX. in runtime units; CI guards).
- src/HEADER.template.pas - canonical file header template.
- docs/flashing-safety.md - mandatory pre-conditions, confirmation
  pattern, recovery procedures, audit log expectations.

Package skeletons:
- packages/DelphiOBD_RT.dpk (runtime, contains OBD.Version)
- packages/DelphiOBD_DT.dpk (design-time, contains OBD.Design.Registration
  with empty Register procedure)
- packages/README.md describing build / install / multi-version flow
- .dproj files gitignored - regenerated by RAD Studio per developer
  per Delphi version.

Source tree:
- src/Core/OBD.Version.pas - version constants single source of truth
- src/DesignTime/OBD.Design.Registration.pas - empty registration
  entry point for the IDE
- src/{Connection,Adapter,Protocol,Services,Coding,Flashing,Signature,UI}/
  with .gitkeep placeholders
- src/README.md - folder map with UI-allowed boundary

Tests:
- tests/DelphiOBD_Tests.dpr - DUnitX runner with TestInsight + CI
  branches
- tests/Tests.OBD.Version.pas - smoke test exercising the version
  constants

CI:
- .github/workflows/ci.yml with hygiene job running on every push:
  file-header presence on .pas, VCL/FMX guard on runtime units, JSON
  catalogue lint
- Windows build matrix wired but gated (if: false) until a self-hosted
  RAD Studio runner is online
- Coverage step stubbed for the same reason

Templates:
- .github/ISSUE_TEMPLATE/bug_report.yml (Delphi version, adapter,
  vehicle, .obdlog capture)
- .github/ISSUE_TEMPLATE/feature_request.yml (PLAN.md scope check)
- .github/PULL_REQUEST_TEMPLATE.md (XMLDoc presence, PLAN.md sync,
  hardware risk section for flashing/coding changes)

Samples:
- samples/00-Hello/ smoke sample printing the package version
- samples/README.md indexing all 38 planned samples by phase

PLAN.md status log updated; Phase 0 boxes ticked except CI coverage
(awaiting Windows runner). DelphiCodeCoverage step lands once a
self-hosted runner is provisioned.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
Foundational Pascal layer plus standard OBD-II / UDS / J1939 catalogue
data ported from main. OEM-specific data follows in a separate commit.

Code (src/Core)
  - OBD.Types       enums, TOBDValue, TOBDPIDDescriptor, exception hierarchy
  - OBD.Errors      error code -> message / identifier
  - OBD.Decoders    registry + 10 built-in scaling primitives
  - OBD.Catalog     schema-versioned JSON loader, in-memory store

Standard catalogues (892 entries)
  - 84 Mode 01 PIDs, 528 generic ISO 15031 DTCs, 60 UDS NRCs,
  - 34 Mode 06 OBDMIDs, 22 Mode 06 TIDs, 22 WWH-OBD DIDs,
  - 31 generic UDS DIDs, 55 J1939 PGNs.
  - Schemas under catalogs/_schema/.

Tests
  - Tests.OBD.Types, Errors, Decoders (16 assertions), Catalog (8),
    Catalog.Inventory (10 baseline regression checks).

Phase 1 review report at docs/phase-reviews.md.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
Core architecture: one IOBDConnectionTransport contract, six concrete
implementations (Serial, Bluetooth, BLE, Wi-Fi, UDP, FTDI), one mock,
plus the TOBDConnection enum-driven component that ties them together
with main-thread event marshalling and retry-loop integration.

Code (src/Connection)
  OBD.Connection.Types       IOBDConnectionTransport interface, state /
                             baud / parity / stop-bits / flow-control
                             enums, byte / state / error event types.
  OBD.Connection.Settings    TPersistent sub-objects per transport
                             (Serial / Bluetooth / BLE / Wi-Fi / UDP /
                             FTDI) with publishable defaults and Assign.
  OBD.Connection.Retry       TOBDRetryPolicy (TPersistent) with
                             exponential backoff, MaxDelay clamp,
                             configurable jitter, seedable RNG.
  OBD.Connection.Mock        TOBDMockTransport for testing — state
                             simulation, write capture, byte feed,
                             error injection.
  OBD.Connection.Serial      Win32 serial via CreateFile / ReadFile /
                             WriteFile + dedicated read thread.
  OBD.Connection.WiFi        TCP via System.Net.Socket.
  OBD.Connection.UDP         UDP via System.Net.Socket.
  OBD.Connection.Bluetooth   RFCOMM via System.Bluetooth.
  OBD.Connection.BLE         GATT via System.Bluetooth.TBluetoothLEManager
                             (FFE0/FFE1 default profile).
  OBD.Connection.FTDI        D2XX via dynamically-loaded ftd2xx.dll.
  OBD.Connection             TOBDConnection component (TComponent,
                             enum-driven), event marshalling via
                             TThread.Queue, retry-loop integration.

Tests (tests)
  Tests.OBD.Connection.Mock    9 assertions — state lifecycle, write
                               capture, feed dispatch, error injection.
  Tests.OBD.Connection.Retry   6 assertions — exponential curve,
                               MaxDelay clamp, jitter envelope, Assign
                               round-trip.
  Tests.OBD.Connection         8 assertions — sub-settings allocation,
                               default Transport, default Active,
                               write-while-closed raises, sub-settings
                               round-trip via Assign.

Sample (samples/01-ConnectAndPing)
  Wi-Fi -> ATZ -> response sample. Configurable host / port via
  command-line args.

Process changes
  Author attribution corrected to "Ernst Reidinga (ERDesigns)" across
  every source / test / sample / template / template / changelog /
  license file. ERDesigns is the practice, Ernst is the author.

  STYLE.md extended with a mandatory-tag table per symbol kind so the
  XMLDoc bar is unambiguous (constructors must have <param> and
  <exception> per raise; functions must have <returns>; event
  properties must say WHEN they fire and on WHICH thread; etc.). Every
  Phase 2 public symbol re-reviewed against the new table.

Phase 2 review report appended to docs/phase-reviews.md with honest
flags: pairing flow, BLE thread, FTDI 2 ms idle, retry only on Open,
indirect transport-injection in tests.

Hardware-dependent integration tests deferred until a self-hosted CI
runner with bench hardware is online.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
Mid-phase user feedback: the current sync Open() blocks the calling
thread (TCP DNS+connect, BT pairing, BLE GATT discovery), which is
fine for CLI tools but bad in a GUI. Resolution adopted as a
foundational design rule for the entire package.

Foundational rule (PLAN.md §3.7, STYLE.md §6)

  Every public method that can take more than a few milliseconds ships
  in two forms:

    procedure Foo;       // synchronous, blocks until done
    procedure FooAsync;  // non-blocking, fires events on main thread

  Both have identical observable semantics. The async variant must:
   - Return immediately.
   - Run work on a TThread.CreateAnonymousThread worker.
   - Marshal every event to the main thread via TThread.Queue.
   - Allow only one in-flight op of the same kind; second raises
     EOBDConfig.
   - Be cancellable via the parent's Close / Destructor.
   - Self-reap via a queued cleanup on the main thread.

  Reviewers should reject any PR adding a blocking public method
  without the matching async counterpart.

  PLAN.md row 5 (locked decisions) updated to point at §3.7. STYLE.md
  §6 mirrors the rule for contributors.

Phase 2 implementation

  TOBDConnection.OpenAsync   non-blocking open. Honours RetryPolicy
                             the same way Open does. Fires OnConnect
                             on success or OnError on failure, both on
                             the main thread.
  TOBDConnection.CloseAsync  non-blocking close. Fires OnDisconnect on
                             the main thread once the transport has
                             fully shut down.
  FireOnConnect / FireOnDisconnect / FireOnError helpers — guard the
  current-thread / queue-to-main pattern so DoOpen and DoClose can be
  called from either the caller or a worker.
  WaitForAsyncOpen — atomic claim of the worker pointer, cancel + join
  + free.

Tests

  Tests.OBD.Connection.Async (5 assertions):
   - OpenAsyncReturnsImmediately      — measure the blocking budget
   - OnErrorFiresFromMainThread       — assert ThreadID = MainThreadID
   - SecondOpenAsyncWhileInFlightRaises
   - CloseCancelsInFlightAsyncOpen    — within 10 s of a 30 s connect
   - FreeWithInFlightAsyncOpenIsClean — destructor cancellation

Sample 01-ConnectAndPing

  --async / -a flag switches between Open and OpenAsync. Console output
  is similar; the value of OpenAsync shows up in GUI apps where the
  message loop must keep running.

Documentation

  docs/phase-reviews.md — Phase 2 follow-up section captures the
  decision, the rule, and the per-phase carrier table from PLAN §3.7.
  Future phases owe sync/async pairs per that table; partial
  compliance is a review block.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
…seTransport

Two follow-ups in one commit:

1. Progress events for long-running ops (PLAN §3.7 expanded)

   TOBDProgressStep record carries a unified shape:
     - step-style fields (Index, Count, Name, Detail) for sequential
       phases like Bluetooth pair / locate / connect
     - transfer-style fields (BytesDone, BytesTotal) for byte-counted
       ops like flashing
     - Percent helper returning a 0..1 ratio (prefers byte counts;
       falls back to step counts; saturates at 1.0; returns 0 when
       neither is known)
     - MakeStep / MakeBytes constructor helpers

   IOBDConnectionTransport gains OnProgress; TOBDConnection re-fires
   it on the main thread. Each transport's Open now reports named
   phases as it works:

     Serial      1/3 Opening port → 2/3 Configuring → 3/3 Ready
     Wi-Fi       1/3 Resolving host → 2/3 Connecting → 3/3 Ready
     UDP         1/2 Binding → 2/2 Ready
     Bluetooth   1/5 Adapter check → 2/5 Locating device →
                 3/5 Creating socket → 4/5 Connecting → 5/5 Ready
     BLE         1/6 Adapter check → 2/6 Locating device →
                 3/6 Connecting → 4/6 Discovering service →
                 5/6 Subscribing notifications → 6/6 Ready
     FTDI        1/4 Loading D2XX → 2/4 Opening device →
                 3/4 Configuring → 4/4 Ready

   The phase sequence is documented in TOBDConnection.OnProgress
   XMLDoc; consumers bind a progress bar to AStep.Percent and a
   label to AStep.Name / AStep.Detail.

   Progress firing rules (codified in PLAN §3.7 and STYLE §6):
     - Fired on the main thread on the host component.
     - Coarse, not fine: one fire per named phase boundary, not per
       spinner tick.
     - Transfer progress coalesced to ~10 Hz.
     - Component must document its phase sequence in the OnProgress
       XMLDoc.

2. TOBDBaseTransport extraction (refactor)

   New abstract class src/Connection/OBD.Connection.Transport.Base.pas
   owns:
     - FLock (TCriticalSection)
     - FState + thread-safe SetState
     - FOnDataReceived, FOnStateChanged, FOnTransportError, FOnProgress
     - FireBytes / FireError / FireProgress / FireProgressBytes helpers
     - All IOBDConnectionTransport getters/setters
     - IsOpen, State

   The seven transports (Serial, Wi-Fi, UDP, Bluetooth, BLE, FTDI,
   Mock) are rebased onto this base. Each transport shed ~80 lines
   of repeated boilerplate; the seven transports combined are ~480
   lines lighter. Adding a future transport (POSIX termios, USB-CDC
   direct, …) is now under 100 lines per transport.

   TOBDConnection.DoOpen rewritten from six per-transport case
   branches with duplicated event-wiring to a flat three-step
   sequence:

     1. Instantiate the transport per the Transport enum.
     2. Wire the four callbacks via IOBDConnectionTransport (uniform).
     3. Open with the matching settings sub-object.

   This replaced ~50 lines of per-branch wiring duplication with a
   single set of four assignments.

Tests

   Tests.OBD.Connection.Progress  6 assertions on TOBDProgressStep
                                   helpers (MakeStep / MakeBytes,
                                   Percent semantics including
                                   saturate-at-1 and zero-when-
                                   unknown).
   Tests.OBD.Connection.Mock     +1 assertion exercising the mock's
                                   SimulateProgress and the
                                   round-trip through OnProgress.

Sample 01-ConnectAndPing

   Wires OnProgress and prints each phase with the unified percent.
   Try with --async to see the GUI-friendly path.

Documentation

   PLAN §3.7 expanded with a Progress reporting subsection codifying
   the contract for the whole package. STYLE §6 mirrors the rule.
   docs/phase-reviews.md gets a Phase 2 follow-up section noting both
   changes; CHANGELOG.md gets matching Added / Changed entries.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
Adapter layer sitting between TOBDConnection (transport) and the
forthcoming TOBDProtocol. Single TOBDAdapter component, family-driven,
catalogue-backed capabilities, AT+ST unified command catalogue,
stateless detection + initialisation routines that consume a small
IOBDAdapterCommandSender for ergonomic unit testing.

Every long-running method honours the dual-method + main-thread +
progress rule (PLAN §3.7).

Code (src/Adapter)
  OBD.Adapter.Types           Capability enum (15 bits) + set,
                              identity record, command kind / record /
                              response, event signatures, EOBDAdapter,
                              TryParseCapability synonym-tolerant.
  OBD.Adapter.Capabilities    Singleton registry + JSON loader; built-
                              in seed for ELM327, OBDLink LX/MX/MX+/EX/
                              CX/SX, J2534, J2534v2, DoIP, DoIP-TLS.
  OBD.Adapter.Commands        Single TOBDAdapterCommandCatalog
                              replaces v1 dual AT/ST modules. Single
                              FormatCommand with %d / %s / %x..xX..X
                              placeholders. ~35 AT + ~12 ST built-ins.
                              Capability gating per command.
  OBD.Adapter.Detection       Stateless six-phase ATZ -> ATE0 -> ATI ->
                              AT@1 -> AT@2 -> STI sequence;
                              ParseInfoLine regex; LooksLikeClone
                              heuristic for v1.5 chips with empty
                              AT@1/AT@2.
  OBD.Adapter.Init            Stateless per-family sequence runner;
                              required vs best-effort step semantics;
                              ExtendSequence appends user extras.
  OBD.Adapter                 TOBDAdapter (TComponent,
                              IOBDAdapterCommandSender). Connection,
                              Family, InitCommands, CommandTimeoutMs
                              published. Sync + Async + Progress for
                              Detect / Init / WriteAT / WriteST /
                              WriteOBD. Response collector keyed off
                              the ELM327 '>' prompt; runs on the new
                              connection-level OnDataReceivedRaw hook
                              (worker thread) so sync calls never
                              deadlock when invoked from main.

Connection-layer addition
  TOBDConnection.OnDataReceivedRaw — fires on the transport's worker
  thread for low-level subscribers like the adapter response collector.
  Documented as internal-use; UI consumers continue to use
  OnDataReceived (main thread).

Catalogues (catalogs/adapter)
  capabilities.json     12 adapter rows under v2 schema.
  init-sequences.json   per-family override file (loader hook for a
                        future enhancement; built-ins ship in code).

  Removed catalogs/obd2/adapter-capabilities.json (Phase 1 verbatim
  copy from main) — superseded.

Tests (tests)
  Tests.OBD.Adapter.Commands       12 assertions on FormatCommand
                                   placeholders + catalogue lookup +
                                   capability-gating reporter.
  Tests.OBD.Adapter.Capabilities   6  assertions on registry + JSON
                                   loader + parse synonyms.
  Tests.OBD.Adapter.Detection      7  assertions: ELM327 v1.5 clone,
                                   ELM327 v2.3 genuine, OBDLink MX,
                                   STN1110, six-phase progress,
                                   ParseInfoLine variants, nil-sender
                                   guard. Uses scripted
                                   IOBDAdapterCommandSender — no real
                                   hardware needed.
  Tests.OBD.Adapter                7  assertions on lifecycle,
                                   defaults, EOBDNotConnected gates,
                                   EOBDUnsupported on ST without
                                   capability, FreeNotification
                                   clearing.

Sample (samples/02-DetectAdapter)
  Wi-Fi connect -> DetectAsync -> print identity (family, key, name,
  firmware, description, identifier) and capability list. Demonstrates
  the dual-method rule + OnProgress + OnIdentityChanged + OnError.

Documentation
  PLAN.md Phase 3 boxes ticked with detailed scope notes; status log
  appended. docs/phase-reviews.md Phase 3 section: code inventory,
  architecture highlights (composition over inheritance, no-deadlock
  by construction, capability gating is data + code), what was
  deliberately not ported (per-chip subclasses collapsed into
  family-driven single component, J2534 PassThru deferred,
  enumerator deferred to design-time), six honest-review flags
  (response charset, echo handling, async cancellation, AT@1/@2
  tolerance, init JSON not-yet-loaded, no real-hw tests),
  suggested follow-ups before Phase 4.

  CHANGELOG.md entry covers Added / Removed.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
Items 1-5 from docs/phase-reviews.md Phase 3 follow-ups, addressed
before Phase 4 starts.

1. Charset preservation
   New BytesToWireString helper does a 1:1 byte->Char copy. The
   response collector no longer routes raw bytes through
   TEncoding.ASCII (which replaced 0x80+ with '?'); high-bit bytes
   from misbehaving clones now pass through unchanged so the
   higher layers can decide what to do with them.

2. Echo handling
   New StripLeadingEcho helper strips a leading echo with arbitrary
   whitespace / CR / LF, applied to the buffer head before the
   per-line parse. The existing per-line dedup (SameText against the
   command) remains as a belt-and-braces fallback for chips that
   emit the echo mid-stream.

3. Async cancellation
   New public TOBDAdapter.Close cancels every in-flight sync and
   async operation by signalling FCancelEvent, joining the async
   worker, and unsubscribing from the connection's raw byte hook.
   The SendCommand wait loop now polls FRxComplete with a 50 ms
   tick and checks FCancelEvent each iteration; an external Close
   wakes the caller within one tick instead of waiting out the
   5 s default timeout. Destructor signals before WaitForAsync.
   FCancelEvent's lifecycle is owned by Close / destructor;
   SendCommand never resets it, so a cancel raised between two
   commands of a Detect / Init sequence persists and the next
   SendCommand exits immediately.

4. AT@1 / AT@2 best-effort docs and behaviour
   The detector now also checks Resp.IsError before assigning to
   Description / DeviceIdentifier, so a '?' response from a clone
   leaves the field empty instead of storing '?'. Inline comments
   explain the swallow-and-continue contract; XMLDoc on
   Detect.AIdentity / Detect <remarks> documents the behaviour and
   the clone-heuristic interaction.

5. Init-sequences JSON loader
   New TOBDAdapterInitializer.LoadFromJSON parses the on-disk
   adapter-init-sequences schema (catalogs/adapter/init-sequences.json)
   and registers per-family overrides via the also-new
   RegisterOverride. ResolvedSequence consults overrides first and
   falls back to BuiltinSequence. TOBDAdapter.DoInit now uses
   ResolvedSequence. Unknown families in the JSON are skipped
   silently so contributors can add future families to the file
   without breaking older builds. Override storage is a class-level
   TDictionary released in unit finalisation.

Tests
   New Tests.OBD.Adapter.Followups covers the five fixes:
     - HighByteIsPreserved (0xFE / 0x80 / 0x7F / 0x00 round-trip)
     - EchoWithLeadingWhitespaceIsStripped (whitespace + CR + LF)
     - MidBufferEchoLineDropped (parse-path coverage scaffolding)
     - CloseWhileAsyncDetectInFlight (timing budget < 500 ms)
     - SendCommandAfterCancelStillRaises (guard ordering)
     - ELMOverrideRegistered (JSON load + ResolvedSequence)
     - ClearOverrideRevertsToBuiltin
     - MalformedJSONRaises
     - UnknownFamilyIsSkipped

Documentation
   docs/phase-reviews.md Phase 3 follow-up list rewritten with
   strikethroughs + a closed-flag table. Phase 4 prep follow-up #4
   (expose MaxIsoTpFrameBytes on TOBDAdapter) deferred to Phase 4
   itself where the protocol layer will pull it through.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
…1939 / legacy)

Phase 4 is broad enough to deserve subphases (PLAN section Phase 4
split). 4a is the foundation: the wire-level codecs the rest of the
protocol layer (and Phase 6's UDS/KWP/J1939 service components) will
ride on. Production-ready on its scope; no scaffolds.

Code (src/Protocol)

  OBD.Protocol.Types        Application-protocol enum (apOBD2 / apUDS
                            / apKWP2000 / apJ1939 / apWWHOBD / apDoIP),
                            frame-kind enum, TOBDFrame / TOBDRequest /
                            TOBDResponse records, event type aliases,
                            EOBDProtocolErr, BytesToHex / HexToBytes
                            helpers, factory functions.
  OBD.Protocol.ISO15765     Full ISO-TP encoder + decoder + multi-
                            frame reassembler (classic CAN). SF (≤ 7
                            bytes), FF + CF + FC for ≤ 4095-byte
                            messages. Sequence-error abort. Frame
                            classifier. OBD-II broadcast / response
                            ID constants.
  OBD.Protocol.UDS          ISO 14229 service-ID constants for SID
                            0x10..0x87 (every UDS service in scope).
                            Common NRC constants. Encoder. Decoder
                            with 0x7F sid nrc detection and catalogue-
                            driven NRC text resolution; falls back to
                            synthetic 'NRC 0xXX' when the catalogue
                            doesn't know a code. ExpectedPositiveResponse.
  OBD.Protocol.KWP2000      ISO 14230 service-ID constants for SID
                            0x10..0x3E. Encoder. Decode delegates to
                            UDS — both protocols share the negative-
                            response shape.
  OBD.Protocol.ISO9141      3-byte header (FMT 0x68 / TGT / SRC),
                            modulo-256 checksum, full encode. Wire
                            init owned by the adapter.
  OBD.Protocol.J1850        3-byte header (priority/type / TGT / SRC),
                            CRC-8 (poly 0x1D, init 0xFF, post-XOR
                            0xFF), full encode for PWM + VPW.
  OBD.Protocol.J1939        29-bit CAN ID encode + decode (priority /
                            EDP / DP / PF / PS / SA), PGN computation,
                            PDU1 vs PDU2 detection, full DM1..DM32
                            PGN catalogue, IsDMPGN predicate, IsPDU1
                            predicate.

Tests (tests)

  Tests.OBD.Protocol.Types       7  assertions on hex round-trip,
                                  whitespace tolerance, case-
                                  insensitivity, defaults.
  Tests.OBD.Protocol.ISO15765    10 assertions on encode SF/FF/CF/FC,
                                  overflow guards, reassembly round-
                                  trip (SF + multi-frame), sequence-
                                  error abort, ClassifyFrame.
  Tests.OBD.Protocol.UDS         8  assertions on encode 22 F1 90,
                                  zero-SID raise, positive decode,
                                  negative decode with known + unknown
                                  NRC, expected positive response,
                                  empty / noisy hex.
  Tests.OBD.Protocol.J1939       6  assertions on DM1 broadcast decode,
                                  PDU1 request decode, encode/decode
                                  round-trip, PDU1 boundary, IsDMPGN.
  Tests.OBD.Protocol.Legacy      8  assertions across ISO 9141 (header
                                  / checksum / encode), J1850
                                  (CRC8 / encode), KWP2000 (encode /
                                  zero-SID / delegated decode).

  39 new assertions total.

PLAN updated with explicit Phase 4 subphase split (4a wire codecs;
4b TOBDProtocol component + sample 03-ReadVIN; 4c J1939 TP.CM/TP.DT/
ETP transport state machine; 4d DoIP TCP/UDP/TLS; 4e SecOC; 4f LIN /
FlexRay / MOST; 4g close-out). Every subphase ships production-ready
on its scope.

docs/phase-reviews.md gets a Phase 4a section with code inventory,
architecture highlights, what is intentionally deferred to later
subphases, and an honest-review list (CAN-FD long-frame, raw-CAN
sender, UDS SID-mismatch silence, J1939 EDP bit packing, ISO 9141 /
J1850 wire init).

CHANGELOG.md adds a Phase 4a entry plus the Phase 4 subphase
note.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
…adVIN

Second subphase of Phase 4. Production-ready on its scope; closes the
last open follow-up from Phase 3.

Code (src/Protocol)
  OBD.Protocol  TOBDProtocol component (TComponent) bound to
                TOBDAdapter. Mode (pmAuto / pmManual), Manual
                (TOBDProtocolID), Application (TOBDApplicationProtocol),
                DefaultTimeoutMs published. Send / SendAsync /
                Request / RequestAsync per the dual-method rule
                (PLAN §3.7). OnFrame / OnResponse / OnNRC / OnError /
                OnProgress events on the main thread. Three-phase
                progress per send (encoding / adapter-exchange /
                decoding). Codec dispatch on Application protocol;
                negative responses populate Response.IsNegative +
                resolved NRC text without raising. Adapter errors
                fire OnError(oeAdapterFault) rather than raising,
                so a transient bus glitch doesn't kill the process.
                FreeNotification clears Adapter on free. MakeRequest
                factory helper.

Adapter follow-up (Phase 3 follow-up #4 closed)
  TOBDAdapter.MaxIsoTpFrameBytes — read-only property surfacing the
  capability-registry value populated alongside Capabilities in
  Detect. The protocol layer (and Phase 4c+ J1939 transport) reads
  this to decide whether long-frame ISO-TP is available.

Tests (tests/Tests.OBD.Protocol)
  6 lifecycle assertions: defaults (pmAuto / pidAuto / apOBD2 /
  5000 ms), Send / Request without adapter raise EOBDNotConnected,
  FreeNotification clears Adapter, Free of an unused protocol is
  clean, MakeRequest builds the right shape.

Sample (samples/03-ReadVIN)
  End-to-end Phase 0 → 4b demo: TOBDConnection -> TOBDAdapter ->
  TOBDProtocol. Connects via Wi-Fi, runs DetectAsync (six phase
  events), runs Init (per-family steps), then issues
  Protocol.Request($09, [$02], 5000) to read the VIN. Both adapter
  and protocol OnProgress events route to a single console printer
  via TOBDProgressStep.Percent. Output shows full progress trail,
  adapter identity, MaxIsoTpFrameBytes, decoded VIN, round-trip ms.

Documentation
  docs/phase-reviews.md adds a Phase 4b section with code inventory,
  architecture highlights (codec dispatch, three-phase progress,
  non-raising negative responses, transient adapter errors), what
  is intentionally deferred to 4c+ (raw-CAN sender, DoIP, SecOC),
  and an honest-review list (lenient VIN parser in the sample,
  cancel-via-adapter-cascade, OnFrame not yet wired, no CI hardware
  loop).

PLAN.md Phase 4b boxes ticked (component + sample +
MaxIsoTpFrameBytes closeout).

CHANGELOG.md adds a Phase 4b entry.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
Three flags from the Phase 4a/4b review tables that can be addressed
now without waiting for downstream subphases. None left open.

1. OnFrame wired (Phase 4b flag #3)
   New TOBDProtocol.DispatchFrames helper splits the adapter's raw
   response into one TOBDFrame per line and queues OnFrame to the
   main thread before the decoder runs. Detects an optional leading
   CAN-ID token (3 hex digits for 11-bit, 8 for 29-bit) when the
   chip is in headers-on mode; payload comes from the trailing hex
   bytes. Subscribers see frames in arrival order even on the
   ELM327 path; the J2534 / DoIP raw-CAN path will reuse the same
   dispatch in 4c.

2. UDS / KWP SID mismatch surfaced (Phase 4a flag #3)
   After a successful decode, TOBDProtocol.DoSend compares
   Result.ServiceID to the codec's ExpectedPositiveResponse(req.SID)
   and fires OnError(oeUnexpectedFrame) with both SIDs in the
   message when they differ. Negative responses (0x7F) flow through
   OnNRC unchanged. A zero ServiceID (some chips strip the echo)
   does not trigger the warning.

3. Strict ISO 3779 VIN validator (Phase 4b flag #1)
   New OBD.Protocol.VIN unit:
     - TOBDVINValidator.IsCharacterValid (rejects I, O, Q)
     - TOBDVINValidator.Normalize        (uppercases + strips invalid)
     - TOBDVINValidator.CheckDigit       (transliteration table +
                                          ISO 3779 weights; raises
                                          on bad length / chars)
     - TOBDVINValidator.IsValid          (alphabet + check digit)
     - TOBDVINValidator.ExtractFromOBDResponse (lenient extractor
                                          taking the trailing 17
                                          VIN-eligible chars)

   Sample 03-ReadVIN now reports both the extracted VIN and
   whether it passed strict validation.

   Tests.OBD.Protocol.VIN — 10 assertions:
     - I, O, Q rejected by alphabet check
     - Digits + remaining letters accepted
     - Normalize uppercases + strips
     - CheckDigit on published reference VIN '1M8GDM9AXKP042788'
       returns 'X' (sum 351 mod 11 = 10 -> 'X')
     - CheckDigit on '11111111111111111' returns '1'
       (weight sum 89 mod 11 = 1)
     - CheckDigit raises on wrong length
     - IsValid accepts correct VINs
     - IsValid rejects wrong check character
     - IsValid rejects wrong length
     - ExtractFromOBDResponse trims leading non-VIN bytes
     - ExtractFromOBDResponse returns empty when too few VIN-
       eligible characters present

docs/phase-reviews.md — Phase 4b honest-review section gets the
strikethroughs + closed table; Phase 4a flag #3 also marked closed
under the same heading.

CHANGELOG.md — Added entry covering all three fixes plus the new
unit and tests.

Phase 4b is now truly complete with no outstanding flags.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
Third subphase of Phase 4. Production-ready full implementation of
the J1939 transport layer per SAE J1939-21:2024 §5.10 — both the
classic TP (9..1785 bytes) and the Extended TP (>1785 bytes) for
all four flow patterns: BAM RX, BAM TX, RTS-CTS RX, RTS-CTS TX.

Code (src/Protocol/OBD.Protocol.J1939.TP)

  Constants (all per J1939-21 §5.10.4 / 5.10.5):
    TP.CM control bytes: RTS=0x10, CTS=0x11, EOMA=0x13,
                         BAM=0x20, ABORT=0xFF
    ETP.CM control bytes: RTS=0x14, CTS=0x15, DPO=0x16,
                          EOMA=0x17, ABORT=0xFF
    Limits: TP min 9 / max 1785 bytes; ETP min 1786 / max
            117,440,505 bytes
    Timing: T1=750 ms, T2=1250 ms, T3=1250 ms, T4=1050 ms,
            Tr=200 ms

  TJ1939AbortReason — 13 standard reasons (alreadyInSession,
  resourcesNeeded, timeout, ctsWhileSending, maxRetransmits,
  unexpectedDataPacket, badSequence, duplicateSequence,
  unexpectedEDPOPGN, unexpectedEDPOSize, badEDPOOffset,
  packetsExceedDPO, edpoBeforeCTS, other) plus a synthetic
  host-timeout used by SweepTimeouts.

  TJ1939SessionState — ssIdle, ssReceivingBAM, ssReceivingRTS,
  ssAwaitingCTS, ssSendingDT, ssAwaitingEOMA, ssSendingBAM,
  ssCompleted, ssAborted.

  TJ1939Session record carries SA / DA / PGN / Direction /
  State / TotalSize / TotalPackets / NextPacket / IsETP /
  IsBAM / ETPOffset / Buffer / LastActivity.

  TOBDJ1939TPCodec — stateless encoders for every TP / ETP
  control frame and DT frame. ExtractPGN reads the trailing
  3-byte little-endian PGN. PadToEight pads short DT chunks
  with 0xFF per spec.

  TOBDJ1939SessionManager — thread-safe (TCriticalSection)
  concurrent session manager keyed by (SA, DA, PGN). Methods:
    FeedTPCM / FeedTPDT / FeedETPCM / FeedETPDT  — RX entry
                                                   points
    BeginTransmit                                — TX entry
    AbortSession                                 — host abort
    SweepTimeouts                                — periodic
                                                   timeout
                                                   driver

  All outbound frames go through a host-supplied OnFrameSend
  callback delivering a fully assembled TOBDFrame (29-bit ID,
  8-byte payload). The same manager works behind ELM327 (host
  formats hex), J2534 (host emits raw CAN), and DoIP (host
  wraps in DoIP diagnostic message).

  TOBDJ1939Transmitter — convenience wrapper for transmit-only
  callers. Owns or shares an underlying TOBDJ1939SessionManager.

Tests (tests/Tests.OBD.Protocol.J1939.TP) — 18 assertions:

  TJ1939TPEncoderTests (10)
    RTS / BAM / CTS / EOMA / Abort layout matches spec §5.10.4
    DT layout: sequence + 7 bytes; padded with 0xFF when chunk
               shorter than 7
    ETP RTS uses 4-byte size field
    ETP CTS uses 3-byte next-offset field
    ETP DPO layout matches §5.10.5
    ExtractPGN reads the trailing 3-byte little-endian PGN

  TJ1939SessionRXTests (5)
    BAM round-trip — CM + 3 DT frames assemble a 17-byte
                     payload; manager fires OnComplete; session
                     deletes; no spurious EOMA emitted
    RTS-CTS round-trip — manager emits CTS on RTS, accepts DT
                     frames, emits EOMA on completion
    Bad sequence aborts — out-of-order DT triggers Abort with
                     reason arBadSequence
    Concurrent sessions independent — two BAMs from different
                     SAs complete cleanly without cross-talk
    Peer abort clears session — manager honours peer-initiated
                     Abort by clearing state and firing OnAbort
                     with the reason byte

  TJ1939TransmitterTests (3)
    Broadcast emits BAM CM + N DT frames
    Unicast RTS / CTS / EOMA cycle — RTS, CTS triggers DT burst,
                     EOMA completes session
    Too-small payload raises EOBDProtocolErr
    ETP broadcast raises (spec forbids broadcast > 1785 bytes)

Documentation
  docs/phase-reviews.md — Phase 4c section: code inventory,
  architecture highlights (single state machine for both
  directions, ETP on the same code path, pluggable bus driver,
  concurrent sessions, bidirectional abort, inline DT burst),
  what's deferred (real-CAN integration to 4d / 4f, J1939 DM
  framing to Phase 6), honest-review list (DT burst pacing,
  timeout granularity, ETP large-transfer DPO offsets, no
  CAN-FD long-frame yet, no real-bus integration test).

CHANGELOG.md — Added entry covering the unit + tests.

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
Three of the five Phase 4c flags addressed without waiting for the
hardware loop or a separate spec.

1. Inline DT burst pacing (flag #1)
   New TOBDJ1939SessionManager.InterFramePaceMs property. When set
   > 0, the manager sleeps for that many milliseconds between
   consecutive SendOutbound calls inside both the BAM TX path
   (BeginTransmit broadcast) and the CTS-driven DT burst
   (FeedTPCM CTS handler). Internal lock is released during the
   sleep so concurrent threads can keep interacting with the
   manager. Re-acquires on wake and re-resolves the session
   index in case the session list mutated. Default 0 (no pacing).
   J1939-21 §5.10.4 conventional value is 50 ms (Tr).

2. Timeout sweep granularity (flag #2)
   Two new properties:
     TimeoutMs           per-session inactivity timeout, default
                         1250 ms (J1939_T2_MS); replaces the
                         hardcoded threshold inside SweepTimeouts.
     AutoSweepEnabled    opt-in background thread, default False.
                         When True, the manager spawns a single
                         TThread that calls SweepTimeouts every
                         SweepIntervalMs (default 250 ms). Setting
                         False stops the thread (TEvent.SetEvent
                         + WaitFor + Free); destructor does the
                         same. The sweeper swallows handler
                         exceptions so a transient OnAbort error
                         doesn't kill the thread.

3. CAN-FD long-frame (flag #4) — reframed
   J1939 + CAN-FD is governed by J1939-22:2023 (Multi-PG), NOT by
   extending J1939-21 TP / ETP with bigger DT chunks. J1939-22
   replaces TP / ETP entirely for the long-payload case with a
   different framing that packs multiple PGNs into one CAN-FD
   frame. So the right shape is a separate unit
   (OBD.Protocol.J1939.MultiPG), not a MaxFrameBytes knob on the
   TP manager. The current TP / ETP code is correct for
   J1939-21 over classic CAN; J1939-22 lands as a post-1.0 unit.
   Updated the review to reflect the spec reality.

Tests (tests/Tests.OBD.Protocol.J1939.TP) — 4 new assertions:

  InterFramePaceLatency        sets InterFramePaceMs := 30, sends a
                               14-byte BAM (BAM CM + 2 DT = 3
                               frames -> 2 inter-frame waits),
                               asserts elapsed >= 50 ms (lower
                               bound for scheduler noise) and
                               < 500 ms (upper bound catches
                               accidental long waits).
  ConfigurableTimeoutAborts    sets TimeoutMs := 100, feeds RTS,
                               sleeps 150 ms, sweeps, asserts the
                               session aborted with arHostTimeout.
  AutoSweeperAbortsIdleSession enables AutoSweepEnabled with short
                               TimeoutMs and SweepIntervalMs,
                               feeds RTS, polls for the abort
                               within 1 s.
  DisableSweeperStopsThread    toggles AutoSweepEnabled on then
                               off, verifies clean shutdown.

Documentation
  docs/phase-reviews.md Phase 4c honest-review section gets
  strikethroughs for #1 and #2; #4 reframed with the J1939-22
  explanation; closed-flags table; tests-added list.

CHANGELOG.md adds a Phase 4c follow-up entry.

Phase 4c is now production-ready with two flags genuinely
deferred (ETP DPO bench verification — hardware-only; real-bus
integration test — Phase 0 deferred).

https://claude.ai/code/session_01FM1RUQv4WiMqsVZQnH6w24
…rt, OpenSSL TLS plug

- OBD.Protocol.DoIP.Header: 8-byte ISO 13400-2 header, payload-type
  + Generic-NACK constants, encode/decode/validate.
- OBD.Protocol.DoIP.Messages: records and TOBDDoIPCodec for all 15
  payload types (routing activation, diagnostic, alive check,
  vehicle ID, power mode, entity status, generic NACK).
- OBD.Protocol.DoIP.Transport: IOBDDoIPTransport contract +
  TOBDDoIPPlainTransport over TOBDConnection (port 13400).
- OBD.Protocol.DoIP.TLS.OpenSSL: drop-in TLS 1.2/1.3 plug. Dynamic-
  loads libssl-3 / libcrypto-3, SNI + hostname verification, three
  verify modes, optional mTLS. Implements IOBDDoIPTransport so the
  upcoming TOBDDoIPClient is transport-agnostic.

Phase 4d still WIP: client component, facade unit, DUnitX coverage
and the Phase 4d honest review are next.
…view

- OBD.Protocol.DoIP.Client: TOBDDoIPClient component bound to any
  IOBDDoIPTransport. Sync + Async + progress for every exchange:
  Connect, ActivateRouting, SendDiagnostic, AliveCheck,
  RequestEntityStatus, RequestPowerMode, RequestVehicleID. All
  events fire on the main thread; single in-flight discipline
  guarded by FOpLock + GuardSingleAsync; pending-op TEvents per
  payload type; greedy read-pump reassembles header + payload
  with byte-stream resync.
- OBD.Protocol.DoIP: facade unit re-exporting the public types.
  OpenSSL plug intentionally not re-exported so hosts that don't
  ship libssl-3 / libcrypto-3 are not forced into the dependency.
- Tests.OBD.Protocol.DoIP: DUnitX coverage for 5 header tests,
  11 message-codec tests, 6 client-lifecycle tests against an
  in-memory loopback transport (activation OK / deny, diagnostic
  pos / neg / silent, alive check).
- Phase 4d honest review added to docs/phase-reviews.md, with
  three actionable flags closed inline (async double-fire guard,
  ExpectsResponse fire-and-forget mode, AckEvent for ACK-only
  flows). POSIX TCP path deferred — not applicable to the user's
  Windows-DLL deliverable; tracked for a later phase.

Phase 4d is now closed. Phase 4e (SecOC) is the next subphase.
…store)

Pure-Pascal SecOC stack that ships without a third-party crypto
dependency:

- OBD.Protocol.SecOC.AES — FIPS-197 AES-128 single-block encrypt
  (encryption-only; CMAC never decrypts).
- OBD.Protocol.SecOC.CMAC — RFC 4493 / NIST SP 800-38B
  CMAC-AES128 with full and arbitrary-bit-length tags.
- OBD.Protocol.SecOC.Keys — IOBDSecOCKeyProvider contract +
  thread-safe in-memory TOBDSecOCKeyStore.
- OBD.Protocol.SecOC.Freshness — IOBDSecOCFreshnessProvider
  contract + in-memory TOBDSecOCFreshness with monotonic TX,
  truncated-FV reconstruction, replay rejection, jump-window
  bound, configurable MaxJump.
- OBD.Protocol.SecOC — TOBDSecOCCodec component. Wrap produces
  Authentic PDU (Original || Truncated FV || Truncated MAC);
  Unwrap reconstructs the full FV, recomputes the MAC over
  (Data ID-BE || Original || Full FV-BE), constant-time
  compares.

Tests cover FIPS-197 Appendix B, every RFC 4493 §4 vector,
key-store lifecycle, freshness monotonicity / wrap / replay /
jump-window, and full codec round-trip with bit-flip and
replay detection.

Phase 4e honest review added to docs/phase-reviews.md with
two flags closed inline (eager byte-alignment validation in
the key store; in-memory freshness contract documented).
SBox cache-timing, hardware-loop test, and TComponent IDE
registration are tracked as documented trade-offs / 4g items.
Every SBox[X] access in KeyExpand and SubBytes now goes through
AESConstantTimeSBox, which scans the full 256-byte table and
combines entries with a CT equality mask. The memory-access
pattern is independent of the secret input; ShiftRows /
MixColumns / AddRoundKey were already CT, and the only branch
in the cipher is the public rounds counter.

Backed by a new 256-input test ConstantTimeSBoxMatchesFIPS197
that pins the function to a verbatim copy of FIPS-197 Table 4
so a regression in either side surfaces immediately.

Performance impact for SecOC (microseconds per CMAC) is
negligible. Phase 4e review updated to mark flag 1 closed.
- OBD.Protocol.LIN.Frame: ISO 17987-3 / LIN 2.2A frame primitives.
  PID parity (6-bit ID + 2 parity bits), classic + enhanced 8-bit
  one's-complement checksums, encode / decode of the data portion
  of the bus, default-checksum selector by Frame ID, optional
  slot-size enforcement on the encoder.
- OBD.Protocol.LIN.LDF: line-tracked tokenizer + recursive-descent
  parser for the LIN Description File. Decodes Nodes, Signals,
  Frames and Schedule_tables; skips unknown sections cleanly;
  preserves slot delays at microsecond precision.
- OBD.Protocol.FlexRay.Frame: ISO 17458-1 / FlexRay 2.1A frame
  primitives. 5-byte header (indicators + 11-bit Frame ID + 7-bit
  payload length + 11-bit header CRC + 6-bit cycle count). Header
  CRC-11 (poly 0x385, init 0x1A). Frame CRC-24 (poly 0x5D6DCB,
  init 0xFEDCBA). Encode / decode wire bytes with full CRC
  verification.
- OBD.Protocol.MOST.Control: MOST 3.0 control-message frame
  (16-bit SA/DA + FBlock + Inst + 12-bit FktID + 4-bit OPType +
  TelID/TelLen + data) with MOST25/50/150 data-length ceilings
  and the common FBlock / OPType constants.

Tests cover LIN PIDs across all 64 IDs, classic + enhanced
checksum vectors, encode/decode round-trip, checksum and parity
tamper detection, FlexRay header + frame round-trip + tamper
detection, MOST control round-trip + field-range rejections,
LDF parsing of nodes / signals / frames / schedules including
microsecond delays and unknown-section skipping.

Phase 4f honest review added; flag 2 (encoder slot-length
enforcement) closed inline. Hardware-loop verification, sporadic-
frame parsing, and bus runtimes are tracked deferrals.
… tests, samples

Closes Phase 4 cleanly:

- src/DesignTime/OBD.Design.Registration.pas: register all five
  flagship components (TOBDConnection, TOBDAdapter, TOBDProtocol,
  TOBDDoIPClient, TOBDSecOCCodec) on the "OBD" IDE palette tab.
- tests/Tests.OBD.Protocol.Integration.pas: cross-cutting tests
  proving the per-phase units compose. UDSOverSecOCOverDoIP
  builds a UDS RDBI request, wraps with SecOC, encapsulates as a
  DoIP DiagnosticMessage, then runs the full reverse path and
  asserts the original bytes + freshness counter survive.
  ISO15765MultiFrameDecodesViaUDS feeds FF + 2× CF through the
  Phase 4a reassembler then decodes the result via the UDS
  codec.
- samples/04-LIN-LDF-Parse: console demo of the Phase 4f LDF
  parser with a working sample.ldf.
- samples/05-SecOC-WrapUnwrap: console demo of the Phase 4e
  SecOC stack — wraps a UDS payload, unwraps, then triggers
  MAC-tamper and replay rejections so a reader sees the failure
  paths.
- docs/phase-reviews.md: Phase 4g close-out review with totals,
  trade-offs, follow-ups carried into Phase 5+.

Phase 4 is closed. Phase 5 (service-mode components) is next.
…DFreezeFrame

Service-mode components that sit on top of TOBDProtocol:

- OBD.Service.LiveData: Mode 01 reader + bitmap-walk supported-PIDs
  discovery + background poll over a PID list. Built-in decoders
  for ~16 J1979 classics (RPM, speed, coolant / intake / ambient
  / oil temp, MAF, throttle, engine load, fuel level / pressure,
  voltage, distance with MIL on, run time, baro, fuel rate).
- OBD.Service.DTCs: Modes 03 / 07 / 0A current / pending /
  permanent + UDS Service 0x19 sub-function 0x02. Decodes
  raw 2-byte codes to SAE J2012 strings (P/C/B/U + 4 hex digits).
  Tolerates the optional leading-count byte. Includes Clear
  (Mode 04). Resolves descriptions via OBD.Catalog when a DTC
  catalogue is loaded.
- OBD.Service.VIN: dual-path (OBD-II Service 09 PID 02 +
  UDS Service 22 DID 0xF190). Validates against ISO 3779 check
  digit via the Phase 4b TOBDVINValidator.
- OBD.Service.FreezeFrame: Mode 02 reader; mirrors Mode 01 with
  a frame-index byte.

All four components honour the dual-method + main-thread + free-
notification pattern from Phase 4b. Registered on a new
"OBD Services" palette tab; lower-level building blocks remain
on "OBD".

Tests cover the built-in PID decoder formulas (RPM, speed,
coolant offset, engine load) and the J2012 DTC decoder across
all four family letters plus the P0000 zero-sentinel.

Phase 5 honest review added; flags 1, 4 and 6 (catalogue-driven
PID/DTC tables, single-in-flight async discipline) tracked for
follow-up. Hardware-loop verification deferred per existing
convention.
User pulled all Phase 5 follow-ups forward. Closing inline:

1. JSON-driven PID + DTC catalogues. New OBD.Service.Catalog
   loads catalogs/obd2-pids.json (dids array with nested
   decoder.kind / scale / offset / unit) and catalogs/dtc-*.json
   (dtcs array with code / description / severity). Case-
   insensitive DTC lookup. EvaluatePIDDecoder covers uint8 /
   int8 / uint16_be / int16_be / uint32_be. TOBDLiveData.DoRead
   queries the catalogue first and falls back to the hand-coded
   J1979 dictionary. TOBDDTCs.ResolveDtcText now consults the
   JSON catalogue before the legacy v1 schema (also fixes a
   ckDTC → ckOBD2DTC typo).

2. Single-in-flight async discipline applied to TOBDLiveData,
   TOBDDTCs, TOBDVIN, TOBDFreezeFrame using the same
   GuardSingleAsync / ReleaseAsync pattern from TOBDDoIPClient.
   Concurrent ReadAsync calls now raise EOBDConfig.

3. Mode 06 — TOBDOnBoardMonitor. Reads MID-keyed test results
   into a flat TArray<TOBDMonitorResult> (TID + ComponentID +
   UnitAndScale + signed 16-bit Value/Min/Max). Sync + Async +
   main-thread events.

4. Mode 08 — TOBDActuator with AutoExecute=False default
   safety gate per the original Phase 1 discussion. Send raises
   EOBDConfig until the gate is opened explicitly. OnBeforeSend
   fires on the main thread with a Cancel out-parameter for
   last-ditch UI confirmation. Synchronous and asynchronous
   sends share the gate.

Both new components register on the "OBD Services" palette tab.
Tests cover catalogue load round-trip, decoder formulas (uint8
+ offset, uint16_be + scale, int16_be negative path), and the
Mode 08 safety gate. Hardware-loop verification remains the
only deferred item, per existing convention.
Four non-visual components on a new "OBD Coding" palette tab:

- OBD.Coding.SecurityAccess — TOBDSecurityAccess. ISO 14229-1
  §9.4 SecurityAccess (SID 0x27) request-seed → compute-key →
  send-key handshake. The OEM-specific seed → key transform is
  attached via a TFunc (SeedToKey) or an event (OnComputeKey).
  Validates odd request-seed level, handles the zero-byte-seed
  "already unlocked" reply per §9.4.5.2. Sync + Async + Progress.

- OBD.Coding.DataIdentifierIO — TOBDDataIdentifierIO. UDS Read
  (0x22) / Write (0x2E). Multi-DID reads split the response per
  DID echo. Writes gated by AutoExecute=False (default).

- OBD.Coding.RoutineControl — TOBDRoutineControl. UDS Service
  0x31 with all three sub-functions: Start / Stop / RequestResults.
  Start + Stop gated by AutoExecute; results queries unrestricted.

- OBD.Coding.Flasher — TOBDFlasher. UDS download trio:
  0x34 RequestDownload (negotiates max block length, accounting
  for the 2-byte SID+BSC overhead); 0x36 TransferData chunked
  with BSC counter (1..255 then wraps to 1); 0x37 RequestTransfer
  Exit. AutoExecute=False default. OnBeforeFlash fires on the
  main thread under TThread.Synchronize with a Cancel out-
  parameter for last-ditch UI confirmation. OnProgress per chunk.

Every gated entry-point raises EOBDConfig before any wire access.
Single-in-flight async via GuardSingleAsync mirrored from DoIP.
FreeNotification on Protocol everywhere.

Tests cover safety-gate rejection on every gated entry-point,
even-level rejection on Unlock, transform-not-configured
rejection, empty-DID-list and empty-image rejections, and
OnBeforeFlash cancel honoured.

Phase 6 honest review documents the multi-DID heuristic split,
flasher staging deferral, missing per-chunk retry, and the
deferred TOBDUploader / TOBDFlashSession orchestrator. Hardware-
loop verification continues to be the only no-deferral exception.
User pulled all Phase 6 deferrals forward.

1. TOBDUploader (UDS Service 0x35) — mirror of TOBDFlasher.
   0x35 RequestUpload → 0x36 TransferData (read-side, response
   carries data) → 0x37 RequestTransferExit. Same NRC 0x78
   pending-retry budget and per-chunk retry budget. Reads are
   non-destructive so AutoExecute is not gated; OnBeforeUpload
   still available for confirmation UIs.

2. TOBDFlashSession orchestrator — composes the classic UDS
   reflash choreography into a single call: 0x10/02 programming
   session → 0x27 SecurityAccess → 0x31/01 erase RID → flasher →
   0x31/01 verify RID → 0x11/01 hardReset. Every step is
   configurable (SecurityLevel, EraseRoutineID, VerifyRoutineID,
   ResetAfterFlash, SessionAtStart); RID := 0 disables a step.
   Children lazily created and inherit Protocol / AutoExecute /
   SeedToKey from the orchestrator.

3. NRC 0x78 (responsePending) handling. New RequestWithPending
   helper in both flasher and uploader retransmits on 0x78 up
   to MaxPendingRetries (default 10) with PendingDelayMs (50 ms)
   between attempts. Applied to RequestDownload, RequestUpload,
   every TransferData chunk, and RequestTransferExit.

4. Per-chunk retry budget on TransferData. New MaxChunkRetries
   (default 3) and ChunkRetryDelayMs (default 20 ms) on flasher
   and uploader. Each retry surfaces via OnProgress so a UI can
   display "BSC 12 retry 2/3 (...)".

5. Strict-mode multi-DID read. New
   TOBDDataIdentifierIO.ReadStrict(ADIDs, ALengths) splits the
   response deterministically by declared length instead of the
   heuristic next-DID-echo scan. Length-array size mismatch
   raises EOBDConfig eagerly.

6. All-zero seed unlocked detection in TOBDSecurityAccess.
   Treats both a zero-byte seed and an all-zero-byte seed as
   "already unlocked" per ISO 14229-1 §9.4.5.2.

TOBDUploader and TOBDFlashSession register on the "OBD Coding"
palette tab. New tests cover retry-budget defaults, upload zero-
size rejection, flash-session AutoExecute gate, empty-image
rejection, strict-mode length-mismatch, and the security-access
guard order. Hardware-loop verification stays the only standing
deferral.
Six new units shipping the XCP/CCP/A2L calibration stack and the
IsoBus / Tachograph speciality-bus surface:

- OBD.Calibration.A2L — TOBDA2L parser. Tokenizer-based
  recursive-descent over MEASUREMENT, CHARACTERISTIC, COMPU_METHOD;
  unknown blocks (RECORD_LAYOUT, AXIS_PTS, FUNCTION, …) skipped
  cleanly with brace counting. Convert evaluates IDENTICAL /
  LINEAR / RAT_FUNC.
- OBD.Calibration.XCP.Transport — IOBDXCPTransport contract so
  the master never touches a CAN driver directly.
- OBD.Calibration.XCP — TOBDXCP master. CONNECT / DISCONNECT /
  GET_STATUS / GET_ID / GET_SEED + UNLOCK / SET_MTA / UPLOAD /
  SHORT_UPLOAD / DOWNLOAD / SHORT_DOWNLOAD / SET_CAL_PAGE /
  GET_CAL_PAGE / START_STOP_DAQ_LIST / START_STOP_SYNCH. Honours
  the slave-declared byte order on every address field.
- OBD.Calibration.CCP — TOBDCCP master. Legacy CAN-only ASAP1a;
  CONNECT / EXCHANGE_ID / GET_VERSION / GET_SEED + UNLOCK /
  SET_MTA / DNLOAD / UPLOAD / SELECT_CAL_PAGE / START_STOP. CTR
  byte advances per command; addresses big-endian per spec.
- OBD.Speciality.IsoBus — TOBDIsoBus. ISO 11783-5 NAME encode/
  decode (LSB-first), priority comparison through 64-bit value,
  address-claim conflict resolution, registry, PGN-request
  builder. OnAddressLost fires when the local stack must yield.
- OBD.Speciality.Tachograph — TOBDTachograph. EU 2016/799 Annex
  IC TimeReal ↔ TDateTime, Activity / Event / Fault record
  decoders, ASCII-string padding stripper.

Tests cover the A2L parser end-to-end, the XCP master through a
queue-backed stub transport (CONNECT decode, SHORT_UPLOAD address
byte order, UNLOCK key layout, ERR raises), CCP packet shape,
IsoBus NAME round-trip + priority + claim conflict, and Tachograph
TimeReal pinned to 2024-01-01 = 1704067200.

TOBDXCP / TOBDCCP / TOBDIsoBus register on a new "OBD Calibration"
palette tab; A2L and Tachograph are stateless decoders.

Phase 7 honest review documents the eight tracked follow-ups
(sample IOBDXCPTransport, full DAQ programming, ProgramFlash,
CCP DAQ, A2L MOD_COMMON, COMPU_VTAB tables, IsoBus VT/TC/FS/GNSS,
Tachograph CalibrationRecord + PC/SC). Hardware-loop verification
remains the only standing deferral.
User pulled all Phase 7 deferrals forward. Eight closures:

1. Sample IOBDXCPTransport — new OBD.Calibration.XCP.Loopback
   ships TOBDXCPLoopbackTransport: in-process queue + event
   implementation, useful as a reference and as test
   infrastructure for code that drives the master.

2. Full XCP DAQ programming. TOBDXCP gained FreeDAQ, AllocDAQ,
   AllocODT, AllocODTEntry, SetDAQPtr, WriteDAQ, SetDAQListMode
   — every multi-byte field honours the slave-declared byte
   order.

3. XCP ProgramFlash (PGM). TOBDXCP gained ProgramStart,
   ProgramClear, Program_, ProgramReset, ProgramVerify. Same
   byte-order discipline as UPLOAD/DOWNLOAD.

4. CCP DAQ programming. TOBDCCP gained GetDAQSize, SetDAQPtr,
   WriteDAQ, StartStopAll. Big-endian addresses per ASAP1a.

5. A2L MOD_COMMON / MOD_PAR. New TOBDA2LModuleCommon +
   TOBDA2LModulePar records on the cluster; parser fills
   ByteOrder / Deposit / Alignment* and EpkValue / EpkAddress /
   Customer / Version. HasCommon / HasPar flags signal presence.

6. A2L COMPU_VTAB / table interpolation. New verbal-table +
   numeric-table entry types, parser branches for COMPU_VTAB /
   COMPU_VTAB_RANGE / COMPU_TAB, and a new ConvertVerbal lookup.
   Convert now interpolates cmTabIntp and does nearest-lower
   lookup for cmTabNointp, with edge-clamping.

7. IsoBus VT / TC / FS / GNSS. Four new units shipping the
   framing helpers: TOBDIsoBusVT (Get_Memory, Get/Load/Store/
   Delete_Version, End_Of_Object_Pool, Audio_Signal,
   Change_Active_Mask, Soft_Key + VT_Status decoders),
   TOBDIsoBusTC (Status decoder, Value / SetValue / Request* /
   ProcessDataAck builders + Value round-trip), TOBDIsoBusFS
   (Open/Read/Write/Seek/Close + CWD), TOBDIsoBusGNSS (NMEA
   2000 PGN 129025 / 129026 / 129029 decoders).

8. Tachograph CalibrationRecord + PC/SC. DecodeCalibration
   covers the Gen-1 fixed-offset record per Annex IB §2.39
   (purpose, workshop name, card number, date, VIN, w / k
   constants, tyre size, authorised speed). New
   OBD.Speciality.Tachograph.PCSC dynamic-loads winscard.dll /
   libpcsclite.so.1 with SCardEstablishContext / ListReaders /
   Connect / Transmit / Disconnect — same pattern as the
   Phase 4d OpenSSL plug.

22 new tests across 5 fixtures. Hardware-loop verification
remains the only standing deferral.
…helpers

Fifteen new units, the longest-running phase since Phase 4:

Generic write surface
- OBD.UDS.WriteMemory — TOBDUDSWriteMemory (UDS 0x3D
  WriteMemoryByAddress), AutoExecute=False default.
- OBD.KWP.WriteID — TOBDKWPWriteID (KWP2000 0x3B
  WriteDataByLocalIdentifier).
- OBD.Coding.Diff — byte-level diff/apply/revert with length
  + before-byte mismatch detection.
- OBD.Coding.AuditLog — append-only JSONL with optional
  HMAC-CMAC chain over (prev || line-without-hmac); Verify
  walks the chain and returns the first-tamper line index.
  Reuses the Phase 4e CMAC primitive — no new crypto.
- OBD.Coding.Session — orchestrator: snapshot every step
  → write all → verify all → rollback-on-fail. Snapshots
  taken before any writes so a partial-batch failure rolls
  back the entire batch. Wires audit log when assigned.

Per-OEM helpers (parse/get/set primitives, no OEM catalogues)
- VAG: long-coding parse/format/Get-SetBit/Byte + adaptation
  channel encode/decode.
- BMW: CAFD/NCS TLV walk, FindEntry, WriteValue, Read/SetBit
  on multi-byte values, ParseVehicleOrder S-codes.
- Ford: AsBuilt section parse/format, two's-complement
  checksum compute/verify/seal, Get/SetByte, FindSection.
- HMG (Hyundai/Kia/Genesis): configuration-word parse +
  GetOption / SetOption (1/2/4-byte values).
- Honda: flat-array customisation entries with ranged setter.
- Mercedes: variant-coding bit + sub-byte field access; SCN
  decode/encode.
- Stellantis: FCA Proxi parameter parse/get/set by ID.
- Toyota: customisation menu parse/get/set.
- VAG Component Protection: CP DID catalogue + status decoder
  + challenge → AuthFunc-callback → authorisation flow.
  No OEM secrets shipped — host wires the Geko/SVM bridge.

21 tests across 4 fixtures: write-safety gates, diff
round-trip + mismatch detection, audit-log three-entry
round-trip + clean-verify + tamper-verify, every OEM
primitive with a hand-derived vector.

Five new components register on the "OBD Coding" palette
(WriteMemory, KWP WriteID, AuditLog, Session, VAG CP).
The OEM primitive units are pure helpers; no palette entry.

Phase 8 honest review documents six follow-ups (OEM option
catalogues, dry-run mode, RLE diff for firmware, additional
component-protection vendors, label-file parsers,
hardware loop). Hardware loop continues to be the only
standing deferral.
User pulled all but the hardware-loop test forward.

1. JSON-Schema-validated OEM coding-option catalogue.
   data/schemas/oem-coding-catalog.schema.json ships the
   schema; OBD.Coding.OptionCatalog ships the loader. Seven
   addressing kinds covered (byte_bit, byte_field, byte_range,
   tlv_id, config_word, asbuilt_section, menu_index) with
   per-kind validation, optional value labels, optional tag
   arrays. No OEM content shipped — hosts populate from their
   own ground-truth sources.

2. TOBDCodingSession dry-run mode. New DryRun: Boolean
   property; snapshots still read so the audit trail captures
   the pre-state, but write / verify / rollback skip the wire
   and emit audit entries with 'dry-run' notes.

3. TOBDCodingDiffRLE — run-length-encoded diff for firmware-
   scale buffers. Configurable gap budget; TransferSize for
   pre-flash planning. Round-trip + revert + gap-merging +
   reject paths covered.

4. BMW CAS / Mercedes EZS / Stellantis SGW component-
   protection helpers. Each parallels TOBDComponentProtectionVAG
   with vendor-default DIDs (overridable) and the same
   AuthFunc callback contract. All three register on the
   "OBD Coding" palette tab. No OEM secrets shipped — hosts
   wire ISTA / DAS-Xentry / SGW token bridges.

5. VAG .lbl label-file parser. Line-oriented Ross-Tech format:
   bit-position labels, bit-range labels, whole-byte labels,
   adaptation channels, inline value tables, header / inline
   comments. BMW / Ford / Mercedes vendor formats are
   proprietary; for those, the JSON option catalogue is the
   supported path.

22 new tests across 4 fixtures. Hardware-loop verification
remains the only Phase 8 deferral.
Phase 9 split into six subphases. 9a ships the foundation that
the rest of the flashing pipeline drives.

OBD.UDS.Transfer — TOBDUDSTransfer. Full ISO 14229-1 §14
data-transfer state machine: Idle → RequestingDownload →
Transferring → RequestingExit → Completed (or → Aborted on
any failure). Chunked TransferData with BSC counter, NRC 0x78
auto-retransmit budget, per-chunk retry budget, cooperative
Cancel, OnStateChange / OnProgress / OnError. Resumable —
hosts pass a TOBDTransferCursor (offset + BSC + MaxChunkBytes)
to skip the prefix already accepted by the ECU. AutoExecute
defaults to False per the standing safety contract.

OBD.J1939.MemoryAccess — TOBDJ1939MemoryAccess. SAE J1939-73
DM14 / DM15 / DM16 / DM17 / DM18 PGN catalogue + framing
helpers. EncodeDM14 / DecodeDM14 / EncodeDM15 / DecodeDM15 /
EncodeDM16 / DecodeDM16. Length-overflow rejected at encode.
Stateless; the host wires PGN frames through the Phase 4c
J1939 transport.

Tests cover safety gates (AutoExecute=False raises; empty
image raises; resume-on-complete-cursor raises; resume-with-
mismatching-image raises), retry-budget defaults, and DM14 /
DM15 / DM16 round-trip with hand-derived vectors.

The Phase 6 TOBDFlasher in OBD.Coding.Flasher remains as a
simple one-shot helper. Phase 9c will ship the orchestrator
that drives the new transfer engine plus the safety / audit /
voltage-gate / signature pipeline.
OBD.Flash.VoltageGate — TOBDVoltageGate. Background voltage
monitor with configurable MinimumVoltage / PollIntervalMs /
HoldTimeMs (defaults 12.0 V / 200 ms / 1000 ms). Hosts wire a
measurement source via SourceFunc or OnRequestVoltage; the gate
fires OnVoltageLow on every dip and latches OnAbort once the
reading has been below the threshold for HoldTimeMs. Transient
dips shorter than HoldTimeMs do NOT latch. Source missing →
gate fires OnAbort and stops.

OBD.Flash.Checkpoint — TOBDFlashCheckpoint. File-backed JSON
checkpoint store for resumable transfers. Captures the
TOBDTransferCursor + a SHA-256 of the firmware image so a host
that resumes against a different image gets a hard-fail. Atomic
write (write-to-temp + rename) so a crash mid-write doesn't
corrupt the previous good checkpoint. Schema version pinned at
1; load rejects mismatching versions.

OBD.Flash.Phases — TOBDFlashPhase enum (preflight / verify-image
/ enter-programming / transfer / verify / reset / finalise);
TOBDFlashCheckList collection that runs checks per phase in
insertion order; severity buckets (csInfo / csWarning /
csError); first csError failure aborts the phase, warnings are
logged-only. TOBDFlashChecks ships built-in helpers for the
standard preflight checks: EngineOff, VoltageFloor,
AmbientTemperature, IgnitionOn — each takes a host-supplied
TFunc<…> measurement source.

Tests cover voltage-gate latch + transient-dip recovery, a
SHA-256 hash vector pinned to "abc", checkpoint round-trip,
image-tamper detection, version-mismatch rejection, and the
phase runner across all three severity levels with a visitor
that walks every result.
OBD.Flash.Pipeline — TOBDFlashPipeline. Composes every Phase 9
building block into one safe-by-default end-to-end run:

  fpPreflight        — host-supplied checks
  fpVerifyImage      — image hash / signature checks
  fpEnterProgramming — host wires session + security + erase
                       through OnEnterProgramming
  fpTransfer         — TOBDUDSTransfer (Phase 9a)
  fpVerify           — host wires verify routine through
                       OnVerifyRoutine
  fpReset            — 0x11/01 ECUReset hardReset (configurable)
  fpFinalise         — close audit log

Safety surface:
- AutoExecute defaults False. Without an OnConfirmExecute
  handler, Flash aborts with EOBDConfig (PLAN.md §785).
- OnConfirmExecute fires on the main thread under
  TThread.Synchronize so a worker waits for the user.
- Voltage gate integration: a TOBDVoltageGate plugged in
  monitors the supply during the entire run; an abort
  cancels TOBDUDSTransfer at the next chunk boundary. A
  pipeline with NO voltage gate logs a WARN audit entry at
  start-of-flash but proceeds (PLAN §785: developer-choice).
- Audit log integration: every phase change, every check
  result (info/warn/error), every wire-side phase boundary,
  and every error fires through TOBDCodingAuditLog when one
  is attached.
- Checkpoint integration: when CheckpointFile is set, the
  pipeline atomically writes the cursor + image SHA-256
  after every accepted chunk through TOBDFlashCheckpoint.
- Phase visitor: OnPhaseChange / OnCheckResult / OnComplete
  fire on the main thread for UI binding.

TOBDUDSTransfer + TOBDVoltageGate + TOBDFlashPipeline register
on a new "OBD Flashing" palette tab.

Tests cover the safety gate (empty image, missing protocol,
no-handler-and-not-AutoExecute, OnConfirmExecute cancel),
Checks list exposure, and per-spec defaults.
OBD.Signature — TOBDSignatureVerifier abstract base + four
backend implementations + a process-wide registry. Hosts
register backends at startup; the flash pipeline queries the
registry for an algorithm and gets routed to the first matching
backend.

Algorithm coverage:
  Classical: RSA-PSS-SHA256, RSA-PKCS1-SHA256, ECDSA-P256/P384,
             Ed25519
  Post-Q  : Dilithium-2/3/5 (ML-DSA), Falcon-512/1024,
             SPHINCS+ SHA2-128f / 192f (SLH-DSA)

OBD.Signature.BCrypt — Windows CNG. Dynamic-loads bcrypt.dll;
classical algorithms only. Verify path resolves the public-
key blob (CNG RSAPUBLICBLOB / ECCPUBLICBLOB), hashes the
message via BCryptCreateHash, and runs BCryptVerifySignature
with the right padding info per algorithm.

OBD.Signature.OpenSSL — OpenSSL 3.x. Same dynamic-load pattern
as the Phase 4d DoIP TLS plug, different symbol set (libcrypto
EVP_*). Accepts PEM or DER public keys (sniffs the leading
bytes). RSA-PSS / RSA-PKCS#1 / ECDSA / Ed25519 covered.

OBD.Signature.HSM — PKCS#11 scaffolding. Dynamic-loads the
host-specified vendor driver via LibraryPath; ships the
property surface and Supports / IsAvailable hooks. Verify
itself is wired to raise until the host plugs a vendor shim
(Vector / Thales / Utimaco SDKs all provide a thin one);
hosts using OpenSSL with the HSM's PKCS#11 engine get
verification through the OpenSSL backend.

OBD.Signature.PQC — Open Quantum Safe (liboqs). Dynamic-loads
oqs.dll / liboqs.so.0; routes Dilithium / Falcon / SPHINCS+
through OQS_SIG_new + OQS_SIG_verify. Reports IsAvailable =
False when liboqs isn't installed so the registry falls back
to BCrypt / OpenSSL for classical algorithms.

Tests cover registry routing (first-supporting wins), no-
backend-supports raises, per-backend Supports surface, HSM
unavailable-without-LibraryPath, and AlgorithmName strings.
claude added 30 commits May 12, 2026 06:28
Tiny convenience wrappers ported from v1's OBD.OEM.Helpers. Lets
vendor extensions build their catalogues as array literals —
DIDs := [DID($1234, 'name', 'desc'), DID($5678, 'name2', 'desc2', $7E0)] —
rather than two-line record-literal blocks. Both DID and Routine
have a global overload and an ECU-scoped overload; ECU is the
bus-map record builder.

Registered in DelphiOBD_RT.dpk.
OBD.OEM.Catalog.JSON: full TOBDOEMJSONCatalog parser ported from
v1 — DIDs, routines, ECUs, DTC ranges, coding blocks, adaptations,
actuator tests, live PIDs and DTC extended-data records, each
carrying source + verified provenance. Decoder spec compiles
enum + bitmask lookup maps and DecodePayload renders the parsed
value with scale/offset/unit. AsBaseDIDs / AsBaseRoutines /
AsBaseECUs cast to the public schema for vendor BuildCatalog
hooks. Plus the v1 ParseOEMDecoderKind / ParseCodingFieldKind /
ParseAdaptationKind / ParseActuatorResponseKind /
ParseLivePIDMode / ParseDtcExtendedKind enum-string mappers.

OBD.OEM.Catalog.Loader: SetCatalogSearchPath / ResolveCatalogPath
(user override → exe-dir/catalogs → ../catalogs → cwd/catalogs,
each also probed under motorcycle/agricultural/marine/powersports
subdirs), MergeCatalogJSON (2- and 4-arg overloads) with
replace-by-DID / replace-by-Identifier / replace-by-Address
semantics, MergeExtendedCatalogJSON for the extended-catalogue
sections, VINMatchesCatalog for JSON-driven WMI routing.
Malformed-catalog failures fall through silently to keep app
startup robust.

OBD.OEM.DTC: TOBDDtcCatalog.LoadFromFile parses { "dtcs": [ ... ] }
into RegisterEntry calls; EOBDDtcCatalog raised on file/parse errors.

OBD.OEM.DTC.Loader: MergeDtcCatalog (resolves through the shared
search path, silently no-ops on missing file).

Tests.OBD.OEM.CatalogLoader fixture: 32 assertions across every
catalogue / loader surface — header parsing, every section
loader, decoder rendering (uint16 + enum), AsBase* casts, error
paths, ResolveCatalogPath search order (override + vehicle-class
subdir + miss), MergeCatalogJSON replace + append + ECU
preservation + no-file no-op, MergeExtendedCatalogJSON across all
five extended sections, VIN matching (case-insensitive + non-match
+ short-VIN), DTC loader load / miss / malformed, and the enum
parsers.
OBD.OEM.Catalog.CSV: TOBDCatalogCSVImporter ports the v1
community-CSV → vendor-JSON converter. Minimal RFC-4180 reader
handles comma separation, double-quoted fields with embedded
commas, escaped quotes (""), comment lines (#) and blanks.
Required columns (did, name, description) are validated;
optional source / verified / ecu_address / decoder columns
flow through to the emitted JSON with the decoder column
re-parsed as an embedded sub-object. Convert() round-trips
through TFile and ForceDirectories the destination path.

Tests.OBD.OEM.CatalogCSV: 12-assertion fixture covering header
validation, body-empty rejection, quoted comma + escaped-quote
parsing, decoder-as-embedded-object, verified true/false/blank
emission, comment/blank skip, file round-trip, missing-file
exception.
OBD.OEM.GoldenCheck: GoldenVector / CheckGoldenVectors —
framework-neutral spot-check helper that returns a failure list
rather than raising. Verifies each (DID, payload, expected
substring) tuple against an extension's DecodeDID and surfaces
empty-output or substring-mismatch regressions.

OBD.OEM.HD: shared base for heavy-duty (J1939) OEM extensions.
Maps the J1939-71 source-address constants (engine, transmission,
brakes, aftertreatment, off-board tool, etc.), provides
TOBDHDSessionNegotiator (3-second tester-present for trucks where
2s races with DM1 broadcast), and the SPN-FMI helpers
FormatSPNFMI / ParseDM1DTC (J1939-73 packed-DTC unpack).

OBD.OEM.SCN.Mercedes: XENTRY SCN coding-flow framing.
TMBSCNVersionRequest/Response + TMBSCNCodingRequest/Response
records and their Encode/Decode wire-format functions
(VIN(17) + BE16 length-prefixed payloads), plus the IMBSCNSolver
contract a dealer-portal client implements and
TMBSCNSolverNotAvailable which raises EOBDMBSCNNoSolver — the
shipped default since real coding requires NDA-bound back-end
credentials.

Tests.OBD.OEM.Support: 18-assertion fixture covering golden-vector
round-trip, all-pass / empty-output / missing-substring /
empty-expected paths, the HD negotiator's 3-second cadence and
display name, SPN-FMI formatting + DM1 unpack + short-payload
return-empty, the J1939 source-address constants, and SCN
version/coding request+response round-trips, wrong-VIN-length and
wrong-byte-length raises, truncated coding-request raise, and the
NotAvailable solver raising for both methods.
OBD.OEM.Coding: hex helpers (HexStringToBytes accepts space / dash
/ underscore / colon / dot separators and raises EOBDCodingError
on bad chars or odd-length input; BytesToHexString takes an
optional separator) and GetBit / SetBit byte-array bit-field
accessors with range checking.

OBD.OEM.RoutineControl: full UDS service-0x31 helpers ported from
v1. BuildStartRoutine / BuildStopRoutine /
BuildRequestRoutineResults emit the standard 31 SF HiRID LoRID
frames. ParseRoutineResponse validates the 71 SF RID echo and
raises EOBDRoutineError on negative replies (7F 31 NRC), wrong
SID, sub-function or RID mismatch, or under-length payloads.
TOBDRoutineRequestBuilder offers typed AddUInt8/16BE/32BE,
AddInt16BE/32BE, AddAscii (with FixedLength padding), AddBcdDate
and AddBcdYear plus ToFrame which wraps the payload with the SID
header. TOBDRoutineResponseReader is a cursor-based reader with
RequireBytes() under-read protection. DecodeRoutineOutput renders
a TOBDRoutineSchema's output fields into "name = value" lines
including enum lookups and bitmask name joins; trailing optional
fields are skipped cleanly when the payload is short.

OBD.OEM.ServiceFunction: canonical TOBDServiceFunctionKind enum
covering 19 dealer-style service functions (oil-life reset, EPB
service, SAS calibration, battery registration, DPF regen, TPMS
relearn, throttle / idle / transmission / crank relearn, brake
bleed, air-suspension calibration, immo relearn, hybrid battery
test, Haldex calibration, basic setting, reset adaptations, DEF
quality, fuel-trim reset). TOBDServiceFunctionRegistry maps each
kind to a token list and classifies routine names as
case-insensitive substring matches. FindServiceFunction /
ListServiceFunctions walk a vendor extension's Routines and emit
TOBDServiceFunction records; BuildServiceFunctionFrame produces
the StartRoutine frame; ServiceFunctionKindName renders a UI
label. Registry shutdown is done through a class procedure so the
unit-level finalization compiles against the strict-private class
var.

Tests.OBD.OEM.Routines: 27-assertion fixture covering hex
round-trip + separators + odd-length / bad-char raises, bit
set/get + out-of-range, routine frame builders, positive +
negative + wrong-SID / wrong-RID / short response parses, builder
typed fields + ASCII fixed-length pad + oversize raise + BCD date
+ to-frame, reader round-trip + under-read raise, schema decode
of mixed uint16+ascii + short-payload graceful stop, service-fn
name classification (known / unknown), Find / List against an
extension, BuildServiceFunctionFrame uses 31 01, and KindName
labels.
OBD.OEM.Session.Runner: TOBDSessionRunner executes a
TOBDSessionPlan synchronously against a bound TOBDProtocol.
sskATCommand steps go through Adapter.SendCommand and match
ExpectedResponse as ASCII; sskUDSRequest steps split the UDS
payload into SID + Data, call Protocol.Request, reassemble the
on-wire response (ServiceID + Data) and match the
ExpectedResponse byte prefix exactly the way v1 did. Each step
records duration via TStopwatch and surfaces exceptions as
ErrorMessage. Execute() truncates the audit at the first failure
so the result reflects only the steps actually run.

TOBDTesterPresentThread is the heartbeat thread. It splits the
plan's TesterPresentRequest into SID + body and fires
Protocol.Request at the configured interval; timeout exceptions
are swallowed (sub-function 0x80 suppresses the positive
response, so a timeout is the expected outcome) but other failures
break out so a dropped adapter stops the heartbeat instead of
spinning. StopGracefully() signals the thread + joins.

Constructor raises EOBDSessionRunnerError when the protocol is
nil or has no bound adapter — fail fast at wiring time rather
than at the first plan step.

Rebased onto the v2 transport (TOBDProtocol + TOBDAdapter)
because v2 has no async-future surface; the equivalent v1
TOBDConnectionAsync.OBDAsync futures are replaced with
synchronous Request calls.
Vendor extensions can now port verbatim. The bottleneck was that
v1 vendor units register seed-key algorithms via the interface-
based contract (TOBDSeedKeyKWP2000TwosComplement.Create, etc.)
while v2's stub only exposed an anonymous-function registry.

OBD.OEM.SeedKey: replaced the lambda-only stub with the v1
surface — IOBDSeedKeyAlgorithm interface, TOBDSeedKeyRegistry
with per-level multi-algorithm LIFO lists,
TOBDSeedKeyAlgorithmBase + four reference implementations
(KWP2000 two's-complement, XOR mask, byte rotation, fixed-key)
and the ISO 14229 §10 frame helpers (RequestSeedFrame /
SendKeyFrame / ExtractSeed). Algorithms carry Description /
Source / Verified provenance metadata so production callers can
filter unverified algorithms out of flashing paths.

Backward-compatibility shims for the existing v2 stub callers:
TOBDSeedKeyAlgorithm lambda type (wrapped via internal
TOBDLambdaSeedKeyAlgorithm adapter), RegisterAlgorithm lambda
overload, ComputeKey(Level, Seed) convenience, level-only
UnregisterAlgorithm overload, RegisteredLevels (sorted-ascending
alias for Levels), EOBDSeedKey alias for EOBDSeedKeyError. The
existing v2 lambda-based test fixture passes with one small
change: the nil-argument test needs a typed cast now that
RegisterAlgorithm is overloaded.

OBD.OEM: thin compatibility umbrella that re-exports the v2
OBD.OEM.Types records and OBD.OEM.Extensions
contract/base/registry under the names v1 vendor units expect
(IOBDOEMExtension, TOBDOEMExtensionBase, TOBDOEMRegistry). v2
split the v1 OBD.OEM monolith into two units; this shim lets a
vendor file keep its single 'uses ..., OBD.OEM, OBD.OEM.Session,
OBD.OEM.SeedKey, OBD.OEM.DTC;' import line unchanged. The unit
also documents the v1->v2 rename for the registry class
(TOBDOEMRegistry was the vendor registry in v1; in v2 the same
name lives in OBD.OEM.Registry as the runtime overlay resolver
— the alias here points at the vendor registry,
TOBDOEMExtensionRegistry).
Removed inline references to prior versions / phases / port
provenance from headers, history notes and XMLDoc summaries
across the OEM unit family and four affected test fixtures.
The History block keeps dates and brief notes describing what
the unit does; STYLE.md requires that version intent live only
in those dedicated header sections, not in narrative throughout
the source.

Files touched: OBD.OEM, OBD.OEM.Types, OBD.OEM.Session,
OBD.OEM.Session.Runner, OBD.OEM.Extensions, OBD.OEM.SeedKey,
OBD.OEM.DTC, OBD.OEM.Helpers, OBD.OEM.RoutineControl,
OBD.OEM.Coding, OBD.OEM.ServiceFunction, OBD.OEM.Catalog.JSON,
OBD.OEM.Catalog.CSV, OBD.OEM.Catalog.Loader,
OBD.OEM.DTC.Loader, OBD.OEM.GoldenCheck, OBD.OEM.HD,
OBD.OEM.SCN.Mercedes, plus Tests.OBD.Catalog.Inventory,
Tests.OBD.Tachograph, Tests.OBD.UI.LegacyPorts,
Tests.OBD.Version. No code behaviour changed.
Audit finding: the DTC loader was reading three fields from each
catalogue entry (code, description, notes) and silently dropping
the other ten that catalogs/dtc-*.json ship — severity,
possible_causes, verified, symptoms, repair_guidance,
monitor_type, freeze_frame_relevant, related_dids,
related_routines, oem_bulletin — plus the file-level
default_source. The principle stands: catalogues are the
reference and code adheres to them, so every catalogue field has
to be available and usable.

OBD.OEM.Types: enriched TOBDDtcCatalogEntry with the missing
fields and added the supporting enums (TOBDDtcSeverity,
TOBDDtcSystem, TOBDDtcMonitorType). Existing fields (Code,
Description, Notes) keep their original positions so existing
callers and the existing JSON fixture remain compatible.

OBD.OEM.DTC: LoadFromFile now reads every schema field, applying
the file-level default_source as the per-entry source fallback
(matches OBD.OEM.Catalog.Loader's resolution policy for DID /
Routine source). Added the J2012 helpers EncodeDtc / FormatDtc
(byte and bytes overloads) / ParseDtcSystem / FormatDtcSystem /
IsManufacturerDtc / ParseSeverity / FormatSeverity /
ParseMonitorType / FormatMonitorType and the EOBDDtcError
exception they raise. Helpers are pure functions exported at
unit level.
Audit finding: catalogs/key-platforms-{ford,hmg,toyota}.json
ship with rich chassis applicability data (chassis_key,
display_name, access classification, notes) but no code path
reads them. Tools that need to gate destructive PATS / smart-key
routines on chassis access had no way to query "is this
gateway-locked?"; the only references in src/OEM/ were
documentation comments.

OBD.OEM.KeyAdaptation.Platforms (new): exposes
TFordPlatformAccess / TFordPlatformInfo + FindFordPlatform,
THMGPlatformAccess / THMGPlatformInfo + FindHMGPlatform,
TToyotaPlatformAccess / TToyotaPlatformInfo +
FindToyotaPlatform. Loads all three catalogues once at unit
initialisation through the standard ResolveCatalogPath
resolution. Unknown chassis return a fail-safe default
(gateway-locked / certificate-required) so hosts default to
refusing destructive operations. ReloadKeyPlatformCatalogues
re-reads for runtime refresh.

Catalogues are parsed inside a TProc<TJSONObject> visit
callback shared across the three loaders — malformed files
silently leave the registry untouched so application startup
is never blocked, matching the resolution policy of
OBD.OEM.Catalog.Loader and OBD.OEM.DTC.Loader.

OBD.RadioCode.Aftermarket: corrected a stale History note that
referenced a deprecated catalogue path; the Becker4/5 loaders
already read from catalogs/radio-code/.
Three audit follow-ups in one change.

1. Removed catalogs/radiocode-becker{4,5}.json at repo root.
   md5-identical to catalogs/radio-code/becker{4,5}.json which
   is what TOBDRadioCodeBecker4/5 actually loads; the root
   copies were unreachable orphans.

2. Extracted the Ford V-series factory radio-code lookup table
   to catalogs/radio-code/ford-v.json. 999,999 entries, 1-based
   serial -> 4-digit code, sourced from the same dataset that
   TOBDRadioCodeFordV.EnsureLoaded expects. Spot-checks against
   the original embedded array confirm round-trip for serial 1,
   2 and 999999. JSON header carries schema_version / format /
   brand / description / count for tools that want metadata
   without parsing the codes array.

3. OBD.KWP.Catalog (new): loads catalogs/kwp/common-ids.json
   into two registries — ECU-Identification (Service 0x1A,
   0x80..0x9B range) and Common-Identifiers (Service 0x22,
   F180..F199). Exposes FindKwpIdentifier(family, id) +
   KwpEcuIdentifications + KwpCommonIdentifiers (sorted
   ascending). Handles the catalogue's mixed length field
   (numeric or "variable") via LengthVariable flag.
   Tolerant resolution: missing or malformed file leaves the
   registries empty so application startup never blocks.
Vendor extensions receive their DIDs / routines / coding
blocks / adaptations / actuator tests / live PIDs / DTC
extended-data records via TOBDOEMJSONCatalog's AsBase* + the
loader's Convert* helpers. The lean form previously dropped the
schema's provenance fields (source, verified) and the live-PID /
DTC-extended decoder's structural maps (size, enum values, bit
names). The OEM JSON catalogue carries that data; tools and
vendor code now see it.

OBD.OEM.Types: added Source + Verified to TOBDOEMDataIdentifier,
TOBDOEMRoutine, TOBDOEMCodingBlock, TOBDOEMAdaptation,
TOBDOEMActuatorTest, TOBDOEMLivePID, TOBDDtcExtendedDataRecord.
TOBDOEMDecoderSpec (new) packages the full decoder schema
(kind, size, scale, offset, unit, enum values, bit names) as a
value-type record; TOBDOEMDataIdentifier carries one directly,
and TOBDOEMLivePID / TOBDDtcExtendedDataRecord expose it
alongside the existing flat decoder fields (kept for
back-compat, mirrored from the structured form).

OBD.OEM.Catalog.JSON: TOBDLivePIDEntry / TOBDDtcExtendedDataEntry
now also parse the decoder's "size", "values" and "bits"
sub-objects. Added ParseBitNames helper. AsBaseDIDs +
AsBaseRoutines populate the new public fields, including a
ToOEMDecoderSpec converter that flattens the internal
class-backed TOBDDecoderSpec into the value-type
TOBDOEMDecoderSpec.

OBD.OEM.Catalog.Loader: every Convert* function populates
Source + Verified; ConvertLivePID and ConvertDtcExtended
additionally populate the structured Decoder subrecord.
OBD.OEM.VW: Volkswagen Audi Group extension. Loads vw.json +
uds-standard.json into the base catalogue and vw.json's
extended slice into the coding / adaptation / actuator-test /
live-PID / DTC-extended tables. SeedDefaultDtcCatalog overlays
the ISO 15031-6 P0xxx baseline plus dtc-vw.json. The session
negotiator (TOBDVWSessionNegotiator) prepends the AT SH + AT
CRA handshake that VCDS / ODIS issue against the request +
request-plus-8 CAN-IDs and uses a 2000 ms tester-present cadence
matching ODIS service mode 2. Seed-key starter registers the
textbook KWP2000 two's-complement at Level 1 for the pre-2008
KWP components; production callers replace it via
RegisterAlgorithm. DecodeDID specialises battery_voltage
(0xF405), vehicle_speed (0xF40D), and VIN (0xF190).

OBD.OEM.Bentley: Bentley Motors (Crewe). VAG-group sister
brand; loads bentley.json + uds-standard.json + the standard
DTC overlay. ApplicableToVIN reads applicable_wmis from
bentley.json. DecodeDID renders VIN (0xF190) and the Bentley
metadata block 0xF1A0/A2/A4/A6/A8 as ASCII strings.

Both extensions register themselves through
TOBDOEMRegistry.RegisterExtension at unit init; the package
includes both new units.
OBD.OEM.BMW: Bayerische Motoren Werke. Catalogue +
DTC overlay (bmw.json + dtc-bmw.json + iso-15031 baseline).
TOBDBMWSessionNegotiator declares SecurityAccess required for
extended / programming / OEM-specific sessions and a 1500 ms
tester-present cadence (the stock 2000 ms causes session drops
on E-series DMEs). Seed-key starter is the documented community
XOR-mask placeholder (community-pr, Verified=False);
production callers replace it via RegisterAlgorithm.
DecodeDID specialises battery_voltage (D051), mileage (D050),
and VIN (F190).

OBD.OEM.MINI: BMW Group sub-brand with its own WMIs (WMW
Oxford, SAW BMW Brilliance China). Inherits the BMW E-Sys
session lineage (same RequiresSecurityAccess + 1500 ms cadence).
Catalogue mini.json + dtc-mini.json. Same seed-key starter as
BMW. DecodeDID renders VIN and the MINI chassis-code metadata
DID (F1A4).

OBD.OEM.RollsRoyce: Rolls-Royce Motor Cars (Goodwood). BMW
Group sub-brand sharing the E-Sys / ISTA stack. Catalogue
rolls-royce.json + dtc-rolls-royce.json. Same negotiator
shape; no separate seed-key override (uses the base default
which production callers replace anyway). DecodeDID renders
VIN plus the Rolls-Royce metadata DID block F1A0/A2/A4/A6.

Package updated to include all three units.
OBD.OEM.Mercedes: Mercedes-Benz Group. Catalogue + DTC overlay
(mercedes.json + dtc-mercedes.json) layered on the ISO 15031-6
baseline. TOBDMercedesSessionNegotiator extends the inherited
plan with a 22 F198 workshop-code probe after non-default
session entry — XENTRY ECUs gate later routines on a known
last-writer ID. Tester-present cadence matches XENTRY (1500 ms).
Seed-key starter is the textbook KWP2000 two's-complement at
Level 1 for legacy HHTwin / Star Diagnosis. DecodeDID renders
VIN (F190), BCD manufacturing-date (F18B), 24-bit mileage
(0202), engine-running seconds (0203) and the programming-
status enum (F19E).

OBD.OEM.Smart: smart Automobile Co. — Mercedes-Geely JV.
Catalogue smart.json + dtc-smart.json. Seed-key starter is the
Mercedes lineage's KWP2000 two's-complement (modern SEA-platform
algorithms are NDA-protected; production callers register the
real one). DecodeDID renders VIN and the smart metadata trio
(F1A0 model_code, F1A2 drivetrain, F1A4 battery_pack); rewrote
v1's case-as-expression construct into a plain inner case
statement so the unit compiles under the standard Delphi
grammar.
OBD.OEM.SecurityAccess: TOBDSecurityAccessClient runs the full
ISO 14229-1 §10 service 0x27 dance against a bound TOBDProtocol.
Given a level it requests the seed (27 LL), resolves the
algorithm from a TOBDSeedKeyRegistry, computes the key, sends it
(27 LL+1 + key), and reports the outcome via a structured
TOBDSecurityAccessResult. Honours the documented negative-
response semantics: 0x35 invalidKey surfaces as failure, 0x36
exceededNumberOfAttempts surfaces as ECU lockout (the lockout
is the ECU's, not ours, so the client refuses further
attempts), 0x37 requiredTimeDelayNotExpired triggers a
caller-configurable backoff loop (RetryDelayMs, MaxDelayRetries).
A zero-length seed is treated as "already unlocked at this
level" per §10.5.2.

Algorithms stay user-supplied via RegisterAlgorithm so the
client itself never contains any OEM-specific crypto; production
deployments register their NDA-protected implementations at
startup and the client just sequences the round trips.

OBD.OEM.SeedKey: three new reference algorithms, all flagged
Verified=False, all sourced from public-domain examples in
ISO 14229-1 Annex A / SAE J2186 textbook material:

  - TOBDSeedKeyTwosComplementLE: variant of the existing
    two's-complement that propagates the +1 carry from byte 0
    upwards rather than byte N downwards. Documented Bosch ME7 /
    EDC15 KWP2000 family shape.
  - TOBDSeedKeyNibbleSwapXor: key[i] = nibble_swap(Mask[i] XOR
    seed[i]). Caller supplies the mask. Common public shape on
    several legacy KWP2000 / KLine ECUs.
  - TOBDSeedKeyCrc32XorMask: CRC-32 (reflected IEEE-802.3 / zlib
    poly 0xEDB88320) of the seed bytes XOR'd with a caller-
    supplied 4-byte mask. Common community bench-flash template.
OBD.OEM.Stellantis: Stellantis (FCA + PSA) extension. Covers
Fiat / Chrysler / Jeep / Dodge / RAM / Alfa Romeo / Lancia /
Maserati / Peugeot / Citroen / DS / Opel / Vauxhall.
Negotiator appends an F198 workshop-code probe after non-default
session entry — PSA DiagBox always reads it; the expected-
response prefix is left empty so FCA's 7F 22 31 NACK does not
fail the plan. Seed-key starter: KWP2000 two's-complement for
the legacy PSA BSI / FCA Body Computer modules. DecodeDID
specialises VIN (F190), BCD programming-date (F199), 24-bit
mileage (1A02), fuel-level percent (1B01), engine-run-time
seconds (1B02), and battery voltage (1B03).

OBD.OEM.Bentley + OBD.OEM.RollsRoyce: backfilled
SeedDefaultSeedKeyAlgorithms. Bentley inherits the VAG-lineage
KWP2000 two's-complement at Level 1; Rolls-Royce inherits the
BMW-lineage XOR-mask placeholder. Both starters are flagged
community-pr / Verified=False so production callers replace
them via RegisterAlgorithm — every ported vendor now ships a
non-empty seed-key registry so TOBDSecurityAccessClient.Unlock
produces a non-trivial round trip even with the defaults.
OBD.OEM.Ford: Ford Motor Company. Negotiator prepends AT ST 32
to extend the ELM327 OBD timeout for programming sessions
(~3.2 s to match FDRS post-1002 stabilisation). Seed-key
starter is the ForScan-documented byte-rotate placeholder for
pre-2010 Visteon PCMs. DecodeDID handles VIN, mileage (DD00),
fuel level (DE00), engine-run-time (DE01), battery voltage
(DE02), and the calibration-id metadata trio (DF00/01/02);
rewrote v1's case-as-expression construct into an inner case.

OBD.OEM.GM: General Motors. Negotiator prepends AT SP 6 to
lock the adapter onto ISO 15765-4 11-bit / 500 kbps (GMLAN)
regardless of any prior auto-protocol state. Seed-key starter
is the GMLAN Class B trial-mode constant key (four zero bytes).
DecodeDID handles VIN, mileage (1981), engine-run-time (1982),
battery voltage (1983), and the metadata pair (F1A0
broadcast_code / F1A4 engineering_part_number) — same
case-as-expression rewrite.

OBD.OEM.JLR: Jaguar Land Rover. JSON-driven catalogue;
seed-key starter is the legacy DDW2000 / IDS KWP2000
two's-complement. DecodeDID handles VIN, jlr_model_code
(F1A0), jlr_assembly_plant (F1A2).

OBD.OEM.AstonMartin: Aston Martin Lagonda. Ford-lineage
byte-rotate starter (pre-2018 Visteon PCM family); AMG-era
AML ECUs use Mercedes XENTRY crypto which production callers
register separately. DecodeDID handles VIN and the aml_meta
DID block (F1A0/A2/A4/A6/A8).
All four units load their JSON catalogue + DTC overlay, register
a SecurityAccess starter algorithm at Level 1, and decode the
brand-specific F1Ax metadata DIDs into typed labels.

- OBD.OEM.Tesla: starter is the textbook KWP2000 two's-complement
  placeholder; Tesla service-mode unlock is NDA-protected so
  production callers register the real implementation via
  RegisterAlgorithm. DecodeDID surfaces tesla_firmware_version
  (F1A0) and tesla_hardware_id (F1A2).
- OBD.OEM.Lucid: same KWP2000 two's-complement starter; in-house
  Wunderbox toolchain ships the real algorithm. DecodeDID surfaces
  the full Lucid metadata quartet (model_code, drivetrain,
  battery_pack, software_release).
- OBD.OEM.Rivian: same starter; in-house service-mode unlock.
  DecodeDID surfaces rivian_model_code + drivetrain.
- OBD.OEM.Polestar: KWP2000 two's-complement matching the Volvo
  / SPA2 lineage; SEA-platform Polestars use proprietary crypto.
  DecodeDID surfaces polestar_model_code + drivetrain.

Per the "every extension ships a non-empty seed-key registry"
policy, all four register a Level 1 starter so
TOBDSecurityAccessClient.Unlock has something to call against
defaults; every starter is flagged Verified=False with
community / placeholder provenance so production callers know
to replace it.
OBD.OEM.HyundaiKia: Hyundai Motor Group (Hyundai / Kia / Genesis)
on the hmg.json + dtc-hmg.json catalogues. Session negotiator
picks a 1500 ms tester-present cadence — GDS / KDS keep the
extended session alive at that rate. Seed-key starter is the
community 'HMC' XOR-mask. DecodeDID surfaces VIN, hmg_rom_id
(F193), hmg_calibration_id (F1A0), hmg_vehicle_option_code (F1B0).

OBD.OEM.Toyota: Toyota Motor Corporation (Toyota + Lexus +
Daihatsu — Subaru is its own extension). Seed-key starter is the
textbook KWP2000 two's-complement accepted by pre-2010
2AZ-FE / 2GR-FE controllers. DecodeDID surfaces VIN and
toyota_calibration_id (F1A0).

OBD.OEM.Honda: Honda Motor Co. (incl. Acura). Seed-key starter
is the community 'SHMO' XOR-mask used by pre-2010 PCMs.
DecodeDID surfaces VIN, honda_chassis_code (F1A0),
honda_factory_code (F1A2).

OBD.OEM.Mazda: Mazda Motor Corporation. Pre-2014 PCMs (Ford
Visteon lineage) accept the KWP2000 two's-complement; modern
M-MDS uses NDA crypto. DecodeDID surfaces VIN,
mazda_as_built_code (F1A0), mazda_market_code (F1B0).

All four register a Level 1 seed-key starter per the
every-extension policy so TOBDSecurityAccessClient.Unlock has
something to call against the defaults; production callers
register their NDA implementations via RegisterAlgorithm.
- OBD.OEM.Nissan: Nissan / Infiniti / Datsun. KWP2000 starter.
  DecodeDID surfaces VIN, nissan_chassis_code (F1A1),
  nissan_market_code (F1B0).
- OBD.OEM.Mitsubishi: Mitsubishi Motors Corp. KWP2000 starter.
  DecodeDID surfaces VIN, mitsu_chassis_code (F1A0),
  mitsu_market_code (F1B0).
- OBD.OEM.Subaru: Subaru Corporation. Seed-key starter is the
  SSM-community byte-rotate ('SB' mask, rotate 4) accepted by
  pre-2012 ECUs documented in OpenECU. DecodeDID surfaces VIN,
  subaru_chassis_code (F1A0).
- OBD.OEM.Suzuki: Suzuki Motor Corp. (incl. Maruti Suzuki).
  KWP2000 starter. DecodeDID surfaces VIN, suzuki_chassis_code
  (F1A0).
- OBD.OEM.Isuzu: Isuzu Motors Ltd. KWP2000 starter (GM Powertrain
  Diesel Engineering lineage on pre-2015 EFI-Live / Tech 2).
  DecodeDID surfaces VIN, isuzu_model_code (F1A0),
  isuzu_engine_code (F1A2).

All five register a Level 1 starter; production callers replace
via RegisterAlgorithm.
- OBD.OEM.Renault: Renault Group (Renault / Dacia / Alpine).
  Seed-key starter is the CLIP-community 'RNLT' XOR-mask for
  pre-2014 EMS modules. DecodeDID surfaces VIN,
  renault_calibration_id (F1A0), renault_market_code (F1A2).
- OBD.OEM.Dacia: Automobile Dacia (Renault Group). Inherits the
  Renault-lineage 'RNLT' XOR-mask starter so the registry is
  non-empty by default. DecodeDID surfaces VIN + the dacia_meta
  block (F1A0/A2/A4/A6).
- OBD.OEM.Volvo: Volvo Cars (Geely). Session negotiator picks a
  5000 ms tester-present cadence (VIDA / DiCE keep-alive rate).
  Seed-key starter is the KWP2000 two's-complement for pre-SPA2
  ECUs. DecodeDID surfaces VIN, volvo_struct_week (F1A0),
  volvo_factory_code (F1A2), volvo_pno_code (F1B0).
- OBD.OEM.Ferrari: Ferrari N.V. KWP2000 starter for pre-2015
  SDC ECUs. DecodeDID surfaces VIN, ferrari_model_code (F1A0),
  ferrari_paint_code (F1A2), ferrari_options_block (F1A4),
  ferrari_assembly_data (F1A6), ferrari_warranty_block (F1B0).
- OBD.OEM.McLaren: McLaren Automotive. KWP2000 starter; in-house
  MSO toolchain ships the real algorithm. DecodeDID surfaces
  VIN + mcl_meta block (F1A0/A2/A4/A6).
- OBD.OEM.Porsche: Dr. Ing. h.c. F. Porsche AG. KWP2000 starter;
  PIWIS uses proprietary algorithms. DecodeDID surfaces VIN,
  porsche_model_code (F1A0), porsche_paint_code (F1A2),
  porsche_options_block (F1A4).

All three register a Level 1 seed-key starter; production
callers replace via RegisterAlgorithm.
All five register a Level 1 KWP2000 two's-complement starter so
TOBDSecurityAccessClient.Unlock has something to call against
defaults; production callers replace via RegisterAlgorithm.

- OBD.OEM.BYD: BYD Auto Co. Ltd. DecodeDID surfaces VIN,
  byd_model_code (F1A0), byd_battery_pack_id (F1A2).
- OBD.OEM.Geely: Geely Auto / Lynk & Co. DecodeDID surfaces VIN,
  geely_platform_code (F1A0), geely_market_code (F1A2).
- OBD.OEM.GreatWall: Great Wall Motor (Haval / WEY / ORA / Tank /
  Poer). DecodeDID surfaces VIN, gwm_brand_code (F1A0),
  gwm_platform_code (F1A2).
- OBD.OEM.NIO: NIO Inc. DecodeDID surfaces VIN, nio_model_code
  (F1A0), nio_battery_swap_id (F1A2).
- OBD.OEM.Xpeng: Xpeng Motors. DecodeDID surfaces VIN,
  xpeng_model_code (F1A0), xpeng_xpilot_version (F1A2).
All three register a Level 1 KWP2000 two's-complement starter;
production callers replace via RegisterAlgorithm.

- OBD.OEM.Tata: Tata Motors Ltd. DecodeDID surfaces VIN,
  tata_model_code (F1A0), tata_variant_code (F1A2),
  tata_engine_code (F1A4).
- OBD.OEM.Mahindra: Mahindra & Mahindra Ltd. DecodeDID surfaces
  VIN, mahindra_model_code (F1A0), mahindra_variant_code (F1A2),
  mahindra_engine_code (F1A4).
- OBD.OEM.Lada: AvtoVAZ. Granta / Vesta / Niva Legend / Niva
  Travel / Largus on the Bosch ME17.9.7 / EDC17 lineage.
  DecodeDID surfaces VIN + the lada_meta block (F1A0/A2/A4/A6).
… Scania + VolvoTrucks)

All seven extend TOBDOEMExtensionBase + import OBD.OEM.HD for
the J1939 source-address constants. All register a Level 1
KWP2000 two's-complement starter; production callers replace via
RegisterAlgorithm.

- OBD.OEM.Cummins: engine-OEM (Ram HD, Iveco / IH commercials,
  marine / industrial gensets). DecodeDID: F1A0 engine_serial,
  F1A1 calibration_id. No VIN — engines are not whole vehicles.
- OBD.OEM.DetroitDiesel: engine-OEM (Daimler Truck — Freightliner /
  Western Star, gensets). DecodeDID: F1A0 engine_serial, F1A1
  calibration_id, F1A2 emissions_family.
- OBD.OEM.Iveco: Iveco Group (Daily / Eurocargo / S-Way / Iveco Bus
  / Iveco Defence). DecodeDID: VIN, F1A0 model_code, F1A2
  emissions_pkg.
- OBD.OEM.MAN: MAN Truck & Bus (Traton). DecodeDID: VIN, F1A0
  chassis_code, F1A2 engine_serial.
- OBD.OEM.PACCAR: PACCAR Inc. (Peterbilt / Kenworth / DAF /
  Leyland). DecodeDID: VIN, F1A0 chassis_code, F1A2 factory_code.
- OBD.OEM.Scania: Scania AB (Traton). DecodeDID: VIN, F1A0
  chassis_number, F1A2 specification_code, F1A4 engine_serial.
- OBD.OEM.VolvoTrucks: Volvo Group commercial (Volvo Trucks / Mack
  / Renault Trucks — distinct from Volvo Cars / Polestar passenger).
  DecodeDID: VIN, F1A0 chassis_code, F1A2 emissions_pkg.
…wersports)

Each unit is a thin registration shell on top of an abstract
base class. Brand subclasses only declare a JSON filename + the
identity strings; the base wires the catalogue load, extended-
catalogue merge, ISO 15031-6 + brand-specific DTC overlay, and
a Level 1 KWP2000 two's-complement seed-key starter so
TOBDSecurityAccessClient.Unlock has something to call against
the defaults. Catalogue files live in
catalogs/<class>/<oem>.json and are resolved through the
standard ResolveCatalogPath subdirectory search.

- OBD.OEM.Agricultural (8): JohnDeere, CNH, CaterpillarAgri,
  Komatsu, Kubota, AGCO, Claas, VolvoCE.
- OBD.OEM.Marine (6): MercuryMarine, VolvoPenta, YanmarMarine,
  MTU, CumminsMarine, YamahaMarine.
- OBD.OEM.Motorcycles (14): Ducati, HarleyDavidson, Triumph,
  BmwMotorrad, KTM, YamahaMoto, HondaMoto, Kawasaki, SuzukiMoto,
  IndianMotorcycle, RoyalEnfield, MVAgusta, Aprilia,
  HusqvarnaMoto.
- OBD.OEM.Powersports (5): Polaris, CanAmBrp, ArcticCat,
  YamahaWaverunner, KawasakiJet.

That closes the 38 car + 7 HD + 4 speciality vendor port plan.
Closes out the runtime tooling I had on hold during the vendor
port.

OBD.OEM.ServiceRoutines: workshop service-routine registry
loaded from catalogs/service-routines.json. Exposes lookup by
key / category / OEM applicability, plus BuildRoutineControlFrame
to render the on-wire 31 SF RID-hi RID-lo OptionRecord bytes.

OBD.UDS.NRC: ISO 14229-1 negative-response-code catalogue loaded
from catalogs/uds-nrc.json. DescribeNRC returns a typed record
(with category) and falls back to a synthetic entry for unknown
codes; FormatNRC renders a log line; IsTransientNRC classifies
the busy / repeat-request / response-pending / temporarily-
unavailable codes as retry-friendly.

OBD.OEM.SessionHelper: one-call wrapper around service-routine
execution. Opens session, consults voltage gate when the routine
demands it (srsBatteryMin12V5), sends RoutineControl, optionally
reads result, always attempts to close. Voltage source is a
TOBDVoltageSourceFunc matching OBD.Flash.VoltageGate so a host
can reuse one function across both.

OBD.OEM.DiagSession: stateful diagnostic-session wrapper around
TOBDProtocol + IOBDOEMExtension. Owns the tester-present
heartbeat thread, delegates SecurityAccess to the new
TOBDSecurityAccessClient, and provides ReadDID (raw + decoded),
StartRoutine / StopRoutine / RequestRoutineResults helpers.
Last-error reporting via the LastError property; never raises
from runtime methods.

OBD.Async: lightweight futures + cancellation tokens
(IOBDFuture<T> / IOBDPromise<T> / IOBDCancellationToken). The
producer creates a promise, the consumer gets the future facet
and Awaits / polls / attaches OnComplete. Shared tokens
propagate a single Cancel call to every in-flight observer.

OBD.OEM.UdsClient: high-level catalogue-driven UDS client. Given
a TOBDOEMJSONCatalog + an IOBDDiagnosticTransport it exposes
ReadDID(name) → typed value, WriteAdaptation with min/max
clamp, ExecuteRoutine, ReadCodingBlock + WriteCodingBlock with
bit-level pack/unpack and read-modify-write safety,
RunActuatorTest with a safety-warning gate, ReadDtcs(statusMask)
and StreamLivePIDs polling.

OBD.OEM.UdsClient.Async: future-based facade over IOBDUdsClient.
Single-worker serialisation per session; cancellation drops
the result before observation rather than interrupting the
underlying SendReceive (UDS has no out-of-band cancel).

OBD.OEM.Captures: v2-native rewrite of the capture validator.
Walks an .obdlog as TArray<TOBDLogEntry> (the recorder produces
these directly), pairs Frame → next Response / NRC by ServiceID,
and runs each 22 hi lo pair through the OEM extension's
DecodeDID — reports which DIDs were catalogued, which decoded
cleanly, which fell back to a hex dump.
Five focused fixtures covering the units shipped in the
remaining-infra commit.

Tests.OBD.OEM.ServiceRoutines: BuildRoutineControlFrame
encodes Start / Stop / RequestResults sub-functions, appends
option-record bytes after the RID, and rejects invalid
sub-functions with EOBDServiceRoutine.

Tests.OBD.UDS.NRC: DescribeNRC falls back to a synthetic
record (Category=nrcReserved) for uncatalogued codes; FormatNRC
renders the documented one-line form; IsTransientNRC classifies
0x21 / 0x22 / 0x78 / 0x94 as retry-friendly and rejects the
common non-transient codes.

Tests.OBD.OEM.SessionHelper: drives RunServiceRoutine via lambda
callbacks. Covers the happy path, OpenSession refusal,
StartRoutine refusal, voltage-gate failure when reading below
threshold, voltage-gate pass at or above threshold, and the
missing-required-callback raise.

Tests.OBD.Async: SetResult + Await returns the value;
SetError raises through Await; cancellation via shared token
raises EOBDOperationCancelled; an unsettled future times out
with EOBDFutureTimeout; OnComplete attached pre- and
post-settlement both fire as documented.

Tests.OBD.OEM.Captures: synthesises TArray<TOBDLogEntry> and
verifies ExtractCapturePairs strips the SID + DID echo on a
0x22 reply, marks NRC entries IsNegative, flushes a hanging
Frame without a response, and ValidateAgainstExtension reports
catalogued DID names + skips decode for non-22 service IDs.

Skipped: TOBDSecurityAccessClient + TOBDDiagSession need a
mock TOBDProtocol with a working adapter (heavyweight); the
underlying primitives (SeedKey algorithms, RoutineControl
frame helpers, NRC catalogue, plan runner) are covered
elsewhere. UdsClient tests require an OEM JSON catalogue
fixture; the unit's transport-abstraction shape means hosts
exercise it directly with their mock transport in production.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants