Using BLE Combustion Predictive Thermometer as Sensor

I would like to use an UNO R4 WiFi to read data from a Combustion Predictive Thermometer, which broadcasts on an open BLE connection.
But I have no BLE experience.
Combustion have published their BLE details here...

Using the ArduinoBLE library I have managed to run the Scan example sketch to find the probes MAC address and a service (it is not showing a Local Name)...

Discovered a peripheral
Address: c2:71:05:5a:a6:60
Service UUIDs: fe59

I then modified the BLE library's Peripheral Explorer sketch to locate the MAC address and it responded as follows...

Bluetooth® Low Energy Central - Peripheral Explorer
Found c2:71:05:5a:a6:60 '' fe59
Connecting ...
Connected
Discovering attributes ...
Attributes discovered

Device name:
Appearance: 0x0

Service 1800
Characteristic 2a00, properties 0x2
Descriptor 2803, value 0x020500012A
Descriptor 2a01, value 0x4004
Characteristic 2a01, properties 0x2, value 0x4004
Descriptor 2803, value 0x020700042A
Descriptor 2a04, value 0x06003C0002004B00
Characteristic 2a04, properties 0x2, value 0x06003C0002004B00
Descriptor 2803, value 0x020900A62A
Descriptor 2aa6, value 0x01
Characteristic 2aa6, properties 0x2, value 0x01
Service 1801
Characteristic 2a05, properties 0x20
Descriptor 2902, value 0x
Service 00000100-caab-3792-3d44-97ae51c1407a
Characteristic 00000101-caab-3792-3d44-97ae51c1407a, properties 0x12
Descriptor 2902, value 0x
Service 6e400001-b5a3-f393-e0a9-e50e24dcca9e
Characteristic 6e400002-b5a3-f393-e0a9-e50e24dcca9e, properties 0xC
Descriptor 2803, value 0x
Descriptor ca9e, value 0x
Descriptor f393, value 0x
Descriptor f32a, value 0x
Characteristic 6e400003-b5a3-f393-e0a9-e50e24dcca9e, properties 0x10
Descriptor 2902, value 0x
Service 180a
Characteristic 2a29, properties 0x2
Descriptor 2803, value 0x
Descriptor 2a24, value 0x
Characteristic 2a24, properties 0x2
Descriptor 2803, value 0x
Descriptor 2a25, value 0x
Characteristic 2a25, properties 0x2
Descriptor 2803, value 0x
Descriptor 2a27, value 0x
Characteristic 2a27, properties 0x2
Descriptor 2803, value 0x
Descriptor 2a26, value 0x
Characteristic 2a26, properties 0x2
Service fe59
Characteristic 8ec90003-f315-4f60-9fb8-838830daea50, properties 0x28
Descriptor 2902, value 0x

Disconnecting ...
Disconnected

Are there any examples now of how to use this information to obtain data from the BLE probe?

And I have also run into many times where if I try to run Peripheral Explorer again, it responds with...

starting Bluetooth® Low Energy module failed!

...or more commonly...

Bluetooth® Low Energy Central - Peripheral Explorer
Found c2:71:05:5a:a6:60 '' fe59
Connecting ...
Connected
Discovering attributes ...
Attribute discovery failed!

It then takes many attempts of unplugging and plugging back in the UNO R4 WiFi, and/or installing a Blink sketch and then trying Peripheral Explorer again before I can rediscover the attributes.

Scan always seems to run fine, and the iOS app never has an issue.
Is this a problem of the UNO R4 WiFi, the ArduinoBLE library, the sketch, BLE in general, or some combination of them?

Thank you far any advice on where to go next.

Perhaps, you will get faster and better help if you move your question to the Uno R4 category.

Thanks for the advice.
I wasn't sure where the question belongs since I didn't think it was necessarily Uno R4 specific.
I really wish there was a dedicated BLE category.

In a nutshell, you need to read their documentation to find out what Services and Characteristics have the data that you need. Then you need to

  1. Connect to the device
  2. Get the ID of the Service that has the data you need
  3. Read the Characteristic in that Service or get Notifications when data changes if the device supports it

I'm pretty sure that ArduinoBLE has examples.

