Skip to content

Fuel rate in gallons, plus mpg and the two things the schema won't do - #10

Closed
mwvolo wants to merge 93 commits into
OBDb:mainfrom
mwvolo:fuel-in-gallons-and-the-ways-to-read-it
Closed

mwvolo wants to merge 93 commits into
OBDb:mainfrom
mwvolo:fuel-in-gallons-and-the-ways-to-read-it

Conversation

@mwvolo

@mwvolo mwvolo commented Sep 25, 2026

Copy link
Copy Markdown

What changed

  • Fuel Rate is now gal/h. The hidden divisor on 22F444 is rescaled (32768 / 11.516 = 2845.54) so MAF divided by it is US gallons per hour, and the synthetic declares gallonsPerHour, which is in the schema's unit enum.
  • Four more fuel synthetics, all from DIDs already polled, so the budget stays at 4.100 req/s:
    • Fuel Rate (gal/min)
    • Fuel Economy (mpg)
    • Fuel Use (gal/100 mi)
    • Fuel Economy (mpg, lambda-corrected, test)
  • README fuel section rewritten, CLAUDE.md gains trap 9, TESTING.md gains a next-drive check. CLAUDE.md's command count (66 to 91) and demand (3.922 to 4.100) were stale since Upgrade to Claude Code workbench #8 and are fixed.

Why it looks like this

  • The unit enum has gallonsPerHour and nothing else for fuel, so mpg, gal/min and gal/100 mi are scalar with the unit in the name.
  • A synthetic ratio takes two operands. Exact mpg needs speed, MAF and lambda, so the mpg and gal/100 mi signals assume lambda 1. That holds in closed loop and cost 3% over the 2026-09-23 drive (17.3 vs 17.8 mpg); it reads about 25% optimistic under full-throttle enrichment.
  • The lambda-corrected mpg divides a hidden mph copy of speed by the Fuel Rate synthetic. Whether a synthetic may read a synthetic is undocumented and no other OBDb repo tries it. A number on the next drive means keep it and drop the plain mpg; blank means delete it.

What this does not fix

Decimal places. The schema's fmt has scaling, range, unit and map fields and no precision field, and synthetics have fewer. The float on the fuel rate and the one decimal on Fahrenheit temperatures are the app's formatting, so they are a Pelican feature request.

Validation

  • Schema: 0 errors against OBDb/.schemas signals.json.
  • Synthetic references: none dangling.
  • command_support.yaml: unchanged, still parses with no non-string entries.
  • Last drive (2026-09-23, 15.2 km): formula integrates to 0.54 gal, tank gauge fell 0.45 gal, one gauge step is 0.09 gal.

The starvation reading was wrong. It is not the budget - demand is 10.21
req/s against the 11-13 available - and it is not freq, because F40C and
F40D are both freq 1 and only F40D runs. Eight commands simply stopped on
2026-08-30, the day f782e4d and 527a47f rewrote every freq in the file.

The cost is concrete: engineSpeed has never been filled in fourteen months.
010C carries rpm 2511 times per drive and Pelican eats standard PIDs
internally, so the F40C alias was the only path to a recorded signal and it
is not running. THROTTLE_TRACKING and PEDAL_AGREEMENT cannot compute either.
1153 and 1154 are a charging pair at 1/256 V that 1cf20db called dead; 1154
sits at exactly 14.00 V and both dip on crank, and every sample behind the
original call was taken parked. F408 and F409 are the bank-2 trims, never
once requested on this truck, which is the only way to measure the
asymmetry directly now that F407 is silent. 113F and 2104 move but parked
data cannot tell a raw Celsius reading from a -40 one, so they go in as
scalars. 726/0202 is the single hit from today's sweep.

Demand goes to 10.76 req/s, still inside the budget, and F41F against
standard 011F tells us whether the alias path works at all.
Everything the truck has ever replied to that this file does not already
name, recorded as a raw scalar at the observed byte width. Cadence follows
what the logs already show each one doing: 60s for the six that vary, 120s
for the nine that flip between two values, 600s for the thirty-six that
have never moved - though most of those were only ever sampled parked, and
the suspension ones at a single ride height.

Costs 0.235 req/s, putting total demand at 10.99.
Cadence per tier, and the caveat that matters: every 'never moved' reading
came from a parked truck, and 3B00/3B01/3B08 were read at one ride height
where they could not have moved regardless. Records the 197C and D11C
reversal rather than quietly undoing it.

