How to Use Particle's Powerful Bluetooth API

I had a chance to play around with the Bluetooth API over the past two days. Seems solid!! My hat is tipped to @ielec and team for an awesome feature add. :slight_smile:

I ended up creating a way to update the RGB LED from an app like Light Blue Exploerer. Plus, as extra credit, got it to push the state over the local mesh network.

This tutorial is a cross post from my blog. I warn you it is lengthy. On the plus side it has every detail that I could think of to make it useful. You can just skip the parts that you already know/understand. :wink:

Here we go.

Stage 1: Setting Up Bluetooth

Write the Code

We want to set up a service with 3 characteristics. The characteristics relate to the intensity of the RGB LEDs respectively. Here’s how to get your Bluetooth Set Up:

  1. In your Setup() function enable app control of your LED

    RGB.control(true);
    
  2. Set up your UUIDs at the top of your .ino file

     const char* serviceUuid = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E";
     const char* red         = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E";
     const char* green       = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E";
     const char* blue        = "6E400004-B5A3-F393-E0A9-E50E24DCCA9E";
    

    UUIDs are unique identifiers or addresses. They’re used to differentiate different services and characteristics on a device.

    The above UUIDs are used in previous Particle examples. If you want to create your own you can use uuidgen on the OSX command line. You can also go to a website like Online GUID Generator.

    Use the above settings to get your own UUID. You can then create your service and characteristic UUIDS from this generated one:

     const char* serviceUuid = "b425040**0**-fb4b-4746-b2b0-93f0e61122c6"; //service
     const char* red         = "b4250401-fb4b-4746-b2b0-93f0e61122c6"; //red char
     const char* green       = "b4250402-fb4b-4746-b2b0-93f0e61122c6"; //green char
     const char* blue        = "b4250403-fb4b-4746-b2b0-93f0e61122c6"; //blue char
    

    There’s no right or wrong way to do this. But you have to be careful you’re not using the UUIDs reserved by the Bluetooth SIG. This is highly unlikely. If you do want to double check you can go here and here.

    For now, we’ll stick with the first set of UUIDs.

  3. In Setup(), initialize your service.

     // Set the RGB BLE service
     BleUuid rgbService(serviceUuid);
    

    This is the first step of "registering’ your service. More on this below.

  4. Initialize each characteristic in Setup()

     BleCharacteristic redCharacteristic("red", BleCharacteristicProperty::WRITE_WO_RSP, red, serviceUuid, onDataReceived, (void*)red);
     BleCharacteristic greenCharacteristic("green", BleCharacteristicProperty::WRITE_WO_RSP, green, serviceUuid, onDataReceived, (void*)green);
     BleCharacteristic blueCharacteristic("blue", BleCharacteristicProperty::WRITE_WO_RSP, blue, serviceUuid, onDataReceived, (void*)blue);
    

    For this setup, we’re going to use the WRITE_WO_RSP property. This allows us to write the data and expect no response.
    I’ve referenced the UUIDs as the next two parameters. The first being the characteristic UUID. The second being the service UUID.

    The next parameter is the callback function. When data is written to this callback, this function will fire.

    Finally the last parameter is the context. What does this mean exactly? We’re using the same callback for all three characteristics. The only way we can know which characteristic was written to (in deviceOS at least) is by setting a context. In this case we’re going to use the already available UUIDs.

  5. Right after defining the characteristics, let’s add them so they show up:

     // Add the characteristics
     BLE.addCharacteristic(redCharacteristic);
     BLE.addCharacteristic(greenCharacteristic);
     BLE.addCharacteristic(blueCharacteristic);
    
  6. Set up the callback function.

     // Static function for handling Bluetooth Low Energy callbacks
     static void onDataReceived(const uint8_t* data, size_t len, const BlePeerDevice& peer, void* context) {
    
     }
    

    You can do this at the top of the file (above Setup()) We will define this more later.

  7. Finally, in order for your device to be connectable, we have to set up advertising. Place this code at the end of your Setup() function

     // Advertising data
     BleAdvertisingData advData;
    
     // Add the RGB LED service
     advData.appendServiceUUID(rgbService);
    
     // Start advertising!
     BLE.advertise(&advData);
    

    First we create a BleAdvertisingData object. We add the rgbService from Step 3. Finally, we can start advertising so our service and characteristics are discoverable!

Time to test