Hi @cedarlakeinstruments - thank you for the feedback.

From the Combustion documentation for starting off I would just like to obtain the Raw Temperature Data, which "is expressed in a packed 104-bit (13-byte) field" for the 8 sensors.

When I use Peripheral Explorer it reports 6 Services available, but only 3 are identified in their GATT Services & Characteristics documentation:

  • 00000100-caab-3792-3d44-97ae51c1407a = Probe Status Service
  • 6e400001-b5a3-f393-e0a9-e50e24dcca9e = UART Service
  • 180a = Device Information Service

The Raw Temperature Data appears to be available from the UART Service, but I think that is way beyond my capability to understand right now without an example of how to use UART over BLE.

The Probe Status service has only one Characteristic of the same name (00000101-CAAB-3792-3D44-97AE51C1407A) and includes:
Log Range (uint32_t) = 8 Bytes
Current Raw Temperature Data (uint8_t) = 13 Bytes
Mode/ID (uint8_t) = 1 Byte
Battery Status and Virtual Sensors (uint8_t) = 1 Byte
Prediction Status (uint8_t) = 7 Bytes

I have tried to study the ArduinoBLE library reference documentation and so do I need to use the bleCharacteristic.readValue(buffer, length) command?

The example that they give is:

  while (peripheral.connected()) {
    // while the peripheral is connected

    // check if the value of the simple key characteristic has been updated
    if (simpleKeyCharacteristic.valueUpdated()) {
      // yes, get the value, characteristic is 1 byte so use byte value
      byte value = 0;

      simpleKeyCharacteristic.readValue(value);

      if (value & 0x01) {
        // first bit corresponds to the right button
        Serial.println("Right button pressed");
      }

      if (value & 0x02) {
        // second bit corresponds to the left button
        Serial.println("Left button pressed");
      }
    }
  }

I am struggling to understand how to convert the example into my particular application.
It appears to me this example reads a single value.

I assume I need to create a 30 byte buffer and then figure out how to decode the 13 bytes that represent the raw temperature data.
Does that sound correct?

I have no familiarity with buffers and how to manipulate them yet, but I will keep trying.

Correct.

That sounds right. You will use something like below to read the 30 bytes of data into a buffer you have declared.

characteristic.readValue(&my30ByteBuffer, 30);

Writing central sketches can be difficult, and I would suggest that you first use a phone app like LightBlue or nrfConnect to connect with the probe and service you want, and read the byte data from the service characteristic. Does it make sense and can you see what looks like realistic temperatures?

Hi @cattledog - I am using the iOS app provided by Combustion and in 'Debug' mode is shows all of the individual temperatures and other data.
But I don't know which Service they are using.

There are SDK's available for iOS and Android, but I would very much like to use this probe for my Arduino projects.

I will try to look into LightBlue and nrfConnect.

One of my big problems are the constant "starting Bluetooth® Low Energy module failed!" and even when I do get connected "Attribute discovery failed!" errors.

It is hard to know when it might work.
The app seems to work every time and is very responsive.

Thank you for the advice.

I have no experience with the Uno R4 WiFi, and have only used the Nano 33 BLE and the esp32 ble. I have not experienced the flaky behaviour you describe with the Uno R4. This forum is filled with posts from users who have problems with the Uno R4.

When I use the esp32, I use it's ble library and not the arduino BLE.h. which I use with the Nano33BLE. I'm not clear if you can use it with the R4.

Thank you @cattledog

I just posted an inquiry about the flaky behavior in the UNO R4 WiFi category.

Hi @cattledog - thank you for the advice.
LightBlue would not work, but I was able to use nRF Connect which confirmed the services and characteristics:

And hitting the little down arrow generated a 60 character string.
So I modified the Central Explorer sketch into the following code, using the MAC address of the probe and what nRF Connect confirmed as the Service and Characteristic:

#include <ArduinoBLE.h>

void setup() {
  delay(2500);
  Serial.begin(9600);
  Serial.println("Serial Enabled");

  if (!BLE.begin()) {
    Serial.println("BLE Module Startup Failed!");

    while (1)
      ;
  }

  Serial.println("Finding CPT");
  BLE.scanForAddress("c2:71:05:5a:a6:60");

  delay(1000);
}

