/
BLE on the Arduino Nano 33 BLE using mbed

BLE on the Arduino Nano 33 BLE using mbed

Although the ArduinoBLE library offers a quick way to get started with BLE on the Arduino Nano 33 BLE, you may wish to instead use the underlying mbed libraries. These give you more control over the hardware, and in particular allow you to set a higher transmit power than the default used in the ArduinoBLE library. If you are not familiar with BLE or the ArduinoBLE library, you may wish to first start with "BLE Basics and the ArduinoBLE library". 


In this page I will take you through an example program which shows how to setup BLE, setting up custom characteristics, setting custom advertising data to configure transmit power and long range mode, and handling of events such as reads or writes.

mbedBLEExample.ino


This example has 7 characteristics split across 4 services which demonstrate reading a sensor and updating its characteristic, characteristics of different data types, including arrays, taking action after a characteristic has be read from or written to, and setting up a characteristic for notifications. In this example I have still used Arduino libraries for things like GPIO to demonstrate how Arduino and mbed code can be blended together, you may wish to go all mbed in your project.


Includes, Defines and Global Variables

At the start of the example file, we start by importing our libraries. As we are using the Arduino IDE, we first need to import the mbed library, and using namespace mbed allows us to use mbed functions without prefixing them with mbed::. Next we import the required BLE libraries. BLE.h is the main BLE library, Gap.h is the library for "Generic Access Profile", which we will be using to setup the advertising for the BLE connection, which lets us set the name of the device as well as control the transmit power and the encoding used for transmission, and GattServer.h is for "Generic ATTribute Profile", which is used to setup services and characteristics that we can use to send and receive information. Our final library is ARB.h, which is the library for use with the ARB board.


Next we have a large block of #defines. These are used to define the UUIDs used to identify our custom services and characteristics.


Next we have some global variables used to store some of the data we will be sending, and some constant variable used to store some of the parts that are required for setting up BLE.


Finally we have a function prototype used to declare a helper function for reading the ultrasonic sensor.

// Import the mbed library and use its namespace to let us use mbed functions
#include <mbed.h>
using namespace mbed;

// Required libraries for BLE
#include "ble/BLE.h"
#include "ble/Gap.h"
#include "ble/GattServer.h"

#include <ARB.h>

// Constants to hold our UUIDs for the service and the characteristic
// Short UUIDs need to be defined as numbers, long UUIDs need to be defined as strings 

#define HELLO_WORLD_SERVICE_UUID 0x180A // 0x180A = "Device Information"
#define HELLO_WORLD_TEXT_UUID 0x2A25 // 0x2A25 = "Serial Number String"

#define USONIC_SERVICE_UUID "5a060000-0027-4090-bbcf-ca3ff3a88f63"
#define USONIC_CHARA_UUID "5a060001-0027-4090-bbcf-ca3ff3a88f63"

#define LED_SERVICE_UUID "7eca0000-67d9-4473-8015-5dbe1681ae1a"
#define LED_CHARA_R_UUID "7eca0001-67d9-4473-8015-5dbe1681ae1a"
#define LED_CHARA_G_UUID "7eca0002-67d9-4473-8015-5dbe1681ae1a"
#define LED_CHARA_B_UUID "7eca0003-67d9-4473-8015-5dbe1681ae1a"
#define LED_CHARA_MONO_UUID "7eca0004-67d9-4473-8015-5dbe1681ae1a"

#define COUNTER_SERVICE_UUID "73150000-4179-4433-9f08-164aaa2f0de6"
#define COUNTER_CHARA_UUID "73150001-4179-4433-9f08-164aaa2f0de6"

// Timeout of 200,000uS for reading the USONIC sensors
#define USONIC_TIMEOUT 200000

// Stores the duration and distances from the USONIC sensors
int uS_duration, uS_prev_cm, uS_cm;

// Counter to hold how many times the value has been read
int counter = 0;

// Our base lorem ipusm string to pad out the characteristic to the max size of 512 bytes
char helloworld[] = "Hello World!";
const static char DEVICE_NAME[] = "Demo";                    // Device name when detected on Bluetooth
static events::EventQueue event_queue(10 * EVENTS_EVENT_SIZE);  // Create Event queue
static const uint16_t MAX_ADVERTISING_PAYLOAD_SIZE = 59;        // Advertising payload parameter
const ble::phy_set_t CodedPHY(ble::phy_t::LE_CODED);            // Creating a Coded Phy set

