BLE GATT Design for UUIDs Data Formats Notifications and Version Compatibility
A BLE connection can succeed while the product integration fails. The app may discover the wrong characteristic, decode a signed measurement incorrectly, or stop receiving updates after a firmware upgrade. A usable GATT interface needs a written contract covering identity, bytes, delivery semantics, security and version changes. This guide develops that contract for a custom sensor and control interface.
Define the interface before writing the app
Record every service and characteristic UUID, property, permission, maximum length and error response. Reuse a Bluetooth SIG service only when its defined meaning and format fit the product. For proprietary semantics, assign a stable 128-bit UUID rather than inventing an unassigned short UUID. The SIG’s data types specification reserves short service UUID use for SIG-assigned values.
Keep UUIDs in one version-controlled registry shared by firmware, app and test tools. Do not use a device name as the only identity: names can change and several units can advertise the same name. Likewise, do not hard-code attribute handles across firmware releases. Discover the service, select the intended instance, and resolve characteristics within that service.
| Characteristic | Suggested operation | Contract to document |
|---|---|---|
| Protocol information | Read | Schema major/minor version, capabilities and maximum application frame |
| Measurement stream | Notify | Units, sequence counter, time reference and overflow policy |
| Configuration | Read and write with response | Ranges, authorization, validation and persistence timing |
| Command result | Indicate or notify with application acknowledgment | Transaction ID, result code and completion meaning |
Specify a byte format that can be tested
The following is an illustrative 12-byte measurement frame, not a Bluetooth standard profile. Every multibyte field is little-endian. A fixed-point temperature avoids dependencies on a language’s floating-point serialization. A separate protocol-information read advertises the minor version and supported capabilities.
| Offset | Field | Definition |
|---|---|---|
| 0 | major | Unsigned 8-bit schema major, initially 1 |
| 1 | flags | Bit 0 means temperature valid; other bits reserved and zero |
| 2–3 | sequence | Unsigned 16-bit counter, modulo 65536 |
| 4–7 | uptime_ms | Unsigned 32-bit sample time since boot; wraps modulo 2³² |
| 8–9 | temperature | Signed 16-bit, 0.01 °C per count; ignored when invalid |
| 10–11 | battery_mV | Unsigned 16-bit millivolts; 65535 means unavailable |
For this example, sequence and uptime reset at reboot; a boot identifier read during session setup distinguishes a restart from wraparound. Negative temperature, invalid flags, truncated frames and counter rollover belong in shared golden test vectors. This Python decoder illustrates explicit validation:
import struct
def decode_v1(payload):
if len(payload) != 12:
raise ValueError("expected 12 bytes")
major, flags, seq, ms, temp, mv = struct.unpack("<BBHIhH", payload)
if major != 1 or flags & 0xFE:
raise ValueError("unsupported schema or flags")
return {
"sequence": seq, "uptime_ms": ms,
"temperature_C": temp / 100 if flags & 1 else None,
"battery_mV": None if mv == 65535 else mv,
}
A golden vector is 01 01 2A 00 E8 03 00 00 2E FB E4 0C: major 1, valid temperature, sequence 42, uptime 1000 ms, −12.34 °C and 3300 mV. Decode it on both sides before connecting real hardware.
The strict reserved-bit rule means this particular decoder cannot accept new flag semantics silently. Preserve the v1 frame, negotiate a supported extension, or introduce a new major format. Appending bytes is backward-compatible only if the existing parser was explicitly designed to accept them.

Separate subscription from reliable application delivery
Notifications have no ATT confirmation; indications do. Neither proves that a measurement reached a cloud database or that a motor completed a command. BLE’s connected link has its own retransmission behavior, but disconnects, full software queues and application restarts still require an end-to-end policy. Use sequence numbers for detecting missing samples and transaction IDs for matching command outcomes. The ATT specification defines the underlying procedures.
Subscribe through the platform API and check completion before declaring the stream ready. At the GATT layer, the Client Characteristic Configuration Descriptor, UUID 0x2902, enables notifications with 0x0001 or indications with 0x0002. Subscription state is per client. Bonded and non-bonded persistence differs, so reconnect logic must restore the local callback and establish that the subscription is active. See the GATT specification.
Set a bounded queue and a documented response to overload: discard oldest samples, retain only the newest value, or pause acquisition when the product permits it. Surface a dropped-sample counter. For commands, distinguish “accepted,” “executing” and “completed.” Retrying the same transaction after a disconnect must not accidentally execute an actuator twice.
Make MTU and platform differences visible
For a conventional single-handle notification, the value must fit within ATT_MTU minus 3 bytes; the attribute-value ceiling is separately 512 bytes. A default LE ATT MTU of 23 therefore leaves 20 bytes for this notification. Larger values require an agreed application fragmentation scheme or another suitable transfer procedure. Increasing MTU does not by itself select a faster PHY or increase link-layer data length. See ATT packet formats.
On Android 14 and later, the first GATT client MTU request causes the stack to request 517 and later requests are disregarded. Use the negotiated callback result, never the requested number, to size frames. On Apple platforms, query maximumWriteValueLength(for:) for the selected write type rather than copying Android assumptions. Check both Android’s BluetoothGatt reference and Apple’s write-length reference.
Serialize asynchronous setup steps unless the chosen stack explicitly supports the intended concurrency. A successful call often means the request was queued; the callback carries the result. Log the negotiated MTU, selected characteristic UUID, security state, subscription result and parser version together.
Plan compatibility across firmware and app releases
Protocol schema version, firmware version and GATT database layout are separate concerns. Keep existing field meanings stable within a supported schema. Expose capabilities instead of making clients infer features from a firmware string. Refuse an unsupported major version with a useful diagnostic before sending control commands.
If an update changes the service database, implement the applicable Service Changed and caching behavior; Database Hash supports change detection where available. These mechanisms do not translate changed application payloads. Exercise upgrade and rollback with previously bonded clients as well as fresh installations. The GATT caching rules explain the database-level requirements.
Use a release matrix that exposes failures
| Test | Failure to provoke | Evidence to retain |
|---|---|---|
| Old app with new firmware; new app with old firmware | Unknown major, absent optional field or changed layout | Capability read and explicit acceptance or rejection |
| MTU 23 and larger negotiated MTU | Oversize payload, bad fragment length or truncation | Received length and decoded golden vector |
| Disconnect during command and subscription | Duplicate actuation or silent stream | Transaction ID, subscription completion and first valid sample |
| Upgrade and rollback with bonds retained | Stale handle cache or wrong characteristic access | Discovery/cache transition and UUID mapping |
| Slow consumer and full queue | Memory growth or unexplained loss | Queue high-water mark and drop accounting |
| Unauthenticated or unauthorized client | Configuration accepted at the wrong security level | Expected error and unchanged configuration |
When a stream is silent, check the notify property, subscription completion, permissions and producer activity in that order. When values are implausible, compare raw bytes against signedness, scaling and endianness before tuning RF parameters. When only upgraded devices fail, examine database caching and schema negotiation first.
Prepare a reviewable integration brief
Bring the UUID registry, packet specification, golden vectors, supported phone/OS list and compatibility matrix to a firmware review. Obeita’s firmware and BSP diagnostics service provides a relevant starting point. The delivered WS63 wireless-module integration case illustrates the hardware and radio-mode scoping context; it does not establish that this example GATT contract has been implemented or validated on that module.