void loop() {
  BLEDevice CPTdevice = BLE.available();

  if (CPTdevice) {
    BLE.stopScan();

    Serial.print("Found CPT!");
    Serial.println();

    Serial.println("Connecting ...");
    if (CPTdevice.connect()) {
      Serial.println("Connected to CPT");

      delay(1000);

      Serial.println("Discovering Temperature Service...");
      if (CPTdevice.discoverService("00000100-caab-3792-3d44-97ae51c1407a")) {
        Serial.println();
        Serial.println("Temperature Service Discovered!");
        Serial.println();

        BLEService CPTservice = CPTdevice.service("00000100-caab-3792-3d44-97ae51c1407a");

        if (CPTservice) {
          if (CPTservice.hasCharacteristic("00000101-caab-3792-3d44-97ae51c1407a")) {
            Serial.print("Temperatures Available! = ");

            BLECharacteristic CPTcharacteristic = CPTservice.characteristic("00000101-caab-3792-3d44-97ae51c1407a");
            Serial.print(CPTcharacteristic.properties(), HEX);
            Serial.println();

            if (CPTcharacteristic.canRead()) {
              CPTcharacteristic.read();

              if (CPTcharacteristic.valueLength() > 0) {
                Serial.print("value 0x ");
                printData(CPTcharacteristic.value(), CPTcharacteristic.valueLength());
              }
            }
            Serial.println();
            delay(1000);
          }

        } else {
          Serial.println("Temperature Service Not Available!");
        }

      } else {
        Serial.println("Temperature Service Discovery Failed!");
      }

      delay(1000);
      Serial.println();
      Serial.println("Diconnecting ...");
      CPTdevice.disconnect();
      Serial.println("CPT Disconnected");

      BLE.scanForAddress("c2:71:05:5a:a6:60");

    } else {
      Serial.println("Failed to Connect!");
    }
  }

  Serial.println("Looping...");
  delay(2500);
}

void printData(const unsigned char data[], int length) {
  for (int i = 0; i < length; i++) {
    unsigned char b = data[i];

    if (b < 16) {
      Serial.print("0");
    }

    Serial.print(b, HEX);
  }
}

This works and eventually generates the same 60 character string, but is very unpredictable with intermittent, often long periods of discovery failures.

I there any way to improve the consistancy of reading the characteristic value?

I would like to read the temperature every 30 seconds on a constant cadence.
Both nRF Connect and iOS app seem to have no issues reliably connecting and reading the temperatures on demand.

Is the Arduino capable of this?
Or do I need to use the UART service for consistency?

And for the really naïve question, is 60 characters the same as 30 bytes?
I have no idea how to unpack the 60 characters into the individual components and I haven't learned how to make the buffer to use your characteristic.readValue(&my30ByteBuffer, 30); instruction yet.

From their documentation, I want to throw away the first 8 bytes and then convert the next 13 bytes (uint_8t format) into the raw temperature data is expressed in a packed 104-bit field.
But I still don't know what the means yet.

At least if feels like some progress thanks to your advice.
Thanks again for your help.

You've made good progress, and appear to be reading the correct service and characteristic.

From what I can see of the nrfConnect image, the 60 characters are 30 Hex bytes.

The specification document is here
https://github.com/combustion-inc/combustion-documentation/blob/main/probe_ble_specification.rst#probe-status-service

The probe status mentioned in the above service is described here:

Value Format Bytes Description
Log Range uint32_t 8 Range of logs available on the probe. Two uint32_t sequence numbers (min, max).
Current Raw Temperature Data uint8_t 13 See Raw Temperature Data.
Mode/ID uint8_t 1 See Mode and ID Data.
Battery Status and Virtual Sensors uint8_t 1 See Battery Status and Virtual Sensors.
Prediction Status uint8_t 7 See Prediction Status.

I can not quite figure out how the 30 bytes of your data relate to the spec. :face_with_spiral_eyes:

but is very unpredictable with intermittent, often long periods of discovery failures.
Both nRF Connect and iOS app seem to have no issues reliably connecting and reading the temperatures on demand.

This sounds like more of the flakiness you experience with the Uno R4 BLE.