At this point we have a minimally viable program. Let’s compile it and program it to our Particle hardware. This should work with any Mesh enabled device. (Xenon, Argon, Boron)

  1. Before we start testing, temporarily add SYSTEM_MODE(MANUAL); to the top of your file. This will prevent the device connecting to the mesh network. If the device is blinking blue on startup, you’ll have to set it up with the Particle App before continuing.

  2. Download the 1.3.0-rc.1 image here. For Xenon, you’ll need xenon-system-part1@1.3.0-rc.1.bin. For others look for boron-system-part1@1.3.0-rc.1.bin and argon-system-part1@1.3.0-rc.1.bin. The files are at the bottom of the page under Assets

  3. Put your device into DFU mode. Hold the Mode Button and momentarily click the Reset Button. Continue holding the Mode Button until the LED blinks yellow.

  4. In a command line window, change directories to where you stored the file you downloaded. In my case the command is cd ~/Downloads/

  5. Then run:

     particle flash --usb xenon-system-part1@1.3.0-rc.1.bin
    

    This will install the latest OS to your Xenon. Once it’s done it will continue to rapidly blink yellow. Again if you have a different Particle Mesh device, change the filename to match.

  6. In Visual Code, use the Command + Shift + P key combination to pop up the command menu. Select Particle: Compile application (local)

  7. Fix any errors that may pop up.

  8. Then, open the same menu and select Flash application (local)

  9. When programming is complete, pull out your phone. Then, open your favorite Bluetooth Low Energy app. The best ones are NRF Connect and Light Blue Explorer. I’m going to use Light Blue Explorer for this example.

  10. Check if a device named “Xenon-” is advertising. Insert with the unique ID for your device.

  11. Find your device and click the name.

  12. Look at the list of services & characteristics. Does it include the service and characteristic UUID’s that we have set so far? For instance, does the service UUID show up as 6E400001-B5A3-F393-E0A9-E50E24DCCA9E?

    If everything shows up as you expect, you’re in a good place. If not go through the earlier instructions to make sure everything matches.

Stage 2: Handling Data

The next stage of our project is to process write events. We’ll be updating our onDataReceived function.

Write the Code

  1. First, let’s create a struct that will keep the state of the LEDs. This can be done at the top of the file.

     // Variables for keeping state
     typedef struct {
       uint8_t red;
       uint8_t green;
       uint8_t blue;
     } led_level_t;
    
  2. The second half of that is to create a static variable using this data type

     // Static level tracking
     static led_level_t m_led_level;
    

    The first two steps allows us to use one single variable to represent the three values of the RGB LED.

  3. Next, let’s check for basic errors inside the onDataReceive function For instance we want to make sure that we’re receiving only one byte.

     // We're only looking for one byte
       if( len != 1 ) {
         return;
     	}
    
  4. Next, we want to see which characteristic this event came from. We can use the context variable to determine this.

     // Sets the global level
       if( context == red ) {
         m_led_level.red = data[0];
       } else if ( context == green ) {
         m_led_level.green = data[0];
       } else if ( context == blue ) {
         m_led_level.blue = data[0];
       }
    

    Remember, in this case context will be equal to the pointer of either the red, green, or blue UUID string. You can also notice we’re setting m_led_level. That way we can update the RGB led even if only one value has changed.

  5. Finally, once set, you can write to the RGB object

     // Set RGB color
     	RGB.color(m_led_level.red, m_led_level.green, m_led_level.blue);
    

Test the Code

Let’s go through the same procedure as before to flash the device.

  1. Put your device into DFU mode. Hold the Mode Button and click the Reset Button. Continue holding the Mode Button until the LED blinks yellow.

  2. Then, open the same menu and select Flash application (local)

  3. Once it’s done programming, connect to the device using Light Blue Explorer.

  4. Tap on the characteristic that applies to the red LED.

  5. Write FF. The red LED should turn on.

  6. Write 00. The red LED should turn off.

  7. Do the same for the other two characteristics. We now have full control of the RGB LED over Bluetooth Low Energy!

Stage 3: Sharing Via Mesh

Finally, now that we’re successfully receiving BLE message, it’s time to forward them on to our mesh network.

