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.

What you need: POOM with the current BLE GATT firmware, a USB connection to the CLI, Chrome or Edge on a desktop for the embedded console, a phone with a BLE explorer such as nRF Connect, and Python with Bluetooth enabled for the final exercise.
Before sending commands: open THE BEAST → CLI on POOM. Disconnect nRF Connect before using Bleak because an active phone connection can prevent the Python client from connecting.

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:

PropertyMeaning
readThe client can request the current value.
writeThe client can change the value.
notifyThe server can push updates when the value changes.
Current runtime limit: 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 UUIDElement
180FBattery Service
2A19Battery 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
Characteristic values are hexadecimal bytes. 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
Keep the base experiment intact: do not delete characteristic 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

  1. Run ble-gatt-start and confirm it with ble-gatt-status.
  2. Open nRF Connect or another BLE explorer on your phone.
  3. Grant the Bluetooth permission requested by the operating system and start a scan.
  4. Find LabSensor and connect.
  5. Open Battery Service, UUID 180F.
  6. 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
Do not name your script 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())
Platform detail: on macOS, Bleak reports a system UUID instead of a Bluetooth MAC address.

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 UUIDFull UUID
180F0000180f-0000-1000-8000-00805f9b34fb
2A1900002a19-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.

Restart after structural changes. If you change the name, service, UUID, or properties while GATT is active, stop and restart the runtime so clients discover the new configuration.

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
  1. Choose a Service UUID.
  2. Define a UUID and properties for each Characteristic.
  3. Decide how each Value will be encoded as bytes.
  4. Build the interface from the POOM CLI.
  5. 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.