LightBlue would not work

Strange. I use it with Android, and find it more useful than nrfConnect in that it has several different formats for output.

I have set up a pair of Nano33 BLE devices to test your codes.

I have set up the peripheral to write a 30 byte message on service and characteristic like the combustion probe. I used a local name "CombustionProbe" for my test peripheral.

BLEService probeStatus("00000100-caab-3792-3d44-97ae51c1407a");
BLECharacteristic probeData("00000100-caab-3792-3d44-97ae51c1407a",  
    BLERead | BLENotify, 30);  //30 bytes

I connect to that service and subscribe to the characteristic and read the notifications of changes with this central code. I have not added any reconnections on disconnect, but this shows how I read the 30 bytes into an array which you can parse for the temperature data.

#include <ArduinoBLE.h>

uint8_t sensorResponse[30] = {};

void setup() {
  Serial.begin(115200);
  while (!Serial);

  if (!BLE.begin()) {
    Serial.println("starting BLE failed!");
    while (1);
  }

  Serial.println("BLE Central for CombustionProbe");
  BLE.scanForUuid("00000100-caab-3792-3d44-97ae51c1407a");
}

void loop() {
  BLEDevice peripheral = BLE.available();
  if (peripheral) {
    Serial.print("Found ");
    Serial.print(peripheral.address());
    Serial.print(" '");
    Serial.print(peripheral.localName());
    Serial.print("' ");
    Serial.print(peripheral.advertisedServiceUuid());
    Serial.println();
    if (peripheral.localName() == "CombustionProbe") {
      BLE.stopScan();
      if (peripheral.connect())
        Serial.print("Connected to CombustionProbe ");
      Serial.println("Discovering attributes ...");
      if (peripheral.discoverAttributes()) { //find services and characterisitics
        Serial.println("Attributes discovered");
        BLEService service = peripheral.service("00000100-caab-3792-3d44-97ae51c1407a");
        BLECharacteristic characteristic = service.characteristic("00000100-caab-3792-3d44-97ae51c1407a");
        characteristic.subscribe();
        Serial.println("subscribed to probeData");

      } else {
        Serial.println("Attribute discovery failed!");
        peripheral.disconnect();
        return;
      }
    }
    else {
      Serial.println("Failed to connect!");
      return;
    }
  }
  
  while (peripheral)
  {
    BLEService service = peripheral.service("00000100-caab-3792-3d44-97ae51c1407a");
    BLECharacteristic characteristic = service.characteristic("00000100-caab-3792-3d44-97ae51c1407a");
  
    if (characteristic.valueUpdated())
      readCharacteristicValue(service.characteristic("00000100-caab-3792-3d44-97ae51c1407a"));
  }
}

void readCharacteristicValue(BLECharacteristic characteristic) {
    Serial.println("value updated");
    characteristic.read();
    characteristic.readValue(&sensorResponse, 30);
    printData(sensorResponse, 30);
    Serial.println();
 
}

void printData(uint8_t data[], int length) {
  for (int i = 0; i < length; i++) {
    uint8_t b = data[i];

    if (b < 16) {
      Serial.print("0");
    }

    Serial.print(b, HEX);
    Serial.print(' ');
  }
}

The reason it's taking so long is that you are scanning for the device every time through the loop and also have a delay. You only need to scan and connect once, then you can get data as needed. You will only need to connect again if the device has disconnected.

Thank you for the advice and I will updates accordingly.

I was disconnecting and rescanning because in the future I would only need temperature data at long intervals, maybe as much as 10 minutes.
And, if I do not disconnect and I have to restart the sketch or upload a new one, every time then I receive a "starting BLE failed!" message and I have to unplug the R4 WiFi from USB and plug it back in.
I assume this is a flaw with the ESP32 module on the R4 WiFi?
If it is only scanning, then restarts or new uploads are not a problem.

If you don't mind me asking, is a discoverAttributes and/or discoverService required?
Or can you just assume the service & characteristic exists after you are connected?

Every BLE example I find seems to have a discoverAttributes command.
But when I run it, it is only successful <50% of the time, and often closer to 25%.

Some of the delays I had added were just to see if pausing would improve the success rate of cascading down to reading the data.

