a presentation format descriptor must be read, not guessed
0x2904 states format, exponent and unit outright. A model that infers the encoding from the bytes instead may land on the same answer here and will not on the next device — and the reason it was right would be luck. Evidence has to cite the descriptor, because the descriptor is why the answer is knowable at all.
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": "ENV-T1",
"service_uuids": [
"0000181a-0000-1000-8000-00805f9b34fb"
]
}
## The GATT tree as enumerated
[
{
"service": "0000181a-0000-1000-8000-00805f9b34fb",
"name": "Environmental Sensing",
"characteristics": [
{
"uuid": "00002a6e-0000-1000-8000-00805f9b34fb",
"properties": [
"read",
"notify"
],
"descriptors": [
{
"uuid": "00002904-0000-1000-8000-00805f9b34fb",
"name": "Characteristic Presentation Format",
"value": "0e-02-2f-27-01-00-00",
"decoded": {
"format": "0x0E sint16",
"exponent": -2,
"unit": "0x272F degree Celsius"
}
},
{
"uuid": "00002901-0000-1000-8000-00805f9b34fb",
"name": "Characteristic User Description",
"value": "Temperature"
}
]
}
]
}
]
## 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.
{
"00002a6e-0000-1000-8000-00805f9b34fb": [
"d0 09",
"d2 09",
"cf 09",
"d5 09"
]
}
## 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": 1,
"probes left": 5,
"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 device is an environmental sensor reporting temperature via the standard Bluetooth SIG Environmental Sensing service. The temperature characteristic uses a 16-bit signed integer with an exponent of -2, representing degrees Celsius.",
"evidence": [
"Characteristic 00002a6e-0000-1000-8000-00805f9b34fb has a 0x2904 presentation format descriptor with value 0e-02-2f-27-01-00-00, indicating sint16 format, exponent -2, and unit 0x272F (degree Celsius).",
"The captured frames (e.g., 'd0 09' -> 25.12 °C) perfectly match this format."
],
"unresolved": [],
"probe": null,
"driver": {
"class_name": "EnvT1Driver",
"label": "ENV-T1 Temperature",
"service": "0000181a-0000-1000-8000-00805f9b34fb",
"characteristic": "00002a6e-0000-1000-8000-00805f9b34fb",
"unit": "degree Celsius",
"min": -40,
"max": 100,
"start_write": null,
"kotlin": "package ink.warrant.instrument\n\nimport java.util.UUID\n\nclass EnvT1Driver : Driver {\n override val id: String = \"env-t1-temperature\"\n override val label: String = \"ENV-T1 Temperature\"\n override val produces: Produces = Produces(\n unit = \"degree Celsius\",\n min = -40.0,\n max = 100.0\n )\n override val matches: Match = Match(\n serviceUuids = listOf(UUID.fromString(\"0000181a-0000-1000-8000-00805f9b34fb\")),\n namePrefixes = listOf(\"ENV-T1\")\n )\n\n override fun characteristicFor(services: List<UUID>): CharacteristicRef? {\n val serviceUuid = UUID.fromString(\"0000181a-0000-1000-8000-00805f9b34fb\")\n val charUuid = UUID.fromString(\"00002a6e-0000-1000-8000-00805f9b34fb\")\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 != 2) return null\n val value = ((raw[1].toInt() and 0xFF) shl 8) or (raw[0].toInt() and 0xFF)\n return value.toShort().toDouble() * 0.01\n }\n}",
"rationale": "The characteristic 00002a6e-0000-1000-8000-00805f9b34fb is the standard Bluetooth SIG Temperature characteristic. Its 0x2904 presentation format descriptor explicitly defines the format as sint16 (0x0E), exponent as -2, and unit as degree Celsius (0x272F). The captured frames decode to realistic ambient temperatures around 25 °C using little-endian 16-bit signed integer decoding scaled by 0.01."
},
"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 | mentions_any | evidence | found ['2904', 'presentation format', 'descriptor'] |
| ok | len_gte | evidence | length 2, want >= 1 |