diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 95508da..a8e4257 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Python Development", - "image": "mcr.microsoft.com/devcontainers/python:3", + "image": "ghcr.io/obdb/devcontainer:latest", "customizations": { "vscode": { "extensions": [ @@ -8,7 +8,8 @@ "ms-python.vscode-pylance", "GitHub.vscode-github-actions", "GitHub.vscode-pull-request-github", - "ClutchEngineering.obdb-tooling" + "ClutchEngineering.obdb-tooling", + "anthropic.claude-code" ], "settings": { "task.quickOpen.skip": true, @@ -23,7 +24,6 @@ } } }, - "postCreateCommand": "pip install pytest pyyaml pytest-xdist", - "postAttachCommand": "cd tests/schemas && git pull && cd ../.. || git clone --depth=1 https://github.com/OBDb/.schemas.git tests/schemas", + "postAttachCommand": "/usr/local/bin/setup-obdb-dev.sh", "remoteUser": "vscode" -} +} \ No newline at end of file diff --git a/.github/workflows/presubmits.yml b/.github/workflows/presubmits.yml index 80d1b54..9732469 100644 --- a/.github/workflows/presubmits.yml +++ b/.github/workflows/presubmits.yml @@ -13,17 +13,14 @@ jobs: validate-signals: runs-on: ubuntu-latest steps: - # Step 1: Check out the current repository - uses: actions/checkout@v4 - # Step 2: Check out the central schema repository - name: Checkout schemas repo uses: actions/checkout@v4 with: repository: OBDb/.schemas path: .schemas - # Step 3: Validate with schema from the central repository - name: Validate signalsets id: json-yaml-validate-v3 uses: GrantBirki/json-yaml-validate@v2.7.1 diff --git a/.github/workflows/response_tests.yml b/.github/workflows/response_tests.yml index ac8e45f..5aea0ea 100644 --- a/.github/workflows/response_tests.yml +++ b/.github/workflows/response_tests.yml @@ -8,39 +8,58 @@ on: permissions: contents: read + pull-requests: write jobs: test: runs-on: ubuntu-latest steps: - - name: Checkout main repository + - name: Checkout repository uses: actions/checkout@v4 - - name: Checkout schemas repo - uses: actions/checkout@v4 + - name: Run tests in devcontainer + id: test + uses: devcontainers/ci@v0.3 with: - repository: OBDb/.schemas - path: tests/schemas + imageName: ghcr.io/obdb/devcontainer + push: never + env: | + GITHUB_SERVER_URL=${{ github.server_url }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SHA=${{ github.sha }} + GITHUB_RUN_ID=${{ github.run_id }} + runCmd: | + pytest tests/ --ignore=tests/schemas -xvs -n auto > ./test-output.txt 2>&1 + TEST_EXIT=$? + cat ./test-output.txt - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.13' - cache: 'pip' + # Format results for PR comment with all required arguments + /usr/local/bin/format-test-results.sh \ + test-output.txt \ + $TEST_EXIT \ + "$GITHUB_SERVER_URL" \ + "$GITHUB_REPOSITORY" \ + "$GITHUB_SHA" \ + "$GITHUB_RUN_ID" \ + > ./test-results.md || true - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r tests/schemas/requirements.txt + # Exit with the actual test result + exit $TEST_EXIT - - name: Run tests - id: run_tests + - name: Set test results output + if: always() && github.event_name == 'pull_request' run: | - pytest tests/ --ignore=tests/schemas -xvs -n auto - continue-on-error: true + if [ -f test-results.md ]; then + { + echo 'TEST_RESULTS<> $GITHUB_ENV + fi - - name: Print fix instructions - if: steps.run_tests.outcome == 'failure' - run: | - echo "::notice title=How to fix failed tests::To fix the failed tests, run: python3 tests/update_yaml_tests.py --verbose" - exit 1 + - name: Comment test results on PR + if: always() && github.event_name == 'pull_request' + uses: thollander/actions-comment-pull-request@v2 + with: + message: ${{ env.TEST_RESULTS }} + comment_tag: response-tests diff --git a/.gitignore b/.gitignore index 3aea39a..17717bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ tests/schemas/ __pycache__ +.claude/skills +.claude/settings.local.json +tests/*.py +.vscode/*.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index da11add..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "python.testing.pytestArgs": [ - "tests", - "-xvs", - "-n", - "auto" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "files.trimTrailingWhitespace": true -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 3bbe224..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Format Signals", - "type": "shell", - "command": "find signalsets/v3 -type f -exec python3 tests/schemas/cli.py '{}' --output '{}' \\;", - "problemMatcher": [], - "group": "test" - }, - { - "label": "Update YAML Tests", - "type": "shell", - "command": "python3 ${workspaceFolder}/tests/update_yaml_tests.py ${input:testUpdateOptions}", - "problemMatcher": [], - "presentation": { - "reveal": "always", - "panel": "new" - }, - "group": "test" - } - ], - "inputs": [ - { - "id": "testUpdateOptions", - "type": "pickString", - "description": "Choose test update options", - "options": [ - "--dry-run --verbose", - "--verbose", - "--year 2022 --verbose", - "--year 2023 --verbose", - "--year 2024 --verbose", - "--year 2025 --verbose" - ], - "default": "--dry-run --verbose" - } - ] -} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..da91ee7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,234 @@ +# Land Rover LR4 (L319) OBDb Signalset + +This repo defines the OBD-II/UDS signalset for a 2016 Land Rover LR4 / +Discovery 4 (L319) — 3.0L supercharged V6 (AJ126), ZF 8HP70, full-time 4WD, +electronic air suspension. It follows the [OBDb](https://github.com/OBDb) +v3 signalset format and is consumed by the Sidecar/Pelican OBD-II app. + +For signal-level detail (what each DID means, how to read boost, altitude +behavior, off-road usage, module identification) and the ongoing findings +log, read `README.md` — it is the primary reference and this file does not +repeat it. + +## Layout + +- `signalsets/v3/default.json` — the signalset itself, 91 commands. The + only file the app actually consumes. +- `tests/test_cases/2016/command_support.yaml` — manifest of which commands + each ECU supports, used by the test suite. +- `README.md` — signal reference, polling budget, and the findings log. + +## Architecture facts + +- The vehicle is **11-bit CAN** throughout. Every module answers on a + `0x7xx` request header with its response at `request + 8` (e.g. `7E0` → + `7E8`). No 29-bit/extended addressing anywhere on this truck. +- **The `F4xx` convention.** `22 F4` returns standard OBD-II PID `01` + with standard scaling, as a UDS request instead of a mode-01 request. This + exists because Pelican consumes standard mode-01 PIDs internally and they + never surface as recordable signals in the app, whereas the same data + requested via the `F4xx` alias arrives as a custom signal and does show + up. Most of the signalset uses this trick rather than requesting the + standard PID directly. +- **Per-ECU DID offsets.** The engine's proprietary DIDs sit `+0x300` from + their base (e.g. `00F2` → `03F2`). The transmission and rear differential + sit `+0x1000` (e.g. `0E69` → `1E69`). A DID that comes back "out of range" + is often just addressed at the wrong offset for that module. +- **`freq` is an interval in seconds, not a rate.** A lower number polls + harder. Total demand is `sum(1/freq)` across all commands and must stay + under **4.2 req/s**, which is measured, not assumed. The adapter delivers + about 13.4 req/s in total, but the app spends roughly 3.3 of that on AT + commands and 5.9 on its own internal mode-01 polling, which never + surfaces as a recordable signal. Signalset traffic gets what is left. + Measured across seven sessions, the UDS service-22 share stayed between + 4.1 and 5.1 req/s no matter how big the signalset was: one session polled + 108 distinct DIDs at 4.19 req/s and another polled 23 at 4.22 req/s. + Current demand is 4.083. Asking for more than the app can deliver does + not slow everything down evenly — see trap 5. + +## Validation + +CI (`.github/workflows/presubmits.yml`) validates `signalsets/v3/` against +the schema in the separate `OBDb/.schemas` repo, which is not checked into +this repo. To validate locally: + +``` +curl -sfL https://raw.githubusercontent.com/OBDb/.schemas/main/signals.json -o /tmp/signals.schema.json +python3 -c " +import json, jsonschema +schema=json.load(open('/tmp/signals.schema.json')) +inst=json.load(open('signalsets/v3/default.json')) +errs=list(jsonschema.Draft7Validator(schema).iter_errors(inst)) +print('errors:', len(errs)) +for e in errs[:5]: print(list(e.path), e.message[:200]) +" +``` + +`path` is constrained by the schema to a fixed enum (`Engine`, `Fuel`, +`Drivetrain`, `Suspension`, `Transmission`, `Trips`, `ECU`, `Electrical`, +`Tires`, `Battery`, `Climate`, `Movement`, and a few others) — `Diagnostics` +is not a valid value; use `ECU` for undecoded/diagnostic raw signals. + +Print total polling demand: + +``` +python3 -c " +import json +d=json.load(open('signalsets/v3/default.json')) +print(sum(1/c['freq'] for c in d['commands'])) +" +``` + +Check that the test manifest YAML parses and that no entry silently became +a float or other non-string type (see trap 1 below): + +``` +python3 -c " +import yaml +d=yaml.safe_load(open('tests/test_cases/2016/command_support.yaml')) +for ecu, entries in d['supported_commands_by_ecu'].items(): + for e in entries: + if not isinstance(e, str): + print('NON-STRING ENTRY:', ecu, repr(e)) +print('ok') +" +``` + +The pytest workflow (`.github/workflows/response_tests.yml`) runs inside the +`ghcr.io/obdb/devcontainer` image, and the test sources it runs live inside +that image, not in this checkout. Running `pytest tests/` locally will +report "no tests ran" — that is expected, not a failure. There is no way to +run the actual response tests outside CI. + +## Traps + +These have all actually bitten: + +1. **YAML float trap.** In `command_support.yaml`, a plain-scalar entry made + up only of digits and dots (e.g. `751.222076`) parses as a YAML float, + not a string, unless it's quoted. Any entry with no hex letter A–F + anywhere in it must be double-quoted. After editing this file, verify no + entry parsed as a non-string (see the validation command above). +2. **Deleting a command can orphan a synthetic.** The top-level `synthetics` + block in `default.json` references signal ids by name (e.g. + `LR4_BOOST_RATIO` reads `LR4_MAP` and `LR4_BARO`). Removing the command + that defines one of those signal ids leaves a dangling reference that is + still valid JSON but semantically broken — the synthetic will never + compute. Grep `synthetics` for a signal id before deleting the command + that defines it. +3. **Entry shape differs per ECU in `command_support.yaml`.** Most ECUs use + `.22` (e.g. `795.221E8A`). `7E1` in the `supported_commands_by_ecu` + section uses `..22` (e.g. `7E1.7E9.221E69`). There is no + reason for the difference — `7E1`'s `rax` is `7E9`, exactly `hdr+8`, the + same as everywhere else. It is just how that block was written. Match + whatever shape the surrounding block already uses; don't normalize + across ECUs. +4. **Agent worktrees branch from `main`, not from the current branch.** Work + that depends on unmerged commits on the current branch must be done in + the main checkout, or the agent must be told explicitly to check out that + branch — otherwise it silently operates on stale files and produces + confident, wrong results. +5. **The app settles on a working set of roughly twenty commands and + ignores the rest, however big this file is.** Taking the longest session + of each day since 2026-09-05, coverage has been 12 to 23 distinct DIDs + every time, including the 137-minute drive on 09-20 that polled 23 — the + widest of any long session in the period. Coverage spikes right after + the signalset changes (108 DIDs on the morning of 09-20, 965 during a + PID detector run) and then settles back within hours. + + The stable core is exactly the metric-carrying commands. Thirteen + commands appear in every long session since 09-05, and they are + precisely thirteen of the sixteen that carried a `suggestedMetric` at + the time. Read with trap 8, the rule is that **the app converges on + polling what it will store.** Modules with no metric-bearing command on + them — `7D3`, `792`, `732`, `726`, `761` — drift out of the rotation + entirely. + + **A command's presence in this file does not mean it is being + collected.** Check the scan logs before relying on one. But do not read + a quiet module as a broken or "retired" one: an earlier version of this + trap claimed the app permanently retires ECUs and told the owner to + reset the app's vehicle profile. That was wrong. It came from reading + one day's sessions without the months behind them, and coverage + recovers on its own every time the file changes. + + A corollary for pruning: a quiet command is not a dead one. Nineteen + commands were deleted on 2026-09-20 for being constant and restored the + same day, because most had between 6 and 30 samples and none of those + samples covered the event the command would report — `1E88`'s six were + all taken with the differential unlocked. A probe costs 0.0017 req/s at + `freq` 600. Deleting it costs the answer. Park it, don't cull it. + + The restored probes settled this within hours. `3B4D` — the one deletion + called conclusive, flat `0x00` across 236 samples — returned `0x04`, + `0x01` and `0x00` within eight minutes of the off-road features actually + being used. Six of the eleven `792` `2A3x` DIDs turned out to be + advancing counters rather than constants. + +6. **Standard mode-01 PIDs are already being polled by the app, constantly, + and a proprietary DID may duplicate one.** Before adding a signal, check + whether the truck already answers it somewhere cheaper, and check whether + two DIDs you both poll are the same measurement. Three duplicates have + been found this way by correlating logged samples: PID 67 sensor 1 equals + the engine coolant PID to 0.0 C over 135 pairs; PID 70 channel A equals + manifold pressure to 1.3 kPa over 525 pairs; and the proprietary oil + temperature DID equals standard PID 5C to 0.7 C over 1,135 pairs once + warm. Correlate against the scan logs before believing a new DID is new. + +7. **Synthetic signals support only `op: ratio`, a plain a/b with no + constant** — but `fmt.div` on an ordinary signal is a `number`, not an + integer, so a scaling constant can be folded into a hidden operand and + the ratio then comes out in real units. `LR4_FUEL_RATE` is built that + way: `LR4_FUEL_DIVISOR` is commanded lambda premultiplied by + 14.7 x 745 / 3600 x 3.785, so MAF divided by it is US gallons per hour. + Two operands is also the ceiling: anything needing three (speed, MAF + and lambda for an exact mpg) has to drop one, which is why the mpg + synthetics assume lambda 1. Whether a synthetic may read another + synthetic is untested; `LR4_MPG_LAMBDA` exists to find out. + +8. **Only signals carrying a `suggestedMetric` are ever written to the + app's signal database, but every signal displays live.** Screenshots on + 2026-09-20 confirm non-metric and `hidden: true` signals alike appear + with current values in the app's section lists — so `hidden` does not + suppress anything, and bandwidth spent on a metric-less signal still + buys a readout. What it does not buy is history. Measured over fifteen months of backups: the + store has held exactly 15 distinct signals, and they are precisely the + ones with a metric. Everything else is requested, answered, decoded and + discarded. During a 137-minute drive on 2026-09-20 the 14 signals + recorded were exactly the metric-carrying signals on commands that were + being polled; the two metric-carrying signals that were missing + (`engineSpeed`, `absoluteEngineLoad`) were missing because their + commands were not polled at all. The other two stores in the backup are + not alternatives: `records` holds manual service and fuel entries, and + `tripLogger` holds the phone's own GPS trace. + + The metric enum has 36 values and no slot for manifold pressure, ride + height, suspension pressure, charge air temperature or anything else + specific to this truck. So most of what this signalset decodes can be + watched live but can never be looked at historically except by reading + the scan logs. Weigh that before spending budget on a signal: a fast + `freq` on a signal with no metric buys a live readout and nothing else. + +9. **Nothing in the signalset controls how many decimal places the app + shows.** The schema's `fmt` has scaling, range, unit and map fields + and no precision field, and the synthetics block has even less. The + app decides: integer-scaled signals print as integers, a Celsius + signal shown in Fahrenheit picks up one decimal from the conversion, + and a synthetic ratio prints its full float. There is no `div`, `max` + or unit trick that changes this; it is a feature request for the app. + +## Where the data comes from + +On macOS, the Sidecar/Pelican app writes zipped SQLite backups to +`~/Library/Mobile Documents/iCloud~com~featherless~apps~electricsidecar/DataBackups/`, +in four folders: + +- `scanSessions` — every raw request/response pair (large, on the order of + a gigabyte or more). This is the ground truth for whether a command + answers and what it actually returns. +- `signals` — decoded signal values. +- `tripLogger` — trip-level data. +- `records` — other recorded data. + +The backups lag live data by up to a day, so a command that looks dead in +the backups may have started responding again since the last sync. diff --git a/README.md b/README.md index 3553b0c..40c96d0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,1328 @@ -# Model make +# Land Rover LR4 (L319) — Signal Reference +OBDb signalset for the 2016 Land Rover LR4 / Discovery 4 — 3.0L supercharged +V6 (AJ126), ZF 8HP70, full-time 4WD with locking rear differential and +electronic air suspension. + +Signals here were confirmed against ~13M logged request/response pairs from +this vehicle (VIN `SALAK2V62GA832217`). Anything still named `*_RAW` responds +on the bus but hasn't been decoded yet. + +--- + +## Modules + +Thirteen modules answer on this truck. Six have never been mined — they +reject unknown DIDs with a negative response rather than staying silent, +which is how we know they're there. Requests go to `hdr`, replies come back +from `rax`. + +| Request | Response | Module | +|---|---|---| +| `7E0` | `7E8` | Engine (ECM) | +| `7E1` | `7E9` | Transmission (TCM) | +| `716` | `71E` | Unmined | +| `726` | `72E` | Unmined — returns the VIN | +| `732` | `73A` | Gear selector | +| `734` | `73C` | Headlamp control module (HCM) | +| `737` | `73F` | Unmined | +| `760` | `768` | ABS / brake control module (probable) | +| `761` | `769` | Body / instrument | +| `792` | `79A` | Unidentified — answers `2A3x` | +| `795` | `79D` | Rear differential | +| `797` | `79F` | Unmined | +| `7D3` | `7DB` | Air suspension (EAS) | + +Two addressing quirks are worth knowing: + +**Per-ECU DID offsets.** The engine's proprietary DIDs sit `+0x300` from +their base (`00F2` → `03F2`), while the transmission and differential use +`+0x1000` (`0E69` → `1E69`). A DID that returns "out of range" is often just +sitting at the other offset. + +**`22F4xx` aliases.** Any standard OBD PID `01XX` is also readable as +`22F4XX` — `010C` (RPM) is the same data as `22F40C`. This matters because +Pelican handles standard PIDs internally, so a signalset entry for `012F` +never surfaces in the app. The `F4xx` alias arrives as a custom signal and +shows up normally. Most of this file uses that trick. + +### Module identification (2026-09-19) + +A UDS identification probe (`22F18C` ECU serial, `22F191` ECU part number) +was run against all six previously unmined modules: + +| Module | `F18C` serial | `F191` part number | +|---|---|---| +| `716` | `0000000004757126` | rejected (NRC 31) | +| `726` | `5224293808` | rejected (NRC 31) | +| `734` | `0080-508580` | `EX53-14C243-AB` | +| `737` | `Q117eeA3516` | rejected (NRC 31) | +| `760` | `1716263MO0507` | `CH32-14C227-AB` | +| `797` | `0-00002C` | rejected (NRC 31) | + +`734`'s part number is a Hella `14C243` headlamp levelling / adaptive front +lighting controller, the same base number used across Jaguar Land Rover +platforms of this era (it also shows up as `8W83-14C243-AA` on the Jaguar +XK) — identified as the Headlamp control module (HCM). `760`'s part number +matches the ABS/brake module in a published Discovery 4 (L319) ECU scan +report; that identification rests on a single external scan report, so +treat it as probable rather than confirmed. The other four modules didn't +resolve a part number, but each returned a distinct serial, so they're +confirmed to be real, separate modules even though what they do is still +unknown. + +`22F187` was also tried against all six and came back NRC 31 (request out +of range) everywhere except `726`, whose reply was malformed and returned +VIN bytes instead of a rejection — worth a retry. + +--- + +## Engine + +| Signal | Address | Notes | +|---|---|---| +| Engine Speed | `7E0` `22F40C` | | +| Calculated Load | `7E0` `22F404` | Percent of available torque at current RPM | +| Absolute Load | `7E0` `22F443` | Percent of *peak possible* airflow — exceeds 100% under boost | +| Coolant Temp | `7E0` `22F405` | Thermostat opens ~88°C; normal running 90–105°C | +| Manifold Pressure | `7E0` `22F40B` | **Absolute**, not gauge. See boost calculation below | +| Barometric Pressure | `7E0` `22F433` | ~101 kPa at sea level, drops with altitude | +| Altitude | `7E0` `22F433` | Barometric converted to feet by lookup table | +| Intake Air Temp | `7E0` `22F40F` | Before the supercharger | +| Charge Air Temp | `7E0` `220520` | **After** the supercharger and intercooler | +| Timing Advance | `7E0` `22F40E` | Degrees before top dead centre | +| Throttle Position | `7E0` `22F411` | Actual plate position | +| Commanded Throttle | `7E0` `22F44C` | What the ECU asked for | +| Accelerator Pedal D / E | `7E0` `22F449` / `22F44A` | Two sensors in one pedal | +| Mass Air Flow | `7E0` `22F410` | Grams of air per second | +| Equivalence Ratio | `7E0` `22F444` | Commanded lambda | +| O2 Lambda B1S1 | `7E0` `22F434` | Measured lambda | +| O2 Pump Current B1S1 | `7E0` `22F434` | Second half of the same response. Current, not voltage — see "Two decodes that were wrong" | +| Catalyst Temp B1S1 / B2S1 | `7E0` `22F43C` / `22F43D` | One per bank | +| Oil Temp | `7E0` `2203F3` | Land Rover's own sensor. The SAE alias `F45C` was dropped — 1,169 samples against this one's 347,015 | +| Oil Level | `7E0` `2203E6` | Millimetres in the sump | +| Oil Volume | `7E0` `2203F2` | | +| O2 Lambda B2S1 | `7E0` `22F438` | Measured lambda, bank 2 — first bank 2 lambda ever read on this truck | +| O2 Pump Current B2S1 | `7E0` `22F438` | Second half of the same response. Current, not voltage | +| O2 Voltage B1S2 | `7E0` `22F415` | Post-catalyst sensor, bank 1 | +| Relative Throttle Position | `7E0` `22F445` | A third throttle-position reading, alongside Throttle Position and Commanded Throttle | +| Absolute Throttle Position B | `7E0` `22F447` | A fourth | +| O2 Sensors Present | `7E0` `22F413` | Bitmask of which O2 sensor positions are physically fitted | + +### Lambda + +Lambda expresses air-fuel ratio relative to perfect combustion (14.7:1 for +gasoline). + +- **1.00** — stoichiometric, the cruise target +- **below 1.00** — rich, extra fuel +- **above 1.00** — lean, extra air + +Under boost the ECU deliberately commands rich (~0.85) because extra fuel +cools the charge and suppresses knock. A value of 2.00 means deceleration +fuel cut — injectors fully off while coasting, sensor reading pure air. + +--- + +## Fuel + +| Signal | Address | Notes | +|---|---|---| +| Fuel Level | `7E0` `22F42F` | | +| Fuel Rail Pressure | `7E0` `22033E` | Proprietary; ~60 bar idle, 130+ under load. The SAE alias `F423` was dropped — 158 samples against this one's 15,910 | +| Short Term Fuel Trim B1 | `7E0` `22F406` | Immediate correction, swings constantly | +| Long Term Fuel Trim B1 | `7E0` `22F407` | Learned correction, drifts slowly | +| Short Term Fuel Trim B2 | `7E0` `22F408` | First recorded 2026-09-20 | +| Long Term Fuel Trim B2 | `7E0` `22F409` | First recorded 2026-09-20 | +| Fuel System Status B1 | `7E0` `22F403` | Enum: open loop cold / closed loop / open loop, load or decel fuel cut / open loop, system fault / closed loop with a feedback fault | +| Fuel System Status B2 | `7E0` `22F403` | Second half of the same response; not yet mapped to named states | +| Commanded Evap Purge | `7E0` `22F42E` | Percent duty cycle on the purge valve | +| O2 Trim B1S2 | `7E0` `22F415` | Second half of the same response — post-catalyst trim, bank 1 | +| Long Term Secondary O2 Trim B1 | `7E0` `22F456` | | + +--- + +## Duplicates found and removed + +Four signals were checked against another reading of the same physical +quantity, using paired samples across logged history rather than a single +drive. Two turned out to be the same measurement and were dropped; two +turned out to be genuinely separate sensors and were kept. + +**PID 67 sensor 1 is the engine coolant PID.** Mean difference 0.0°C over +135 paired samples. Deleted. Sensor 2 of the same PID is kept, renamed to +Charge Cooler Coolant Temp: it ran 11.7°C above ambient and 24°C below +charge air temperature over those same samples, which is where the AJ126's +separate low-temperature intercooler circuit belongs. A genuinely new +signal, not a rename in name only — but it carries no `suggestedMetric`, +so it will never be stored as history by the app; see "Only signals with a +metric are ever recorded" under Polling. + +**PID 70 channel A is manifold pressure, at finer resolution — not a +separate boost sensor.** This corrects "F470 was boost pressure all along," +decoded earlier the same day. PID 70's support byte reads `0x02`; three of +its four defined channels read constant zero across 525 samples and were +dropped. The fourth, channel A, tracks Manifold Pressure to within 1.3 kPa +mean over 525 pairs (r = 0.85) — the same manifold sensor, reported at 1/32 +kPa instead of 1 kPa, not a pre-throttle sensor. Kept, renamed to Manifold +Pressure (Fine), slowed to 120s since the extra resolution isn't worth a +faster poll. + +**The proprietary oil temperature DID and standard PID 5C agree to 0.7°C** +over 1,135 paired samples once warm, diverging by up to 15°C during +warm-up with PID 5C reading higher. Dropping PID 5C earlier was correct, +and it stays out. + +**PID 68 sensor 2 tracks the proprietary charge air temperature DID to +2.7°C mean** — close to redundant, but kept: sensor 1 of the same PID is +the pre-supercharger inlet, running 9.2°C above ambient, and is a real +second reading the truck doesn't report anywhere else. Both kept at 120s +with descriptions. + +--- + +## Drivetrain + +| Signal | Address | Notes | +|---|---|---| +| Gear Selector | `732` `22D928` | 0 Park · 1 Reverse · 2 Neutral · 3 Drive · 7 Sport | +| Gearbox Temp | `7E1` `221E69` | ZF 8HP70 sump | +| Locking Diff Oil Temp | `795` `221E8A` | | + +--- + +## Suspension + +All four corners report gauge pressure in the air spring. + +| Signal | Address | +|---|---| +| Pressure Front Left / Right | `7D3` `223B04` / `223B03` | +| Pressure Rear Left / Right | `7D3` `223B06` / `223B05` | +| Height Offset | `7D3` `222B12` | Signed, millimetres from nominal | +| Compressor Activity | `7D3` `223B07` | ~110 at rest, over 1,300 while the truck raises | +| Ride Height Mode | `7D3` `223B3C` | Enum: 1 Normal, 2 Off-Road, 4 Access, 13 In Transit. Corrected 2026-09-20 — see "Air suspension notes" under Off-road | +| Height Sensor Front / Rear | `7D3` `223B71` / `223B72` | **Inverted** — falls as the truck rises | +| `D11A` Raw | `7D3` `22D11A` | Was called Module Voltage; it is not battery voltage — see "Two decodes that were wrong" | + +Normal standing pressures are roughly 40 psi per corner at normal ride +height, rising with load and with raised height modes. + +**None of this module's signals carry a `suggestedMetric`, and the app +only ever stores signals that do** — see "Only signals with a metric are +ever recorded" under Polling. Corner pressures, ride height, compressor +activity and module voltage are all live-only: even when `7D3` is being +polled, nothing from this table will ever show up in the app's own +history, only in the raw scan logs. It is also why the app drifts away +from asking `7D3` anything at all between signalset changes. + +--- + +## Reading signals together + +Individual numbers are useful. Pairs are where the diagnostics live. + +### Boost — Manifold Pressure minus Barometric Pressure + +The LR4 reports manifold pressure as *absolute*, so at idle it reads well +below atmospheric and looks wrong. Subtract barometric and you get true +boost: + +``` +boost = Manifold Pressure − Barometric Pressure +``` + +Negative is vacuum (closed throttle, coasting). Zero is atmospheric. Positive +is supercharger boost. Because barometric is a live reading, this stays +correct as you gain or lose altitude. + +### Heat-limited power — Charge Air Temp with Timing Advance + +Charge air temp is the truck's honest report of whether the intercooler is +keeping up. Watch it with timing advance: + +| Charge air temp | Timing advance | Meaning | +|---|---|---| +| Near ambient | Normal | Healthy, full power available | +| Climbing | Retarding | ECU pulling timing to prevent knock — power is fading | +| High, stable after stopping | — | Heat soak; recovers once moving | + +Timing retarding *without* a temperature rise points at fuel quality or knock +sensor activity instead. + +### Fueling health — Commanded vs Measured Lambda + +Equivalence Ratio is the target, O2 Lambda is reality. They should track +closely. A persistent gap means fueling isn't achieving its target — failing +sensor, vacuum leak, or fuel delivery shortfall. + +### Total fuel correction — Short Term plus Long Term Trim + +Add them. Their sum is how far off the base fuel map the engine is running. + +| Sum | Meaning | +|---|---| +| Within ±5% | Normal | +| Around +20% at warm idle | Adding fuel to compensate — vacuum leak or lazy MAF | +| Persistently negative | Running rich — leaking injector or high fuel pressure | + +Short term swinging while long term stays near zero is normal closed-loop +operation. Long term drifting away from zero is the engine *learning* a +problem, which is the one to act on. + +### Sensor cross-checks + +Several values are measured twice by independent paths. Agreement validates +both; disagreement localises the fault. + +| Pair | Should agree within | +|---|---| +| Accelerator Pedal D vs E | A percent or two — they're redundant by design | + +Oil Temp and Fuel Rail Pressure each had an SAE alias checked against the +proprietary signal early on (0.7°C and similar magnitude respectively); +both aliases were dropped as duplicates and are gone from this file, so +there's nothing left to cross-check them against day to day. + +Pedal D and E disagreeing is significant: the ECU compares them itself and +will enter limp mode if they diverge. + +### Bank imbalance — Catalyst Temp B1 vs B2 + +The two banks should run within ~30°C of each other. A persistent split means +one bank is working harder — a misfire, an injector, or an O2 sensor on the +cooler side. + +### Load distribution — the four suspension pressures + +Front and rear pairs should be near-symmetrical left to right. A single +corner reading consistently low is either a load imbalance or an air spring +losing pressure. Compare against Height Offset: if pressure is low but height +is nominal, the system is compensating for a slow leak. + +### Warm-up sequence — Coolant, Oil, Gearbox + +These heat at different rates, and the order tells you about circulation. +Coolant reaches operating temperature first, oil follows several minutes +later, gearbox last. Oil temp lagging far behind coolant on a long drive +suggests restricted flow. Gearbox temperature climbing above normal while +towing is the signal to back off. + +### Airflow sanity — MAF against RPM and Load + +At idle, roughly 3–8 g/s. Under full boost, ten times that or more. MAF +staying flat while load and RPM climb means the sensor is under-reporting — +which the fuel trims will then try to correct, showing up as a rising +positive trim. + +--- + +## Computed signals + +The signalset defines twelve synthetic signals. Seven are a **ratio between +two readings that should hold a known value**, which makes them suited to +a display: you learn the normal number once, and anything else is a +signal. The eighth, Fuel Rate, is a different shape — a real physical +quantity rather than a 1.0-normal ratio — and is described separately +below. (An earlier eighth ratio, Rail Pressure Crosscheck, was removed +when its SAE rail-pressure alias was dropped as a duplicate; Fuel Rate +took its slot.) + +| Signal | Ratio | Normal | Meaning when it moves | +|---|---|---|---| +| Boost Pressure Ratio | MAP / Barometric | 1.0 idle, up to ~1.8 | Above 1.0 is supercharger boost, self-correcting for altitude | +| Lambda Tracking | Measured / Commanded lambda | 1.0 | Fuelling isn't hitting its target — sensor, leak, or delivery | +| Bank Balance | Cat temp B1 / B2 | 1.0 | One bank working harder — misfire or injector on the low side | +| Pedal Agreement | Pedal D / Pedal E | 1.0 | Redundant pedal sensors disagreeing; the ECU limps if they diverge | +| Throttle Tracking | Actual / Commanded throttle | 1.0 | Plate not following orders — sticky or carbonned throttle body | +| Suspension Balance Front | Front left / right pressure | 1.0 | A corner losing air, before the dash warns | +| Suspension Balance Rear | Rear left / right pressure | 1.0 | Same, rear axle | + +All seven sit at **1.0 when healthy**, so a single glance covers fuelling, +ignition balance, pedal and throttle integrity, and air springs. + +Boost Pressure Ratio is the exception and the one to watch for fun: it's the +closest thing to a boost gauge this vehicle exposes. The schema's only +operation is division, so true gauge boost (`MAP − Barometric`) isn't +expressible as a synthetic — but the ratio carries the same information and +needs no altitude correction. + +### Fuel rate, now computed + +The ECM's own supported-PID bitmasks (`0100 = BFBFACD3`, `0120 = A007B119`, +`0140 = FED08511`, `0160 = 07010000`) say PIDs `5E` and `9D` are both +unsupported. The truck does not report fuel rate. Any app showing one is +computing it, and now this signalset does too — added 2026-09-20 in litres +per hour, switched to US gallons per hour on 2026-09-24. + +``` +Gallons per hour = MAF (g/s) / (Lambda × 14.7 × 745 / 3600 × 3.785) + = MAF / (Lambda × 11.516) +``` + +MAF is grams of air per second; 14.7 is the stoichiometric air-to-fuel +ratio for gasoline; 745 g/L is the density of gasoline; 3600 converts +seconds to hours; 3.785 L/gal converts to US gallons. Commanded lambda +scales the stoichiometric ratio to whatever the ECU is actually targeting. + +The schema's synthetic operation is a plain ratio with no constant, so the +constant is folded into a hidden operand: `LR4_FUEL_DIVISOR` reads Commanded +Equivalence Ratio (`22F444`) with `div` set to `2845.54` (`32768 / 11.516`), +which yields `Lambda × 11.516` directly. `LR4_FUEL_RATE` is Mass Air Flow +divided by that, already in gallons per hour, and carries +`suggestedMetric: fuelRate`. The unit is declared `gallonsPerHour`, one of +the schema's enum values, rather than relying on the app to convert. + +#### The other ways to look at it + +Five fuel synthetics now exist. Every one is built from the same three +polled DIDs, so none costs any bandwidth; the budget is unchanged at +4.100 req/s. + +| Signal | Formula | Unit | Lambda? | +|---|---|---|---| +| Fuel Rate | MAF / (λ × 11.516) | gal/h | yes | +| Fuel Rate (gal/min) | MAF / (λ × 690.9) | scalar, labelled | yes | +| Fuel Economy (mpg) | (mph × 11.516) / MAF | scalar, labelled | assumed 1 | +| Fuel Use (gal/100 mi) | MAF / (mph × 0.1152) | scalar, labelled | assumed 1 | +| Fuel Economy (mpg, lambda-corrected, test) | mph / Fuel Rate | scalar, labelled | yes, if it works | + +Two constraints shape that table. The unit enum has `gallonsPerHour` and +nothing else for fuel: no gallons per minute, no miles per gallon, no +gallons per hundred miles, no litres per 100 km. Those signals are declared +`scalar` and carry the unit in their name. And a ratio has room for only +two operands, so an exact mpg (speed, MAF *and* lambda) is one operand too +many. The mpg and gal/100 mi signals drop lambda and assume 1.0, which is +true whenever the ECU is in closed loop — most of any drive — and wrong +under enrichment (lambda 0.8 at full throttle, so the readout is 25% +optimistic) and during decel fuel cut (commanded lambda reads 2.0, the +real consumption is zero, the readout is finite). On the 2026-09-23 drive +the stoichiometric assumption cost 3% over the trip: 17.3 mpg against 17.8 +with lambda. + +The lambda-corrected mpg is an experiment. It divides a hidden mph copy of +vehicle speed by the Fuel Rate synthetic itself, which only works if the +app resolves a synthetic operand that is another synthetic. Nothing in the +schema says either way, and no other OBDb signalset tries it. If it shows a +number on the next drive, it is the mpg to keep and the stoichiometric one +can go. If it stays blank, the app evaluates synthetics against raw +signals only, and the stoichiometric version is as good as a ratio gets. + +Ways that were considered and do not work with what this truck offers: +injector duty or pulse width (no DID found), the SAE fuel rate PIDs `5E` +and `9D` (unsupported, above), and fuel level delta over distance. The last +one is real but coarse — the gauge is 8-bit, one step is 0.34 L, and the +trip logger already records tank level at the start and end of every +journey in its own store, so it makes a check on the formula rather than a +live signal. + +Validated twice. Integrating the formula over 1,480 MAF samples on the +2026-09-20 drive gives 11.21 L (2.96 gal) burned; the tank gauge fell from +87.8% to 76.9%, which on the 86.3 L tank is 9.48 L (2.50 gal) — agreement to +about 18%, on the pessimistic side, over 91.5 km (19.2 mpg by the +formula). On the 2026-09-23 drive, 15.2 km in 21 minutes, the formula gives +0.54 gal against 0.45 gal on the gauge, where a single gauge step is +0.09 gal. Treat all five as directionally useful, not a trip computer. + +#### Decimal places are the app's call + +The stored fuel-rate values are full floats (`44.309198338607594`), and +that is what the app prints for a synthetic. Nothing in the signalset +changes it: the schema's `fmt` block has scaling, range, unit and map +fields and no precision field, and a synthetic has fewer fields still. The +same is true of the one decimal that appears on temperatures once the app +is set to Fahrenheit: the signal is an integer in Celsius, the conversion +makes it fractional, and the app's formatter keeps one place. Both are +requests for the app (support@clutch.engineering), not for this file. + +--- + +## Altitude and mountain driving + +### Reading altitude + +There's no altitude signal, but **Barometric Pressure is one** — air thins +predictably with height. Read `22F433` directly: + +| Altitude | Barometric | In psi | +|---|---|---| +| Sea level | 101 kPa | 14.7 | +| 2,000 ft | 94 kPa | 13.7 | +| 4,000 ft | 88 kPa | 12.7 | +| 6,000 ft | 81 kPa | 11.8 | +| 8,000 ft | 75 kPa | 10.9 | +| 10,000 ft | 70 kPa | 10.1 | +| 12,000 ft | 64 kPa | 9.3 | + +Roughly 1 kPa per 300 ft near sea level, stretching to about 340 ft +per kPa above 8,000 ft. Weather shifts it a few kPa, so treat +it as approximate unless you calibrate against a known elevation. + +### Measured on this truck + +From the September 2025 mountain trip, barometric pressure ranged **65–76 kPa, +median 70** — roughly 7,800 to 11,800 ft, spending most of its time near +10,000. Those runs give real baselines to compare against: + +| Signal | Mountains, Sept 2025 | Notes | +|---|---|---| +| Charge Air Temp | 6–81°C, median 47 | Wide swing with grade and airflow | +| Gearbox Temp | 8–85°C, median 64 | Never troubling | +| Diff Temp | 0–60°C, median 35 | Cooler than a hot-weather lowland drive | +| Coolant Temp | up to 103°C | Normal ceiling on sustained climbs | +| Mass Air Flow | peak 216 g/s | Full-load airflow | + +Worth noting the differential ran *hotter* on flat August driving (median +71°C) than in the mountains (35°C). Ambient temperature matters more to it +than terrain does. + +### Fuel trim climbs with altitude + +Long term fuel trim tracks altitude clearly across ~76,000 logged samples: + +| Barometric | Altitude | Median LTFT | +|---|---|---| +| 100 kPa | ~400 ft | +8.6% | +| 80 kPa | ~6,400 ft | +7.8% | +| 75 kPa | ~7,900 ft | +12.5% | +| 70 kPa | ~9,400 ft | +14.1% | +| 65 kPa | ~10,900 ft | +14.8% | + +Altitude adding fuel trim is expected — the mass air flow sensor tends to +under-report in thin air and the ECU compensates. **The part worth watching +is the low-altitude baseline of around +8%.** A healthy engine sits within +±5%, so there's a mild lean condition present before altitude adds anything, +and the two stack: near +15% at 11,000 ft is approaching where a lean code +would set. + +Most likely cause on this engine is a small unmetered air leak — the +supercharged V6 has a lot of intake plumbing and hose joints. Not urgent, but +if the truck ever feels flat at altitude, this is the reason to look at +first. + +### What altitude does to the engine + +A naturally aspirated engine loses about 3% power per 1,000 ft. Yours loses +much less, because the supercharger is a fixed-displacement blower geared to +the crank — it keeps stuffing the same volume in regardless of ambient +pressure. What you'll see instead: + +- **Boost Pressure Ratio holds steady.** It's normalised against barometric, + so it reads the same at 10,000 ft as at sea level. That's the point of + using the ratio rather than raw manifold pressure. +- **Mass Air Flow drops** at the same throttle and RPM. Thinner air, less + mass, less fuel, less power — the loss the blower can't fully erase. +- **Fuel trims may drift positive** as the ECU adapts. Mild drift climbing a + long pass is normal, not a fault. + +### What to watch climbing + +Sustained high load is the hardest thing you'll ask of the truck. + +| Signal | Watch for | +|---|---| +| Charge Air Temp | Climbing on a long pull, with Timing Advance retarding — the ECU protecting itself, and where your power goes | +| Gearbox Temp | The one that matters. Sustained climbing in a low gear heats the ZF fast | +| Coolant Temp | Should stay near 100°C. Rising past that on a grade means the cooling system is at its limit | +| Absolute Load | Pinned near 100% for long stretches means you're asking for everything available | + +### What to watch descending + +Engine braking sends heat somewhere different. + +- **Lambda goes to 2.00** on a closed-throttle descent. That's deceleration + fuel cut, working exactly as intended — you're using no fuel at all. +- **Gearbox Temp still climbs** in a low gear, even off-throttle. +- **Coolant Temp can rise** on a long descent despite low load, because + airflow is low and the engine is being driven by the wheels. + +--- + +## Off-road + +### The mountain drive that tested off-roading collected no off-road data + +Session 2525 (2026-09-20, 15:14–17:32, 137.5 minutes, 91.5 km / 56.9 mi at +7,800–11,000 ft) deliberately included light off-roading to exercise the +suspension and rear differential. It produced none of that data. Every +signal the test was aimed at lives on a module the app had already stopped +addressing before the drive started: ride height, height sensors, corner +pressures, compressor activity and Terrain Response all live on `7D3`, +which the app had not addressed since 14:07 that day. The two +rear-differential-lock candidates lived on `795`, which the app was still +addressing, but neither DID was among the 23 commands it polled that +session. + +This is not damage and it is not new. Long sessions have settled to +between 12 and 23 commands for six weeks, and this drive's 23 was the +widest coverage of any long session in that period — see "The app +converges on a small working set" under Polling. The suspension module +carries no metric-bearing command, so the app has no reason to keep asking +it anything once the novelty of a signalset change wears off. + +Everything below about air suspension and the rear differential was +recovered from earlier sessions already present in the same log file, not +from this drive. + +The deeper constraint is separate and permanent: none of the suspension +signals carry a `suggestedMetric`, so none of them will ever be recorded as +*history* by the app — see "Only signals with a metric are ever recorded" +under Polling. Getting `7D3` polled again restores live readouts and raw +scan-log coverage, not a trip history for ride height or corner pressure. + +### Air suspension + +The four corner pressures are the most useful thing you have off-road. On +uneven ground the truck articulates, and pressures diverge as weight shifts. + +- **Suspension Balance Front / Rear** — the two synthetic ratios. At 1.0 the + axle is evenly loaded. Away from 1.0, weight has moved to one side. +- A corner going very low while its opposite goes high means that wheel is + unloading — the first hint of lifting a wheel. +- **Height Offset** shows where you are relative to normal ride height. + +Back on pavement both balance ratios should settle at 1.0. If one doesn't, +you've either shifted your load or picked up a slow leak. + +### Watch on the trail + +| Signal | Why | +|---|---| +| Gearbox Temp | Low-range crawling generates heat with almost no airflow. The number to respect | +| Locking Diff Oil Temp | Same reason, and it climbs fast when the diff is locked and working | +| Coolant Temp | Low speed means low airflow; the fan is doing the work alone | +| Suspension Balance | Articulation and weight transfer, live | +| Gear Selector | Confirms what the transmission thinks it's in | + +A drive on 2026-09-20 that changed ride height confirmed Ride Height, the +height sensors and the corner pressures all move together and in the +directions their labels predict — see "Air suspension notes" below. `3B4D`, +once considered for Drive Mode or Terrain Response, is neither: a later +audit across 236 samples spanning many drives and several terrain modes +found it constant at `0x00` throughout. It stays in the file as a 600s +probe. Terrain +Response itself remains unidentified on this truck — nothing currently in +the signalset is known to carry it. + +### Before you go + +Air suspension raises the truck at low speed and drops it as you speed up. If +you're crawling and it lowers unexpectedly, watch Height Offset and the +corner pressures — an EAS fault normally means the truck sits down, which +matters a lot more with rocks under it than in a parking lot. + +--- + +## Undecoded + +These respond on the bus but their meaning isn't established. They're in the +signalset so their values get logged during normal driving, which is how the +formulas eventually get worked out. + +| Address | Signals | +|---|---| +| `7E1` / `7E9` | `1E68`, `DD01` — `1E68` is a neighbour of the gearbox temp and varies most of anything unmined on this module; `DD01` shares its DID with the engine's odometer but isn't one | +| `761` / `769` | `197C`, `D11C` — the most active unmined DIDs on the truck, four distinct values each in four samples | +| `7D3` / `7DB` | `3B01`, `3B02`, `3B0B` — `3B01` looks like the ride-height state word (see "Air suspension notes" under Off-road); `3B02` answers four single-byte values that look like one per corner | + +### Nineteen commands deleted, and restored + +These were deleted on 2026-09-20 and restored the same day, because the +deletion was a mistake worth recording. +The reasoning was that each was constant across its logged samples. The +sample counts were the problem: + +| Command | Samples | Distinct values | Was the evidence conclusive? | +|---|---|---|---| +| `726` / `0202` | 13 | 1 | No | +| `792` / `2A32`–`2A3C` (eleven) | 10–19 each | 1–3 | No | +| `795` / `1E88` | 6 | 1 | No | +| `795` / `1E89` | 25 | 2 | No | +| `7E1` / `1E6A` | 29 | 1 | No | +| `7D3` / `3B00` | 55 | 2 | Borderline | +| `7D3` / `3B08` | 64 | 1 | Borderline | +| `7D3` / `3B4D` | 236 | 1 | Yes | +| `7E0` / `F458` | 1 | 1 | No, and the reading was wrong | + +Six samples is not evidence that a rear differential lock DID is dead; it +is evidence that the differential was never locked while anyone was +looking. A signal that only moves during a rare event looks constant until +the event happens, and deleting it guarantees the event is never caught. + +`F458` was worse than weak — it was misread. `62F458 7F` is a *positive* +reply carrying the data byte `0x7F`, not a `7F` negative response. PID 58 +is the long term secondary oxygen sensor trim for bank 2, the counterpart +to PID 56 which this file already carried, and `0x7F` decodes to -0.78%. +It is now mapped properly as `LR4_SEC_O2_TRIM_B2`. + +All nineteen are back, at exploratory cadences rather than their old ones: +the event-driven candidates (`1E88`, `1E89`, `3B4D`) at 60s, `3B00` and +`3B08` at 120s, and the rest parked at 600s. Restoring the whole set costs +0.03 req/s against a 4.2 req/s ceiling, so the budget was never the real +argument for removing them. `3B4D` stays in despite being the one genuinely +conclusive case, because at 600s it is free and the cost of being wrong +about it is another six weeks of not knowing. + +### What the restored probes did within hours + +The probes went back in, the change was merged, and one short evening drive +on 2026-09-20 in which the off-road features were deliberately used settled +three of them. The merge also did what it was predicted to: coverage on the +next sessions jumped from 23 distinct DIDs and 10 modules to **70 DIDs and +13 modules**, with 87 of the file's 90 commands polled. Every module that +had drifted out — `726`, `732`, `761`, `792`, `7D3` — came back without +anyone touching the app. + +**`3B4D` is alive, and it was the one deletion called conclusive.** Constant +`0x00` across 236 samples, deleted, restored at 60s on the argument that the +samples had never covered the event. Within eight minutes of the features +being used it returned `0x04`, `0x01` and `0x00`. It does not track the ride +height mode PID, so it is reporting something else, and Terrain Response is +back to being the leading candidate. It now polls at 30s and needs one +sample per selectable mode to map. + +**Six of the eleven `792` counters advance.** They were written off on 10 to +19 samples each, all taken within one morning. Nine hours and 111 km later: + +| DID | Was | Now | Gain | Per km | +|---|---|---|---|---| +| `2A32` | 404,720 | 420,200 | +14,000 | 126 | +| `2A33` | 212,490 | 212,970 | +480 | 4.3 | +| `2A34` | 2,240 | 3,200 | +960 | 8.6 | +| `2A35` | 335,400 | 337,570 | +2,170 | 19.5 | +| `2A36` | 43,275 | 43,363 | +88 | 0.8 | +| `2A37` | 107 | 108 | +1 | 0.01 | + +They are counters on a module still not identified. What they count is +unknown — none of the per-km rates is a clean unit and the elapsed window +mixes driving with nine hours parked. They now poll at 120s so a single +drive yields enough samples to regress against distance and running time. +`2A38` through `2A3C` have still never moved. + +**`3B01`'s last bit is pinned.** `0x400` was Off-Road by elimination and +unconfirmed. It was caught twice at a front sensor reading of 85 to 89, the +truck at its highest, with the mode PID reading Off-Road. `3B01` is now a +mapped signal rather than a raw word: `0x100` Normal, `0x400` Off-Road, +`0x800` Access. + +**Two decodes in this file were wrong, and the short drive exposed both.** + +The second word of `F434` and `F438` was being read as a voltage. SAE J1979 +PIDs 34 to 3B report equivalence ratio plus sensor *current*; the voltage +variants are PIDs 24 to 2B, which this ECM does not support. Read as a +voltage it produced a flat 4.0 across 28,432 samples, which should have been +the giveaway. Read correctly it spans -0.95 to +1.36 mA around a mean of ++0.16, exactly what a wideband pump cell does. Both are now +`O2 Pump Current`, in milliamps. + +`D11A` was called Suspension Module Voltage and scaled to volts. It is not +battery voltage. Across 221 paired samples its raw value swings 57 to 105, +an 84% range, while control module voltage moved only 12.51 to 14.75, an 18% +range, and the implied volts-per-count scatters by 9%. The old scaling put +it at 16.8 V, which no 12 V system reaches. The best correlation found for +it is 0.58 against compressor activity, which is not enough to name +anything, so it goes back to being an undecoded raw byte. + +**The two fuel rail pressures are not the same measurement.** `033E` and +PID 23 were expected to be one quantity scaled two ways. Across 157 paired +samples the ratio between them drifts from 0.41 to 0.56 instead of holding +constant, and in one stretch PID 23 sat pinned near 197 bar while `033E` +climbed from 79 to 110. A scaling error gives a fixed ratio; this does not. +Two different quantities, plausibly a commanded rail target against a +measured one, and separating them needs hard sampling under load. + +**There is one ride-height state nobody has logged.** Holding the lower +button puts the truck into a held-Access mode that stays down rather than +self-levelling, and it appears in none of the 484 `3B3C` or 142 `3B01` +samples. Both words have exactly one unfilled slot: `3B01` bit 9 (`0x200`), +sitting between Normal at `0x100`, Off-Road at `0x400` and Access at +`0x800`; and `3B3C` bit 3 (`0x08`), which has only ever been seen inside +`0x0D` with the truck in motion between heights. One of them is the likely +home for it. + +Because a mapped signal renders nothing for a value it has not been taught, +both DIDs now carry an untranslated twin — `Ride Height State Raw` and +`Ride Height Mode Raw`. They cost no extra request and they mean an +unrecognised state arrives as a number instead of a blank. `3B4D` showing +empty in the app is exactly that failure mode, and it is why the value went +unnoticed for so long. + +Two more suspension DIDs turn out not to be flat either. `3B00` reads +`0x03` in 111 of 113 samples with one `0x103`, taken with the truck at +Access; `3B02` carries a bit-8 flag that toggles the same way; and `3B08` +reads `0x00` in 130 of 131 with a single `0x01`, taken with Off-Road +engaged and the truck at its highest. One sample each is not a decode, but +none of the three is dead. + +**The ride height map checks out end to end.** A screenshot at 20:19 shows +Ride Height Mode reading **Access** and the rear sensor at 128; the scan log +for the same moment has `3B3C` at `0x04`, `3B01` at `0x800` and the rear +sensor at 128. Access had never once displayed before the map was corrected. + +**All six new probes answered.** PID 01 reports the MIL off with zero stored +DTCs. PID 13 returns `0x77`, six oxygen sensors, three per bank. PID 51 is +gasoline, PID 1C is OBD-II. PID 23 ranges from 29 to 197 bar across the +logs, against the proprietary `033E` reading 41 to 46 bar over the same +evening — both are live and they disagree, which is the comparison worth +making under load. + +The standard this file now holds deletions to: a DID needs enough samples +to have covered the event it would report, not merely a lot of samples. +Hundreds of readings taken while the differential was never locked say +nothing about a differential lock DID. When in doubt, park it at 600s +instead — a probe costs 0.0017 req/s and deleting it costs the answer. + +**The suspension decodes check out.** Replaying every logged sample through +the formats in this file gives corner pressures of 34-50 psi, a ride height +offset correctly signed at -91 to +50 mm, and module voltage of 9.12-14.56 V, +all physically sensible. + +**Terrain Response and `3B4D`.** `3B4D` was labelled Drive Mode and was the +candidate for Terrain Response. Across all logged history it returns a +constant `0x00` — 236 samples, spanning many drives and several terrain +modes, not just the one 2026-09-20 drive that first flagged it as flat. +That's not "unconfirmed," it's answered: `3B4D` is not Terrain Response and +appears to be nothing. It is nonetheless still in the file at 600s, because +that costs 0.0017 req/s and leaves the door open — see "Nineteen commands +deleted, and restored" above. + +What the audit could not do is say anything about suspension health. Across +fourteen months only 36 minutes have all four corner pressures captured +together, because the `7D3` module receives one or two requests per drive +despite its `freq` of 10. + +**Tire pressures remain unsolved, but they exist.** Module `751` returns no +data to any request, and nothing found so far resembles four tire pressures. +The decisive clue is the spare: this truck warns when the *spare* is low, and a +spare does not rotate — so there is no wheel-speed difference to infer it from. +That rules out an indirect system and means real pressure sensors are fitted and +reporting to some module. Six modules answer negative responses but have never +been mined — `716`, `726`, `734`, `737`, `760`, `797` — and each is now probed +with the Jaguar tire-pressure DID as a locator. A positive response from any of +them finds the module. + +### Air suspension notes + +A 33-minute drive on 2026-09-20 that changed ride height twice gave the first +real test of these fields, and most of them passed it. + +`3B71` and `3B72` track the height sensors, and they read **inverted** — the +number falls as the truck rises. Observed: 115/115 at normal height, 93/101 +raised; on the 2026-09-20 drive `3B71` fell from `6A` to `56` on the raise, +then rose to `85` by the end. Treat a falling value as the truck going up. + +The four corner pressures (`3B03`-`3B06`) tracked the same drive unambiguously: +around `00C5` cruising, down to about `00AC`, up to about `00FE` at 16:22:42 +UTC as the truck raised, then down to `0096`-`009F` at 16:23:49 as it lowered. +Compressor activity (`3B07`) jumped from around `00A0` to `05F0` and `0617` +during those changes. + +The driver believed the low "access" height didn't register. It did — the +corner pressures and height sensors both recorded a third, lower level at +16:23:49. What was wrong was the map, not the sampling, and a later audit +across all logged history settled it. + +**The ride height map was wrong, and that is why Access never registered.** +`3B3C` across all logged history returns `0x01` (216 samples), `0x02` (11), +`0x04` (5) and `0x0D` (4). The map in this file was ordinal, 0 through 3, +with 0 labelled Access — but the value 0 never once occurs, and `0x04` and +`0x0D` weren't in the map at all. Pinned against the front height sensor, +which is inverted so a falling value means the truck is rising: + +| Value | Front height sensor mean | State | +|---|---|---| +| `0x01` | 111.4 (range 91–122) | Normal | +| `0x02` | 90.4 (85–95), highest the truck sits | Off-Road | +| `0x04` | 133.0, lowest the truck sits | Access | +| `0x0D` | 120.5, between Normal and Access | In Transit | + +The map is now `1, 2, 4, 13`. `3B01` carries the same state in parallel: +`0x100` pins to a front mean of 111.6 (Normal) and `0x800` to 133.0 +(Access). `0x400` has been seen once and never alongside a height sample, so +it's Off-Road by elimination — unconfirmed, unlike the other three. + +The two balance ratios respond to cargo, not just faults. With the load area +full, front balance read 0.99 and rear read 0.92 — the rear axle carrying more +on one side. Check the ratios unloaded before reading a low number as a leak. + +--- + +## Polling + +### The budget was measured against the wrong ceiling + +For most of this file's history the budget was set against **13 +request/response round trips per second**, treated as the adapter's +ceiling. That number is real, but it's the whole bus, and most of it +belongs to the app, not to this signalset. Measured on 2026-09-20 (session +2525): total adapter throughput was 13.43 req/s, splitting into AT +commands (adapter setup, header changes, filter changes) at 3.27/s, the +app's own internal mode-01/mode-09 polling — which never surfaces as a +recordable signal — at 5.94/s, and this signalset's UDS service-`22` +traffic at 4.22/s. + +Across seven sessions on 2026-09-19 and 2026-09-20, the UDS share stayed +pinned between 4.1 and 5.1 req/s no matter how large the signalset was. +Session 2504 polled 108 distinct DIDs and got 4.19 req/s. Session 2525 +polled 23 distinct DIDs and got 4.22 req/s — five times fewer commands, +identical throughput. **The signalset's real ceiling is about 4.2 req/s, +not 11.** This file had been asking for 10.53. + +Service `22` is read-only. Polling cannot harm anything; it can only +compete with itself, and with the app's own traffic, for a share that +doesn't grow no matter how much is asked of it. + +**`freq` is a minimum interval in seconds, not a rate.** Pelican documents it as +the maximum frequency at which a command may be sent, expressed in seconds, so +a *smaller* number polls *harder*. There is no priority field anywhere in the +v3 format — command count and `freq` are the only levers. + +The budget in this file, cut to fit the measured ceiling on 2026-09-20 and +rebalanced on 2026-09-25 so the fuel readout updates every few seconds: + +| Interval | Commands | Contents | +|---|---|---| +| 2s | 3 | Engine speed, vehicle speed, mass air flow | +| 3s | 1 | Commanded equivalence ratio | +| 5s | 2 | Calculated load, manifold pressure | +| 10s | 3 | Throttle position, gear selector, ride height mode | +| 15s | 6 | Coolant temp, short term trim B1, lambda B1S1, both height sensors, `3B01` | +| 30s | 20 | Remaining temperatures (oil, charge air, gearbox, diff), fuel rail pressure, timing advance, absolute load, both pedal sensors, fuel trims, commanded throttle, fuel level, all four corner pressures, compressor activity, `3B4D` | +| 60s | 17 | Height offset, MAF A/B, lambda B2S1, ambient, barometric/altitude, battery, catalyst temps, IAT, rail gauge pressure, charge cooler coolant, `3B02`, `3B0B`, `D11A`, `1E68`, `1E88`, `1E89` | +| 120s | 20 | Slower diagnostics: fuel system status, evap purge, O2 voltage B1S2, secondary O2 trims, relative/absolute throttle, IAT 1/2, manifold pressure (fine), run time, `197C`, `D11C`, `3B00`, `3B08`, `2A32` to `2A37` | +| 300s | 1 | Distance since codes cleared | +| 600s | 18 | Odometer, oil level, oil volume, distance with MIL on, warm-ups since codes cleared, monitor status, fuel type, OBD standard, `DD01`, `1E6A`, `726 0202`, `2A38` to `2A3C` | + +That's **91 commands at 4.083 req/s**, under the measured 4.2 req/s +ceiling. The 2026-09-20 cut took it from 85 commands at 10.528 to 66 at +3.922; the probes restored the same day and the decodes of 2026-09-23 took +it to 91 at 4.100; and the 2026-09-25 rebalance paid for mass air flow at +2s and commanded lambda at 3s (up from 5s each) by slowing throttle +position and the gear selector to 10s, lambda B1S1 to 15s, rail pressure, +timing advance, absolute load and both pedal sensors to 30s, and MAF A/B +to 60s. Engine speed is on a real cadence (2s) and carries the +`engineSpeed` metric; it had not been polled since 2026-08-30 — see below +for why. + +**What the drives of 2026-09-22 and 2026-09-23 actually polled.** Three +drives, 14, 10 and 22 minutes, and the app sent 19, 20 and 21 distinct +DIDs: the `7E0` `F4xx` core plus oil temp, charge air temp, gearbox temp, +diff temp and the odometer. Not one request went to `7D3`, `732`, `792`, +`761` or `726`. The last time any suspension, gear-selector or counter +probe was sent was 2026-09-21 02:20 UTC, in the session right after the +probes were restored. So the open items that need a probe to be alive +(held-Access, the Terrain Response map, the `792` counters) got nothing +from these drives, exactly as trap 5 predicts: the app converged on the +metric-bearing set and the probes dropped out of rotation. The 2026-09-23 +drive also predates the merge of the ride-height and wideband decodes, so +the reshuffle a file change usually causes has not had a drive yet. The +four sessions logged after it contain no traffic at all. + +The cadence the app delivered on that drive is worth having: commands +asked at 5s were answered every 5.7s, at 15s every 15.5s, and the two at 2s +every 3.05s. So a `freq` is honoured within about 15% except at the top, +where 2s buys 3s. The 21 commands it chose add up to about 1.8 req/s, well +under the ceiling; the app was not short of budget, it was short of +interest. The fuel operands were on the 5.7s cadence, which is why the +fuel readout felt slow. + + +### The app converges on a small working set, and it is roughly what it can store + +An earlier version of this section claimed the app "retires ECUs +permanently" in a one-way ratchet, and recommended resetting the app's +vehicle profile. **That was wrong, and it was wrong because it read a +single day's sessions without checking the months behind them.** The +correction is recorded here rather than deleted, because the wrong version +was convincing. + +What the daily history actually shows, taking the longest session of each +day since 2026-09-05 — the only sessions comparable to a drive: + +| Session | Date | Minutes | Distinct DIDs polled | +|---|---|---|---| +| 2397 | 09-05 | 67.2 | 13 | +| 2402 | 09-06 | 25.9 | 17 | +| 2426 | 09-09 | 63.9 | 17 | +| 2441 | 09-10 | 57.8 | 17 | +| 2460 | 09-11 | 63.9 | 17 | +| 2470 | 09-13 | 35.5 | 17 | +| 2495 | 09-19 | 50.0 | 19 | +| 2497 | 09-19 | 38.7 | 18 | +| 2525 | 09-20 | 137.7 | **23** | + +Long sessions have settled to between 12 and 19 commands for six weeks. +The off-road drive polled 23, which is *more than any other long session in +the period*. The drive was not degraded. It was the best long session on +record, and the 61-of-84 figure is simply what this app has always done. + +The stable core is the interesting part. Thirteen commands appear in every +long session going back to 09-05: `03F3`, `1E69`, `DD01`, `F404`, `F405`, +`F406`, `F40D`, `F410`, `F42F`, `F431`, `F434`, `F442`, `F444`. Those are +exactly thirteen of the sixteen commands that carried a `suggestedMetric` +at the time. Not approximately — exactly. The three metric-carrying +commands missing from the core are `F40C`, `F411` and `F443`, and `F411` +joined the working set later. + +Read alongside "Only signals with a metric are ever recorded" below, the +behaviour is coherent: **the app converges on polling the commands whose +values it is going to keep.** Commands without a metric get exercised for a +while after the signalset changes and then thin out, because the app has +nothing to do with their values. + +That also explains the peripheral modules without inventing a ratchet. +`7D3`, `792`, `732`, `726` and `761` do not carry a single metric-bearing +command between them. They are not being punished; there is nothing on them +the app would store. + +The one-day pattern that produced the ratchet theory is real but means +something duller. Coverage spikes after the signalset changes and then +settles: 108 distinct DIDs at 09:57 on 09-20, then 54, 58, 19 and 23 as the +day went on, with several PRs merged in between. Session 2493 on 09-19 hit +965 DIDs, which was the PID detector sweep. Coverage recovers on its own +every time the file changes. **Nothing is stuck, and nothing needs +resetting from inside the app.** + +**A command's presence in this file still does not mean it is being +collected** — check the scan logs before relying on one. But the reason is +ordinary triage by the app, not damage. + +What remains untested is whether cutting the request budget changes which +commands make the working set. Asking for 10.53 req/s against a 4.2 req/s +ceiling meant the app chose the 40% it would serve; asking for 3.922 means +it does not have to choose. Whether it then serves all 66 is exactly what +the next drive measures. + +### Eight commands went quiet for three weeks — the first sighting of the working set, not a separate incident + +This section originally explained an isolated 2026-08-30 incident as a +stale copy of the signalset on the app's side. It wasn't isolated, and the +stale copy was only half of it. Those eight commands were sitting outside +the working set the app settles on between signalset changes, described +above. The history below is kept as-is because the reasoning in it — ruling +out the request budget and the `freq` tiers before landing on "the app must +be holding stale data" — was a reasonable read of the evidence available +that day, and because the refresh really did bring them back for a while. + +Between 2026-08-30 and 2026-09-19, eight commands in this file were never +requested on a drive: `F40C`, `F411`, `F40E`, `F443`, `F449`, `F44A`, `F407` +and `033E`. The cost was real — `engineSpeed` is wired to `F40C`, and no +engine speed value reached the signal database in fourteen months. Two +synthetics could not compute either, `LR4_THROTTLE_TRACKING` needing `F411` +and `LR4_PEDAL_AGREEMENT` needing `F449` and `F44A`. + +**Looked resolved on 2026-09-20, briefly.** Every command in the file was +polled that morning. `F40C` was requested 172 times and answered 172 +times, and engine speed was recorded for the first time in fourteen +months. At the time this was credited to the app picking up a fresher copy +of the signalset, and that reading has held up: coverage does spike right +after the file changes. What it does not do is stay there. By the 15:14 +session the working set was back to 23 commands, and `F40C` was outside it +again. The eight commands were never broken; they sit outside the set the +app settles on between signalset changes, which is roughly the commands +whose values it stores. `F40C` carries `engineSpeed` and should have been +in that set, and its absence is the one part of this that is still +unexplained. + +Getting to the "stale signalset" explanation meant ruling out the two +obvious causes, and both remain worth knowing even though the explanation +built on them was wrong. It was not the request budget: demand was 10.21 +req/s against the 11-13 the adapter appeared to deliver — appeared to, +because that 11-13 figure was the whole-bus number, not this signalset's +real share. It was not the `freq` values either, because the dead commands +shared tiers with live ones — `F40C` and `F40D` are both `freq` 1 and only +`F40D` ran; `F411`, `F443`, `F449` and `F44A` sat at `freq` 3 alongside +`F404`, `F434` and `F444`, which all ran. + +The lesson that outlives the incident, revised: **a command's presence in +this file means nothing until the scan logs confirm the app is actually +addressing its module this session.** Before concluding a command is +unsupported, or that a fix worked, check which headers the app is +addressing — not just which commands answer when you send them by hand. + +### The seven probes, and how they turned out + +Seven commands were added on 2026-09-20 to be watched rather than trusted. +The 2026-09-20 drive settled all of them. + +**`7E0/F408` and `7E0/F409` answered — the bank-2 fuel trims exist.** They +had never been requested on this vehicle before. They are now real signals +rather than probes, and what they show matters: see below. + +**`7E0/1153`, `7E0/1154`, `7E0/113F` and `7E1/2104` returned `7F 22 31`.** +They are session-gated, not dead — see the manual-probe list in +`TESTING.md`. The charging-voltage reading behind `1153`/`1154` still +stands (`1154` pinned at exactly `0E00`, 14.00 V, with both dipping to +5.7-10.1 V on crank, tracking nothing that `F442` does), but it can only be +read by hand in an extended diagnostic session, so those commands have been +removed from this file. + +**`726/0202` answered and stayed at `00`** across twelve samples. + +`F41F` also recorded, which was the incidental test of whether the `F4xx` +alias mechanism works at all against a standard PID the app already polls. +It does. + +### Both banks are running lean, and they agree with each other + +First bank-2 data from this truck, measured over the 2026-09-20 drive: + +| | Bank 1 | Bank 2 | +|---|---|---| +| Short term | -0.55% (n=305) | -5.36% (n=7) | +| Long term | **+9.31%** (n=13) | **+7.32%** (n=8) | + +Two points apart on the long-term trims is not an asymmetry. Both banks are +correcting lean together by 7-9%, which points at something shared — fuel +delivery, MAF calibration, or unmetered air upstream of where the intake +splits — rather than a fault on one side. Any earlier reading of this as a +bank asymmetry came from having only bank 1 to look at. + +Sample sizes are small: one drive, and single digits on three of the four +figures. Treat the direction as real and the magnitude as provisional. + +`F403`, `F42E` and `F438`, added to the file below, exist specifically to +test this: whether the engine was in closed loop when the trims were +measured, whether a stuck-open purge valve is pulling in unmetered air, and +what bank 2's own oxygen sensor is doing behind its trim. + +### The debug batch, and what the first drive did to it + +On 2026-09-20, 51 commands went in — one for every DID this truck had ever +answered that nothing here decodes — each a raw scalar at its observed byte +width, named `LR4__RAW`. The first real drive cut that back hard. + +**Thirty-six of them are session-gated.** The whole `7E0` `10xx`/`11xx` +family and the `7E1` `0302`-`0305`, `0805`, `101A`, `2104`, `210F`, +`2B1B`-`2B1E` block returned `7F 22 31`. In fourteen months of logs they +have answered in exactly two sessions: one in December 2025 that sent +`10 03` to open an extended diagnostic session, and one in June 2025 doing +unusual protocol and flow-control setup. Every session since that did not +open an extended session has been rejected. + +The app polls in the default session, so it can never reach them. All 36 +are out of this file and listed in `TESTING.md` as manual-probe-only. This +also rewrites the history: `1cf20db` dropped many of them as "dead probes", +and they were never dead — they were session-gated, and nobody had noticed +the December session opened `10 03` first. + +What survived, and how it was re-tiered after the drive: + +| Command | Change | Why | +|---|---|---| +| `7D3/3B01` | `freq` 600 → 10 | Returned three single-bit values; looks like the ride-height state word | +| `7D3/3B00`, `7D3/3B08` | `freq` 600 → 30 | Still flat through a real height change, but on 3 samples each — one more look before deletion | +| `761/197C`, `761/D11C` | `freq` 60 → 30 | Four distinct values in four samples each; the most active unmined DIDs on the truck | +| `792/2A32`, `792/2A37` | unchanged | Each ticked by a small amount, consistent with counters. The rest of the `2A3x` block stayed flat | +| `726/0202`, `795/1E88`, `795/1E89` | unchanged | Still flat; the diff-lock cycle is what will decide the `795` pair | + +The file is now 73 commands at 10.55 req/s. + +A DID that moves on a real drive earns a proper decode. One that stays flat +through a full cycle — including a ride-height change and a terrain-mode +switch — can be judged on evidence rather than on suspicion. Do not read a +`7F 22 31` as "unsupported" without checking whether an extended session +would have changed the answer. + +### Sixteen more, found by reading the ECM's own PID list, and how they did + +The ECM's `0100`/`0120`/`0140`/`0160` bitmasks report 54 supported mode-01 +PIDs. Twenty-five had no entry here; after removing metadata PIDs and the +two SAE aliases already dropped, sixteen went in on 2026-09-20 and were +tested the same day. **All sixteen answered.** Ten earn their place. + +| Command | Result | +|---|---| +| `22F466` | **Two mass airflow sensors.** A 2.09 g/s against B 1.78 g/s, summing to 3.88 against `F410`'s 4.16 a second later. One idle sample — see the correction below | +| `22F467` | **Two coolant temperature sensors**, 61 °C and 35 °C | +| `22F468` | **Two intake air temperature sensors**, 36 °C and 59 °C | +| `22F403` | Fuel system status, two states over 14 samples: open loop on load or decel ×9, closed loop ×5 | +| `22F42E` | Evap purge duty, 0 to 72.2% | +| `22F438` | Bank 2 lambda 0.853-1.985 over 29 valid samples; railed at `FFFF` on 23 of 52 | +| `22F415` | Post-catalyst O2, 0.13-0.93 V and switching | +| `22F445` / `22F447` | Relative throttle 2.7-22.7%, absolute throttle B 12.5-22.4% | +| `22F470` | Decoded against SAE J1979 PID 70: channel A reads 24.72 kPa at idle. Later shown to be manifold pressure at finer resolution, not a separate boost sensor — see "Duplicates found and removed" above | + +**The dual mass airflow sensors exist, but the 17.5% disagreement claim +above was wrong — it rested on a single idle sample.** With 1,474 paired +samples from the 2026-09-20 drive, the least-squares slope of (A+B) against +the single MAF PID through the origin is 0.88, not 1.0, and individual +pairs scatter badly: one pair reads 7.03 g/s against the single PID's +18.16 g/s at the same instant. The split between the two channels is not +trustworthy at this polling rate. Use the single MAF PID for anything +quantitative; treat A and B as present but unvalidated. + +`F403` matters for the same investigation: the engine does reach closed +loop, so the 7-9% long-term trims are real measurements rather than +open-loop artifacts. + +**Four were declared wrong and recorded nothing useful.** `F466`, `F467`, +`F468` and `F470` were added at `len` 8 because their payload widths were +unknown. The first byte of each turns out to be a sensor-support bitmask, +not data, so all four logged a constant 3 or 2. The figures above came from +decoding raw frames by hand. Widths are now known and declared properly. + +**Three did nothing and are gone.** `F416`, `F419` and `F41A` return `FFFF`, +not-available, on every sample. The ECM's bitmask claims them; the values do +not exist. + +**One did its whole job in a single reply and is also gone.** `F413` +returned `0x77`: bank 1 sensors 1, 2 and 3 and bank 2 sensors 1, 2 and 3 are +present. That can never change, so it is recorded here instead of polled. + +Two still needed samples at the time: `F456` (three samples, all near +zero) and `F458` (one sample, `7F`, meaning unknown). `F458` is now +resolved — it returns a `7F` negative response consistently and has been +deleted; see "Nineteen commands deleted, and restored" under Undecoded. `F456` stayed in +at `freq` 120. + +`F470` was read against the SAE J1979 definition of PID 70: its ten bytes +are commanded boost and measured boost for two channels, at 1/32 kPa. The +support byte is `0x02`, flagging channel A as present, and channel A read +24.72 kPa at idle. At the time this looked like the first direct boost +measurement on this truck. A wider check on 2026-09-20 against 525 paired +samples found channel A tracking Manifold Pressure to within 1.3 kPa mean +(r = 0.85) — it is the same manifold sensor at finer resolution, not a +separate boost sensor. See "Duplicates found and removed" above. Boost is +still only available by subtracting Barometric Pressure from Manifold +Pressure, as below. + +Checking the rest of the signalset the same way found nothing else. Against +the canonical SAE signalset, the only other unread fields in replies we +already make are sensor-present bits and channels this engine does not have: +`F468` carries six intake air temperature slots and answers `FF` on four, +`F466` and `F467` expose support bits for sensors we already read, and +`F408`/`F409` reserve a second byte for bank 4, which a V6 does not have. + +Barometric pressure moved from `freq` 1 to `freq` 5 to pay for this. It does +not change at 1 Hz, and it was consuming 0.9 req/s of a budget with no slack. + +### From 89 commands back down to 66 + +The narrative above tracks the file growing to 89 commands at 10.93 req/s +over the course of 2026-09-20. Later the same day, session 2525 showed +that budget had been measured against the wrong ceiling — 11 req/s that +was never really available, against a true UDS share of about 4.2 req/s — +and that the app settles on a working set of roughly twenty commands +between signalset changes regardless of what the file asks for. The +response was a re-tiering rather than a cull: nineteen commands were +deleted and then restored as slow probes once it was clear the evidence +against most of them was six to thirty samples (see "Nineteen commands +deleted, and restored" under Undecoded), the duplicate signals resolved under +"Duplicates found and removed" (PID 67 sensor 1 deleted outright, three of +PID 70's four channels dropped as dead alongside it), and a re-tiering of +everything that survived. The result is the 66-command, 3.922 req/s budget +at the top of this section. + +### Only signals with a metric are ever recorded + +This is the most consequential thing measured on 2026-09-20, and it sets the +boundary on what this whole project can deliver. + +**The app writes a signal to its database only if that signal carries a +`suggestedMetric`. Everything else is requested, answered, decoded and +thrown away.** + +Across fifteen months of backups, from 2025-06-29 to 2026-09-20, the signal +database has held exactly 15 distinct signals. Not 84, not the 98 the file +carried at its peak. Fifteen. + +The 137-minute drive on 2026-09-20 recorded 14, and they are precisely the +metric-carrying signals whose commands were being polled. The signalset held +16 metrics that day. The two missing from the database are `engineSpeed` and +`absoluteEngineLoad`, and they are missing for the unrelated reason that +their commands, `F40C` and `F443`, were among the 61 never sent. The match +is exact in both directions: every metric-carrying polled signal was stored, +and no signal without a metric ever was. + +The other two stores in the backup are not alternatives. `records` holds +manual service and fuel-up entries and is empty. `tripLogger` holds the +phone's own CoreLocation trace — latitude, longitude, altitude, speed — with +no OBD content in it. + +What that costs, stated plainly: the metric enum has no slot for manifold +pressure, ride height, suspension pressure, corner pressure, charge air +temperature, barometric pressure, ambient temperature, rear differential +temperature or boost. **None of those can ever be looked at historically +through the app.** They exist live, or in the raw scan logs, and nowhere +else. + +That includes the suspension work in its entirety. Even when the +`7D3` module is being polled, no ride height and no corner pressure will be +recorded as history. It includes the charge cooler coolant temperature newly +decoded from PID 67 sensor 2 — a real signal, genuinely new, that will never +persist. + +One limit on the claim, now resolved: what was measured is that these +signals are never *stored*. Whether the app renders them live was a separate +question the logs could not answer, and screenshots on 2026-09-20 settled it +— **non-metric signals do display live**, with current values, in the app's +section lists. Manifold pressure, charge air temperature, the suspension +pressures and every raw probe are all on screen. So the bandwidth spent on +them buys a real readout; what it does not buy is history. + +The practical rule: a fast `freq` on a signal with no metric buys a live +readout and nothing else. That can be worth paying for. It should be a +deliberate choice rather than an accident. + +### Metric slots, seventeen of thirty-six + +The schema defines 36 `suggestedMetric` values — the slots Pelican treats as +connectable. Per the section above they are also the only signals that get +recorded at all, which makes this table the boundary of what is knowable +about this truck over time, not just a list of gauges. This signalset fills +17, up from 16 on 2026-09-20 when +`fuelRate` was added as a computed signal (see "Fuel rate, now computed" +above). The remaining 19 break down as: + +| Slots | Why they stay empty | +|---|---| +| 10 | Electric and hybrid only: state of charge and health, traction battery, charging, electric range, CVT deterioration | +| 8 | Tire pressure and temperature, which this truck has not yet given up | +| 1 | `fuelRange` is a proprietary Ford DID | + +`fuelRate` looked like it belonged in this list too — the schema's only +formula operation is `ratio`, a plain a divided by b with no constant and no +scaling — but the constant can be folded into a hidden operand on one side +of the ratio instead of the ratio itself, which is how it got filled. See +above for the mechanism. Tire pressure is the one still worth chasing: eight +of the nineteen empty slots are tires, and `TESTING.md` records what the +Jaguar signalset does and what has never been tried here. + +### Hidden signals + +Thirty-seven of the file's 119 signals (including synthetics) carry +`hidden: true`: the `*_RAW` probes, the `3B02` byte splits and the +synthetic operands. + +**`hidden: true` does not appear to do anything in this app.** Screenshots +taken on 2026-09-20 show `3B00 Raw`, `3B01 Raw`, `3B02 Byte 1`, `3B4D Raw`, +`3B08 Raw`, `2A32 Raw`, `2A3A Raw`, `197C Raw`, `726 0202 Raw`, `D11C Raw`, +`Monitor Status Raw`, `O2 Sensors Present Raw`, `OBD Standard Raw` and +`Fuel Type Raw` all listed with values in the Suspension, Fuel and ECU +sections. Every one of those carries `hidden: true` in this file. Whatever +the flag is for, it is not suppressing them from the section lists, so it +should not be relied on to keep clutter down. It also does not affect +recording: none of these carries a metric, so none was ever stored either +way. + +Eighteen signals carry a `description`, concentrated on the probes and on +the decoded signals with a catch — the inverted height sensors, the bank 2 +lambda that rails at `FFFF`, altitude being a barometric lookup rather than +GPS. + +### Safety + +Service `22` is read-only by definition and cannot change vehicle state. It +is safe to send anything at it. + +Service `10 03` (extended diagnostic session) does change ECU state. It's a +standard service that auto-reverts after a few seconds, but run it parked — +some modules reduce normal messaging while an extended session is open. + +Avoid entirely unless you know exactly what you're doing: `2F` (actuator +control), `31` (routine control), `2E` (write), `11` (ECU reset), `14` (clear +codes), `27` (security access). On this vehicle `2F` and `31` sent to the +suspension module can physically raise or lower the truck, and `14` erases +emissions readiness monitors, which means failing inspection until a full +drive cycle completes. diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..8c80942 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,331 @@ +# Testing + +The README records what's known about this signalset. This file records what +isn't — the open questions that can only be answered in the truck, and the +pass/fail criteria for the debug parameters added on 2026-09-20 (51 in the +first batch, 8 more added later that day from the ECM's own supported-PID +bitmask). Work through it as drives happen; check items off with evidence, +not on suspicion. + +--- + +## Open in-vehicle tasks + +- [x] **Get the suspension module back into the polling rotation.** Done 2026-09-20 by merging PR #7. Coverage went from 23 distinct DIDs across 10 modules to 70 across 13, with 87 of 90 commands polled. `726`, `732`, `761`, `792` and `7D3` all returned on their own. No app reset was needed, and deleting the vehicle would have been the wrong move. +- [ ] **Capture the held-Access mode** (hold the lower button and the truck stays down, for car parks). It has never appeared in any log. Both height-state DIDs have exactly one slot never filled: `3B01` bit 9 (`0x200`), sitting between Normal at `0x100`, Off-Road at `0x400` and Access at `0x800`; and `3B3C` bit 3 (`0x08`), seen only inside `0x0D` while the truck was moving between heights. One of those is its likely home. Engage it, hold it a minute, then read `Ride Height State Raw` and `Ride Height Mode Raw` — both were added so an unmapped state shows as a number rather than a blank. +- [ ] **Map `3B4D` to the Terrain Response modes.** Alive after all: constant `0x00` across 236 samples, then `0x04`, `0x01` and `0x00` within eight minutes of the off-road features being used on 2026-09-20. It does not follow the ride height mode PID, so it is reporting something else. Now at 30s. Select each terrain mode in turn, hold about a minute, and note the order. +- [ ] **Work out what the `792` counters count.** `2A32` through `2A37` all advance; `2A38` through `2A3C` still never move. Over 111 km and nine hours they gained 14,000 / 480 / 960 / 2,170 / 88 / 1, but that window mixed driving with parked time so the rates are not separable. They now poll at 120s, so one uninterrupted drive gives enough samples to regress each against distance and against running time. +- [ ] **Read the fuel signals on the next drive.** Mass air flow is now asked at 2s and commanded lambda at 3s, so the fuel readout should move every 3 to 4 seconds instead of every 6. Fuel Rate should now print in gal/h (a warm cruise sits around 1.5 to 3 gal/h, wide open throttle near 10). If it still says L/h, the app is converting by its own unit preference and the fix is in Settings, not this file. Then check `Fuel Economy (mpg, lambda-corrected, test)`: a number means a synthetic can read another synthetic and the plain `Fuel Economy (mpg)` can go; blank means synthetics resolve raw signals only, and the test signal is the one to delete. +- [ ] **Verify the cut-down signalset on the next drive.** The polling budget was cut from 10.53 to 3.922 req/s (85 commands down to 66) because measurement showed the app only ever delivers about 4.2 req/s to signalset traffic, no matter how many commands are in the file. Asking for 10.53 against a 4.2 ceiling meant the app picked which 40% to serve; asking for 3.922 means it does not have to pick. Two pass criteria: (1) all 66 commands are actually polled, and (2) coverage on a long drive beats 23 distinct DIDs, which is the best any long session has managed in six weeks. +- [ ] **Re-run `22F187` against module `726`.** Every other module rejected it with NRC 31, but `726` returned a malformed reply carrying VIN bytes instead. Terminal sequence: `ATSH 726`, `ATCRA 72E`, `22F187`. (Unaffected by the profile-reset issue above — this is a hand-typed terminal probe, not app polling.) +- [x] **Confirm which signalset version the Pelican app is actually holding.** Done 2026-09-20: the app was holding a stale signalset. A forced refresh brought all 109 commands into the poll cycle — `F40C` answered 172 of 172 requests, and engine speed was recorded for the first time. +- [x] **A drive with a ride-height change.** Done 2026-09-20: `3B01`, `3B3C`, all four corner pressures, both height sensors and the compressor all moved. Access height did register — in the pressures and height sensors — contrary to what it looked like from the driver's seat. Update 2026-09-20 (log review): the reason Access looked unreliable was ours — `3B3C` is a bitfield, not an ordinal. Observed values across all history are `0x01` (216 samples), `0x02` (11), `0x04` (5), `0x0D` (4); the old map ran 0-3 with 0 labelled Access, and 0 never occurs. Pinned against the inverted front height sensor, where a falling value means the truck is rising: `0x01` (front mean 111.4) is Normal, `0x02` (front mean 90.4, truck highest) is Off-Road, `0x04` (front mean 133.0, truck lowest) is Access, `0x0D` (front mean 120.5) is in transit. Map corrected to `1, 2, 4, 13`. Confirmed 2026-09-20 from a screenshot: Ride Height Mode displays **Access**, with the rear sensor at 128, and the scan log for that same moment has `3B3C` at `0x04` and `3B01` at `0x800`. Access had never displayed before the map was corrected. +- [x] **Confirm `3B01` bit `0x400`.** Done 2026-09-20. Caught twice at a front sensor reading of 85 to 89 — the truck at its highest — with the mode PID reading Off-Road at the same moment. All three bits are now measured: `0x100` Normal, `0x400` Off-Road, `0x800` Access. `3B01` is a mapped signal now, not a raw word. +- [ ] **Catch the rear differential locking while `1E88` and `1E89` are being sampled.** Both look constant — `1E88` flat `0000`, `1E89` flat `09C4` — but on 6 and 25 samples respectively, none of them taken with the diff actually locked. That is not evidence they are dead, it is evidence nobody was watching at the right moment. Both are now polled at 60s. Lock the diff deliberately, hold it, and check the scan logs afterwards. If they stay flat through a real lock cycle, *then* they are dead and a manual probe sweep of `795` is the next move. +- [ ] **A cold start.** Needed for the warm-up curves on `113F` and `2104`, and to catch the crank dip on `1153`/`1154`. +- [ ] **Identify module `792`.** Still unidentified, but no longer a throwaway: six of its eleven `2A3x` DIDs are advancing counters, not constants, so whatever it is keeps a running tally of something. All eleven are back in the signalset. Worth one terminal sweep alongside the counter regression — try `22F18C` and `22F191` against it the way the other six unmined modules were probed on 2026-09-19. +- [ ] **Mine the four still-unidentified modules** — `716`, `726`, `737`, `797`. Each returned a distinct ECU serial via `22F18C` but rejected `22F191`, so they are real and separate but their function is unknown. (`726`'s only known DID, `0202`, has stayed `00` across 13 samples, which is far too few to call it — it is parked at 600s rather than removed. The module itself is still unidentified.) +- [ ] **Sweep module `760` for wheel speeds.** `760` was identified as the ABS/brake module on 2026-09-19 and has never been mined. Individual wheel speeds would enable dragging-brake detection, a tire-size mismatch check, and a better speed reference than the single `F40D` value. A parked terminal sweep — no drive needed. +- [ ] **Read fault codes.** Partly answered 2026-09-20: standard PID 01 is now polled and reports the MIL off with zero stored DTCs, so there is nothing currently set. That covers the headline only — modes `03` (stored), `07` (pending), `0A` (permanent), plus UDS service `19` for the non-OBD modules. Pending codes are the earliest warning available on a vehicle this age and have never been looked at. This probably can't be expressed in an OBDb signalset, so it stays a terminal task rather than something to add to `default.json`. +- [ ] **Sanity-check the computed fuel-rate synthetic against real fill-ups.** PID `5E` isn't supported per the ECM's own supported-PID bitmask, so fuel rate can't be read directly — it's now derived from MAF and commanded lambda instead. Validated to within about 18% of the tank gauge over one 91.5 km drive; comparing against a few real fill-ups is the only way to actually calibrate it. + +--- + +## Tire pressures: what the Jaguar signalset suggests + +Jaguar reads all four tire pressures and temperatures, and fills eight Pelican +metric slots this truck leaves empty. It does it on header `751`, response +`759`, with DIDs `2076`-`2079` for pressure at 1/0.01373 bar and `2A0A`-`2A0D` +for temperature at -50 °C offset. + +On this truck `751` is silent. It has been asked `2076`-`2079` 1,386 times +each and answers `NO DATA` every time, and it does not answer `0100` or +`0902` either, so nothing is listening at that address. + +But the temperature DIDs have never been tried anywhere useful. `2A0A`-`2A0D` +have only ever gone to header `710` and to `FC00F1`, both of which are silent. +They have never been sent to a module that actually answers. + +- [ ] **Sweep `2A0A`-`2A0D` across every module that responds** — `716`, + `726`, `732`, `734`, `737`, `760`, `761`, `792`, `795`, `797`, `7D3`. About + 45 requests, parked, a couple of minutes. `2076` has already been tried on + `716`, `726`, `734`, `737`, `760` and `797` and drew `7F 22 31` from the + live ones, meaning those modules are there but do not carry that DID. +- [ ] **Try `2076`-`2079` on `761` and `7D3`**, the two answering modules that + were skipped in the earlier round. + +The spare tire warning proves the sensors exist. Something is receiving them. + +--- + +## Terminal cheat sheet + +The adapter terminal puts you on the CAN bus as a diagnostic tester. Everything +you type is either **a setting that decides who you are talking to and who you +are listening to**, or **a question you ask**. AT commands are settings, handled +by the adapter itself and never sent to the car. Anything else is a question, +put on the bus verbatim. + +### The pattern you keep repeating + +``` +ATSH 726 set header - address requests to this module +ATCRA 72E receive filter - show only this module's replies +22F187 question +22F18C question +22F191 question +``` + +`ATSH hhh` is "Set Header": the 11-bit CAN ID your requests go out with, which +is the module's request address. `ATCRA hhh` is "set CAN Receive Address" — a +filter on what comes back. On this truck every reply address is the request +address plus 8, so `726` is answered by `72E`, `7E0` by `7E8`, `7D3` by `7DB`. + +You need both because they do different jobs. Without `ATCRA` you see every +module that felt like answering, and the adapter sits waiting for more replies +before it gives you the prompt back — slow and noisy. With it, the adapter +returns the moment your module answers. + +The block repeats because **each pair retargets the conversation**. Ask the same +three questions of `726`, then re-point at `716` and ask again, and so on. The +questions stay the same; only the two settings change. + +### The command at the end + +`ATAR` is "Automatic Receive" — it undoes `ATCRA` and lets the adapter choose +the receive address again. **Always finish with it.** Leave a filter set and the +next thing you try is still listening to the last module you probed, so a +perfectly healthy request looks dead. `ATCRA` with no argument resets the +filters the same way. + +### Asking a question + +| You type | Meaning | +|---|---| +| `22 xxxx` | UDS ReadDataByIdentifier — service `22`, then a four-digit DID. This is the one to hunt with. It is read-only. | +| `01 xx` | Legacy OBD-II mode 1 PID. Mostly useless here — the app consumes these internally and they never become recorded signals. | +| `09 02` | VIN. | +| `22F18C` / `22F191` / `22F187` | ECU serial / part number / spare part number. Ask these first of any new module: cheap, and often enough to identify what it is. | + +Stay on service `22`. Services like `10 03` (extended session), `2F` (I/O +control) and `31` (routine control) change ECU state rather than read it. + +### Reading the reply + +A positive answer. This is a real frame from the logs, the reply to `22F405`, +which arrives as `7E80462F40582` with spaces suppressed: + +| Part | Meaning | +|---|---| +| `7E8` | who answered | +| `04` | how many bytes follow | +| `62` | `0x22 + 0x40` — "yes, ReadDataByIdentifier" | +| `F405` | echo of the DID you asked for | +| `82` | the payload, `0x82` = 130, so 130 − 40 = 90 °C coolant | + +A rejection — `7E8 03 7F 22 31`: `7F` means refused, `22` is the service you +asked for, `31` is the reason — request out of range, i.e. this module has no +such DID. **This is the normal answer when hunting** and not a problem. + +`NO DATA` means nothing replied before the timeout: either the module is not +there or the header is wrong. + +Long answers arrive split. Module `726` answering `22F18C` gives +`72E101362F18C353232`, then `72E2134323933383038`, then `72E2200000000000000`. +After the address, `10` marks the first frame and `13` is the total byte count +(19); `21` and `22` are continuation frames counting up. Drop the leading byte +of each continuation and join the rest — here that spells the ECU serial +`5224293808`. + +### Watching instead of asking + +`ATMA` is "Monitor All" — it sends nothing and dumps every frame on the bus. +Good for finding data that modules broadcast without being asked. Reset the +filter with `ATCRA` first or you will only see one ID. Any key stops it. + +### The setup line the app sends + +You do not normally type these, but they explain the noise at the top of a log. + +| Command | Meaning | +|---|---| +| `ATWS` | warm start — reset the adapter without unplugging it | +| `ATE0` | echo off | +| `ATH1` | headers on — **essential**, without it you cannot tell which module replied | +| `ATS0` | drop spaces from the output | +| `ATSP6` | protocol 6: ISO 15765-4 CAN, 11-bit, 500 kbaud | +| `ATAT1` | adaptive timing | +| `ATM0` | memory off | +| `ATFCSM0` | flow control fully automatic | +| `ATDPN` | which protocol is in use — answers `6` here | +| `ATRV` | battery voltage at the socket | + +### Hunting recipe + +1. Point at the module: `ATSH `, `ATCRA `. +2. Ask `22F18C`, `22F191`, `22F187` first. A serial proves the module exists; a + part number often names it. +3. Sweep DIDs. Remember the per-ECU offsets — engine proprietary DIDs sit + +0x300 from base, transmission and rear differential +0x1000 — so an "out of + range" is often the right DID at the wrong offset. +4. Note the payload width of anything that answers `62`. +5. `ATAR` when you move on. +6. Ask the same DID again under different conditions. A value that never moves + tells you nothing, and most of this signalset's unknowns are unknown + precisely because they were only ever read from a parked truck. + +--- + +## Debug parameter criteria + +A DID that moves on a real drive earns a proper decode and a real name. One +that stays flat through a full drive cycle — including a ride-height change +and a terrain-mode switch — can be deleted with evidence rather than on +suspicion. The `freq` 600 tier is the least trustworthy classification in the +table below: every sample behind it came from a parked truck, so "never +moved" so far only means "never moved while parked." + +| Module | DID | `freq` | What we think it is | What would count as a result | +|---|---|---|---|---| +| `761` | `197C` | 30 | Unknown, 4 distinct values in 4 samples on the 2026-09-20 drive — with `D11C`, the most active unmined DIDs on the truck | Any correlation with a cabin or body state | +| `761` | `D11C` | 30 | Unknown, 3A-41 (58-65); 4 distinct values in 4 samples on the 2026-09-20 drive — with `197C`, the most active unmined DIDs on the truck | Plausible temperature; compare against F446 ambient | +| `7D3` | `3B01` | 15 | Solved 2026-09-20. All three bits measured against the front height sensor; now a mapped signal: `0x100` Normal, `0x400` Off-Road, `0x800` Access | Done | +| `7E0` | `F41F` | 30 | Run time since start (PID 1F) | Records as a signal - this is the alias-mechanism test against standard 011F | +| `7E1` | `DD01` | 600 | Undecoded, constant 025166 in 1 samples (all parked) | Drop if still flat after a full drive cycle | +| `7E0` | `F466` | 300 | Raw single-byte probe from the ECM's own supported-PID bitmask decode; no data read yet | Whether it answers at all, and what payload width comes back | +| `7E0` | `F467` | 300 | Raw single-byte probe from the ECM's own supported-PID bitmask decode; no data read yet | Whether it answers at all, and what payload width comes back | +| `7E0` | `F468` | 300 | Raw single-byte probe from the ECM's own supported-PID bitmask decode; no data read yet | Whether it answers at all, and what payload width comes back | +| `7E0` | `F470` | 300 | Raw single-byte probe from the ECM's own supported-PID bitmask decode; no data read yet | Whether it answers at all, and what payload width comes back | + +**Deleted and restored 2026-09-20:** `726/0202`, all eleven of `792`'s `2A3x` DIDs (`2A32`-`2A3C`), `795/1E88`, `795/1E89`, `7E1/1E6A`, `7D3/3B00`, `7D3/3B08`, `7D3/3B4D` and `7E0/F458` — nineteen commands, removed for being constant and then put straight back. The evidence did not hold up: most had between 6 and 30 samples, and `1E88`'s six samples were all taken with the differential unlocked. Even `3B4D`, the one case that looked conclusive at 236 samples, was wrong: it produced three distinct values within eight minutes once the off-road features were actually used. `F458` was simply misread — `62F458 7F` is a positive reply carrying `0x7F`, not a negative response, and it is now decoded as long term secondary O2 trim bank 2. **The rule going forward: a probe needs enough samples to have covered the event it would report, not merely a lot of samples. When in doubt, park it at 600s.** + +--- + +## Set up for a long drive with off-roading + +A two-hour drive with light off-roading on 2026-09-20 was set up to exercise +the features that short commuter runs never touch. What was re-tiered for it, +and what to look for afterwards: + +| Command | `freq` | What the drive was meant to produce | +|---|---|---| +| `7E0/F466` | 5 | Whether the 17.5% disagreement between the two mass airflow sensors holds up under load, or was an idle artifact | +| `795/1E88`, `795/1E89` | 30 | First data through a rear differential lock cycle. Both have been flat at `0000` and `09C4` on every sample ever taken | +| `7D3/3B4D` | 30 | Answered 2026-09-20: `0x04`, `0x01`, `0x00` within eight minutes of the off-road features being used. Needs one sample per terrain mode to map | +| `7D3/3B01` | 15 | Three single-bit values seen so far. Enough samples across repeated height changes should map bit to height — done, see above | +| `7E0/F470` | 15 | Ten undecoded bytes on a supercharged engine, under sustained load for the first time | +| `7E0/F42E` | 10 | Purge duty against fuel trims over two hours, which is the cleanest test of the stuck-purge-valve hypothesis | + +Barometric pressure dropped from `freq` 1 to `freq` 5 to pay for the above. +It does not change at 1 Hz and was taking 0.9 req/s of a budget that had no +slack. Total demand is 10.53 req/s across 85 commands. + +**Outcome, found in the scan logs afterward:** this is the drive that caused +the working-set discovery described at the top of this file. The app addressed +every module at 09:57, right after a signalset change, then narrowed +through 54, 58 and 19 distinct DIDs as the day went on, and was down to a +23-command working set by the time the 15:14 off-road segment started. +10.53 req/s across 85 commands was never a demand the app could sustain; it +only ever delivers about 4.2 req/s to signalset traffic. The suspension and +differential re-tiering above (`7D3/3B01`, `7D3/3B4D`, `795/1E88`, +`795/1E89`) collected nothing, because none of those commands were in that +working set. Worth keeping in proportion: 23 distinct DIDs is the widest +coverage any long session has managed since 09-05, so the drive was not an +unusually bad one. The budget +has since been cut to 3.922 req/s across 66 commands — see the verification +task in "Open in-vehicle tasks" above. + +Three rows in that table are now answered, from earlier sessions in the same +log rather than from the drive itself. `7D3/3B4D` looked dead — constant +`0x00` across 236 samples — but that verdict did not survive the evening of +2026-09-20, when deliberately using the off-road features produced `0x04`, +`0x01` and `0x00` in eight minutes. The 236 samples had simply never +covered the event. `795/1E88` and +`795/1E89` are *not* settled on the same evidence — six and 25 samples, none +with the differential locked — and both are back at 60s to catch a real +lock cycle. `7E0/F466`'s +17.5% disagreement did not survive contact with data: across 1,474 samples +the two channels sum to 0.88 of the single mass airflow PID by least squares, +with individual pairs scattering far too widely to trust the split. The +17.5% figure came from one idle sample. `7E0/F470` turned out to be manifold +pressure at 1/32 kPa rather than a separate boost sensor, tracking the +manifold pressure PID to within 1.3 kPa over 525 pairs. + +## Manual-probe-only DIDs + +The 2026-09-20 drive's biggest finding: 36 DIDs return `7F 22 31` (request +out of range) on every normal poll and always have. Across fourteen months +of logs they have answered only twice — once in December 2025, in a session +that had sent `10 03` (extended diagnostic session), and once in June 2025, +in a session with unusual protocol and flow-control setup. Every session +since that did not open an extended session first has been rejected. + +**They are session-gated, not dead.** The app polls in the default session +and can never reach them. All 36 have been removed from the signalset — +this is why the command count dropped from 109 to 73 on this drive — and +they are now readable only by hand. + +`7E0`: `1044`, `10E0`, `112C`, `1139`, `113F`, `1151`, `1152`, `1153`, +`1154`, `1155`, `1156`, `1158`, `1159`, `115B`, `1160`, `1164`, `1187`, +`11BA`, `11BB`, `11BD`, `11BE`, `11C4`, `11C5`, `11CC` + +`7E1`: `0302`, `0303`, `0304`, `0305`, `0805`, `101A`, `2104`, `210F`, +`2B1B`, `2B1C`, `2B1D`, `2B1E` + +The two most interesting: `1153` and `1154`, a charging-voltage pair at +1/256 V. `1154` sits pinned at exactly `0E00` (14.00 V); both dip to +5.7-10.1 V on crank. + +### Reading them + +`10 03` is Diagnostic Session Control, sub-function `03` — extended +diagnostic session. **This is the one place in this document where a +command changes ECU state instead of reading it.** Do it deliberately: +parked, engine running, never while driving — and close the session +afterward with `10 01` (return to default session). + +The extended session times out after a few seconds of silence unless kept +alive with `3E 00` (tester present). That matters here: probe a long list +without it and the session drops back to default between questions, and +every DID on it goes back to answering `7F 22 31`. + +``` +ATSH 7E0 set header +ATCRA 7E8 receive filter +10 03 open extended diagnostic session +3E 00 tester present - repeat every few seconds to hold the session +22 1153 question +22 1154 question +... more questions, with 3E 00 between any pause +10 01 return to default session +``` + +Retarget `ATSH`/`ATCRA` to `7E1`/`7E9` for the second module and open a +fresh `10 03` there — the extended session is per-module, not global. + +--- + +## How to check results + +Sidecar writes zipped SQLite backups to +`~/Library/Mobile Documents/iCloud~com~featherless~apps~electricsidecar/DataBackups/` +on macOS. `scanSessions` holds every raw request and response; `signals` holds +decoded values. Backups lag live data by up to a day, so check the day after a +drive, not immediately after. + +The useful check after a drive, per DID: how many distinct payloads came back, +and over what range. A DID that returned the same byte string every time is +still flat. A DID that returned two or three distinct values is worth a second +drive before deciding anything. A DID that swept a range correlated with +something else on the bus (RPM, load, ride height, gear) is ready for a real +decode. + +--- + +## Known-unresolved + +Open questions that aren't in-vehicle tasks — they need log analysis or a +decision, not a drive: + +- Both banks are correcting lean together by 7-9% long term (B1 +9.31%, B2 +7.32%), which points at fuel delivery, MAF calibration or an unmetered air leak rather than a per-bank fault. One drive, single-digit sample counts — needs confirming. +- What `761/197C` and `761/D11C` measure. diff --git a/signalsets/v3/default.json b/signalsets/v3/default.json index 7176ccf..32b1071 100644 --- a/signalsets/v3/default.json +++ b/signalsets/v3/default.json @@ -1,4 +1,492 @@ { "commands": [ - +{ "hdr": "732", "cmd": {"22": "D928"}, "freq": 10, + "signals": [ + {"id": "LR4_GEAR_SELECTOR", "path": "Transmission", "name": "Gear Selector", "fmt": {"len": 8, "map": { + "0": { "description": "Park", "value": "Park" }, + "1": { "description": "Reverse", "value": "Reverse" }, + "2": { "description": "Neutral", "value": "Neutral" }, + "3": { "description": "Drive", "value": "Drive" }, + "7": { "description": "Sport", "value": "Sport" } + }} + } + ]}, +{ "hdr": "761", "rax": "769", "cmd": {"22": "197C"}, "freq": 120, + "signals": [ + {"id": "LR4_197C_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 16, "max": 65535, "unit": "scalar" }, "name": "197C Raw", "description": "Undecoded. Nine distinct values in fourteen samples, the most variable unmined DID on the truck."} + ]}, +{ "hdr": "761", "rax": "769", "cmd": {"22": "D11C"}, "freq": 120, + "signals": [ + {"id": "LR4_D11C_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "D11C Raw", "description": "Undecoded. Ranges 0x3A to 0x41, which would be a plausible temperature."} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B01"}, "freq": 15, + "signals": [ + {"id": "LR4_RIDE_HEIGHT_STATE", "path": "Suspension", "name": "Ride Height State", "description": "Runs in parallel with the ride height mode PID and agrees with it. All three observed bits are measured against the front height sensor: 0x100 at a front reading around 110 (Normal), 0x400 around 85 with the truck at its highest (Off-Road), 0x800 at 133 with it at its lowest (Access). Remember the sensor is inverted. Bit 9 (0x200) sits between Normal and Off-Road and has never been seen in 142 samples; the held-Access mode, where the lower button is held and the truck stays down, is the obvious candidate and has never been captured.", "fmt": {"len": 32, "map": { + "256": { "description": "Normal", "value": "Normal" }, + "1024": { "description": "Off-Road", "value": "Off-Road" }, + "2048": { "description": "Access", "value": "Access" } + }} + }, + {"id": "LR4_RIDE_HEIGHT_STATE_RAW", "path": "Suspension", "fmt": { "len": 32, "max": 4294967295, "unit": "scalar" }, "name": "Ride Height State Raw", "description": "Untranslated twin of Ride Height State. A mapped signal renders nothing for a value it has not been taught, so an unmapped state would be invisible in the app and only recoverable from the scan logs. This costs no extra request and makes a new state show up as a number."} + ]}, +{ "hdr": "7E1", "rax": "7E9", "cmd": {"22": "DD01"}, "freq": 600, + "signals": [ + {"id": "LR4_DD01_RAW", "hidden": true, "path": "Drivetrain", "fmt": { "len": 24, "max": 16777215, "unit": "scalar" }, "name": "DD01 Raw"} + ]}, +{ "hdr": "795", "cmd": {"22": "1E8A"}, "freq": 30, + "signals": [ + {"id": "LR4_REAR_DIFF_TEMP", "path": "Drivetrain", "fmt": { "len": 8, "max": 150, "min": -40, "add": -40, "unit": "celsius" }, "name": "Locking Diff Oil Temp"} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "2B12"}, "freq": 60, + "signals": [ + {"id": "LR4_SUSPENSION_2B12", "path": "Suspension", "fmt": { "len": 16, "max": 32767, "min": -32768, "sign": true, "unit": "millimeters" }, "name": "Suspension Height Offset"} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B02"}, "freq": 60, + "signals": [ + {"id": "LR4_3B02_B0", "hidden": true, "path": "Suspension", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "3B02 Byte 1"}, + {"id": "LR4_3B02_B1", "hidden": true, "path": "Suspension", "fmt": {"bix": 8, "len": 8, "max": 255, "unit": "scalar" }, "name": "3B02 Byte 2"}, + {"id": "LR4_3B02_B2", "hidden": true, "path": "Suspension", "fmt": {"bix": 16, "len": 8, "max": 255, "unit": "scalar" }, "name": "3B02 Byte 3"}, + {"id": "LR4_3B02_B3", "hidden": true, "path": "Suspension", "fmt": {"bix": 24, "len": 8, "max": 255, "unit": "scalar" }, "name": "3B02 Byte 4"} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B03"}, "freq": 30, + "signals": [ + {"id": "LR4_SUSPENSION_PRES_FR", "path": "Suspension", "fmt": {"bix": 8, "len": 8, "max": 51, "div": 5, "nullmin": 0, "unit": "psi" }, "name": "Suspension Pressure Front Right"} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B04"}, "freq": 30, + "signals": [ + {"id": "LR4_SUSPENSION_PRES_FL", "path": "Suspension", "fmt": {"bix": 8, "len": 8, "max": 51, "div": 5, "nullmin": 0, "unit": "psi" }, "name": "Suspension Pressure Front Left"} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B05"}, "freq": 30, + "signals": [ + {"id": "LR4_SUSPENSION_PRES_RR", "path": "Suspension", "fmt": {"bix": 8, "len": 8, "max": 51, "div": 5, "nullmin": 0, "unit": "psi" }, "name": "Suspension Pressure Rear Right"} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B06"}, "freq": 30, + "signals": [ + {"id": "LR4_SUSPENSION_PRES_RL", "path": "Suspension", "fmt": {"bix": 8, "len": 8, "max": 51, "div": 5, "nullmin": 0, "unit": "psi" }, "name": "Suspension Pressure Rear Left"} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B07"}, "freq": 30, + "signals": [ + {"id": "LR4_SUSPENSION_COMPRESSOR", "path": "Suspension", "fmt": { "len": 16, "max": 65535, "nullmin": 0, "unit": "scalar" }, "name": "Compressor Activity"} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B0B"}, "freq": 60, + "signals": [ + {"id": "LR4_3B0B_RAW", "hidden": true, "path": "Suspension", "fmt": { "len": 16, "max": 65535, "nullmin": 0, "unit": "scalar" }, "name": "3B0B Raw", "description": "Undecoded suspension DID, narrow range around 0x3D5."} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B3C"}, "freq": 10, + "signals": [ + {"id": "LR4_RIDE_HEIGHT", "path": "Suspension", "name": "Ride Height Mode", "description": "A bitfield, not an ordinal: bit 0 Normal, bit 1 Off-Road, bit 2 Access. 0x0D is 0x01|0x04|0x08 and has only been seen with the truck part-way between Normal and Access, so bit 3 reads as movement rather than a mode of its own. Bit 3 has never been seen set on its own, which is where a held-Access mode would most likely appear.", "fmt": {"len": 8, "map": { + "1": { "description": "Normal", "value": "Normal" }, + "2": { "description": "Off-Road", "value": "Off-Road" }, + "4": { "description": "Access", "value": "Access" }, + "13": { "description": "In Transit", "value": "In Transit" } + }} + }, + {"id": "LR4_RIDE_HEIGHT_RAW", "path": "Suspension", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "Ride Height Mode Raw", "description": "Untranslated twin of Ride Height Mode, for the same reason as the state word: an unmapped value renders as a blank, and a blank teaches nothing."} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B71"}, "freq": 15, + "signals": [ + {"id": "LR4_RIDE_HEIGHT_FRONT", "path": "Suspension", "fmt": { "len": 8, "max": 255, "nullmin": 0, "unit": "scalar" }, "name": "Height Sensor Front (inverted)", "description": "Inverted: a falling value means the truck is rising."} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B72"}, "freq": 15, + "signals": [ + {"id": "LR4_RIDE_HEIGHT_REAR", "path": "Suspension", "fmt": { "len": 8, "max": 255, "nullmin": 0, "unit": "scalar" }, "name": "Height Sensor Rear (inverted)", "description": "Inverted: a falling value means the truck is rising."} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "D11A"}, "freq": 60, + "signals": [ + {"id": "LR4_D11A_RAW", "path": "Suspension", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "D11A Raw", "description": "Was called Suspension Module Voltage and scaled to volts. It is not battery voltage: across 221 paired samples its raw range spans 57 to 105, an 84% swing, while control module voltage moved only 12.51 to 14.75, an 18% swing, and the implied scale scatters by 9%. The old scaling also put it at 16.8 V, which no 12 V system reaches. Best correlation found so far is 0.58 against compressor activity, which is not enough to name it. Undecoded until something fits."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "033E"}, "freq": 30, + "signals": [ + {"id": "LR4_FUEL_RAIL_P", "path": "Fuel", "fmt": { "len": 16, "max": 30000, "unit": "kilopascal" }, "name": "Fuel Rail Pressure"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "03E6"}, "freq": 600, + "signals": [ + {"id": "LR4_MEASURED_OIL_LEVEL", "path": "Engine", "fmt": { "len": 8, "max": 255, "unit": "millimeters" }, "name": "Oil Level"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "03F2"}, "freq": 600, + "signals": [ + {"id": "LR4_ENGINE_OIL_VOLUME_GAL", "path": "Engine", "fmt": { "len": 8, "max": 6.74, "mul": 0.0264172, "unit": "gallons" }, "name": "Oil Volume"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "03F3"}, "freq": 30, + "signals": [ + {"id": "LR4_ENGINE_OIL_TEMP", "path": "Engine", "fmt": { "len": 8, "max": 120, "min": -40, "add": -40, "unit": "celsius" }, "name": "Oil Temp", "suggestedMetric": "engineOilTemperature"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "0520"}, "freq": 30, + "signals": [ + {"id": "LR4_CHARGE_AIR_TEMP", "path": "Engine", "fmt": { "len": 8, "max": 215, "min": -40, "add": -40, "unit": "celsius" }, "name": "Charge Air Temp"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "DD01"}, "freq": 600, + "signals": [ + {"id": "LR4_ODOMETER", "path": "Trips", "fmt": { "len": 24, "max": 16777215, "nullmin": 0, "unit": "kilometers" }, "name": "Odometer", "suggestedMetric": "odometer"} + ]}, +{ "hdr": "726", "rax": "72E", "cmd": {"22": "0202"}, "freq": 600, + "signals": [ + {"id": "LR4_726_0202_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "726 0202 Raw", "description": "The only non-identification DID ever found on module 726, from a sweep of 0000-03FF. Has only ever returned 00."} + ]}, +{ "hdr": "7E1", "rax": "7E9", "cmd": {"22": "1E6A"}, "freq": 600, + "signals": [ + {"id": "LR4_1E6A_RAW", "hidden": true, "path": "Drivetrain", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "1E6A Raw", "description": "Undecoded transmission DID, neighbour of the gearbox temperature."} + ]}, +{ "hdr": "795", "rax": "79D", "cmd": {"22": "1E88"}, "freq": 60, + "signals": [ + {"id": "LR4_1E88_RAW", "hidden": true, "path": "Drivetrain", "fmt": { "len": 16, "max": 65535, "unit": "scalar" }, "name": "1E88 Raw", "description": "Undecoded rear differential DID. Flat at 0000 on every sample so far; a lock cycle is the test."} + ]}, +{ "hdr": "795", "rax": "79D", "cmd": {"22": "1E89"}, "freq": 60, + "signals": [ + {"id": "LR4_1E89_RAW", "hidden": true, "path": "Drivetrain", "fmt": { "len": 16, "max": 65535, "unit": "scalar" }, "name": "1E89 Raw", "description": "Undecoded rear differential DID. Flat at 09C4 on every sample so far."} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A32"}, "freq": 120, + "signals": [ + {"id": "LR4_2A32_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 32, "max": 4294967295, "unit": "scalar" }, "name": "2A32 Raw", "description": "A counter, not a constant. Held 404720 across 19 samples taken over a single morning and was written off; nine hours and 111 km later it read 420200. 2A32 through 2A37 all advance. What they count is unknown - 2A32 gained 126 per km, 2A33 4.3, 2A34 8.6, 2A35 19.5, 2A36 0.8, 2A37 0.009 - and pinning it needs several samples within one drive, which is why these now poll at 120s."} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A33"}, "freq": 120, + "signals": [ + {"id": "LR4_2A33_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 32, "max": 4294967295, "unit": "scalar" }, "name": "2A33 Raw"} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A34"}, "freq": 120, + "signals": [ + {"id": "LR4_2A34_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 32, "max": 4294967295, "unit": "scalar" }, "name": "2A34 Raw"} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A35"}, "freq": 120, + "signals": [ + {"id": "LR4_2A35_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 32, "max": 4294967295, "unit": "scalar" }, "name": "2A35 Raw"} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A36"}, "freq": 120, + "signals": [ + {"id": "LR4_2A36_RAW", "hidden": true, "path": "Tires", "fmt": { "len": 24, "max": 16777215, "nullmin": 0, "unit": "scalar" }, "name": "2A36 Distance Counter"} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A37"}, "freq": 120, + "signals": [ + {"id": "LR4_2A37_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 24, "max": 16777215, "unit": "scalar" }, "name": "2A37 Raw"} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A38"}, "freq": 600, + "signals": [ + {"id": "LR4_2A38_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 24, "max": 16777215, "unit": "scalar" }, "name": "2A38 Raw"} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A39"}, "freq": 600, + "signals": [ + {"id": "LR4_2A39_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 24, "max": 16777215, "unit": "scalar" }, "name": "2A39 Raw"} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A3A"}, "freq": 600, + "signals": [ + {"id": "LR4_2A3A_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 24, "max": 16777215, "unit": "scalar" }, "name": "2A3A Raw"} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A3B"}, "freq": 600, + "signals": [ + {"id": "LR4_2A3B_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 32, "max": 4294967295, "unit": "scalar" }, "name": "2A3B Raw"} + ]}, +{ "hdr": "792", "rax": "79A", "cmd": {"22": "2A3C"}, "freq": 600, + "signals": [ + {"id": "LR4_2A3C_RAW", "hidden": true, "path": "ECU", "fmt": { "len": 24, "max": 16777215, "unit": "scalar" }, "name": "2A3C Raw"} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B00"}, "freq": 120, + "signals": [ + {"id": "LR4_3B00_RAW", "hidden": true, "path": "Suspension", "fmt": { "len": 32, "max": 4294967295, "unit": "scalar" }, "name": "3B00 Raw", "description": "Undecoded. Reads 0x03 in 111 of 113 samples; the exception is 0x103, once, with the truck sitting at Access. Bit 8 looks like a height-event flag, and 3B02 carries a bit-8 flag that behaves the same way."} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B08"}, "freq": 120, + "signals": [ + {"id": "LR4_3B08_RAW", "hidden": true, "path": "Suspension", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "3B08 Raw", "description": "Undecoded. Reads 0x00 in 130 of 131 samples; the exception is 0x01, once, with Off-Road engaged and the truck at its highest. One sample is not a decode, but it is not dead either."} + ]}, +{ "hdr": "7D3", "rax": "7DB", "cmd": {"22": "3B4D"}, "freq": 30, + "signals": [ + {"id": "LR4_3B4D_RAW", "hidden": true, "path": "Suspension", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "3B4D Raw", "description": "Terrain Response candidate, and back in contention. It held a constant 0x00 across 236 samples and was written off as dead; the first short drive on which the off-road features were deliberately exercised produced 0x04, 0x01 and 0x00 within eight minutes. It does not track the ride height mode PID, so it is reporting something else. Needs one sample per selectable terrain mode to map."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F458"}, "freq": 120, + "signals": [ + {"id": "LR4_SEC_O2_TRIM_B2", "path": "Fuel", "fmt": { "len": 8, "max": 99.2, "min": -100, "mul": 100, "div": 128, "add": -100, "unit": "percent" }, "name": "Long Term Secondary O2 Trim B2", "description": "SAE J1979 PID 58, the bank 2 counterpart to PID 56. Previously carried as an undecoded raw byte and briefly deleted on the mistaken reading that its single logged reply was a negative response; 62F458 7F is a positive reply meaning -0.78%."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F423"}, "freq": 60, + "signals": [ + {"id": "LR4_FUEL_RAIL_GAUGE_P", "path": "Fuel", "fmt": { "len": 16, "max": 655350, "mul": 10, "unit": "kilopascal" }, "name": "Fuel Rail Gauge Pressure", "description": "SAE J1979 PID 23, ranging 29 to 197 bar, the right shape for this engine's direct injection rail. It is not the same measurement as the proprietary DID 033E: across 157 paired samples the ratio between them drifts from 0.41 to 0.56 rather than holding constant, and in one stretch this PID sat pinned near 197 bar while 033E climbed from 79 to 110 bar. A fixed scaling error would give a fixed ratio, so these are two different quantities - plausibly a commanded rail target against a measured one. Worth sampling hard under load to separate them."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F401"}, "freq": 600, + "signals": [ + {"id": "LR4_MONITOR_STATUS", "hidden": true, "path": "ECU", "fmt": { "len": 32, "max": 4294967295, "unit": "scalar" }, "name": "Monitor Status Raw", "description": "SAE J1979 PID 01: MIL state, stored DTC count and readiness monitors. Listed as supported by the ECM's own bitmask but never once requested. Top byte is the DTC count with the MIL bit; a non-zero value here is the cheapest possible check for a stored fault."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F441"}, "freq": 600, + "signals": [ + {"id": "LR4_MONITOR_THIS_CYCLE", "hidden": true, "path": "ECU", "fmt": { "len": 32, "max": 4294967295, "unit": "scalar" }, "name": "Monitor Status This Cycle Raw", "description": "SAE J1979 PID 41, readiness monitors for the current drive cycle. Supported per the ECM bitmask, never requested. Changes across a drive as monitors complete, so unlike PID 01 it should not be static."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F413"}, "freq": 600, + "signals": [ + {"id": "LR4_O2_SENSORS_PRESENT", "hidden": true, "path": "ECU", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "O2 Sensors Present Raw", "description": "SAE J1979 PID 13, a bitmask of which oxygen sensor positions exist. Answered 0x77 on its one logged sample. Static, but it says exactly which O2 PIDs are worth probing."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F451"}, "freq": 600, + "signals": [ + {"id": "LR4_FUEL_TYPE", "hidden": true, "path": "Fuel", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "Fuel Type Raw", "description": "SAE J1979 PID 51. Supported per the ECM bitmask, never requested. Static, one sample settles it."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F41C"}, "freq": 600, + "signals": [ + {"id": "LR4_OBD_STANDARD", "hidden": true, "path": "ECU", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "OBD Standard Raw", "description": "SAE J1979 PID 1C, which OBD standard this ECM claims. Supported per the bitmask, never requested. Static."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F403"}, "freq": 120, + "signals": [ + {"id": "LR4_FUEL_SYS_B1", "path": "Fuel", "fmt": { "len": 8, "max": 255, "map": { "1": { "description": "Open loop, engine cold", "value": "OpenLoopCold" }, "2": { "description": "Closed loop", "value": "ClosedLoop" }, "4": { "description": "Open loop, load or decel fuel cut", "value": "OpenLoopLoad" }, "8": { "description": "Open loop, system fault", "value": "OpenLoopFault" }, "16": { "description": "Closed loop with a feedback fault", "value": "ClosedLoopFault" } } }, "name": "Fuel System Status B1"}, + {"id": "LR4_FUEL_SYS_B2", "path": "Fuel", "fmt": { "bix": 8, "len": 8, "max": 255, "unit": "scalar" }, "name": "Fuel System Status B2", "description": "Bank 2 fuel system status. Has mirrored bank 1 on every sample so far."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F438"}, "freq": 60, + "signals": [ + {"id": "LR4_O2_LAMBDA_B2", "path": "Engine", "fmt": { "len": 16, "max": 1.999, "div": 32768, "unit": "scalar" }, "name": "O2 Lambda B2S1", "description": "Bank 2 sensor 1. Rails at FFFF on roughly half of samples; treat those as invalid rather than lean."}, + {"id": "LR4_O2_CURRENT_B2", "path": "Engine", "fmt": { "bix": 16, "len": 16, "max": 128, "min": -128, "div": 256, "add": -128, "unit": "milliamps" }, "name": "O2 Pump Current B2S1", "description": "Bank 2 counterpart. See O2 Pump Current B1S1 - PID 38 reports current, not voltage."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F42E"}, "freq": 120, + "signals": [ + {"id": "LR4_EVAP_PURGE", "path": "Fuel", "fmt": { "len": 8, "max": 100, "mul": 100, "div": 255, "unit": "percent" }, "name": "Commanded Evap Purge"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F415"}, "freq": 120, + "signals": [ + {"id": "LR4_O2_S2_VOLTAGE", "path": "Engine", "fmt": { "len": 8, "max": 1.275, "div": 200, "unit": "volts" }, "name": "O2 Voltage B1S2"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F456"}, "freq": 120, + "signals": [ + {"id": "LR4_SEC_O2_TRIM_B1", "path": "Fuel", "fmt": { "len": 8, "max": 99.2, "min": -100, "mul": 100, "div": 128, "add": -100, "unit": "percent" }, "name": "Long Term Secondary O2 Trim B1"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F445"}, "freq": 120, + "signals": [ + {"id": "LR4_THROTTLE_REL", "path": "Engine", "fmt": { "len": 8, "max": 100, "mul": 100, "div": 255, "unit": "percent" }, "name": "Relative Throttle Position"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F447"}, "freq": 120, + "signals": [ + {"id": "LR4_THROTTLE_B", "path": "Engine", "fmt": { "len": 8, "max": 100, "mul": 100, "div": 255, "unit": "percent" }, "name": "Absolute Throttle Position B"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F466"}, "freq": 60, + "signals": [ + {"id": "LR4_MAF_A", "path": "Engine", "fmt": { "bix": 8, "len": 16, "max": 2047.96, "div": 32, "unit": "gramsPerSecond" }, "name": "Mass Air Flow A", "description": "One of two intake tract sensors. Across 1,474 samples A and B sum to 0.88 of the single MAF PID by least squares, but the scatter between individual pairs is large enough that the split between the two is not trustworthy at this polling rate. Use the single MAF PID for anything quantitative."}, + {"id": "LR4_MAF_B", "path": "Engine", "fmt": { "bix": 24, "len": 16, "max": 2047.96, "div": 32, "unit": "gramsPerSecond" }, "name": "Mass Air Flow B", "description": "One of two intake tract sensors. See Mass Air Flow A."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F467"}, "freq": 60, + "signals": [ + {"id": "LR4_CAC_COOLANT_TEMP", "path": "Engine", "fmt": { "bix": 16, "len": 8, "max": 215, "min": -40, "add": -40, "unit": "celsius" }, "name": "Charge Cooler Coolant Temp", "description": "The AJ126 runs a separate low-temperature circuit for the supercharger intercooler. Over 135 paired samples this sat 11.7 C above ambient and 24 C below charge air temperature, which is where that circuit belongs. Sensor 1 of the same PID was dropped: it matched the engine coolant PID to 0.0 C."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F468"}, "freq": 120, + "signals": [ + {"id": "LR4_IAT_1", "path": "Engine", "fmt": { "bix": 8, "len": 8, "max": 215, "min": -40, "add": -40, "unit": "celsius" }, "name": "Intake Air Temp Sensor 1", "description": "Pre-supercharger inlet air, measured 9.2 C above ambient over 135 samples."}, + {"id": "LR4_IAT_2", "path": "Engine", "fmt": { "bix": 16, "len": 8, "max": 215, "min": -40, "add": -40, "unit": "celsius" }, "name": "Intake Air Temp Sensor 2", "description": "Post-supercharger charge air. Tracks the proprietary charge air temperature DID to within 2.7 C on average, so it is close to redundant with it."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F470"}, "freq": 120, + "signals": [ + {"id": "LR4_BOOST_A", "path": "Engine", "fmt": { "bix": 24, "len": 16, "max": 2047.96875, "div": 32, "unit": "kilopascal" }, "name": "Manifold Pressure (Fine)", "description": "SAE J1979 PID 70 channel A. Over 525 samples it tracks the 1 kPa manifold pressure PID to within 1.3 kPa on average, so it is the same measurement at 1/32 kPa resolution, not a separate pre-throttle sensor. The other three channels the PID defines read a constant zero and have been removed."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F404"}, "freq": 5, + "signals": [ + {"id": "LR4_LOAD_CALC", "path": "Engine", "fmt": { "len": 8, "max": 100, "mul": 100, "div": 255, "unit": "percent" }, "name": "Calculated Load", "suggestedMetric": "engineLoad"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F405"}, "freq": 15, + "signals": [ + {"id": "LR4_COOLANT_TEMP", "path": "Engine", "fmt": { "len": 8, "max": 215, "min": -40, "add": -40, "unit": "celsius" }, "name": "Coolant Temp", "suggestedMetric": "engineCoolantTemperature"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F406"}, "freq": 15, + "signals": [ + {"id": "LR4_SHRTFT1", "path": "Fuel", "fmt": { "len": 8, "max": 99.2, "min": -100, "mul": 100, "div": 128, "add": -100, "unit": "percent" }, "name": "Short Term Fuel Trim B1", "suggestedMetric": "shortTermFuelTrim"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F407"}, "freq": 30, + "signals": [ + {"id": "LR4_LONGFT1", "path": "Fuel", "fmt": { "len": 8, "max": 99.2, "min": -100, "mul": 100, "div": 128, "add": -100, "unit": "percent" }, "name": "Long Term Fuel Trim B1"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F408"}, "freq": 30, + "signals": [ + {"id": "LR4_SHRTFT2", "path": "Fuel", "fmt": { "len": 8, "max": 99.2, "min": -100, "mul": 100, "div": 128, "add": -100, "unit": "percent" }, "name": "Short Term Fuel Trim B2"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F409"}, "freq": 30, + "signals": [ + {"id": "LR4_LONGFT2", "path": "Fuel", "fmt": { "len": 8, "max": 99.2, "min": -100, "mul": 100, "div": 128, "add": -100, "unit": "percent" }, "name": "Long Term Fuel Trim B2", "description": "Bank 2. There is no long-term fuel trim metric slot, so this does not surface as a connectable."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F40B"}, "freq": 5, + "signals": [ + {"id": "LR4_MAP", "path": "Engine", "fmt": { "len": 8, "max": 255, "unit": "kilopascal" }, "name": "Manifold Pressure"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F40C"}, "freq": 2, + "signals": [ + {"id": "LR4_RPM", "path": "Engine", "fmt": { "len": 16, "max": 16383.75, "div": 4, "unit": "rpm" }, "name": "Engine Speed", "suggestedMetric": "engineSpeed"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F40D"}, "freq": 2, + "signals": [ + {"id": "LR4_SPEED", "path": "Movement", "fmt": { "len": 8, "max": 255, "unit": "kilometersPerHour" }, "name": "Vehicle Speed", "suggestedMetric": "speed"}, + {"id": "LR4_SPEED_MPH", "hidden": true, "path": "Movement", "fmt": { "len": 8, "max": 158.4, "mul": 0.621371, "unit": "milesPerHour" }, "name": "Vehicle Speed (mph operand)", "description": "Speed in mph as a synthetic operand. Divided by the gal/h fuel rate it gives lambda-corrected mpg, if the app lets a synthetic read another synthetic. That is untested; see Fuel Economy (lambda-corrected, test)."}, + {"id": "LR4_MPG_NUMERATOR", "hidden": true, "path": "Movement", "fmt": { "len": 8, "max": 1824.6, "mul": 7.15535, "unit": "scalar" }, "name": "Fuel Economy Numerator", "description": "Speed in mph premultiplied by 14.7 x 745 / 3600 x 3.785, so that dividing it by mass air flow gives mpg at lambda 1. The synthetic ratio has room for only two operands, so commanded lambda is assumed stoichiometric. Closed-loop cruise is; enrichment and fuel cut are not."}, + {"id": "LR4_GP100_DENOMINATOR", "hidden": true, "path": "Movement", "fmt": { "len": 8, "max": 18.25, "mul": 0.0715535, "unit": "scalar" }, "name": "Gallons per 100 Miles Denominator", "description": "Speed in mph times 14.7 x 745 / 3600 x 3.785 / 100, so that mass air flow divided by it gives US gallons per 100 miles at lambda 1."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F40E"}, "freq": 30, + "signals": [ + {"id": "LR4_SPARK_ADV", "path": "Engine", "fmt": { "len": 8, "max": 63.5, "min": -64, "div": 2, "add": -64, "unit": "degrees" }, "name": "Timing Advance"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F40F"}, "freq": 60, + "signals": [ + {"id": "LR4_IAT", "path": "Engine", "fmt": { "len": 8, "max": 215, "min": -40, "add": -40, "unit": "celsius" }, "name": "Intake Air Temp"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F410"}, "freq": 2, + "signals": [ + {"id": "LR4_MAF", "path": "Engine", "fmt": { "len": 16, "max": 655.35, "div": 100, "unit": "gramsPerSecond" }, "name": "Mass Air Flow", "suggestedMetric": "massAirFlow"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F411"}, "freq": 10, + "signals": [ + {"id": "LR4_THROTTLE_POS", "path": "Engine", "fmt": { "len": 8, "max": 100, "mul": 100, "div": 255, "unit": "percent" }, "name": "Throttle Position", "suggestedMetric": "throttlePosition"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F41F"}, "freq": 120, + "signals": [ + {"id": "LR4_RUN_TIME", "path": "Engine", "fmt": { "len": 16, "max": 65535, "unit": "seconds" }, "name": "Run Time Since Engine Start"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F421"}, "freq": 600, + "signals": [ + {"id": "LR4_MIL_DIST", "path": "Trips", "fmt": { "len": 16, "max": 65535, "unit": "kilometers" }, "name": "Distance With MIL On"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F42F"}, "freq": 30, + "signals": [ + {"id": "LR4_FUEL_LEVEL", "path": "Fuel", "fmt": { "len": 8, "max": 100, "mul": 100, "div": 255, "unit": "percent" }, "name": "Fuel Level", "suggestedMetric": "fuelTankLevel"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F430"}, "freq": 600, + "signals": [ + {"id": "LR4_WARMUPS", "path": "Trips", "fmt": { "len": 8, "max": 255, "unit": "scalar" }, "name": "Warm-ups Since Codes Cleared", "description": "Saturated. The PID is a single byte and has been pinned at 255 for as long as it has been logged, so it says only that codes were cleared more than 255 warm-up cycles ago."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F431"}, "freq": 300, + "signals": [ + {"id": "LR4_DIST_SINCE_CLR", "path": "Trips", "fmt": { "len": 16, "max": 65535, "nullmin": 0, "unit": "kilometers" }, "name": "Distance Since Codes Cleared", "suggestedMetric": "distanceSinceDTCsCleared"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F433"}, "freq": 60, + "signals": [ + {"id": "LR4_BARO", "path": "Engine", "fmt": { "len": 8, "max": 255, "unit": "kilopascal" }, "name": "Barometric Pressure"}, + {"id": "LR4_ALTITUDE", "path": "Movement", "name": "Altitude", "description": "Derived from barometric pressure by lookup, not from GPS. Accurate to a few hundred feet and wrong in unusual weather.", "fmt": {"len": 8, "map": { + "55": { "description": "15,955 ft", "value": "15,955 ft" }, + "56": { "description": "15,511 ft", "value": "15,511 ft" }, + "57": { "description": "15,073 ft", "value": "15,073 ft" }, + "58": { "description": "14,641 ft", "value": "14,641 ft" }, + "59": { "description": "14,215 ft", "value": "14,215 ft" }, + "60": { "description": "13,795 ft", "value": "13,795 ft" }, + "61": { "description": "13,380 ft", "value": "13,380 ft" }, + "62": { "description": "12,971 ft", "value": "12,971 ft" }, + "63": { "description": "12,568 ft", "value": "12,568 ft" }, + "64": { "description": "12,169 ft", "value": "12,169 ft" }, + "65": { "description": "11,776 ft", "value": "11,776 ft" }, + "66": { "description": "11,387 ft", "value": "11,387 ft" }, + "67": { "description": "11,003 ft", "value": "11,003 ft" }, + "68": { "description": "10,624 ft", "value": "10,624 ft" }, + "69": { "description": "10,249 ft", "value": "10,249 ft" }, + "70": { "description": "9,878 ft", "value": "9,878 ft" }, + "71": { "description": "9,512 ft", "value": "9,512 ft" }, + "72": { "description": "9,150 ft", "value": "9,150 ft" }, + "73": { "description": "8,792 ft", "value": "8,792 ft" }, + "74": { "description": "8,438 ft", "value": "8,438 ft" }, + "75": { "description": "8,088 ft", "value": "8,088 ft" }, + "76": { "description": "7,742 ft", "value": "7,742 ft" }, + "77": { "description": "7,399 ft", "value": "7,399 ft" }, + "78": { "description": "7,060 ft", "value": "7,060 ft" }, + "79": { "description": "6,724 ft", "value": "6,724 ft" }, + "80": { "description": "6,392 ft", "value": "6,392 ft" }, + "81": { "description": "6,063 ft", "value": "6,063 ft" }, + "82": { "description": "5,737 ft", "value": "5,737 ft" }, + "83": { "description": "5,415 ft", "value": "5,415 ft" }, + "84": { "description": "5,095 ft", "value": "5,095 ft" }, + "85": { "description": "4,779 ft", "value": "4,779 ft" }, + "86": { "description": "4,466 ft", "value": "4,466 ft" }, + "87": { "description": "4,156 ft", "value": "4,156 ft" }, + "88": { "description": "3,848 ft", "value": "3,848 ft" }, + "89": { "description": "3,544 ft", "value": "3,544 ft" }, + "90": { "description": "3,242 ft", "value": "3,242 ft" }, + "91": { "description": "2,943 ft", "value": "2,943 ft" }, + "92": { "description": "2,646 ft", "value": "2,646 ft" }, + "93": { "description": "2,352 ft", "value": "2,352 ft" }, + "94": { "description": "2,061 ft", "value": "2,061 ft" }, + "95": { "description": "1,772 ft", "value": "1,772 ft" }, + "96": { "description": "1,486 ft", "value": "1,486 ft" }, + "97": { "description": "1,202 ft", "value": "1,202 ft" }, + "98": { "description": "920 ft", "value": "920 ft" }, + "99": { "description": "641 ft", "value": "641 ft" }, + "100": { "description": "364 ft", "value": "364 ft" }, + "101": { "description": "sea level", "value": "sea level" }, + "102": { "description": "sea level", "value": "sea level" }, + "103": { "description": "sea level", "value": "sea level" }, + "104": { "description": "sea level", "value": "sea level" }, + "105": { "description": "sea level", "value": "sea level" } + }} + } + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F434"}, "freq": 15, + "signals": [ + {"id": "LR4_O2_LAMBDA", "path": "Engine", "fmt": { "len": 16, "max": 2, "div": 32768, "nullmin": 0, "unit": "scalar" }, "name": "O2 Lambda B1S1", "suggestedMetric": "o2Lambda"}, + {"id": "LR4_O2_CURRENT", "path": "Engine", "fmt": {"bix": 16, "len": 16, "max": 128, "min": -128, "div": 256, "add": -128, "unit": "milliamps" }, "name": "O2 Pump Current B1S1", "description": "The second word of SAE J1979 PID 34 is sensor current, not voltage; the voltage variants are PIDs 24 to 2B, which this ECM does not support. Read as voltage it sat at a meaningless flat 4.0 across 28,432 samples. Read as current it spans -0.95 to +1.36 mA with a mean of +0.16, which is what a wideband pump cell should do."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F43C"}, "freq": 60, + "signals": [ + {"id": "LR4_CAT_TEMP_B1", "path": "Engine", "fmt": { "len": 16, "max": 6513.5, "min": -40, "div": 10, "add": -40, "unit": "celsius" }, "name": "Catalyst Temp B1S1"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F43D"}, "freq": 60, + "signals": [ + {"id": "LR4_CAT_TEMP_B2", "path": "Engine", "fmt": { "len": 16, "max": 6513.5, "min": -40, "div": 10, "add": -40, "unit": "celsius" }, "name": "Catalyst Temp B2S1"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F442"}, "freq": 60, + "signals": [ + {"id": "LR4_BAT_V", "path": "Battery", "fmt": { "len": 16, "max": 65.535, "div": 1000, "unit": "volts" }, "name": "Battery Voltage", "suggestedMetric": "starterBatteryVoltage"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F443"}, "freq": 30, + "signals": [ + {"id": "LR4_LOAD_ABS", "path": "Engine", "fmt": { "len": 16, "max": 25700, "mul": 100, "div": 255, "unit": "percent" }, "name": "Absolute Load", "suggestedMetric": "absoluteEngineLoad"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F444"}, "freq": 3, + "signals": [ + {"id": "LR4_EQ_RATIO", "path": "Engine", "fmt": { "len": 16, "max": 2, "div": 32768, "unit": "scalar" }, "name": "Equivalence Ratio", "suggestedMetric": "commandedLambda"}, + {"id": "LR4_FUEL_DIVISOR", "hidden": true, "path": "Engine", "fmt": { "len": 16, "max": 23.031, "div": 2845.54, "unit": "scalar" }, "name": "Fuel Rate Divisor (gal/h)", "description": "Commanded lambda premultiplied by 14.7 x 745 / 3600 x 3.785 so that mass air flow divided by this value is US gallons per hour. Exists only because synthetic signals support a plain ratio with no constant; it is the constant, folded into the divisor."}, + {"id": "LR4_FUEL_DIVISOR_MIN", "hidden": true, "path": "Engine", "fmt": { "len": 16, "max": 1381.86, "div": 47.426, "unit": "scalar" }, "name": "Fuel Rate Divisor (gal/min)", "description": "The gal/h divisor with a further x60 folded in, so mass air flow divided by it is US gallons per minute."} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F446"}, "freq": 60, + "signals": [ + {"id": "LR4_AMBIENT_TEMP", "path": "Climate", "fmt": { "len": 8, "max": 215, "min": -40, "add": -40, "unit": "celsius" }, "name": "Ambient Air Temp"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F449"}, "freq": 30, + "signals": [ + {"id": "LR4_PEDAL_D", "path": "Engine", "fmt": { "len": 8, "max": 100, "mul": 100, "div": 255, "unit": "percent" }, "name": "Accelerator Pedal D"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F44A"}, "freq": 30, + "signals": [ + {"id": "LR4_PEDAL_E", "path": "Engine", "fmt": { "len": 8, "max": 100, "mul": 100, "div": 255, "unit": "percent" }, "name": "Accelerator Pedal E"} + ]}, +{ "hdr": "7E0", "rax": "7E8", "cmd": {"22": "F44C"}, "freq": 30, + "signals": [ + {"id": "LR4_THROTTLE_CMD", "path": "Engine", "fmt": { "len": 8, "max": 100, "mul": 100, "div": 255, "unit": "percent" }, "name": "Commanded Throttle"} + ]}, +{ "hdr": "7E1", "rax": "7E9", "cmd": {"22": "1E68"}, "freq": 60, + "signals": [ + {"id": "LR4_1E68_RAW", "hidden": true, "path": "Transmission", "fmt": { "len": 8, "max": 255, "nullmin": 0, "unit": "scalar" }, "name": "1E68 Raw (varies most)", "description": "Undecoded transmission DID, neighbour of the gearbox temperature."} + ]}, +{ "hdr": "7E1", "rax": "7E9", "cmd": {"22": "1E69"}, "freq": 30, + "signals": [ + {"id": "LR4_GEARBOX_OIL_TEMP", "path": "Drivetrain", "fmt": { "len": 8, "max": 120, "min": -40, "add": -40, "unit": "celsius" }, "name": "Gearbox Temp", "suggestedMetric": "transmissionFluidTemperature"} + ]} +], +"synthetics": [ + { "id": "LR4_FUEL_RATE", "path": "Fuel", "name": "Fuel Rate", "max": 32, "unit": "gallonsPerHour", "suggestedMetric": "fuelRate", "formula": { + "op": "ratio", "a": "LR4_MAF", "b": "LR4_FUEL_DIVISOR" + }}, + { "id": "LR4_FUEL_RATE_GPM", "path": "Fuel", "name": "Fuel Rate (gal/min)", "max": 0.6, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_MAF", "b": "LR4_FUEL_DIVISOR_MIN" + }}, + { "id": "LR4_MPG", "path": "Fuel", "name": "Fuel Economy (mpg)", "max": 200, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_MPG_NUMERATOR", "b": "LR4_MAF" + }}, + { "id": "LR4_GAL_PER_100MI", "path": "Fuel", "name": "Fuel Use (gal/100 mi)", "max": 100, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_MAF", "b": "LR4_GP100_DENOMINATOR" + }}, + { "id": "LR4_MPG_LAMBDA", "path": "Fuel", "name": "Fuel Economy (mpg, lambda-corrected, test)", "max": 200, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_SPEED_MPH", "b": "LR4_FUEL_RATE" + }}, + { "id": "LR4_BANK_BALANCE", "path": "Engine", "name": "Bank Balance", "max": 3, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_CAT_TEMP_B1", "b": "LR4_CAT_TEMP_B2" + }}, + { "id": "LR4_BOOST_RATIO", "path": "Engine", "name": "Boost Pressure Ratio", "max": 3, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_MAP", "b": "LR4_BARO" + }}, + { "id": "LR4_LAMBDA_TRACKING", "path": "Engine", "name": "Lambda Tracking", "max": 3, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_O2_LAMBDA", "b": "LR4_EQ_RATIO" + }}, + { "id": "LR4_PEDAL_AGREEMENT", "path": "Engine", "name": "Pedal Agreement", "max": 3, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_PEDAL_D", "b": "LR4_PEDAL_E" + }}, + { "id": "LR4_SUSPENSION_BALANCE_F", "path": "Suspension", "name": "Suspension Balance Front", "max": 3, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_SUSPENSION_PRES_FL", "b": "LR4_SUSPENSION_PRES_FR" + }}, + { "id": "LR4_SUSPENSION_BALANCE_R", "path": "Suspension", "name": "Suspension Balance Rear", "max": 3, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_SUSPENSION_PRES_RL", "b": "LR4_SUSPENSION_PRES_RR" + }}, + { "id": "LR4_THROTTLE_TRACKING", "path": "Engine", "name": "Throttle Tracking", "max": 3, "unit": "scalar", "formula": { + "op": "ratio", "a": "LR4_THROTTLE_POS", "b": "LR4_THROTTLE_CMD" + }} ] } diff --git a/tests/test_cases/2016/command_support.yaml b/tests/test_cases/2016/command_support.yaml index 3970e88..c8428f8 100644 --- a/tests/test_cases/2016/command_support.yaml +++ b/tests/test_cases/2016/command_support.yaml @@ -1,7 +1,45 @@ model_year: 2016 supported_commands_by_ecu: + "726": + - "726.220202" "732": - 732.22D928 + "761": + - 761.22197C + - 761.22D11C + "792": + - 792.222A32 + - 792.222A33 + - 792.222A34 + - 792.222A35 + - 792.222A36 + - 792.222A37 + - 792.222A38 + - 792.222A39 + - 792.222A3A + - 792.222A3B + - 792.222A3C + "795": + - 795.221E88 + - 795.221E89 + - 795.221E8A + "7D3": + - 7D3.222B12 + - 7D3.223B00 + - 7D3.223B01 + - 7D3.223B02 + - 7D3.223B03 + - 7D3.223B04 + - 7D3.223B05 + - 7D3.223B06 + - 7D3.223B07 + - 7D3.223B08 + - 7D3.223B0B + - 7D3.223B3C + - 7D3.223B4D + - 7D3.223B71 + - 7D3.223B72 + - 7D3.22D11A "7DF": - 7DF.0101 - 7DF.0103 @@ -34,6 +72,9 @@ supported_commands_by_ecu: - 7DF.015A - 7DF.015C "7E0": + - "0111:TP" + - "0142:VPWR" + - "0145:TP_R" - 0101:ACRF_RDY,ACRF_SUP,AIR_RDY,AIR_SUP,CAT_RDY,CAT_SUP,CCM_RDY,CCM_SUP,CIM_SUP,DTC_CNT,EGR_RDY,EGR_SUP,EVAP_RDY,EVAP_SUP,FUEL_RDY,FUEL_SUP,HCAT_RDY,HCAT_SUP,HTR_RDY,HTR_SUP,MIL,MIS_RDY,MIS_SUP,O2S_RDY,O2S_SUP - 0103:FUELSYS1,FUELSYS2 - 0104:LOAD_PCT @@ -48,7 +89,6 @@ supported_commands_by_ecu: - 010E:SPARKADV - 010F:IAT - 0110:MAF - - 0111:TP - 0113:O2S11_EXISTS,O2S12_EXISTS,O2S13_EXISTS,O2S14_EXISTS,O2S21_EXISTS,O2S22_EXISTS,O2S23_EXISTS,O2S24_EXISTS - 0115:O2S12,SHRTFT11 - 0119:O2S22,SHRTFT22 @@ -65,10 +105,8 @@ supported_commands_by_ecu: - 013C:CATEMP11 - 013D:CATEMP21 - 0141:ACRFCMPL,ACRF_ENA,AIR_CMPL,AIR_ENA,CAT_CMPL,CAT_ENA,CCM_CMPL,CCM_ENA,CIM_SUP_FLAG,EGR_CMPL,EGR_ENA,EVAPCMPL,EVAP_ENA,FUEL_CMPL,FUEL_ENA,HCATCMPL,HCAT_ENA,HTR_CMPL,HTR_ENA,MIS_CMPL,MIS_ENA,O2S_CMPL,O2S_ENA - - 0142:VPWR - 0143:LOAD_ABS - 0144:EQ_RAT - - 0145:TP_R - 0146:AAT - 0147:TP_B - 0149:APP_D @@ -83,24 +121,72 @@ supported_commands_by_ecu: - 0168:IAT_11,IAT_11_SUP,IAT_12,IAT_12_SUP,IAT_13,IAT_13_SUP,IAT_21,IAT_21_SUP,IAT_22,IAT_22_SUP,IAT_23,IAT_23_SUP - 0170:BP_A,BP_A_ACT,BP_A_ACT_SUP,BP_A_CMD,BP_A_CMD_SUP,BP_A_SUP,BP_B,BP_B_ACT,BP_B_ACT_SUP,BP_B_CMD,BP_B_CMD_SUP,BP_B_SUP - 7E0.22033E + - 7E0.2203E6 + - 7E0.2203F2 + - 7E0.2203F3 - 7E0.220520 - - 7E0.22F18C + - 7E0.22DD01 + - 7E0.22F401 + - 7E0.22F403 + - 7E0.22F404 + - 7E0.22F405 + - 7E0.22F406 + - 7E0.22F407 + - 7E0.22F408 + - 7E0.22F409 + - 7E0.22F40B - 7E0.22F40C + - 7E0.22F40D + - 7E0.22F40E + - 7E0.22F40F + - 7E0.22F410 + - 7E0.22F411 + - 7E0.22F413 + - 7E0.22F415 + - 7E0.22F41C + - 7E0.22F41F + - 7E0.22F421 + - 7E0.22F423 + - 7E0.22F42E + - 7E0.22F42F + - 7E0.22F430 + - 7E0.22F431 + - 7E0.22F433 + - 7E0.22F434 + - 7E0.22F438 + - 7E0.22F43C + - 7E0.22F43D + - 7E0.22F441 - 7E0.22F442 + - 7E0.22F443 + - 7E0.22F444 + - 7E0.22F445 + - 7E0.22F446 + - 7E0.22F447 + - 7E0.22F449 + - 7E0.22F44A + - 7E0.22F44C + - 7E0.22F451 + - 7E0.22F456 + - 7E0.22F458 + - 7E0.22F466 + - 7E0.22F467 + - 7E0.22F468 + - 7E0.22F470 "7E1": - 0101:ACRF_RDY,ACRF_SUP,AIR_RDY,AIR_SUP,CAT_RDY,CAT_SUP,CCM_RDY,CCM_SUP,CIM_SUP,DTC_CNT,EGR_RDY,EGR_SUP,EVAP_RDY,EVAP_SUP,FUEL_RDY,FUEL_SUP,HCAT_RDY,HCAT_SUP,HTR_RDY,HTR_SUP,MIL,MIS_RDY,MIS_SUP,O2S_RDY,O2S_SUP - 0104:LOAD_PCT - 0105:ECT - 010C:RPM - 010D:VSS - - 0111:TP + - "0111:TP" - 011C:OBDSUP - 011F:RUNTM - 0121:MIL_DIST - 0130:WARM_UPS - 0131:CLR_DIST - 0133:BARO - - 0142:VPWR + - "0142:VPWR" - 015A:APP_R - 7E1.7E9.0101 - 7E1.7E9.0104 @@ -116,6 +202,10 @@ supported_commands_by_ecu: - 7E1.7E9.0133 - 7E1.7E9.0142 - 7E1.7E9.015A + - 7E1.7E9.221E68 + - 7E1.7E9.221E69 + - 7E1.7E9.221E6A + - 7E1.7E9.22DD01 "7E8": - 7E8.0103 - 7E8.0104 @@ -146,12 +236,12 @@ supported_commands_by_ecu: - 7E8.015C unsupported_commands_by_ecu: "733": - - '733.229924' + - "733.229924" "751": - - '751.222076' - - '751.222077' - - '751.222078' - - '751.222079' + - "751.222076" + - "751.222077" + - "751.222078" + - "751.222079" "795": - 795.220E8A "7E0": diff --git a/tests/test_responses.py b/tests/test_responses.py deleted file mode 100644 index b583341..0000000 --- a/tests/test_responses.py +++ /dev/null @@ -1,41 +0,0 @@ -import glob -import os -import pytest -from pathlib import Path - -# These will be imported from the schemas repository -from schemas.python.json_formatter import format_file -from schemas.python.signals_testing import find_test_yaml_files, register_test_classes - -REPO_ROOT = Path(__file__).parent.parent.absolute() -TEST_CASES_DIR = os.path.join(Path(__file__).parent, 'test_cases') - -# Find all test files grouped by model year -test_files_by_year = find_test_yaml_files(TEST_CASES_DIR) - -# Register test classes dynamically -register_test_classes(test_files_by_year) - -def get_json_files(): - """Get all JSON files from the signalsets/v3 directory.""" - signalsets_path = os.path.join(REPO_ROOT, 'signalsets', 'v3') - json_files = glob.glob(os.path.join(signalsets_path, '*.json')) - # Convert full paths to relative filenames - return [os.path.basename(f) for f in json_files] - -@pytest.mark.parametrize("test_file", - get_json_files(), - ids=lambda x: x.split('.')[0].replace('-', '_') # Create readable test IDs -) -def test_formatting(test_file): - """Test signal set formatting for all vehicle models in signalsets/v3/.""" - signalset_path = os.path.join(REPO_ROOT, 'signalsets', 'v3', test_file) - - formatted = format_file(signalset_path) - - with open(signalset_path) as f: - assert f.read() == formatted - -if __name__ == '__main__': - # Use pytest's main function with xdist arguments - pytest.main([__file__, '-xvs', '-n', 'auto']) diff --git a/tests/update_yaml_tests.py b/tests/update_yaml_tests.py deleted file mode 100644 index 41b3019..0000000 --- a/tests/update_yaml_tests.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -""" -Tool for updating YAML test cases with current signal values. -This is useful when signals are removed from signalsets and tests need to be updated. -""" - -import os -import sys -import argparse -import subprocess -from pathlib import Path - -# Add the current directory to the path so we can import our module -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -def ensure_schemas_repo(): - """Ensure the schemas repository is cloned, similar to devcontainer.json.""" - script_dir = os.path.dirname(os.path.abspath(__file__)) - schemas_dir = os.path.join(script_dir, 'schemas') - - # Check if schemas directory exists and is a git repository - if os.path.isdir(os.path.join(schemas_dir, '.git')): - # Try to pull latest changes - try: - print(f"Updating schemas repository...") - subprocess.run(['git', 'pull'], cwd=schemas_dir, check=True) - return True - except subprocess.CalledProcessError as e: - print(f"Warning: Failed to update schemas repository: {e}") - # Continue with existing repo - return True - elif os.path.exists(schemas_dir): - # Directory exists but might not be a git repository - print(f"Warning: schemas directory exists but might not be a git repository: {schemas_dir}") - return True - else: - # Need to clone the repository - try: - print(f"Cloning schemas repository...") - subprocess.run( - ['git', 'clone', '--depth=1', 'https://github.com/OBDb/.schemas.git', schemas_dir], - check=True - ) - return True - except subprocess.CalledProcessError as e: - print(f"Error: Failed to clone schemas repository: {e}") - return False - -def main(): - parser = argparse.ArgumentParser(description='Update YAML test expected values based on current signalsets') - parser.add_argument('--test-cases-dir', default=None, help='Path to test_cases directory') - parser.add_argument('--year', type=int, action='append', help='Specific model year(s) to update') - parser.add_argument('--dry-run', action='store_true', help='Show what would be changed without making changes') - parser.add_argument('--verbose', '-v', action='store_true', help='Show detailed output') - - args = parser.parse_args() - - # Find test_cases directory - if args.test_cases_dir: - test_cases_dir = args.test_cases_dir - else: - # Try to find it relative to current script - script_dir = os.path.dirname(os.path.abspath(__file__)) - test_cases_dir = os.path.join(script_dir, 'test_cases') - - if not os.path.isdir(test_cases_dir): - print(f"Test cases directory not found: {test_cases_dir}") - print(f"Creating directory: {test_cases_dir}") - os.makedirs(test_cases_dir) - - print(f"Using test cases directory: {test_cases_dir}") - - # Ensure we have the schemas repository - if not ensure_schemas_repo(): - print("Error: Could not set up schemas repository.") - sys.exit(1) - - # Import our module - try: - from schemas.python.yaml_test_updater import update_yaml_tests - update_yaml_tests(test_cases_dir, args.year, args.dry_run, args.verbose) - except ImportError as e: - print(f"Error importing yaml_test_updater module: {e}") - print("Make sure the schemas package is properly installed.") - sys.exit(1) - except Exception as e: - print(f"Error updating YAML tests: {e}") - sys.exit(1) - - print("Done!") - -if __name__ == "__main__": - main()