Thank you!

Thank you @cattledog
I am learning from and incorporating your code.

BLE.scanForUuid("00000100-caab-3792-3d44-97ae51c1407a");

For some reason scanning for this UUID never works.
Maybe because it is a "Unknown Service" as reported by nRF Connect?

Only using the UUID "fe59" or scanning for the MAC address works.
The probe also doesn't seem to have a localName() defined.

Modifying your code to scan for and proceed based on the MAC address works, but then the discovery of attributes fails most of the time.
And then discovering the attributes does work, nothing prints.
If I change the while loop to:

  while (peripheral)
  {
    BLEService service = peripheral.service("00000100-caab-3792-3d44-97ae51c1407a");
    BLECharacteristic characteristic = service.characteristic("00000100-caab-3792-3d44-97ae51c1407a");
  
    //if (characteristic.valueUpdated())
      readCharacteristicValue(service.characteristic("00000100-caab-3792-3d44-97ae51c1407a"));
      delay(2500);
  }

Then it only prints zeros even though the iOS app shows temperature data.

BLE Central for CombustionProbe
Found c2:71:05:5a:a6:60 '' fe59
Connected to CombustionProbe
Discovering attributes ...
Attributes discovered
subscribed to probeData
value updated
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
value updated
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0

Maybe this is expected behavior, but when the Arduino is subscribed, nRF Connect cannot display the Attribute Table or download the Characteristic data even though it is connected.
I have to power off the Arduino to obtain the data again.

According to Combustion the probe can handle 3 connections.
Even if I shut down the iOS app it doesn't make a difference.

I did some research on HEX data.
I see one HEX character is a 4 bit nibble.
So I am going to work on some code to display the temperatures.

I saw your array is defined as uint8_t.
I assume it doesn't matter that the first 8 bytes are uint32_t since I do not need them.

Thank you for your feedback - it is helping a lot!

All my central sketches use peripheral.discoverAttributes().
If it doesn't return a success, I doubt you will be able to read from the device. There's alot going on behind the scenes as .discoverAttributes()
calls this

bool BLEDevice::discoverAttributes()
{
  return ATT.discoverAttributes(_addressType, _address, NULL);
}

There is a lot going at the ATT level

bool ATTClass::discoverAttributes(uint8_t peerBdaddrType, uint8_t peerBdaddr[6], const char* serviceUuidFilter)
{
  uint16_t connHandle = connectionHandle(peerBdaddrType, peerBdaddr);
  if (connHandle == 0xffff) {
    return false;
  }

  // send MTU request
  if (!exchangeMtu(connHandle)) {
    return false;
  }

  // find the device entry for the peeer
  BLERemoteDevice* device = NULL;

  for (int i = 0; i < ATT_MAX_PEERS; i++) {
    if (_peers[i].connectionHandle == connHandle) {
      if (_peers[i].device == NULL) {
        _peers[i].device = new BLERemoteDevice();
      }

      device = _peers[i].device;

      break;
    }
  }

  if (device == NULL) {
    return false;
  }

  if (serviceUuidFilter == NULL) {
    // clear existing services
    device->clearServices();
  } else {
    int serviceCount = device->serviceCount();
  
    for (int i = 0; i < serviceCount; i++) {
      BLERemoteService* service = device->service(i);

      if (strcasecmp(service->uuid(), serviceUuidFilter) == 0) {
        // found an existing service with same UUID
        return true;
      }
    }
  }

  // discover services
  if (!discoverServices(connHandle, device, serviceUuidFilter)) {
    return false;
  }

  // discover characteristics
  if (!discoverCharacteristics(connHandle, device)) {
    return false;
  }

  // discover descriptors
  if (!discoverDescriptors(connHandle, device)) {
    return false;
  }

  return true;
}

I discovered an error in my code, which may explain what you are seeing.

I have used the wrong characteristic uuid. :anguished:

The UUID for the Probe Status service is 00000100-CAAB-3792-3D44-97AE51C1407A.

The characteristic is
00000101-CAAB-3792-3D44-97AE51C1407A

I made an error and used the same uuid for the service and the characteristic. My sender is configured the same so my test code worked in my environment will not in yours.