// Function prototype
int BLESafepulseIn(int pin);

The BLEDemo Class

The mbed BLE library is class-based, and requires you to setup a class that extends some of it's classes to access it's methods. In this section we will go through the main class of this example, which is called BLEDemo.

The class starts with its declaration on line 51. Here we call the class BLEDemo and the class extends the class ble::Gap::EventHandler. This allows this class to handle events coming from the GAP, which includes connection and disconnection. Next we have the class constructor, which will setup the class when it is instantiated. The class takes two parameters, a pointer to the BLE object, and a pointer to the event queue where the BLE event will be stored. Next we have an initialisation list to initialise some of the fields of the class. _ble and _event_queue are local copies of the variables passed to the class, _adv _handle and _adv _data _builder are required for setting the custom advertising data later on, and all of the Chara fields are what will become our custom characteristics. The are all instantiated with the minumum of the UUID, using what we defined earlier, and a pointer to a variable which holds it's initial value. These variables will be declared later in the class, and the rest of the characteristic will be constructed with a template. The one exception is uSonicChara, which has the notify property added to it, as this is not covered by the templates, and we want the central device to be able to subscribe to this characteristic to receive notifications when the value updates.


Next we have the constructor method which is the code that is ran when the object is constructed. This is where we will setup our services and add our characteristics to them. It first checks is it has been ran before to ensure the services are not setup twice, then creates arrays or pointers to each characteristic that belongs to each service. Next the services can be declared, with their UUIDs, the array of characteristics, and the number of characteristics in the array, which is calculated by taking the size of the array and dividing by the size of a single entry to get the number of entries. Once declared, the services are then added to the Gatt Server. We also add a couple of callback functions to the Gatt Server, onDataRead and onDataWritten. We will declare these functions later on in the class and use them to take action when characteristics have either been read from or written to. Finally we set our flag to ensure that the services are not duplicated.


Next we have the class destructor, which will shut down the BLE if the class has been destroyed.


After that we have a method called start which is called after instantiating the class to start up BLE, and another method called pollBLE which we will use in our main loop to check for, and handle BLE events.


Next we have all of our callback methods. First is on_init_complete which is a GAP callback that is called when BLE has finished being initialised. If first checks that BLE has been initialised properly, and print debug statements accordingly. Assuming the initialisation was successful, it goes on to setup the custom advertising parameters that we will use to set the transmit power and the long range coding. 


First we instantiate the AdvertisingParameters object as adv_parameters and passes some parameters that sets up the type of advertising. Next we set the PHY to LE_CODED using setPhy. This is so it will advertise that it supports the long range coding. Next we set the advertised transmit power to 8dBm, which is the maximum that the 33 BLE supports, then we set the preferred PHY to the CodedPHY, which means it will try to use the long range mode when possible.


Next we need to create an advertising set with the parameters we have just defined. Then we clear the existing advertising data, set the required flags, and set the name that the device will advertise itself as.


Finally we can add set the advertising parameters and payload in GAP, and start advertising.


The next two callback methods, onConnectionComplete and onDisconnectionComplete are also from GAP and are called when another devices connects to or disconnects from this device. In this case they both print debug information, but onDisconnectionComplete is also used to re-start advertising once a connected device has disconnected.


Next we have our GATT callbacks, onDataRead and onDataWritten. In these methods we have our first interactions with our characteristics. In onDataRead we increment our counter, then write the update value of that counter to the counter characteristic, counterChara. The callback executes on every read, this means that the counter will be the number of times any read has been performed. onDataWritten is used to here to turn an LED on or off depending on the value written to the corresponding characteristic. First we check which characteristic has been written to by comparing the handle passed to the method with the handles of our characteristics, then we put the written data into the local variable, and finally we update the LED.


Next we have all of our variable declarations for the class. The first couple are the requirements for setting up BLE, the next three are for our custom advertising. Then we have variable to store our characteristics values, and finally we have our characteristics. These are declared using templates, which allow us to specify read-only or read-write, the data type of our characteristic, and in the case of the array characteristic, the size of the array.


