The BLEClientService and BLEClientCharacteristic classes can be used to implement any custom or officially adopted BLE service of characteristic on the client side (most often is Central) using a set of basic properties and callback handlers.
The example below shows how to use these classes to implement the Heart Rate Monitor service, as defined by the Bluetooth SIG. To run this example, you will need an extra nRF52 running peripheral HRM sketch
HRM Service Definition
UUID: 0x180D
Only the first characteristic is mandatory, but we will also implement the optional Body Sensor Location characteristic. Heart Rate Control Point won't be used in this example to keep things simple.
Implementing the HRM Service and Characteristics
The core service and the first two characteristics can be implemented with the following code:
First, define the BLEService and BLECharacteristic variables that will be used in your project:
/* HRM Service Definitions * Heart Rate Monitor Service: 0x180D * Heart Rate Measurement Char: 0x2A37 (Mandatory) * Body Sensor Location Char: 0x2A38 (Optional) */ BLEClientService hrms(UUID16_SVC_HEART_RATE); BLEClientCharacteristic hrmc(UUID16_CHR_HEART_RATE_MEASUREMENT); BLEClientCharacteristic bslc(UUID16_CHR_BODY_SENSOR_LOCATION);
Then you need to initialize those variables by calling their begin().
// Initialize HRM client hrms.begin(); // Initialize client characteristics of HRM. // Note: Client Char will be added to the last service that is begin()ed. bslc.begin(); // set up callback for receiving measurement hrmc.setNotifyCallback(hrm_notify_callback); hrmc.begin();
Client Service + Characteristic Code Analysis
1. The first thing to do is to call .begin() on the BLEClientService (hrms above). Since the UUID is set in the object declaration at the top of the sketch, there is normally nothing else to do with the BLEClientService instance.
2. Since Heart Rate Measurement characteristic (clientMeasurement above) is notifiable. You need to set up callback for it
- '
hrmc.setNotifyCallback(hrm_notify_callback);
' This sets the callback that will be fired when we receive a Notify message from peripheral. This is needed to handle notifiable characteristic since callback allow us to response to the message in timely manner. For this example is just simply printing out value to Serial. - '
hrmc.begin();
' Once all of the properties have been set, you must call .begin() which will add the characteristic definition to the last BLEClientService that was '.begin()ed'.
3. Next, we can start to scan and connect to peripheral that advertises HRM service. Once connected, we need to go through peripheral GATT table to find out the Gatt handle for our interest. In this example they are handle for hrms, hrmc and bslc. This looking up process for interested service/characteristic is called Discovery.
Note: Gatt handle (or just handle) is required to perform any operations at all such as read, write, enable notify. It is required that a client characteristic must be discovered before we could doing anything with it.
The service should be discovered before we could discover its characteristic. This can be done by calling hrms.discover(conn_handle)
. Where conn_handle is the connection ID i.e peripheral that we want to discover since it is possible for Bluefruit nRF52 to connect to multiple peripherals concurrently. If the service is found, the function will return true, otherwise false.
// Connect Callback Part 1 void connect_callback(uint16_t conn_handle) { Serial.println("Connected"); Serial.print("Discovering HRM Service ... "); // If HRM is not found, disconnect and return if ( !hrms.discover(conn_handle) ) { Serial.println("Found NONE"); // disconect since we couldn't find HRM service Bluefruit.Central.disconnect(conn_handle); return; } // Once HRM service is found, we continue to discover its characteristic Serial.println("Found it"); ............. }
4. Afterwards, we continue to discover all the interested characteristics within the service by calling .discover()
. The function return true if characteristics is found, and false otherwise. You could also check with .discovered()
function. A service could contain more characteristics but we don't need to discover them all, only those that we want to interact with.
Advanced: Alternatively, you could discover all the interested characteristics of a service within a function call by usingBluefruit.Discovery.discoverCharacteristic()
(not used in the example). The API can take up to 5 characteristics, if you need more, the variant with passing array of characteristics is also available. The function will return the number of characteristic it found.
Note: when a characteristic is discovered by above API, all necessarily meta data such as handles, properties ( read,write, notify etc ...), cccd handle will be updated automatically. You can then use BLECLientCharacteristic API such as read(), write(), enableNotify() on it provided that its properties support such as operation.
// Connect Callback Part 2 void connect_callback(uint16_t conn_handle) { Serial.print("Discovering Measurement characteristic ... "); if ( !hrmc.discover() ) { // Measurement chr is mandatory, if it is not found (valid), then disconnect Serial.println("not found !!!"); Serial.println("Measurement characteristic is mandatory but not found"); Bluefruit.Central.disconnect(conn_handle); return; } Serial.println("Found it"); // Measurement is found, continue to look for option Body Sensor Location // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.body_sensor_location.xml // Body Sensor Location is optional, print out the location in text if present Serial.print("Discovering Body Sensor Location characteristic ... "); if ( bslc.discover() ) { Serial.println("Found it"); // Body sensor location value is 8 bit const char* body_str[] = { "Other", "Chest", "Wrist", "Finger", "Hand", "Ear Lobe", "Foot" }; // Read 8-bit BSLC value from peripheral uint8_t loc_value = bslc.read8(); Serial.print("Body Location Sensor: "); Serial.println(body_str[loc_value]); }else { Serial.println("Found NONE"); } ............... }
5. Once hrmc is discovered, you should enable its notification by calling hrmc.enableNotify()
. If this succeeded (return true), peripheral can now send data to us using notify message. Which will trigger the callback that we setup earlier to handle incoming data.
// Connect Callback Part 3 void connect_callback(uint16_t conn_handle) { ....... // Reaching here means we are ready to go, let's enable notification on measurement chr if ( hrmc.enableNotify() ) { Serial.println("Ready to receive HRM Measurement value"); }else { Serial.println("Couldn't enable notify for HRM Measurement. Increase DEBUG LEVEL for troubleshooting"); } }
/** * Hooked callback that triggered when a measurement value is sent from peripheral * @param chr Pointer to client characteristic that even occurred, * in this example it should be hrmc * @param data Pointer to received data * @param len Length of received data */ void hrm_notify_callback(BLEClientCharacteristic* chr, uint8_t* data, uint16_t len) { // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml // Measurement contains of control byte0 and measurement (8 or 16 bit) + optional field // if byte0's bit0 is 0 --> measurement is 8 bit, otherwise 16 bit. Serial.print("HRM Measurement: "); if ( data[0] & bit(0) ) { uint16_t value; memcpy(&value, data+1, 2); Serial.println(value); } else { Serial.println(data[1]); } }
/********************************************************************* This is an example for our nRF52 based Bluefruit LE modules Pick one up today in the adafruit shop! Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit! MIT license, check LICENSE for more information All text above, and the splash screen below must be included in any redistribution *********************************************************************/ #include <bluefruit.h> /* HRM Service Definitions * Heart Rate Monitor Service: 0x180D * Heart Rate Measurement Char: 0x2A37 * Body Sensor Location Char: 0x2A38 */ BLEService hrms = BLEService(UUID16_SVC_HEART_RATE); BLECharacteristic hrmc = BLECharacteristic(UUID16_CHR_HEART_RATE_MEASUREMENT); BLECharacteristic bslc = BLECharacteristic(UUID16_CHR_BODY_SENSOR_LOCATION); BLEDis bledis; // DIS (Device Information Service) helper class instance BLEBas blebas; // BAS (Battery Service) helper class instance uint8_t bps = 0; void setup() { Serial.begin(115200); while ( !Serial ) delay(10); // for nrf52840 with native usb Serial.println("Bluefruit52 HRM Example"); Serial.println("-----------------------\n"); // Initialise the Bluefruit module Serial.println("Initialise the Bluefruit nRF52 module"); Bluefruit.begin(); // Set the connect/disconnect callback handlers Bluefruit.Periph.setConnectCallback(connect_callback); Bluefruit.Periph.setDisconnectCallback(disconnect_callback); // Configure and Start the Device Information Service Serial.println("Configuring the Device Information Service"); bledis.setManufacturer("Adafruit Industries"); bledis.setModel("Bluefruit Feather52"); bledis.begin(); // Start the BLE Battery Service and set it to 100% Serial.println("Configuring the Battery Service"); blebas.begin(); blebas.write(100); // Setup the Heart Rate Monitor service using // BLEService and BLECharacteristic classes Serial.println("Configuring the Heart Rate Monitor Service"); setupHRM(); // Setup the advertising packet(s) Serial.println("Setting up the advertising payload(s)"); startAdv(); Serial.println("Ready Player One!!!"); Serial.println("\nAdvertising"); } void startAdv(void) { // Advertising packet Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); Bluefruit.Advertising.addTxPower(); // Include HRM Service UUID Bluefruit.Advertising.addService(hrms); // Include Name Bluefruit.Advertising.addName(); /* Start Advertising * - Enable auto advertising if disconnected * - Interval: fast mode = 20 ms, slow mode = 152.5 ms * - Timeout for fast mode is 30 seconds * - Start(timeout) with timeout = 0 will advertise forever (until connected) * * For recommended advertising interval * https://developer.apple.com/library/content/qa/qa1931/_index.html */ Bluefruit.Advertising.restartOnDisconnect(true); Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds } void setupHRM(void) { // Configure the Heart Rate Monitor service // See: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml // Supported Characteristics: // Name UUID Requirement Properties // ---------------------------- ------ ----------- ---------- // Heart Rate Measurement 0x2A37 Mandatory Notify // Body Sensor Location 0x2A38 Optional Read // Heart Rate Control Point 0x2A39 Conditional Write <-- Not used here hrms.begin(); // Note: You must call .begin() on the BLEService before calling .begin() on // any characteristic(s) within that service definition.. Calling .begin() on // a BLECharacteristic will cause it to be added to the last BLEService that // was 'begin()'ed! // Configure the Heart Rate Measurement characteristic // See: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml // Properties = Notify // Min Len = 1 // Max Len = 8 // B0 = UINT8 - Flag (MANDATORY) // b5:7 = Reserved // b4 = RR-Internal (0 = Not present, 1 = Present) // b3 = Energy expended status (0 = Not present, 1 = Present) // b1:2 = Sensor contact status (0+1 = Not supported, 2 = Supported but contact not detected, 3 = Supported and detected) // b0 = Value format (0 = UINT8, 1 = UINT16) // B1 = UINT8 - 8-bit heart rate measurement value in BPM // B2:3 = UINT16 - 16-bit heart rate measurement value in BPM // B4:5 = UINT16 - Energy expended in joules // B6:7 = UINT16 - RR Internal (1/1024 second resolution) hrmc.setProperties(CHR_PROPS_NOTIFY); hrmc.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); hrmc.setFixedLen(2); hrmc.setCccdWriteCallback(cccd_callback); // Optionally capture CCCD updates hrmc.begin(); uint8_t hrmdata[2] = { 0b00000110, 0x40 }; // Set the characteristic to use 8-bit values, with the sensor connected and detected hrmc.write(hrmdata, 2); // Configure the Body Sensor Location characteristic // See: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.body_sensor_location.xml // Properties = Read // Min Len = 1 // Max Len = 1 // B0 = UINT8 - Body Sensor Location // 0 = Other // 1 = Chest // 2 = Wrist // 3 = Finger // 4 = Hand // 5 = Ear Lobe // 6 = Foot // 7:255 = Reserved bslc.setProperties(CHR_PROPS_READ); bslc.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); bslc.setFixedLen(1); bslc.begin(); bslc.write8(2); // Set the characteristic to 'Wrist' (2) } void connect_callback(uint16_t conn_handle) { // Get the reference to current connection BLEConnection* connection = Bluefruit.Connection(conn_handle); char central_name[32] = { 0 }; connection->getPeerName(central_name, sizeof(central_name)); Serial.print("Connected to "); Serial.println(central_name); } /** * Callback invoked when a connection is dropped * @param conn_handle connection where this event happens * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h */ void disconnect_callback(uint16_t conn_handle, uint8_t reason) { (void) conn_handle; (void) reason; Serial.print("Disconnected, reason = 0x"); Serial.println(reason, HEX); Serial.println("Advertising!"); } void cccd_callback(uint16_t conn_hdl, BLECharacteristic* chr, uint16_t cccd_value) { // Display the raw request packet Serial.print("CCCD Updated: "); //Serial.printBuffer(request->data, request->len); Serial.print(cccd_value); Serial.println(""); // Check the characteristic this CCCD update is associated with in case // this handler is used for multiple CCCD records. if (chr->uuid == hrmc.uuid) { if (chr->notifyEnabled(conn_hdl)) { Serial.println("Heart Rate Measurement 'Notify' enabled"); } else { Serial.println("Heart Rate Measurement 'Notify' disabled"); } } } void loop() { digitalToggle(LED_RED); if ( Bluefruit.connected() ) { uint8_t hrmdata[2] = { 0b00000110, bps++ }; // Sensor connected, increment BPS value // Note: We use .notify instead of .write! // If it is connected but CCCD is not enabled // The characteristic's value is still updated although notification is not sent if ( hrmc.notify(hrmdata, sizeof(hrmdata)) ){ Serial.print("Heart Rate Measurement updated to: "); Serial.println(bps); }else{ Serial.println("ERROR: Notify not set in the CCCD or not connected!"); } } // Only send update once per second delay(1000); }
Text editor powered by tinymce.