BLE GATT Lab — Build Your Own BLE Device
Build a configurable BLE peripheral with POOM, then discover and read it from nRF Connect or Python with Bleak.
So far, you may have used Bluetooth Low Energy to discover devices, inspect advertisements, and identify services. This Maker lab reverses the experiment: instead of asking what another BLE device offers, you will decide what your own device should offer.
POOM will become a peripheral named LabSensor. It will publish the standard Battery Service and one readable Battery Level characteristic. You will build it from the CLI, inspect it from a phone, and read it from Python.
1. Understand the GATT model
Think of a BLE device as a small restaurant. The device is the restaurant, Services are sections of the menu, and Characteristics are the individual items in each section.
LabSensor
Battery Service
Battery Level
A Service groups one capability. A Characteristic stores a value or represents an action. Its properties determine what a client can do:
| Property | Meaning |
|---|---|
read | The client can request the current value. |
write | The client can change the value. |
notify | The server can push updates when the value changes. |
ble-gatt-char-add and ble-gatt-char-set accept read, write, or readwrite. Notifications are part of GATT, but they are not configurable through this CLI yet.BLE identifies every service and characteristic with a UUID. This lab uses two standard 16-bit UUIDs:
| Short UUID | Element |
|---|---|
180F | Battery Service |
2A19 | Battery Level |
POOM
└── Battery Service (180F)
└── Battery Level (2A19)
Property: read
Value: 0x40
2. Build LabSensor from the CLI
The embedded console guides the complete setup. The same commands also work through Minicom or another 115200-baud serial connection.
Reset the editable GATT configuration
ble-gatt-reset
Set the advertised device name
ble-gatt-name-set LabSensor
Configure Battery Service
ble-gatt-service-set 180F
Add a readable Battery Level characteristic
ble-gatt-char-add 2A19 read
Set the initial value
ble-gatt-char-value-set 0 40
40 hex equals 64 decimal. Sending 64 would write 0x64, which equals 100 decimal.Start the peripheral and inspect its status
ble-gatt-start
ble-gatt-status
You now have a peripheral named LabSensor with service 180F, characteristic 2A19, and the one-byte value 0x40. When you finish the lab, stop it with:
ble-gatt-stop
3. Inspect and edit characteristics
The GATT configuration is not a black box. You can inspect and change it from the same CLI.
ble-gatt-char-list
The list shows the index assigned to each characteristic. Use that index when changing its UUID, properties, or value.
ble-gatt-char-set 0 2A19 read
ble-gatt-char-add 2A1A read
ble-gatt-char-list
ble-gatt-char-del 1
2A19 at index 0 until you complete the Battery Level tests.This shortcut writes to characteristic 0:
ble-gatt-char-value 40
For any characteristic index, use the explicit form:
ble-gatt-char-value-set <index> <hex...>
You can provide several space-separated bytes. The client application decides how those bytes should be interpreted.
4. Test LabSensor from a phone
- Run
ble-gatt-startand confirm it withble-gatt-status. - Open nRF Connect or another BLE explorer on your phone.
- Grant the Bluetooth permission requested by the operating system and start a scan.
- Find
LabSensorand connect. - Open Battery Service, UUID
180F. - Find Battery Level, UUID
2A19, and select Read.
The returned byte should be 0x40. Interpreted as an unsigned byte, that value is 64. You have built a real BLE peripheral without creating and compiling another firmware application.
5. Discover LabSensor with Python
Bleak can scan for BLE peripherals and act as a GATT client on Windows, macOS, and Linux.
python -m pip install bleak
bleak.py. That filename shadows the installed library and causes an import error. Use a name such as scan_poom.py.import asyncio
from bleak import BleakScanner
async def main():
device = await BleakScanner.find_device_by_name("LabSensor")
if device is None:
print("LabSensor not found")
return
print("POOM found")
print("Name:", device.name)
print("Address or identifier:", device.address)
asyncio.run(main())
6. Connect and read Battery Level
Scanning only discovers the peripheral. This program connects, walks through the GATT structure, and reads characteristic 2A19.
import asyncio
from bleak import BleakClient
from bleak import BleakScanner
BATTERY_LEVEL_UUID = "00002a19-0000-1000-8000-00805f9b34fb"
async def main():
device = await BleakScanner.find_device_by_name("LabSensor")
if device is None:
print("LabSensor not found")
return
async with BleakClient(device) as client:
print("Connected:", client.is_connected)
for service in client.services:
print("Service:", service.uuid)
for characteristic in service.characteristics:
print(
" Characteristic:",
characteristic.uuid,
characteristic.properties,
)
value = await client.read_gatt_char(BATTERY_LEVEL_UUID)
print("Raw:", value)
print("Bytes:", list(value))
print("Battery Level:", value[0] if value else None)
asyncio.run(main())
BLE expands short UUIDs inside the Bluetooth base UUID:
| Short UUID | Full UUID |
|---|---|
180F | 0000180f-0000-1000-8000-00805f9b34fb |
2A19 | 00002a19-0000-1000-8000-00805f9b34fb |
7. Change values while you develop
Stop the runtime, change the byte, and start it again:
ble-gatt-stop
ble-gatt-char-value-set 0 50
ble-gatt-start
50 hex equals 80 decimal. Run the Python program again and confirm the change. Then simulate a battery level of 20:
ble-gatt-stop
ble-gatt-char-value-set 0 14
ble-gatt-start
14 hex equals 20 decimal.
8. Maker challenge — design your own BLE interface
Use POOM as a BLE GATT playground. Define the interface from the console, then immediately test a mobile app, a Python program, or another BLE client.
BLE Device
Service
Characteristic
UUID
Properties
Value
Try representing one of these devices within the current runtime limit of one configurable service with multiple characteristics:
Robot Service
Speed
Direction
Battery
Status
Environment Service
Temperature
Humidity
Pressure
Coffee Machine Service
Temperature
Water Level
Coffee Ready
- Choose a Service UUID.
- Define a UUID and properties for each Characteristic.
- Decide how each Value will be encoded as bytes.
- Build the interface from the POOM CLI.
- Create a client that discovers the service and reads or writes its values.
POOM is no longer only observing Bluetooth. It is now helping you build, simulate, and test BLE GATT devices.