Also the suspension decode audit - pressures, signed height offset and
module voltage all check out against every logged sample; 3B4D has returned
zero on all 171 samples, and only 36 minutes in fourteen months have all
four corners at once.
CLAUDE.md is what someone needs before touching this repo: layout, the
F4xx convention and why it exists, that freq is an interval and the budget
has 0.01 req/s of slack, runnable validation, and five traps that all bit
during this work. The load-bearing one is that a command's presence in the
signalset does not mean it is being collected.

TESTING.md tracks what can only be answered in the truck - eight open
tasks, and a per-DID table of what each debug param is believed to be and
what would count as a result. Deciding a DID is dead requires a drive with
a ride-height change and a terrain-mode switch, because everything in the
600s tier was classified from a parked vehicle.
The ATSH/ATCRA pair is the part that is not obvious: one sets who you are
addressing, the other filters what comes back, and the block repeats
because each pair retargets the same three questions at the next module.
ATAR at the end matters more than it looks - leave a filter set and the
next healthy request reads as dead.

Worked examples are real frames from this truck's logs rather than
invented ones, so they can be checked: 7E80462F40582 decodes to 90 C
coolant, and 726's multi-frame F18C reply spells serial 5224293808.
Command meanings are taken from the Elm Electronics AT command list.
…answer

Trade the SAE duplicates for three PIDs that answer, name two modules
All 109 commands were polled. F40C answered 172 of 172 and engine speed
reached the signal database for the first time in fourteen months, with
nothing in this file having changed to cause it - the app had been holding
an older copy. The budget and freq theories were both wrong, and they were
worth ruling out only because doing so left no other explanation.

Thirty-six debug params came straight back out. The 7E0 11xx family and the
7E1 03xx/101A/21xx/2B1x block answer only inside an extended diagnostic
session; they answered in December 2025 because that session sent 10 03
first, and nobody had noticed. They are not dead probes, they are
unreachable by polling, and they now live in TESTING.md with the 10 03
recipe and its warning.

F408 and F409 answered, so there is bank-2 data at last, and it says the
banks agree: long term +9.31 and +7.32 percent. Both lean together, which
is a shared cause rather than the asymmetry we thought we had.

The ride-height change moved 3B01, 3B3C, both height sensors, all four
corner pressures and the compressor. Access height did register, in the
pressures rather than the state fields, which were sampled too slowly to
catch it - 3B01 goes to freq 10. 3B4D read zero throughout and is no longer
called Drive Mode.

73 commands at 10.55 req/s.
F423 and F45C came out of the signalset when their proprietary equivalents
won on sample count, but the signal tables kept listing them. Note the
reason on the surviving rows so nobody re-adds them.
First drive on the debug batch: the stale signalset explained, 36 params are session-gated
Decoding the ECM's own 0100/0120/0140/0160 bitmasks says it supports 54
mode-01 PIDs; 25 had no entry here. Dropping the metadata ones and the two
SAE aliases that already lost to their proprietary twins leaves sixteen.

Three of them exist to prosecute the lean finding rather than to collect
data: F403 says whether the engine was even in closed loop when those trims
were measured, F42E tests the stuck-purge-valve hypothesis that would
explain both banks drifting together, and F438 is the bank 2 oxygen sensor
sitting behind the bank 2 trim we only just started reading.

Eight go in as single raw bytes because we do not know what they are. len 8
is deliberate - reading the first byte is safe whatever the real width turns
out to be, and they get widened once something replies.

89 commands at 10.93 req/s.
Probe the sixteen PIDs the ECM says it supports and we never read
F466, F467, F468 and F470 went in at len 8 because nobody knew how wide
they were. The first byte of each turns out to be a sensor-support mask, so
all four have been faithfully recording the constant 3. The real payloads
are two mass airflow sensors, two coolant sensors, two intake air sensors
and ten bytes of boost control, and every number we have from them was read
out of raw frames by hand rather than by the signalset.

The dual MAF is the reason to care: 2.09 g/s against 1.78, a 17.5% split
between intake tracts, summing to roughly what F410 reports on its own. MAF
calibration was one of three explanations for both banks trimming lean
together and this is the first evidence touching it, so F466 goes to freq 5.

F416, F419 and F41A answer FFFF, not available, every time and are gone.
F413 said 0x77 once - both banks have sensors 1 through 3 - which can never
change, so it is written down rather than polled.