Write the Code

  1. First let’s remove MANUAL mode. Comment out SYSTEM_MODE(MANUAL);

  2. At the top of the file let’s add a variable we’ll used to track if we need to publish

     // Tracks when to publish to Mesh
     static bool m_publish;
    
  3. Then initialize it in Setup()

     // Set to false at first
     m_publish = false;
    
  4. Then, after setting the RGB led in the onDataReceived callback, let’s set it true:

     // Set RGB color
     RGB.color(m_led_level.red, m_led_level.green, m_led_level.blue);
    
     // Set to publish
     m_publish = true;
    
  5. Let’s add a conditional in the loop() function. This will cause us to publish the LED status to the Mesh network:

     if( m_publish ) {
     	// Reset flag
     	m_publish = false;
    
     	// Publish to Mesh
       Mesh.publish("red", String::format("%d", m_led_level.red));
       Mesh.publish("green", String::format("%d", m_led_level.green));
       Mesh.publish("blue", String::format("%d", m_led_level.blue));
     }
    

    Mesh.publish requires a string for both inputs. Thus, we’re using String::format to create a string with our red, green and blue values.

  6. Then let’s subscribe to the same variables in Setup(). That way another device can cause the LED on this device to update as well.

     Mesh.subscribe("red", meshHandler);
     Mesh.subscribe("green", meshHandler);
     Mesh.subscribe("blue", meshHandler);
    
  7. Toward the top of the file we want to create meshHandler

     // Mesh event handler
     static void meshHandler(const char *event, const char *data)
     {
     }
    
  8. In this application, we need the event parameter and data parameter. In order use them, we have to change them to a String type. That way we can use the built in conversion and comparison functions. So, inside the meshHandler function add:

       // Convert to String for useful conversion and comparison functions
       String eventString = String(event);
       String dataString = String(data);
    
  9. Finally we do some comparisons. We first check if the event name matches. Then we set the value of the dataString to the corresponding led level.

       // Determine which event we recieved
       if( eventString.equals("red") ) {
         m_led_level.red = dataString.toInt();
       } else if ( eventString.equals("green") ) {
         m_led_level.green = dataString.toInt();
       } else if ( eventString.equals("blue") ) {
         m_led_level.blue = dataString.toInt();
       } else {
     		return;
     	}
    
       // Set RGB color
       RGB.color(m_led_level.red, m_led_level.green, m_led_level.blue);
    

    Then at the end we set the new RGB color. Notice how I handle an unknown state by adding a return statement in the else section. It’s always good to filter out error conditions before they wreak havoc!

Test the Code

  1. Open the Particle App on your phone

  2. Let’s set up the Argon first. If it’s not blinking blue, hold the mode button until it’s blinking blue.

    Note: if you’ve already programmed the app, the LED will be off by default. Hold the mode button for 5 seconds and then continue.

  3. Go through the pairing process. The app walks you though all the steps. Make sure you remember the Admin password for your mesh network.

  4. Program an Argon with the latest firmware (1.3.0) (see Stage 1 - Time to Test - Step 2 for a reminder on how to do this)

  5. Once rapidly blinking yellow, program the Argon with the Tinker app. You can download it at the release page.

  6. Once we have a nice solid Cyan LED (connected to the Particle Cloud) we’ll program the app. Use the Cloud Flash option in the drop down menu.

    As far as I can tell, Particle forces any device flashed locally into safe mode when connecting to the cloud. It may be my setup. Your mileage may vary here. Best to use Cloud Flash.

    Make sure you select the correct deviceOS version (1.3.0-rc1), device type (Argon) and device name (What you named it during setup)

  7. Connect to the Xenon using the phone app

  8. Connect the Xenon to your Mesh network using the phone app

  9. Flash your Xenon using Cloud Flash. Use the name that you gave it during the phone app setup. As long as the device is connected to Particle Cloud or in safe mode (Purple LED), it should program.

  10. Once connected, let’s get to the fun part. Open up Light Blue Explorer. Connect to either the Argon or the Xenon.

  11. Select one of the LED characteristics and change the value.

    The LED should change on all devices!

Final Code

Here’s the final code with all the pieces put together. You can use this to make sure you put them in the right place!!

/*
 * Project ble_mesh
 * Description: Bluetooth Low Energy + Mesh Example
 * Author: Jared Wolff
 * Date: 7/13/2019
 */

//SYSTEM_MODE(MANUAL);

// UUIDs for service + characteristics
const char* serviceUuid = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E";
const char* red         = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E";
const char* green       = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E";
const char* blue        = "6E400004-B5A3-F393-E0A9-E50E24DCCA9E";

// Set the RGB BLE service
BleUuid rgbService(serviceUuid);

// Variables for keeping state
typedef struct {
  uint8_t red;
  uint8_t green;
  uint8_t blue;
} led_level_t;

