BLE Devices and GATT skill for Codex

GATT profiles phones actually reconnect to. Codex writes the GATT table first, with UUIDs, properties, little-endian byte layouts and security per characteristic, then tunes MTU, PHY and connection parameters against measured throughput and current. It handles bonding, iOS GATT caching and Android permission quirks, and proves the device with a bleak script that checks for dropped notifications.

  • Embedded & IoT
  • $ble-devices
  • 159 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- ble-devices

Installs to ~/.agents/skills/ble-devices/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $ble-devices, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Designs and tests Bluetooth Low Energy peripherals - GATT services and characteristics, standard versus 128-bit UUIDs, notify versus indicate, MTU and data length, connection parameters for battery life, advertising payloads, pairing and bonding with LE Secure Connections, and test scripts with Python bleak, nRF Connect and btmon. Use when designing a GATT profile for a device, notifications stop or arrive truncated, throughput is low, iOS or Android will not reconnect or shows stale services, or writing automated BLE tests.

Use it when

  • Designing GATT services and characteristics for a new device
  • Notifications are truncated, slow or stop after reconnecting
  • Phones fail to reconnect or show stale services after a firmware update
  • You need automated BLE tests against real hardware

Use something else when

  • You need firmware updates delivered over BLE with signing and rollback: use firmware-ota
  • Devices talk to a cloud broker rather than a phone: use mqtt-iot

How to check the result

Run the bleak script against the device for at least 30 seconds with zero sequence gaps, and confirm bonded reconnects on both an iPhone and an Android phone after a power cycle.

The mistake it prevents

Assuming a 247-byte MTU before the exchange completes: the first notifications are cut to 20 bytes and the receiver misparses them.

~/.agents/skills/ble-devices/SKILL.md

--- name: ble-devices description: Designs and tests Bluetooth Low Energy peripherals - GATT services and characteristics, standard versus 128-bit UUIDs, notify versus indicate, MTU… license: MIT ---

Done well means: the GATT table is documented with UUIDs, properties, byte layouts and security per characteristic; data moves at the throughput and power the product needs; phones reconnect after bonding, firmware updates and service changes; and an automated script proves all of it against real hardware.

Inspect first

  1. Read AGENTS.md, then the stack in use: NimBLE (ESP-IDF, Apache Mynewt), Zephyr Bluetooth / nRF Connect SDK, SoftDevice, BlueZ, or a vendor stack. APIs differ; the protocol rules below do not.
  2. Find the GATT definition (service tables, BT_GATT_SERVICE_DEFINE, ble_gatt_svc_def) and write it out as the table below before changing it.
  3. Ask which centrals matter (iOS, Android versions, a gateway on Linux). Their limits decide MTU, connection interval and pairing UX.

GATT design

CharacteristicUUIDPropsFormat (little-endian)Security
Battery Level0x2A19 (SIG)Read, Notifyuint8 percentOpen
Sensor Samplea1b20001-...Notifyint16 temp cdegC, uint16 rh 0.01 %, uint32 seqEncrypted
Configa1b20002-...Read, Writestruct v1: uint8 ver, uint16 period_sEncrypted + bonded
Commanda1b20003-...Writeuint8 opcode, payloadEncrypted + bonded
Command Resulta1b20004-...Indicateuint8 opcode, uint8 statusEncrypted + bonded

Rules:

  • Use SIG-assigned services where one fits (Battery 0x180F, Device Information 0x180A, Heart Rate, Environmental Sensing); everything custom gets one random 128-bit base UUID with the 16-bit slot varied per characteristic.
  • Binary, little-endian, fixed-size fields with a version byte. No JSON over BLE: it wastes the small packets and burns battery.
  • Notify for streams (no acknowledgement, highest throughput). Indicate for rare messages that must arrive; one outstanding indication at a time.
  • Each notifying characteristic has a CCCD (0x2902) the central writes to enable it. Do not send before the central subscribes; the stack drops or errors.
  • Include a sequence number in streamed samples so the receiver can detect loss.
  • Always include Device Information (firmware revision) and, for products, the Service Changed characteristic in the GATT service so phones refresh caches after updates.

Throughput and power

LeverDefaultTune to
ATT MTU23 (20 bytes payload)Negotiate 247 (244 payload); iOS typically ends up near 185
Data Length Extension27-byte link packets251 bytes when both sides support it
PHY1M2M for throughput, Coded for range (BLE 5)
Connection intervalCentral decides, 7.5 ms to 4 s in 1.25 ms unitsRequest 15-30 ms while streaming, 500 ms-1 s while idle
Peripheral latency02-4 when idle so the peripheral can skip events
Supervision timeoutCentral decidesMust exceed (1 + latency) x interval x 2
Advertising intervalStack default100 ms for fast discovery, then 1-2 s to save power
  • Payload per notification is MTU minus 3. Size messages to fit, or fragment with a header; never assume the MTU before the exchange completes.
  • Phones ignore parameter requests outside their policies. iOS has published limits in Apple’s accessory design guidelines; check them rather than requesting 7.5 ms.
  • Measure current with a power profiler, not estimates: advertising interval and connection latency dominate battery life.