Finally we have a setter function to allow us to write to our ultrasonic characteristic from outside the class, as the characteristics themselves are declared as private, so are only accessible from within the class.

// This class contains all of the bluetooth stuff
/*------------------------------Bluetooth Device class------------------------------*/
class BLEDemo : public ble::Gap::EventHandler {
public:
    /* Class constructor */
    BLEDemo(BLE &ble, events::EventQueue &event_queue) :
        _ble(ble),                                      // BLE API Class
        _event_queue(event_queue),                      // Event Queue
        _adv_handle(ble::INVALID_ADVERTISING_HANDLE),   // Advertising parameter
        _adv_data_builder(_adv_buffer),                 // Advertising parameter
        //Now we will define our custom characteristics
        //We only need to define the name, UUID and initial value here, the rest will be set by the template later
        helloWorldChara(HELLO_WORLD_TEXT_UUID, helloworld),
        // The templates we se below only cover read and write, so for notify we need to specify it ourselves
        uSonicChara(USONIC_CHARA_UUID, &uSonicValue, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY),
        LEDCharaR(LED_CHARA_R_UUID, &LEDRBool),
        LEDCharaG(LED_CHARA_G_UUID, &LEDGBool),
        LEDCharaB(LED_CHARA_B_UUID, &LEDBBool),
        LEDCharaMono(LED_CHARA_MONO_UUID, &LEDMonoBool),
        counterChara(COUNTER_CHARA_UUID, &counter)
        
        {
            static bool servicesAdded = false;// we only want the services to be added once
            if(servicesAdded){
                return;
            }

            // We need to make arrays of our characteristics so they can be added to their services when we create them
            GattCharacteristic *helloWorldCharaTable[] = {&helloWorldChara};
            GattCharacteristic *uSonicCharaTable[] = {&uSonicChara};
            GattCharacteristic *LEDCharaTable[] = {&LEDCharaR, &LEDCharaG, &LEDCharaB, &LEDCharaMono};
            GattCharacteristic *counterCharaTable[] = {&counterChara};

            // Now we can create our services
            GattService helloWorldService(HELLO_WORLD_SERVICE_UUID, helloWorldCharaTable, sizeof(helloWorldCharaTable) / sizeof(helloWorldCharaTable[0]));
            GattService uSonicService(USONIC_SERVICE_UUID, uSonicCharaTable, sizeof(uSonicCharaTable) / sizeof(uSonicCharaTable[0]));
            GattService LEDService(LED_SERVICE_UUID, LEDCharaTable, sizeof(LEDCharaTable) / sizeof(LEDCharaTable[0]));
            GattService counterService(COUNTER_SERVICE_UUID, counterCharaTable, sizeof(counterCharaTable) / sizeof(counterCharaTable[0]));

            // Now we can add our services to the main Gatt server
            _ble.gattServer().addService(helloWorldService);
            _ble.gattServer().addService(uSonicService);
            _ble.gattServer().addService(LEDService);
            _ble.gattServer().addService(counterService);

            // Here we set callback functions for when data is read or written
            _ble.gattServer().onDataRead(this, &BLEDemo::onDataRead);
            _ble.gattServer().onDataWritten(this, &BLEDemo::onDataWritten);
            
            servicesAdded = true; //set added flag

        }
        

    /* Class destructor */
    ~BLEDemo() {
        if (_ble.hasInitialized()) {
            _ble.shutdown(); //shutdown ble if class is destroyed
        }
    }

    void start() {
        _ble.gap().setEventHandler(this);               // Assign GAP events to this class
        _ble.init(this, &BLEDemo::on_init_complete);     // Initialize Bluetooth
    }

