Let's Build a Temperature Sensor with a Raspberry Pi Part 2

I set up a raspberry pi to emit temperature and humidity data using BLE advertising packets. Then I made an iPhone app that can read that data and display it to me.

This is part two of the two part temperature sensor project. Make sure you check out Part 1 first.

In this part, I set up the pi to emit the temperature and humidity data using BLE advertising packets. I also made an iPhone app that can read that data and display it to me.

Hardware

  • The raspberry pi and temperature sensor from the last post.
  • An iPhone and a Mac.

How To Do This

The first thing to do is to set up the raspberry pi as the Bluetooth peripheral (the thing advertising it's data). Then I will set up the iPhone app which acts as the central device (the thing that reads data from the peripheral)

BLE peripherals can either be advertising themselves, or they can be connected to a central device. If they are advertising themselves, then lots of central devices can “see” them and choose to try and connect to them. Once connected, the peripheral device can only communicate with a single central device.

When a peripheral is advertising, it is emitting a small amount of data in all directions for any other device to see. This is usually data about the device like the name and what it is, but this advertising data could really be anything. So you could just advertise temperature and humidity data and never actually have to “connect” to the device. This will allow any device to read the temperature and humidity, and makes the code easier to write.

Raspberry Pi (Bluetooth Peripheral)

Install bleno

I used bleno, a Node.js module for implementing BLE (Bluetooth Low Energy) peripherals. bleno uses the linux Bluez, so before you can use bleno, you need to install the bluez prerequisite software.

sudo apt-get install -y bluetooth bluez libbluetooth-dev libudev-dev

Then you can install bleno.

npm install bleno

Code the app.

I created a new file and added the following code which is basically coppied from bleno‘s documentation:

const bleno = require('bleno');

// bleno.state must be poweredOn before advertising is started. 
let state;
bleno.on('stateChange', (s) => {
  state = s;
  if (state !== 'poweredOn') {
    bleno.stopAdvertising();    
  }
});

/**
 * Start or restart advertising with custom data.
 * @param {A 31 byte buffer compatible with the ble advertising spec} buffer 
 */
function startAdvertising(buffer) { 
  return new Promise((resolve, reject) => {
    bleno.stopAdvertising();  
    if (state !== 'poweredOn') {
      reject(new Error("not powered on"));
      return;
    }
    bleno.startAdvertisingWithEIRData(buffer, (error) => {
      if (error) {
        reject(error);
        return;
      } 
      resolve("?");
    });
  });
}

I can advertise 31 bytes of data using the startAdvertising function and passing in a 31 byte buffer.

Note:

If you're interested, here's some data on how the 31 bytes should be set up.

All that's left to do here, is pass the temperature and humidity data to this function as a 31 byte buffer. So I wrote the following function that takes in two doubles, and creates a new buffer. The code is a little bit long because of the way an advertising packet works, but here it is:

/**
 * Create a new 31 byte buffer with temperature and humidity data.
 * For more information about how this function works, check out the following links:
 * https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile
 * https://www.silabs.com/community/wireless/bluetooth/knowledge-base.entry.html/2017/02/10/bluetooth_advertisin-hGsf
 * @param {A Double} temperature 
 * @param {A Double} humidity 
 */
function advertisementData(temperature, humidity) {
  if (typeof temperature !== 'number' || typeof humidity !== 'number') {
    throw 'a fit';
  }

  const buffer = Buffer.alloc(31); // maximum 31 bytes

  let bufferIndex = 0;

  // flags
  buffer.writeUInt8(2, bufferIndex++);
  buffer.writeUInt8(0x01, bufferIndex++);
  buffer.writeUInt8(0x06, bufferIndex++);

  // Complete Local Name
  const name = "Sensei" // Change this
  buffer.writeUInt8(1+name.length, bufferIndex++);
  buffer.writeUInt8(0x09, bufferIndex++);
  buffer.write(name, bufferIndex);
  bufferIndex += name.length;

  // Manufacturer Specific Data
  // 4 bytes for each number
  buffer.writeUInt8(1+8+8, bufferIndex++);
  buffer.writeUInt8(0xFF, bufferIndex++);
  buffer.writeDoubleLE(temperature, bufferIndex);
  bufferIndex+=8;
  buffer.writeDoubleLE(humidity, bufferIndex);
  bufferIndex+=8;

  return buffer;
}

This will create a new 31 byte buffer with temperature and humidity data assigned to the Manufacturer Specific Data. https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile

Manufacturer Specific Data: 0xFF
temperature: 8 byte little endian double
humidity: 8 byte little endian double

To start advertising temperature and humidity, you could call the function like this:

const buffer = advertisementData(24.4, 65.6);
startAdvertising(buffer);

This is just using some hardcoded numbers. I'll let you figure out how you want to connect this to the real temperature data from the previous post.

My version of the peripheral code for this can be found here: https://github.com/Sam-Meech-Ward/Sensei-Peripheral-JS

Once you've got the raspberry pi advertising, you can use an app like LightBlue® Explorer to verify that it's working.

Bluetooth Central (iOS)

Warning:

BLE apps must run this on a real iPhone, not the simulator!

Create a new iPhone app.

To interact with other ble devices from an iOS app, you will have to use the CoreBluetooth framework. This is a pretty well documented framework, so it was pretty easy to setup my iPhone as a central device. It still requires more code than I thought was necessary.

Create a new TemperatureDetector class.

I made a TemperatureDetector class and added the following code:

import Foundation
import CoreBluetooth

class TemperatureDetector: NSObject {

  // The Central Manager is what will listen for advertising ble devices.
  var myCentralManager: CBCentralManager!

  override init() {
    super.init()
    myCentralManager = CBCentralManager(delegate: self, queue: nil)
  }

}

extension TemperatureDetector: CBCentralManagerDelegate {
  func centralManagerDidUpdateState(_ central: CBCentralManager) {
    // If ble is supported and available, start scanning; otherwise, stop scanning
    if central.state == .poweredOn {
      myCentralManager.scanForPeripherals(withServices: nil, options: nil)
    } else {
      myCentralManager.stopScan()
    }
  }

  func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {

    // Only continue if we find a peripheral with the name "Sensei"
    // Change this to whatever you've called your peripheral
    guard let name = advertisementData["kCBAdvDataLocalName"] as? String, name == "Sensei" else {
      return
    }

    // Get the Manufacturer Data, that's where we stored the temperature and humidity
    guard let manData = advertisementData["kCBAdvDataManufacturerData"] as? Data else {
      return
    }

    // The data was stored in binary, now we have to read that data as an 8 byte double.
    // Temperature is the first 8 bytes
    let temperature: Double = manData.subdata(in: 0..<8).withUnsafeBytes { $0.pointee }
    // Humidity is the second 8 bytes
    let humidity: Double = manData.subdata(in: 8..<16).withUnsafeBytes { $0.pointee }

    print("Temperature: \(temperature), Humidity: \(humidity)")
  } 
}

When you create a new instance of TemperatureDetector, it will start scanning for BLE peripherals. If it finds the temperature sensor “Sensei” (oh yeah, the sensor's name is “Sensei”), it will print out the temperature and humidity data.

Here's the code for my completed iPhone app: https://github.com/Sam-Meech-Ward/Sensei-Central-iOS

Find an issue with this page? Fix it on GitHub