Re-tiered for a two-hour drive with off-roading: the diff-lock pair to 30,
boost control to 15, purge to 10. Barometric pressure pays for it at freq 5
instead of 1, which frees 0.9 req/s and loses nothing.

85 commands at 10.53 req/s.
…-long-drive

Widen the four PIDs whose first byte was a support bitmask, and set up for a long off-road drive
Read against the SAE J1979 definition of PID 70, the ten bytes we were
storing as three anonymous words are commanded and measured boost for two
channels at 1/32 kPa. The support byte says channel A exists and it reads
24.72 kPa at idle. First direct boost measurement on this truck - the boost
ratio synthetic has only ever inferred it from MAP over barometric - and it
costs nothing, because the reply was already arriving.

Checked every other command the same way against the canonical SAE
signalset. Nothing else is being left behind: the unread fields are
sensor-present bits and channels a V6 does not have.

Also recorded what Jaguar does for tire pressures, since it fills eight
metric slots we leave empty. Its 751 is silent here across 1386 attempts,
but its temperature DIDs 2A0A-2A0D have never been sent to a module that
answers, which is a parked sweep rather than a drive.
Twenty-nine signals are raw probes or byte splits that nobody can read yet.
They belong in the log, not on the screen, so they get hidden: true. SAE
J1979 hides 76 of its 294 the same way; we were hiding none of 98.

Twenty-three get a description, aimed at the ones that would mislead a
reader who trusted the name: the height sensors are inverted, bank 2 lambda
rails at FFFF about half the time and those samples are invalid rather than
lean, altitude is a barometric lookup and not GPS, and 3B4D is emphatically
not the drive mode its old name claimed.

Also recorded why sixteen metric slots is the ceiling rather than an
oversight. Of the twenty unfilled, ten are electric-vehicle only, eight are
tires we cannot read yet, and the last two need data this ECM does not
produce. fuelRate cannot be faked with a synthetic either - the only
supported formula op is a plain ratio with no constant, so the number would
be wrong by a factor of fourteen. An empty slot beats a wrong gauge.
F470 is boost pressure, and what the other OBDb repos suggest
Hide the raw probes, describe the misleading ones, and record why 16 metric slots is the ceiling
The app never gives signalset traffic more than about 4.2 req/s. Measured
over seven sessions: 108 distinct DIDs got 4.19 req/s, 23 DIDs got 4.22.
The adapter's 13.4 req/s is real but the app spends 3.3 on AT commands and
5.9 on its own mode-01 polling. We had been asking for 10.53 against a
ceiling of 11 that was never ours to spend.

Overdrawing it has a cost that isn't slower polling. The app retires ECUs
and never retries them: 726 and 761 went at 10:59, 732 and 792 at 12:40,
7D3 at 14:07, all while answering correctly. On the 137-minute drive that
followed, 61 of 84 commands were never sent once. The off-road tests were
all aimed at 7D3 and collected nothing.

Cut to 66 commands at 3.922 req/s. Nineteen deleted as dead across all
logged history, not one drive, including 3B4D, which was the Terrain
Response candidate and is constant zero across 236 samples spanning
several modes. Engine speed is back on a real cadence.

Three duplicates proven by correlation and resolved: PID 67 sensor 1 is
the coolant PID to 0.0 C over 135 pairs; PID 70 channel A is manifold
pressure to 1.3 kPa over 525 pairs, not a separate boost sensor; the
proprietary oil temp DID is standard PID 5C to 0.7 C over 1,135 pairs.
PID 67 sensor 2 turns out to be the supercharger intercooler's low
temperature circuit, which is new.

Ride height Access never registered because 3B3C is a bitfield, not an
ordinal. 0x04 is Access and was not in the map; the 0 we had labelled
Access never occurs. Pinned against the inverted front height sensor.

Fuel rate is computed. The ECM's bitmask says PID 5E is unsupported, so
it is derived from MAF and commanded lambda with the constant folded into
a hidden operand, since synthetics only allow a bare ratio. Integrates to
11.21 L against 9.48 L from the tank gauge over 91.5 km.

Corrects two claims made earlier today: F470 is manifold pressure at finer
resolution rather than boost, and the two MAF channels do not sum to the
single MAF PID. That rested on one idle sample; 1,474 samples give a
least-squares slope of 0.88 with scatter too wide to trust the split.