// Static level tracking
static led_level_t m_led_level;

// Tracks when to publish to Mesh
static bool m_publish;

// Mesh event handler
static void meshHandler(const char *event, const char *data)
{

  // Convert to String for useful conversion and comparison functions
  String eventString = String(event);
  String dataString = String(data);

  // Determine which event we recieved
  if( eventString.equals("red") ) {
    m_led_level.red = dataString.toInt();
  } else if ( eventString.equals("green") ) {
    m_led_level.green = dataString.toInt();
  } else if ( eventString.equals("blue") ) {
    m_led_level.blue = dataString.toInt();
  } else {
		return;
	}

  // Set RGB color
  RGB.color(m_led_level.red, m_led_level.green, m_led_level.blue);

}

// Static function for handling Bluetooth Low Energy callbacks
static void onDataReceived(const uint8_t* data, size_t len, const BlePeerDevice& peer, void* context) {

  // We're only looking for one byte
  if( len != 1 ) {
    return;
  }

  // Sets the global level
  if( context == red ) {
    m_led_level.red = data[0];
  } else if ( context == green ) {
    m_led_level.green = data[0];
  } else if ( context == blue ) {
    m_led_level.blue = data[0];
  }

  // Set RGB color
  RGB.color(m_led_level.red, m_led_level.green, m_led_level.blue);

  // Set to publish
  m_publish = true;

}

// setup() runs once, when the device is first turned on.
void setup() {

  // Enable app control of LED
  RGB.control(true);

  // Init default level
  m_led_level.red = 0;
  m_led_level.green = 0;
  m_led_level.blue = 0;

  // Set to false at first
  m_publish = false;

  // Set the subscription for Mesh updates
  Mesh.subscribe("red",meshHandler);
  Mesh.subscribe("green",meshHandler);
  Mesh.subscribe("blue",meshHandler);

  // Set up characteristics
  BleCharacteristic redCharacteristic("red", BleCharacteristicProperty::WRITE_WO_RSP, red, serviceUuid, onDataReceived, (void*)red);
  BleCharacteristic greenCharacteristic("green", BleCharacteristicProperty::WRITE_WO_RSP, green, serviceUuid, onDataReceived, (void*)green);
  BleCharacteristic blueCharacteristic("blue", BleCharacteristicProperty::WRITE_WO_RSP, blue, serviceUuid, onDataReceived, (void*)blue);

  // Add the characteristics
  BLE.addCharacteristic(redCharacteristic);
  BLE.addCharacteristic(greenCharacteristic);
  BLE.addCharacteristic(blueCharacteristic);

  // Advertising data
  BleAdvertisingData advData;

  // Add the RGB LED service
  advData.appendServiceUUID(rgbService);

  // Start advertising!
  BLE.advertise(&advData);
}

// loop() runs over and over again, as quickly as it can execute.
void loop() {

  // Checks the publish flag,
  // Publishes to a variable called "red" "green" and "blue"
  if( m_publish ) {

    // Reset flag
    m_publish = false;

    // Publish to Mesh
    Mesh.publish("red", String::format("%d", m_led_level.red));
    Mesh.publish("green", String::format("%d", m_led_level.green));
    Mesh.publish("blue", String::format("%d", m_led_level.blue));
  }

}

Further Thoughts

In comparison, this would take a significantly longer amount of time using the Nordic SDK. Yes, it gives you more options, but at the cost of time and errors! All of this was originally posted on my blog. I’m loving playing around with my Particle Mesh boards so there will be more where this comes from for sure!

Also, any and all feedback is appreciated. I want these tutorials to be as useful for you guys as possible. :slight_smile:

15 Likes

Thanks for a very detailed BLE example! BLE is powerful, especially Chrome Web BLE.

Thanks @RWB, glad you found it handy. :slight_smile:

Hello and thank you for that great topic !

For once on a forum i'm not writing because i have issue(s) but because i fixed them (first steps with the Particle ecosystem may have made it harder to me).
I hade issues to simply make the code run(1), then to get BLE working(2), then to get advertising working(3), then to find the device on the android app(4), then to have addCharacteristic working(5) (program was stopping when addCharacteristic was called).

2 days after hitting my head on the walls, ii finally made it work.

First of all i'm using the Wokbench on VSCode because i don't want my Xenon to be connected to the cloud. Not sure what was wrong for the 2 first issues, maybe i forgot to do some "Clean app" between my hundred of testing, most probably something else noob related...