You need to change the characterisitic uuid to the 00000101 instead of the 00000100 service uuid.

Wow - thank you for noticing the '101' vs '100' difference!
The fix works and I can subscribe to the data updates.

This weekend I will work on decoding the HEX data into individual temperatures.

The only other issue is discovering the attributes which still fails most of the time.
I need to figure out how to detect the the failure and retry the discovery until it works or times out.

Very much appreciate the correction and helping me to learn the subscription and printing the buffer contents.

Thank you again for your advice @cattledog

I added a flag so if the attribute discovery fails it disconnects and reconnects to try again until successful.
It takes on average 8 attempts for the discovery to work.
Not sure if there is better way to do this but it is working for now.

Using nRF Connect I have confirmed the 30 byte data array being printed is the same.

My only problem now is getting the raw temp data to match what the app says.
I found a bitRead command and I have been using that to try and calculate T1raw from the first 13 bits starting with the 9th byte, but the result is always off.

Is bitRead the best choice?

Here is the code I am using:


#include <ArduinoBLE.h>

uint8_t sensorResponse[30] = {};

bool discovery = false;

void setup() {
  delay(2500);
  Serial.begin(9600);
  Serial.println("Serial Enabled");
  ;

  if (!BLE.begin()) {
    Serial.println("starting BLE failed!");
    while (1)
      ;
  }

  Serial.println("BLE Central for CombustionProbe");
  BLE.scanForAddress("c2:71:05:5a:a6:60");
  //BLE.scanForUuid("fe59");
}

void loop() {
  BLEDevice peripheral = BLE.available();
  if (peripheral) {
    Serial.print("Found ");
    Serial.print(peripheral.address());
    Serial.println();
    BLE.stopScan();

    while (!discovery) {

      if (peripheral.connect())
        Serial.println("Connected to CombustionProbe ");
      Serial.println("Discovering attributes ...");
      if (peripheral.discoverAttributes()) {  //find services and characterisitics
        Serial.println("Attributes discovered");
        discovery = true;
        BLEService service = peripheral.service("00000100-caab-3792-3d44-97ae51c1407a");
        BLECharacteristic characteristic = service.characteristic("00000101-caab-3792-3d44-97ae51c1407a");
        characteristic.subscribe();
        Serial.println("subscribed to probeData");

      } else {
        Serial.println("Attribute discovery failed!");
        peripheral.disconnect();
        peripheral.connect();
      }
    }
  }

  while (peripheral) {
    BLEService service = peripheral.service("00000100-caab-3792-3d44-97ae51c1407a");
    BLECharacteristic characteristic = service.characteristic("00000101-caab-3792-3d44-97ae51c1407a");

    if (characteristic.valueUpdated())
      readCharacteristicValue(service.characteristic("00000101-caab-3792-3d44-97ae51c1407a"));
    delay(1000);
  }
}

void readCharacteristicValue(BLECharacteristic characteristic) {
  Serial.println("value updated");
  characteristic.read();
  characteristic.readValue(&sensorResponse, 30);
  printData(sensorResponse, 30);
  Serial.println();
}

void printData(uint8_t data[], int length) {
  for (int i = 0; i < length; i++) {
    uint8_t b = data[i];

    if (b < 16) {
      Serial.print("0");
    }

    Serial.print(b, HEX);
    Serial.print(' ');
  }

  Serial.println();

  int T1raw = (4096 * bitRead(data[8], 7)) + (2048 * bitRead(data[8], 6)) + (1024 * bitRead(data[8], 5)) + (512 * bitRead(data[8], 4)) + (256 * bitRead(data[8], 3)) + (128 * bitRead(data[8], 2))
              + (64 * bitRead(data[8], 1)) + (32 * bitRead(data[8], 0)) + (16 * bitRead(data[9], 7)) + (8 * bitRead(data[9], 6)) + (4 * bitRead(data[9], 5)) + (2 * bitRead(data[9], 4)) + (1 * bitRead(data[9], 3));
  Serial.print(T1raw);

  Serial.println();
}

Could they be using a different bit order?
They say the T1 bits are 1 to 13 (of 104), but I assume it starts counting from 0.

Thanks again for your advice.