Advertising

  • Legacy advertising carries 31 bytes: flags (3), a 128-bit service UUID (18) and a short name barely fit. Put the full name and extras in the scan response.
  • Advertise the primary custom service UUID so apps can filter scans (Android and iOS background scanning need it).
  • Do not put secrets or stable personal identifiers in advertising data; use the stack’s resolvable private addresses when privacy matters.

Security

NeedPairing method
Device has no display or keypad, low riskLE Secure Connections, Just Works (no MITM protection)
Device has a displayNumeric Comparison or Passkey display
Device has a keypadPasskey entry
Out-of-band channel exists (NFC, QR)OOB pairing
  • Require LE Secure Connections; disable legacy pairing.
  • Set permissions per characteristic (encrypted, authenticated) instead of relying on the app to behave. Commands and config should require an encrypted, bonded link.
  • Store bonds persistently and handle bond loss: when the phone forgets the device, the peripheral must allow re-pairing instead of refusing with stale keys forever.
  • BLE link security does not authenticate the user. Application-level authorisation (signed tokens, challenge-response) is still needed for locks, payments and medical data.

Testing with bleak (Python)

import asyncio, struct
from bleak import BleakClient, BleakScanner

SAMPLE = "a1b20001-6f3c-4e8a-9d21-5c7e0b4f2a10"   # random base; use your own
CONFIG = "a1b20002-6f3c-4e8a-9d21-5c7e0b4f2a10"

async def main():
    dev = await BleakScanner.find_device_by_name("Sensor-01", timeout=10)
    if dev is None:
        raise SystemExit("not found: check advertising and that the device is not connected")
    async with BleakClient(dev) as client:
        print("mtu", client.mtu_size)
        seqs = []
        def on_sample(_, data: bytearray):
            temp, rh, seq = struct.unpack("<hHI", data)
            seqs.append(seq)
        await client.start_notify(SAMPLE, on_sample)
        await client.write_gatt_char(CONFIG, struct.pack("<BH", 1, 1), response=True)
        await asyncio.sleep(30)
        await client.stop_notify(SAMPLE)
        gaps = [b - a for a, b in zip(seqs, seqs[1:]) if b - a != 1]
        print(f"received {len(seqs)} samples, {len(gaps)} gaps")

asyncio.run(main())

bleak needs a Bluetooth adapter on the host and usually OS permission; it will not work in a sandbox without hardware. Give the user the script and ask for the output.

Other tools: nRF Connect (phone or desktop) for manual exploration; bluetoothctl and btmon on Linux for HCI-level logs; Wireshark with an nRF sniffer for over-the-air traces when a phone and device disagree about what happened.

Platform traps

  • iOS caches the GATT table per bonded device. After changing services without the Service Changed indication, iOS keeps showing the old table until the user forgets the device.
  • Android 12+ needs BLUETOOTH_SCAN and BLUETOOTH_CONNECT runtime permissions; older versions need location permission for scanning.
  • Android gatt.writeCharacteristic calls must be serialised: one outstanding GATT operation at a time, or later ones silently fail.
  • Many centrals limit simultaneous connections (often 4-8). Disconnect when done.
  • Firmware updates over BLE (MCUmgr/SMP, Nordic DFU, ESP-IDF BLE OTA) belong to firmware-ota.

Verification

  • GATT table documented and matches what nRF Connect discovers on the device
  • Automated script: subscribe, write config, 30+ s stream, zero sequence gaps
  • Measured MTU and connection interval on each target phone, written down
  • Bonded reconnect works on iOS and Android after power cycling the device
  • Unencrypted write to a protected characteristic is rejected with an ATT error
  • Current in advertising and connected-idle measured with a power profiler

Report

**Stack:** NimBLE on ESP32-C3 (ESP-IDF 5.3); centrals iPhone 15 (iOS 18), Pixel 8 (Android 15)
**GATT:** Battery, Device Info, custom sensor service (4 characteristics, table in docs/ble.md)
**Throughput:** MTU 247 (Android) / 185 (iOS); 2M PHY; 38 kB/s sustained, 0 gaps in 10 min
**Security:** LESC numeric comparison, config/command require bonded encryption
**Power:** 11 uA advertising at 1 s, 24 uA connected idle with latency 4

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "BLE Devices and GATT"
  short_description: "GATT profiles phones actually reconnect to"
  default_prompt: "$ble-devices Design the GATT services for our BLE temperature logger"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem