a driver whose numbers did not follow the world must be revised, not re-sent
The honest gate. The previous driver compiled, ran, and produced entirely plausible numbers that did not move when the quantity moved — which means the offset is wrong and the plausibility of the output proves nothing. A model that wants to be helpful re-sends the same decoding with a better rationale. That is the failure being tested.
The answer conformed to the contract and every assertion held.
System instructionassembled from the contract schema, not hand-written
You are meeting a Bluetooth Low Energy device nobody has written a driver for, and your job is to work out which characteristic carries a physical reading and exactly how it is encoded. Prefer evidence over inference, in this order: a 0x2904 presentation-format descriptor states the format, exponent and unit outright and must be read rather than guessed; a Bluetooth SIG assigned service or characteristic has a published encoding you already know; only when neither exists may you infer from the bytes. Probe before you commit — a driver emitted from a single frame is a guess wearing a uniform. You must never emit a driver whose unit you cannot name, and you must never emit one for a characteristic that plausibly carries battery level, firmware revision, a sequence counter or a status flag rather than a reading. Abandoning with a clear reason is a correct outcome and is worth more than a driver that decodes something into a believable wrong number. Return JSON with these fields: - mode (one of probe | emit | abandon, required): probe while anything material about the encoding is unknown. emit once a driver would decode correctly. abandon when this device cannot be driven and you can say why. - understanding (string, required): What you now believe this device is and which characteristic carries the reading, in two sentences. Written every turn so a wrong track is visible early rather than at the end. - evidence (array, required): What in the GATT tree, the advertisement or the frames supports your current belief. Cite the actual UUID, descriptor or byte offset. An empty list means you are guessing and should be probing instead. - unresolved (array, required): Everything still unknown that would change the driver. Empty is the only condition under which you may emit. - probe (object, null when it does not apply): Required when mode is probe. One operation for the phone to perform against the device. - driver (object, null when it does not apply): Required when mode is emit. Kotlin implementing the Driver interface in android/app/src/main/java/ink/warrant/instrument/Driver.kt. - abandon (object, null when it does not apply): Required when mode is abandon. Saying why is the deliverable.
## What the device advertises
{
"name": "ACME-H2",
"service_uuids": [
"6e400001-b5a3-f393-e0a9-e50e24dcca9e"
]
}
## The GATT tree as enumerated
[
{
"service": "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
"name": null,
"characteristics": [
{
"uuid": "6e400003-b5a3-f393-e0a9-e50e24dcca9e",
"properties": [
"notify"
],
"descriptors": []
}
]
}
]
## Frames captured, by characteristic
Hex, in arrival order. A value that never changes across a capture is not necessarily static — it may simply be a quantity that did not move.
{
"6e400003-b5a3-f393-e0a9-e50e24dcca9e": [
"01 4c 11 2c",
"01 4c 11 30",
"01 4c 11 35",
"01 4c 11 33"
]
}
## Frames captured while the quantity was deliberately moved
Someone was asked to make the reading move, and told which way. If your decoding does not move with it, your decoding is wrong.
{
"instruction given": "breathe on the sensor so the humidity climbs",
"frames": [
"01 4c 11 2c",
"01 4c 11 3a",
"01 4c 11 48",
"01 4c 11 56"
]
}
## What you have already tried on this device
Including any driver that was rejected and the gate that rejected it. A gate that failed will not pass because the next attempt is more confident: change the decoding, or abandon.
[
"emitted AcmeH2Driver reading a uint16 little-endian at offset 1, scale 0.01",
"gate 4 (tracking) REJECTED it: decoded 44.28, 44.28, 44.28, 44.28 across frames captured while the quantity was deliberately raised. The numbers were plausible and did not move."
]
## The interface your driver must implement
This is the actual file, read off disk. Your class has to satisfy every member of it — a class carrying only a decode function does not implement this interface and will not compile, however correct its arithmetic is. Note where the UUIDs live: `matches` and `characteristicFor` carry them, so the class names the device it is for.
```kotlin
package ink.warrant.instrument
import android.bluetooth.BluetoothGattCharacteristic
import java.util.UUID
/**
* The driver contract, from `docs/architecture.md` §5.
*
* ```
* Driver
* matches scan filter — service UUID, name prefix, manufacturer data
* produces kind: measurement · unit · range
* read() raw bytes → { value, unit, tool_id, timestamp, raw }
* ```
*
* NOTHING ABOVE THIS CARES WHICH TOOL IT IS. A `measurement` field knows only that a number
* arrived from a paired device without passing through a human, and that is the sole property
* that makes it *measured* rather than typed. A new tool is a driver, not a schema change.
*
* This is the seam Wright writes into: point it at an unfamiliar device, it enumerates the
* GATT services, infers the encoding, and emits one of these.
*/
interface Driver {
/** Stable identifier for the driver itself, not the device. Goes onto the record. */
val id: String
/** Human-facing name, for the pairing screen. */
val label: String
/** What this driver produces. The unit is fixed by the driver, never chosen by a person. */
val produces: Produces
/** The scan filter. A device matches if any of these is satisfied. */
val matches: Match
/**
* Which characteristic on a connected device this driver reads. Returning null means "this
* device advertised the right thing but does not actually expose the characteristic", which
* is a real and common failure and must not be confused with a zero reading.
*/
fun characteristicFor(services: List<UUID>): CharacteristicRef?
/**
* Raw bytes to a value. The one place a wire format is understood.
*
* Returning null means "these bytes are not a reading" — a keep-alive, a status frame, a
* truncated packet. A driver that guesses here produces a plausible number from nonsense,
* which is the single worst thing it could do.
*/
fun decode(raw: ByteArray): Double?
}
data class Produces(
val unit: String,
/** Plausible range. Outside it, the reading is reported but flagged — see [Driver.decode]. */
val min: Double,
val max: Double,
)
data class Match(
val serviceUuids: List<UUID> = emptyList(),
val namePrefixes: List<String> = emptyList(),
)
data class CharacteristicRef(val service: UUID, val characteristic: UUID)
/**
* Whether a value is inside what this driver claims it can produce.
*
* This is a PLAUSIBILITY check and nothing more. It will not catch a wrong scale factor that
* yields a sensible-looking number — see architecture.md §5, which chooses this deliberately
* over the aviation route of certified tooling and formal verification. Good enough, cheap, and
* honest about which it is.
*/
fun Produces.plausible(value: Double): Boolean = value in min..max
/** Convenience for the 16-bit Bluetooth SIG UUIDs, which are all offsets into one base. */
fun sig(short: Int): UUID =
UUID.fromString(String.format("%08x-0000-1000-8000-00805f9b34fb", short))
internal fun BluetoothGattCharacteristic.supportsNotify(): Boolean =
properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0
internal fun BluetoothGattCharacteristic.supportsRead(): Boolean =
properties and BluetoothGattCharacteristic.PROPERTY_READ != 0
```
## Your budget
{
"probes used": 4,
"probes left": 2,
"note": "A driver emitted from a single frame is a guess wearing a uniform. But probing forever is also a failure: if the budget runs out, abandon and say what a person with the vendor's documentation would need to finish."
}{
"mode": "emit",
"understanding": "The ACME-H2 is a humidity sensor where the relative humidity reading is carried in the fourth byte (offset 3) of the notifications from characteristic 6e400003-b5a3-f393-e0a9-e50e24dcca9e. The previous driver failed because it decoded a static uint16 at offset 1, whereas the byte at offset 3 correctly scales and moves with humidity changes.",
"evidence": [
"Characteristic 6e400003-b5a3-f393-e0a9-e50e24dcca9e notifications",
"During deliberate breathing on the sensor, the byte at offset 3 increased from 0x2c (44% RH) to 0x3a (58% RH), 0x48 (72% RH), and 0x56 (86% RH)"
],
"unresolved": [],
"probe": null,
"driver": {
"class_name": "AcmeH2Driver",
"label": "ACME-H2 Humidity Sensor",
"service": "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
"characteristic": "6e400003-b5a3-f393-e0a9-e50e24dcca9e",
"unit": "%RH",
"min": 0,
"max": 100,
"start_write": null,
"kotlin": "package ink.warrant.instrument\n\nimport android.bluetooth.BluetoothGattCharacteristic\nimport java.util.UUID\n\nclass AcmeH2Driver : Driver {\n override val id: String = \"acme-h2\"\n override val label: String = \"ACME-H2 Humidity Sensor\"\n\n override val produces: Produces = Produces(\n unit = \"%RH\",\n min = 0.0,\n max = 100.0\n )\n\n override val matches: Match = Match(\n serviceUuids = listOf(UUID.fromString(\"6e400001-b5a3-f393-e0a9-e50e24dcca9e\")),\n namePrefixes = listOf(\"ACME-H2\")\n )\n\n override fun characteristicFor(services: List<UUID>): CharacteristicRef? {\n val serviceUuid = UUID.fromString(\"6e400001-b5a3-f393-e0a9-e50e24dcca9e\")\n val charUuid = UUID.fromString(\"6e400003-b5a3-f393-e0a9-e50e24dcca9e\")\n return if (services.contains(serviceUuid)) {\n CharacteristicRef(serviceUuid, charUuid)\n } else {\n null\n }\n }\n\n override fun decode(raw: ByteArray): Double? {\n if (raw.size < 4) return null\n val humidity = raw[3].toInt() and 0xFF\n return humidity.toDouble()\n }\n}",
"rationale": "The byte at offset 3 perfectly tracks the humidity increase during the breathing test, moving from 0x2c (44% RH) to 0x56 (86% RH). The previous attempt at decoding a uint16 at offset 1 remained static at 44.28, proving it was not the active humidity reading."
},
"abandon": null
}Assertions pin the part of an answer that decides something, never the whole object — exact-matching a rationale would fail the first time it was reworded, and a suite that cries wolf is abandoned inside a day. Where the reasoning is checked at all, it is to catch an agent that reached the right verdict for no reason.
| asserted | about | what happened | |
|---|---|---|---|
| ok | mentions_none | driver.kotlin | none present |
| ok | mentions_any | understanding | found ['offset', 'byte', 'static', 'previous'] |
| ok | mentions_any | evidence | found ['offset', 'increase', '0x', 'breath'] |