Blog Post

React Native and Bluetooth - Discovering Services and Characteristics and Reading Data


I’ve built a simple app to capture weight data from some bluetooth enabled bathroom scales. The app uses the react-native-ble-plx library from Polidea to handle the Bluetooth connections. The library is great - there’s good documentation - and getting it to work with the scales was pretty straightforward once I’d got a basic understanding of how the scales’ Bluetooth LE services and characteristics work.

I have picked up a few things along the way that might be worth sharing.

In the Bluetooth LE world Characteristics belong to services. They both have unique identifiers - you need to know what they are before you can begin to read and write data.

Characteristics are either Read, Write, Notify or Indicate. The scales I have will send back weight (and BMI) data, but only after they’ve received height data from the app. I couldn’t get that to work at first until I realised that I had to enable the ‘indicate’ characteristic on the scales before sending the height. Here’s the code that enables the indicate characteristic:

 
      this.subscriptionMonitor = device.monitorCharacteristicForService(weightScaleService, weightScaleMeasurementCharac, (error, characteristic) => {
    
         if (error) {
           if(error.errorCode != 2){
             this.error('Scale Error: ' + error.message);
           }
           return;
         }
         if (__DEV__) {
           console.log("Getting a value");
           console.log(characteristic.value);
         }
         //weight value that comes back from scales
         this.saveValue(characteristic.value);
       }
     ) 

   

device.monitorCharacteristicForService takes three parameters - the id of the service, the id of the associated characteristic and a callback listener function. It’s that function that picks up the weight after the scales have been given the height by the app.

Writing the height to the scales is then easy enough. Here’s the code:

 
      //send user height to scales - this will trigger weight to come back
      try{
        const characteristic5 = await device.writeCharacteristicWithResponseForService(userDataService, heightCharac, heightInBase64);
      }catch(err){
        if (__DEV__) {
          console.log("Scale Error: Writing height - " + JSON.stringify(err));
        }
        error.log(err.message);
      }

   

The thing to note here is that the react-native-ble-plx library uses Base64 encoding of Hex values when data is passed between your app and the bluetooth device. I got in a tangle at first and couldn’t work out how to convert my height - 190cm - into Hex and then into Base64. The Polidea documentation saved me.

Polidea’s example uses the Buffer library to do the actual Base64 encoding. You take the height in centimeters and then do 2 bitwise operations to find the most significant and least significant bytes.

Those two values go into an array - least significant byte first - and it’s that array that gets converted to Base64. The Bytes in the array are in Little Endian order. Polidea say in their docs say that the Bluetooth Core Specification usually expects integers to be encoded in Little Endian.

Here’s the code that turns 190cm into Base64 (Little Endian), based on the Polidea example:

 
      let height = this.personHeight;

      heightInCentimetersToWrite = height * 100;

      const heightMostSignificantByte = (heightInCentimetersToWrite >> 8) & 0xFF;
      const heightLeastSignificantByte = heightInCentimetersToWrite & 0xFF;
      
      const heightByteArrayInLittleEndian = [heightLeastSignificantByte, heightMostSignificantByte];
      const heightInBase64 = Buffer.from(heightByteArrayInLittleEndian).toString('base64');

   

That Buffer library is very useful. And they helpfully provide a raw Javascript version that you can copy and include in your React Native project as a simple .js file. You don’t need to worry about importing their Node.js module version into a React Native project.

Remember it’s the callback function that was passed to device.monitorCharacteristicForService() that gets called when the scales transmit the weight. Once you’ve got that value you can convert back to decimal from Base64. You call remove on the subscription and disconnect from the device.

I have the app working on an iPhone 8 and on an even older Android phone. There was a bit of fiddling to do with this.manager.startDeviceScan and this.manager.stopDeviceScan to get the phones to scan for long enough to make the connection with the scales. The Android phone also takes longer to start scanning and I wanted the user to be able to start the scan, put the phone down and then step on the scales. It works - the app tells the phone to scan 20 seconds before disconnecting if no scales are found. It seems to work ok.