Also documents that the app only ever stores signals carrying a
suggestedMetric. Fifteen distinct signals in fifteen months. Everything
else is polled and thrown away.
The suspension section, the off-road no-data section and the new charge
cooler signal all needed the same caveat spelled out locally: resetting
the app's vehicle profile brings those modules back to life, but none of
their signals carry a suggestedMetric, so none of them will ever be
recorded as history either way. Live readouts and scan logs only.

Claude-Session: https://claude.ai/code/session_01Kzsf15spEW5oNrt7zCt8Z7
I read one day of sessions and called it a one-way ratchet in which the
app permanently retires ECUs, then told the owner to delete and re-add the
vehicle. He pushed back and asked whether this was new. It wasn't.

Taking the longest session of each day since 09-05, coverage has been 12
to 23 distinct DIDs every time. The 137-minute off-road drive polled 23 --
the widest of any long session in the period. The drive was not degraded,
it was the best long session on record, and 61-of-84 is simply what this
app has always done.

The stable core explains itself. Thirteen commands appear in every long
session since 09-05 and they are exactly thirteen of the sixteen carrying
a suggestedMetric at the time. The app converges on polling what it will
store. Modules with no metric-bearing command drift out of the rotation,
which covers 7D3, 792, 732, 726 and 761 without needing a ratchet.
Coverage recovers on its own every time the signalset changes.

Nothing is stuck and nothing needs resetting from inside the app. The
retraction is recorded in place rather than deleted, because the wrong
version was convincing.

The signalset itself is untouched: the budget cut, the nineteen deletions,
the duplicate decodes, the ride height bitfield and the fuel rate all rest
on direct measurement, not on this explanation.

Claude-Session: https://claude.ai/code/session_01Kzsf15spEW5oNrt7zCt8Z7
The cull was wrong. I deleted nineteen commands for reading constant, but
most had between 6 and 30 samples and none of those samples covered the
event the command would report. 1E88's six readings were all taken with
the differential unlocked, which says nothing about a diff lock DID. A
signal that only moves during a rare event looks dead until the event
happens, and deleting it guarantees it never gets caught.

F458 was not weak evidence, it was a misread. 62F458 7F is a positive
reply carrying data byte 0x7F, not a 7F negative response. PID 58 is long
term secondary O2 trim bank 2, the counterpart to PID 56 already in the
file, and 0x7F is -0.78%. Now mapped as LR4_SEC_O2_TRIM_B2.

All nineteen are back at exploratory cadences: the event-driven ones
(1E88, 1E89, 3B4D) at 60s, 3B00 and 3B08 at 120s, the rest at 600s. The
whole set costs 0.03 req/s against a 4.2 ceiling, so the budget was never
the real argument for removing them. 3B4D stays despite being the one
genuinely conclusive case, because at 600s it is free.

Also adds six probes the ECM's own supported-PID bitmask says exist and
that this file has never requested: PID 23 fuel rail gauge pressure (158
logged samples, 35 to 197 bar, the right shape for this DI rail and worth
comparing against 033E), PID 01 and PID 41 monitor status, PID 13 O2
sensors present, PID 51 fuel type, PID 1C OBD standard.

91 commands at 4.043 req/s, still under the ceiling, with twenty-five more
things to look at than before the cut.

Claude-Session: https://claude.ai/code/session_01Kzsf15spEW5oNrt7zCt8Z7
…-gives

Fit the budget the adapter actually gives us
The restored probes paid off within hours, and the merge did what it was
supposed to: coverage went from 23 distinct DIDs across 10 modules to 70
across 13, with 87 of 90 commands polled. Every module that had drifted
out came back on its own. No app reset, and deleting the vehicle would
have been wrong.

3B4D is alive. It was the one deletion I called conclusive -- flat 0x00
across 236 samples -- and it returned 0x04, 0x01 and 0x00 within eight
minutes of the off-road features being used. It does not track the ride
height mode PID, so Terrain Response is back to being the leading
candidate. Now at 30s.

Six of the eleven 792 counters advance: 2A32 through 2A37 gained 14000,
480, 960, 2170, 88 and 1 over 111 km and nine hours. They are counters,
not constants. What they count is still unknown because that window mixed
driving with parked time, so they now poll at 120s to make one drive
enough to regress against distance and running time.

3B01's last bit is pinned. 0x400 was Off-Road by elimination; it was
caught twice at a front reading of 85 to 89 with the mode PID reading
Off-Road. 3B01 is a mapped signal now: 0x100 Normal, 0x400 Off-Road,
0x800 Access. The ride height map checks out against a screenshot showing
Access, which had never displayed before the map was fixed.