    // Method to call to handle BLE events
    void pollBLE(){
        _event_queue.dispatch_once();
    }

private:
    /** Callback triggered when the ble initialization process has finished */
    void on_init_complete(BLE::InitializationCompleteCallbackContext *params) {
        if (params->error != BLE_ERROR_NONE) {
            Serial.println("Ble initialization failed.");
            return;
        }
        Serial.println("Ble initialized.");

        /* Create advertising parameters and payload */
        ble::AdvertisingParameters adv_parameters(
            ble::advertising_type_t::CONNECTABLE_UNDIRECTED,    // Advertising Type here : connectable undirected = connectable with extended advertising
            ble::adv_interval_t(ble::millisecond_t(500)),           // Min Advertising time in ms
            ble::adv_interval_t(ble::millisecond_t(500)),           // Max Advertising time in ms
            false                                                   // Legacy PDU : Needed to be OFF in Long Range Mode
        );
        adv_parameters.setPhy(ble::phy_t::LE_CODED, ble::phy_t::LE_CODED);  // Set Advertising radio modulation to LE_CODED Phy (=Long Range)
        adv_parameters.setTxPower(8);                                       // Set radio output power to 8dbm (max) 
        _ble.gap().setPreferredPhys(&CodedPHY, &CodedPHY);                  // Set preferred connection phy to LE_CODED (=long range)

        if (_adv_handle == ble::INVALID_ADVERTISING_HANDLE) {       // Create advertising set with parameters defined before
        _ble.gap().createAdvertisingSet(
                &_adv_handle,
                adv_parameters);
        }
        _adv_data_builder.clear();                                  
        _adv_data_builder.setFlags();
        _adv_data_builder.setName(DEVICE_NAME);                     // Set Bluetooth device name

        /* Setup advertising */
        _ble.gap().setAdvertisingParameters(_adv_handle, adv_parameters); 
        _ble.gap().setAdvertisingPayload(_adv_handle, _adv_data_builder.getAdvertisingData());  

        /* Start advertising */
        _ble.gap().startAdvertising(_adv_handle);
        Serial.println("Start advertising...");
    }

    void onConnectionComplete(const ble::ConnectionCompleteEvent &event) {
        Serial.println("Device connected");
    }

    void onDisconnectionComplete(const ble::DisconnectionCompleteEvent&) {
        Serial.println("Device disconnected");
        _ble.gap().startAdvertising(_adv_handle);
        Serial.println("Start advertising...");
    }

    // This function updates the counter characterstic each time any read is performed
    void onDataRead(const GattReadCallbackParams *params) {
        counter++;
        _ble.gattServer().write(counterChara.getValueHandle(),(uint8_t*)&counter, sizeof(counter));
    }

    // The onDataWritten function is mainly used to update the local variable when the characteristic has been written to
    void onDataWritten(const GattWriteCallbackParams *params){
        if(params->handle == LEDCharaR.getValueHandle()){
            LEDRBool = *params->data;
            digitalWrite(LEDR,!LEDRBool); 
        }
        else if(params->handle == LEDCharaG.getValueHandle()){
            LEDGBool = *params->data;
            digitalWrite(LEDG,!LEDGBool); 
        }
        else if(params->handle == LEDCharaB.getValueHandle()){
            LEDBBool = *params->data;
            digitalWrite(LEDB,!LEDBBool); 
        }
        else if(params->handle == LEDCharaMono.getValueHandle()){
            LEDMonoBool = *params->data;
            digitalWrite(LED_BUILTIN,LEDMonoBool);
        }
    }
    

private:
    /* Class variables declaration*/
    BLE &_ble;
    events::EventQueue &_event_queue;

    uint8_t _adv_buffer[MAX_ADVERTISING_PAYLOAD_SIZE];  // Advertising parameters
    ble::advertising_handle_t _adv_handle;              //
    ble::AdvertisingDataBuilder _adv_data_builder;      //

    // Variables to hold our characteristics' values
    int uSonicValue = 63;
    bool LEDRBool, LEDGBool, LEDBBool, LEDMonoBool=true;

    // Here we can use templates to help finish constructing our characteristics
    ReadOnlyArrayGattCharacteristic<char, 12> helloWorldChara; // This template is used for arrays. It takes the type and the maximum size
    ReadOnlyGattCharacteristic<int> uSonicChara; // These other templates are all for single variables, and just take a type
    ReadWriteGattCharacteristic<bool> LEDCharaR;
    ReadWriteGattCharacteristic<bool> LEDCharaG;
    ReadWriteGattCharacteristic<bool> LEDCharaB;
    ReadWriteGattCharacteristic<bool> LEDCharaMono;
    ReadOnlyGattCharacteristic<int> counterChara;

public:
    // Setter function for the uSonic characteristic
    void writeuSonicChara(int uSonic){
        _ble.gattServer().write(uSonicChara.getValueHandle(), (uint8_t*)&uSonic, sizeof(uSonic));
    }

};

