Skip to content
Document DB 13 - MCP Server, REST API, Field Level Encryption, Transactional Outbox, & More!SHOW ME!!

Mode 06 — On-Board Test Results

Mode 06 is the deepest data OBD-II exposes, and the only mode that answers “how close is this to failing”.

Everything else reports a state: a code is set or it is not, a monitor is complete or it is not. Mode 06 reports the measurement the monitor actually took and the limits it was judged against. A catalyst sitting at 92% of its failure threshold looks perfectly healthy to MonitorStatus and is visibly on its way out here.

There are 224 monitor IDs and an unsupported one returns NO DATA, so read the supported-MID bitmask blocks rather than walking the range.

var supported = new List<byte>();
foreach (var block in MonitorIds.BlockMids) // 00, 20, 40, 60, 80, A0
{
foreach (var mid in await connection.Execute(new OnBoardTestSupportedMidsCommand(block)))
supported.Add(mid);
}

The layout is the same MSB-first bitmask as SupportedPidsCommand: each block reports the 32 MIDs that follow it.

foreach (var mid in supported)
{
foreach (var test in await connection.Execute(new OnBoardTestCommand(mid)))
{
Console.WriteLine(
$"{test.Monitor ?? $"MID {test.Mid:X2}"} test {test.TestId:X2}: " +
$"{test.Value:F2} {test.Unit} " +
$"(limits {test.Minimum:F2}{test.Maximum:F2}) " +
$"{(test.Passed == true ? "PASS" : "FAIL")}{test.BandPosition:P0} of band"
);
}
}

A single MID commonly answers with several records — one per test the monitor runs — which is why the result is a list rather than a single reading.

var atRisk = results
.Where(x => x.Passed == true && x.BandPosition > 0.85)
.ToList();

0 sits at the lower limit, 1 at the upper. A result that passes tells you nothing about trend; a result sitting at 95% of its band, compared against the same reading six months ago, is a component you can schedule rather than wait to fail. This is the number the whole mode exists for, and building a bare pass/fail UI on top of mode 06 throws away most of its value.

Mode 06 does not fix a unit per test. Every value carries a one-byte unit and scaling identifier (UASID) saying how to scale it and what it then means — the same 16-bit number is 0.25 rpm per bit under one identifier and 0.122 millivolts under another.

OnBoardTestResult.Value, .Minimum and .Maximum apply it for you. UnitAndScaling.Lookup exposes the table directly if you need it.

Value, Unit, Passed and BandPosition are all null, while RawValue, RawMinimum and RawMaximum remain.

Passed is deliberately null rather than falling back to a raw comparison: without the scaling there is no way to know whether the value is signed, and comparing a two’s complement negative as unsigned inverts the answer. The raw comparison is still yours to make — the library just will not guess at signedness on your behalf.

MonitorIds.Describe names the standardised MIDs:

Range Monitor
0x01–0x10 Oxygen sensor, B1S1 through B4S4
0x21–0x24 Catalyst, banks 1–4
0x31–0x34 EGR, banks 1–4
0x35–0x38 VVT, banks 1–4
0x39–0x3C EVAP leak tests — 0.150“ (cap off), 0.090“, 0.040“, 0.020“
0x3D Purge flow
0x41–0x50 Oxygen sensor heater, B1S1 through B4S4
0x61–0x64 Heated catalyst, banks 1–4
0x71–0x74 Secondary air, 1–4
0x81–0x84 Fuel system, banks 1–4
0x85–0x86 Boost pressure, banks 1–2
0x90–0x91 NOx adsorber, banks 1–2
0x98–0x99 NOx catalyst, banks 1–2
0xA1 Misfire (general)
0xA2–0xAD Misfire, cylinders 1–12
0xB0–0xB1 PM filter, banks 1–2

The four EVAP MIDs are the leak sizes the monitor tests for, in inches of orifice diameter — 0.020“ is the small-leak test that catches a loose filler cap.

MIDs above 0xDF are manufacturer-defined, so Describe answers null rather than inventing a name. Manufacturers also publish their own definitions for the standardised ranges (GM’s are the best known), which is worth knowing when a result carries a name from here but a value that only makes sense against their documentation.

Mode 06 as decoded here is the CAN (ISO 15765-4) format: a 9-byte record per test, made of MID, test ID, unit/scaling ID, and three 16-bit values.

Pre-CAN protocols used a different and largely manufacturer-specific format. On such a vehicle the response will not divide into whole records, and OnBoardTestCommand throws an ObdException naming that as the likely cause — rather than decoding into confident nonsense.

Finding the emissions components most at risk on a vehicle that currently passes everything:

var results = new List<OnBoardTestResult>();
foreach (var block in MonitorIds.BlockMids)
{
foreach (var mid in await connection.Execute(new OnBoardTestSupportedMidsCommand(block)))
results.AddRange(await connection.Execute(new OnBoardTestCommand(mid)));
}
var failing = results.Where(x => x.Passed == false);
var marginal = results
.Where(x => x.Passed == true && x.BandPosition >= 0.85)
.OrderByDescending(x => x.BandPosition);
foreach (var x in failing)
Console.WriteLine($"FAIL {x.Monitor}: {x.Value:F2} {x.Unit}");
foreach (var x in marginal)
Console.WriteLine($"WATCH {x.Monitor}: {x.BandPosition:P0} of band");