an undocumented vendor service is inferred from frames that move
No registry entry and no descriptor is the case inference actually exists for. The frames rise monotonically in a two-byte little-endian field while a neighbouring byte stays put, which is enough to locate the reading — and the point of the scenario is that inference is the LAST resort reached honestly, not the first idea.
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 00",
"01 5a 11 00",
"01 6f 11 00",
"01 81 11 00",
"01 93 11 00"
]
}
## 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 to raise the humidity",
"frames": [
"01 4c 11 00",
"01 c8 11 00",
"01 40 12 00",
"01 b9 12 00"
]
}
## 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": 3,
"probes left": 3,
"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 that transmits relative humidity readings over the Nordic UART Service TX characteristic. The reading is encoded as a 16-bit little-endian integer representing percent humidity multiplied by 100, preceded by a 0x01 header byte.",
"evidence": [
"Characteristic 6e400003-b5a3-f393-e0a9-e50e24dcca9e notifications",
"Deliberate breathing test showing values rising from 0x114c (44.28%) to 0x12b9 (47.93%)"
],
"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": "%",
"min": 0,
"max": 100,
"start_write": null,
"kotlin": "package ink.warrant.instrument\n\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 override val produces: Produces = Produces(\n unit = \"%\",\n min = 0.0,\n max = 100.0\n )\n override val matches: Match = Match(\n serviceUuids = listOf(UUID.fromString(\"6e400001-b5a3-f393-e0a9-e50e24dcca9e\")),\n namePrefixes = listOf(\"ACME-H2\")\n )\n override fun characteristicFor(services: List<UUID>): CharacteristicRef? {\n if (services.contains(UUID.fromString(\"6e400001-b5a3-f393-e0a9-e50e24dcca9e\"))) {\n return CharacteristicRef(\n UUID.fromString(\"6e400001-b5a3-f393-e0a9-e50e24dcca9e\"),\n UUID.fromString(\"6e400003-b5a3-f393-e0a9-e50e24dcca9e\")\n )\n }\n return null\n }\n override fun decode(raw: ByteArray): Double? {\n if (raw.size < 4) return null\n if (raw[0] != 0x01.toByte()) return null\n val low = raw[1].toInt() and 0xFF\n val high = raw[2].toInt() and 0xFF\n val rawValue = (high shl 8) or low\n return rawValue.toDouble() / 100.0\n }\n}",
"rationale": "The frames start with a 0x01 header byte, followed by a 16-bit little-endian integer representing the relative humidity scaled by 100. When the user breathed on the sensor, the value increased from 44.28% (0x114c) to 47.93% (0x12b9), which perfectly matches the expected physical response and range for relative humidity."
},
"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 | not_equals | mode | is emit, must not be abandon |
| ok | len_gte | evidence | length 2, want >= 1 |