NimBLE BLE Service Guide
Authoritative References
Consult these official documents for UUIDs, format values, unit codes, and specifications:
For NimBLE-specific methods, enums, and properties, check the NimBLE library headers (e.g., NimBLECharacteristic.h, NimBLE2904.h) in the project dependencies.
UUID Conventions
- Check Bluetooth Assigned Numbers PDF for an official UUID that fits the use case
- If an official UUID exists and is appropriate, use the short form (e.g.,
"180F") - If no official UUID fits, generate a custom 128-bit UUID:
python -c "import uuid; print(str(uuid.uuid4()))"
Service Class Template
Copy assets/service-template.h and assets/service-template.cpp as the starting point for a new service, then rename BLESensorService, BLESensorServiceClass, and ble_sensor_service.h to match the new service, fill in real UUIDs, and add the characteristics the service actually needs.
UUIDs are declared as static const members inside the class. This scopes them to the class and prevents naming collisions across libraries.
Variable names should match the UUID constant prefix. For services and characteristics from Bluetooth Assigned Numbers, use their canonical names (e.g., SERVICE_UUID → sensor_service, DATA_CHARACTERISTIC_UUID → data_characteristic).
NimBLE Server Singleton
NimBLE uses a singleton pattern for the BLE server, so there is only one server per device. This means:
- Parameterless construction: obtain the server through
NimBLEDevice::getServer(), so services do not need a server pointer passed in - Global access: the extern singleton pattern lets you call
BLESensorService.setData(...)from anywhere - Simplified initialization: just call
startService()afterNimBLEDevice::createServer()has been called
Descriptor Conventions
Namespace/Description Rule
These fields are linked in the 0x2904 descriptor:
- If
Description = 0x0000→ setNamespace = 0x00 - If
Description != 0x0000(Bluetooth SIG enumeration) → setNamespace = 0x01
Presentation Format Selection for Multi-Field Struct Payloads
The 0x2904 Format field only describes the first logical value in the payload. It cannot describe a whole packed struct. Despite this, the format choice has a real practical effect in tools like nRF Connect:
FORMAT_FLOAT32→ nRF Connect renders the first 4 bytes as a human-readable float. For a struct whose first field is a float, this is immediately readable during testing even though the remaining fields are not displayed. Prefer this for data structs where the first field is the most informative single value.FORMAT_OPAQUE→ nRF Connect renders all bytes as a raw hex dump. Technically more "correct" for a packed struct, but makes the payload completely unreadable during development. Reserve this for payloads that are genuinely opaque to humans (e.g. bitmasks, binary command packets, image data).
Rule of thumb: if a developer looking at nRF Connect during testing would benefit from seeing the first field as a formatted number, use the format of that first field (usually FORMAT_FLOAT32 or FORMAT_UINT8). Use FORMAT_OPAQUE only when no human-readable display is useful.
Common Format/Unit Combinations
| Data Type | Format | Unit |
|---|---|---|
| Percentage | FORMAT_UINT8 |
0x27AD |
| Acceleration (m/s²) | FORMAT_FLOAT32 |
0x2713 |
| Angular velocity (rad/s) | FORMAT_FLOAT32 |
0x2763 |
| Temperature (°C) | FORMAT_SINT16 |
0x272F |
| Boolean/Unitless | FORMAT_BOOLEAN or FORMAT_UINT8 |
0x2700 |
| String | FORMAT_UTF8 |
0x2700 |
Service Ordering
When adding services to the BLE stack, maintain consistent ordering:
- Core/vital services first (Device Information, Error Report)
- Application-specific services in logical groups
- Utility services that rarely change (OTA) last
This ordering should be consistent across the codebase for predictability.