Regarding the 3rd and 5th issue it was apparently due to a mismatch version of the compiled app and the deviceOs installed on my Xenon.
To fix this :

  • Make sure Workbench is up to date, this topic helped me (strangely it didn't install with latest deviceOs until i do that...)
  • Update your card to the latest deviceOs via the command line particle update
  • Check your deviceOS version :
    • Put your card in "listen mode" by pressing "mode" button for 3 seconds. Didn't work for me. I had to keep pressing "mode" button then hit/release "reset button" and release "mode" button once LED was blinking purple. Then the LED should blink blue which means it's in listen mode.
    • Connect to the card via Putty and just type "v" on the prompt window to display the os version, you should get something like this :
      xenonputty
    • Select the matching version on workbench by clicking on the bottom bar :
      xenonwb

Then advertising AND addCharacteristics should work. At first advertising worked me but characteristics didn't work until i had matchin deviceOs versions.

About the 4th issue, the device not poping up on the android APP :

  • Spoiler, it probably actually is displayed with with no name
  • For everyone the device seems to be listen at "Xenon-xxx", it did once for me, but that's all...
  • You can either check all the devices to find the one that declares your custom serviceUuid, or force the name of your BLE device
    Simply add this line to give it a name :

advData.appendLocalName("Your custom name");

Aaaaand... that should be ok.

Enjoy :slight_smile:

3 Likes

Very helpful tutorial, Jared.
I was able to learn a lot from this one.
Thanks!
Gustavo.

1 Like

Awesome. Glad you found it handy @gusgonnet!

@jaredwolff I have a project where I want to take advantage of the Argons BLE Advertise features to broadcast the product system status so the end user can view that data on their phone or tablet.

I see your tutorial for building a iOS Swift Application to communicate with a Argon via a iOS Native App.

Could we do the same thing using NativeScript so we could export the built application for both iOS and Android?

I have Losant to provide data when the Argons are connected to WiFi but I need either a Mobile Application for iOS and Android or a good Chrome BLE web page for local communication when WiFi is not avaliable or desired.

What are your thoughts on this?

Sup @RWB

Hmm good question. I believe Nativescript does have support Bluetooth support across both platforms. I stared that repo but I don’t remember if I dove into using it. There may be other options out there too, that was the first one that came up in a search.

Hope that helps!

I would probably require one of your awesome highly detailed toutorials to even get the initial hang of to get the BLE Advertised Data from a Argon into a Stored Variable on the phone or tablet.

Then once we have that data imported we can integrate it into a custom NativeScript App.

It seems like with your Particle WebSDK + Native Script along with Particle BLE API + Native Script we would be good for importing variable data vai either Local BLE or the WebSDK.

That would leave me with no excuse to not have a simple app for my projects to make them more useful and cool.

If you ever have time to play around with this I would love to see how its done.

You have a way of making stuff like this easier to understand.

1 Like

Yea this stuff can get hairy. If I do end up doing something similar again I’ll let you know.

By the way did you see this example?

I was using a Gen 3 board to listen for advertising packets from a Tile Mate. The concept should be similar enough to what you’re trying to do with your Argon + Phone/Tablet setup. (Though… not exactly the same since the advertising is happening on the Argon rather than some other device…)

Did you ever see the Nativescript Particle SDK Plug in?

I did see your Tile example briefly and its similar to a prototype I had working where I was detecting presence based off another Argon BLE RSSI signal stregnth reading and a security key.

But using a Tile like you did would be a much cheaper way to accomplish the same thing.

I really need to figure out how to build a phone app that can pull variable data in from Argon BLE Advertisement and also send data back to the Argon to change a Variable.

I think you could show me how to do it well enough to get started.

I have $100 for ya whenever you have the time and desire to build a quick guide on how to do this. I know that would be worth the time I would save trying to figure it out from scratch by myself :grinning:

Not sure it’ll be helpful if you need a native app, but there’s some sort of BLE support on the web.

Chrome and Opera supports it (both on desktop an android, Edge as well. Not safari but there’s this app that adds BLE support to a browser environnent. I haven’t tested it yet (I have no iPhone…) so i don’t know if it’s plug&play by just adding missing APIs to JS or if it requires some specifc developments.

I believe the global support of these APIs is subject to debates right now, i believe Firefox said they probably won’t support it, apple as well. So i’m not sure about the long life support of it.