Setup

Now we have constructed our main class, we can get onto some of the setup code. First off in this section we have a callback function that sets up our event queue that is required for mbed BLE that will be used later in the setup.


Next we create our BLE instance object, and create an object from our BLEDemo class we created in the last section. These are declared globally so they can be used in both the setup() function and the loop() function.


Now we have our setup() function, which looks much like a regular Arduino setup function, with assigning pin modes and writing default values to pins, with the exception of two function calls at the end which sets up our event queue and starts the BLE using the start method from our BLEDemo class.

/** Schedule processing of events from the BLE middleware in the event queue. */
void schedule_ble_events(BLE::OnEventsToProcessCallbackContext *context) {
    event_queue.call(Callback<void()>(&context->ble, &BLE::processEvents));
}

BLE &bleObject = BLE::Instance();                 // Create the BLE object in order to use BLE_API function

BLEDemo demo(bleObject, event_queue);              // Create demo object from the class BLEDemo

/*====================== MAIN CODE ======================*/

void setup(){
    ARBSetup(); // Setup the ARB board
    
    // set LED pins to output mode
    pinMode(LEDR, OUTPUT);
    pinMode(LEDG, OUTPUT);
    pinMode(LEDB, OUTPUT);
    pinMode(LED_BUILTIN, OUTPUT);
    
    digitalWrite(LED_BUILTIN, HIGH); // This will turn the LED on
    digitalWrite(LEDR, HIGH); // will turn the LED off
    digitalWrite(LEDG, HIGH); // will turn the LED off
    digitalWrite(LEDB, HIGH); // will turn the LED off
    
    /* Setup Debugging */
    Serial.begin(9600);
    
    bleObject.onEventsToProcess(schedule_ble_events); // Set event schedule
    demo.start();                               // Start Bluetooth Long Range
}

Loop

Finally we have our main code loop. At start of every loop, we call the pollBLE() method to service BLE events. This should be called as often as possible to ensure BLE events are services in a timely manner. The reset of the loop is code to read from an ultrasonic sensor that is similar to code you may have seen in other examples, with the exception that the result is written into the uSonic characteristic with the setter method we created earlier.


Finally, after the loop, we have a helper function which is used to read the pulse back from the ultrasonic sensor, as the Arduino pulseIn function uses a hardware timer that is also used by BLE, so they cannot be used at the same time. You may wish to use a more efficient method in your project, this is here as an example.

void loop(){
    demo.pollBLE();
    
    // Set the pin to output, bring it low, then high, then low to generate pulse
    pinMode(USONIC1, OUTPUT);
    digitalWrite(USONIC1, LOW);
    delayMicroseconds(2);
    digitalWrite(USONIC1, HIGH);
    delayMicroseconds(15);
    digitalWrite(USONIC1, LOW);

    // The same pin is used to read back the returning signal, so must be set back to input
    pinMode(USONIC1, INPUT);
    uS_duration = BLESafepulseIn(USONIC1);
    uS_prev_cm = uS_cm;
    uS_cm = uSecToCM(uS_duration);

    if(uS_prev_cm != uS_cm){
        demo.writeuSonicChara(uS_cm);
    }
    
}

/*=======================================================*/

/*
    Helper function to replace the pulseIn function as it's not compatible
    with BLE. This way is less accurate than the pulseIn
    function, but works without breaking BLE.
*/
int BLESafepulseIn(int pin){
    int previousMicros = micros();
    while(!digitalRead(pin) && (micros() - previousMicros) <= USONIC_TIMEOUT); // wait for the echo pin HIGH or timeout
    previousMicros = micros();
    while(digitalRead(pin)  && (micros() - previousMicros) <= USONIC_TIMEOUT); // wait for the echo pin LOW or timeout
    return micros() - previousMicros; // duration
}

Related content