| name | nimble-service-skill |
| description | Create and edit BLE GATT services with NimBLE. Use when creating, editing, or refactoring BLE services, characteristics, descriptors, or callbacks. |
| metadata | {"author":"Leeor Nahum","version":"1.3"} |
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
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).
Header (.h)
#ifndef BLE_SENSOR_SERVICE_H
#define BLE_SENSOR_SERVICE_H
#include <NimBLEDevice.h>
class BLESensorServiceClass {
public:
static const NimBLEUUID SERVICE_UUID;
static const NimBLEUUID DATA_CHARACTERISTIC_UUID;
static const NimBLEUUID CONTROL_CHARACTERISTIC_UUID;
bool startService();
NimBLEService* getService() { return sensor_service; }
bool setData(uint16_t value, bool notify = false);
bool isDataSubscribed() { return data_subscribed; }
NimBLECharacteristic* getDataCharacteristic() { return data_characteristic; }
NimBLECharacteristic* getControlCharacteristic() { return control_characteristic; }
private:
NimBLEService* sensor_service = nullptr;
NimBLECharacteristic* data_characteristic = nullptr;
NimBLECharacteristic* control_characteristic = nullptr;
bool data_subscribed = false;
;
;
;
;
};
BLESensorServiceClass BLESensorService;
Implementation (.cpp)
#include "ble_sensor_service.h"
const NimBLEUUID BLESensorServiceClass::SERVICE_UUID("...");
const NimBLEUUID BLESensorServiceClass::DATA_CHARACTERISTIC_UUID("...");
const NimBLEUUID BLESensorServiceClass::CONTROL_CHARACTERISTIC_UUID("...");
BLESensorServiceClass BLESensorService;
class BLESensorServiceClass::DataCallbacks : public NimBLECharacteristicCallbacks {
public:
DataCallbacks(BLESensorServiceClass* pService) : service(pService) {}
void onSubscribe(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, uint16_t subValue) override {
service->data_subscribed = (subValue != 0);
}
private:
BLESensorServiceClass* service;
};
class BLESensorServiceClass::ControlCallbacks : public NimBLECharacteristicCallbacks {
public:
void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override {
NimBLEAttValue value = pCharacteristic->getValue();
}
};
bool BLESensorServiceClass::startService {
NimBLEServer* pServer = NimBLEDevice::();
(pServer == ) ;
sensor_service = pServer->(SERVICE_UUID);
(sensor_service == ) {
sensor_service = pServer->(SERVICE_UUID);
}
();
();
sensor_service->();
}
{
(sensor_service == ) ;
(data_characteristic == ) {
data_characteristic = sensor_service->(DATA_CHARACTERISTIC_UUID);
(data_characteristic == ) {
data_characteristic = sensor_service->(
DATA_CHARACTERISTIC_UUID,
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
);
data_characteristic->( ());
NimBLEDescriptor* user_description = data_characteristic->((), NIMBLE_PROPERTY::READ);
user_description->();
NimBLE2904* presentation_format = (NimBLE2904*)data_characteristic->((), NIMBLE_PROPERTY::READ);
presentation_format->(NimBLE2904::FORMAT_UINT16);
presentation_format->();
presentation_format->();
presentation_format->();
presentation_format->();
NimBLEDescriptor* valid_range = data_characteristic->((), NIMBLE_PROPERTY::READ);
range[] = { , };
valid_range->((*)range, (range));
initial = ;
data_characteristic->((*)&initial, (initial));
}
}
}
{
(sensor_service == ) ;
(control_characteristic == ) {
control_characteristic = sensor_service->(CONTROL_CHARACTERISTIC_UUID);
(control_characteristic == ) {
control_characteristic = sensor_service->(
CONTROL_CHARACTERISTIC_UUID,
NIMBLE_PROPERTY::WRITE
);
control_characteristic->( ());
NimBLEDescriptor* user_description = control_characteristic->((), NIMBLE_PROPERTY::READ);
user_description->();
NimBLE2904* presentation_format = (NimBLE2904*)control_characteristic->((), NIMBLE_PROPERTY::READ);
presentation_format->(NimBLE2904::FORMAT_UINT8);
presentation_format->();
presentation_format->();
presentation_format->();
presentation_format->();
}
}
}
{
(data_characteristic == ) ;
data_characteristic->((*)&value, (value));
(notify) {
data_characteristic->();
}
;
}
Usage Example
BLESensorService.startService();
NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID(BLESensorServiceClass::SERVICE_UUID);
BLESensorService.setData(123, true);
NimBLE Server Singleton
NimBLE uses a singleton pattern for the BLE server - there is only one server per device. This means:
- No constructor parameters needed - services don't require a server pointer to be passed in
- Global access - the extern singleton pattern lets you call
BLESensorService.setData(...) from anywhere
- Simplified initialization - just call
startService() after NimBLEDevice::createServer() has been called
Descriptor Conventions
Namespace/Description Rule
These fields are linked in the 0x2904 descriptor:
- If
Description = 0x0000 → set Namespace = 0x00
- If
Description != 0x0000 (Bluetooth SIG enumeration) → set Namespace = 0x01
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.