All six new probes answered. MIL off with zero stored DTCs, six oxygen
sensors, gasoline, OBD-II, and a rail gauge pressure that ranges 29 to
197 bar against 033E's 41 to 46 over the same evening.

Also corrects two documented claims. hidden: true does not hide anything
-- fifteen hidden signals are on screen in the screenshots. And non-metric
signals do display live, which resolves the open question: the bandwidth
buys a readout, just never history.

Claude-Session: https://claude.ai/code/session_01Kzsf15spEW5oNrt7zCt8Z7
Holding the lower button puts the truck into a held-Access mode that stays
down instead of self-levelling. It appears in none of the 484 3B3C or 142
3B01 samples, and both words have exactly one unfilled slot: 3B01 bit 9
(0x200), between Normal at 0x100, Off-Road at 0x400 and Access at 0x800;
and 3B3C bit 3 (0x08), seen only inside 0x0D with the truck in motion.

The problem with catching it is that a mapped signal renders nothing for a
value it has not been taught, so a new state would show as a blank in the
app and survive only in the scan logs. That is exactly how 3B4D went
unnoticed. Both DIDs now carry an untranslated twin, Ride Height State Raw
and Ride Height Mode Raw, deliberately not hidden. They cost no extra
request and turn an unknown state into a number.

Two more suspension DIDs are not flat either. 3B00 reads 0x03 in 111 of 113
samples with one 0x103 taken at Access, and 3B02 carries a bit-8 flag that
toggles the same way; 3B08 reads 0x00 in 130 of 131 with one 0x01 taken
with Off-Road engaged. One sample each is not a decode, but none is dead.

Claude-Session: https://claude.ai/code/session_01Kzsf15spEW5oNrt7zCt8Z7
Decoding every signal in the file against the evening log turned up more
than the three headline movers.

The second word of F434 and F438 was being read as a voltage. SAE J1979
PIDs 34 to 3B carry equivalence ratio plus sensor current; the voltage
variants are 24 to 2B, which this ECM does not support. Read as voltage it
produced a flat 4.0 across 28,432 samples, which should have been the
giveaway. Read as current it spans -0.95 to +1.36 mA around +0.16, which is
what a wideband pump cell does. Both become O2 Pump Current in milliamps.

D11A was called Suspension Module Voltage. It is not battery voltage: over
221 paired samples its raw value swings 57 to 105 while control module
voltage moves 12.51 to 14.75, and the implied scale scatters 9%. The old
scaling put it at 16.8 V. Best correlation is 0.58 against compressor
activity, not enough to name it, so it goes back to an undecoded raw byte.

The two fuel rail pressures are different quantities, not one scaled two
ways. Over 157 pairs the ratio drifts 0.41 to 0.56, and in one stretch
PID 23 sat pinned near 197 bar while 033E climbed 79 to 110. A scaling
error would hold a fixed ratio.

Warm-ups since codes cleared is saturated at 255 and says only that they
were cleared more than 255 cycles ago.

Claude-Session: https://claude.ai/code/session_01Kzsf15spEW5oNrt7zCt8Z7
…oved

Three things I called dead moved as soon as a feature was used
Fuel Rate now divides MAF by a lambda operand prescaled to US gallons
per hour and declares gallonsPerHour, one of the schema's own units,
rather than hoping the app converts. Four more fuel synthetics come
from the same three polled DIDs at no extra bandwidth: gal/min, mpg,
gal/100 mi, and a lambda-corrected mpg that only works if a synthetic
may read another synthetic. That one is an experiment; TESTING.md says
which of the two mpg signals to keep once a drive has answered it.

The mpg and gal/100 mi signals assume lambda 1 because a ratio has
room for two operands and an exact mpg needs three. It cost 3% over
the 2026-09-23 drive.

Decimal places are not fixable here. The schema's fmt has no precision
field, so the float on the fuel rate and the one decimal on Fahrenheit
temperatures are the app's formatting. Recorded as trap 9.

Also fixes CLAUDE.md's command count and demand, stale since PR #8.
@mwvolo

mwvolo commented Sep 25, 2026

Copy link
Copy Markdown
Author

Opened against the wrong repo by mistake; this is being reviewed on the owner's fork first.

@mwvolo mwvolo closed this Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant