Close

Advertising Sensor Values Over BLE

A project log for BLEifying a Honeywell PM Sensor

The Honeywell PMS transmits data via UART; if you wanted to get data you need wires. Adding BLE means it can just advertise data wirelessly.

parasquidparasquid 08/23/2020 at 07:320 Comments

One of the things I really like about the Espruino, as opposed to doing my projects in Arduino or in the mcu's original SDK, is the ease of prototyping. Making adjustments is very easy without having to recompile and upload, or temporarily adding pots or buttons to dynamically adjust settings - you just do them all in your console.

This is especially true for the nrf52-based Espruino boards. Having a wireless console available means (in this case) I can place the sensor/mcu project somewhere away from the computer while still having access. That means I can test whether the sensor actually detects particulate matter by moving it around the house and then looking at the console logs that have been generated.

Another reason is that Espruino have very well-built and comprehensive Bluetooth Low Energy libraries.

I've worked on a few BLE projects in Espruino before, and this case was no exception: it was really easy to add in BLE support. For example, below is the current code that's uploaded in the MDBT42Q breakout with Espruino and you'll notice that the only difference (aside from some error bypass code) is the bottom section where I set the advertising interval.

var s = new Serial();
s.setup(9600,{rx: D15, tx: D14});

let buffer = '';
const header = String.fromCharCode(0x42) + String.fromCharCode(0x4d);
let advertisingData = [];

s.on('data', function (data) {
  buffer = buffer + data;
  if(buffer.length < 32) {
    // get at least 32 bytes
  } else  {
    // find header and discard any previous bytes in buffer
    const index = buffer.indexOf(header);
    if(index != -1) { // found the header
      buffer = buffer.substr(index); // discard previous bytes until header
      if(buffer.length >= 32) {
        buffer = buffer.substr(0, 32);  // get a complete packet
        const arrayBuffer = E.toArrayBuffer(buffer);
        buffer = buffer.substr(32); // set buffer to leftover bytes

        const dataView = new DataView(arrayBuffer);
        const data = {
          header: dataView.getUint16(0),
          length: dataView.getUint16(2),
          pm25: dataView.getUint16(6),
          pm10: dataView.getUint16(8),
          checksum: dataView.getUint16(30),
        };

        const calculatedChecksum = calculateChecksum(new Uint8Array(arrayBuffer));
        console.log(data);
        console.log(calculatedChecksum);

        // early return on bad data
        if (data.pm25 < 0 || data.pm25 > 1000) return;
        if (data.pm10 < 0) data.pm10 = 0;
        if (data.pm10 > 255) data.pm10 = 255;

        if (data.length != 28) return;
        if (calculatedChecksum != data.checksum) return;

        advertisingData = [data.pm25, data.pm10];
        digitalPulse(LED, true, 50);
      }
    } else { // header not found
    }
  }
});

const calculateChecksum = (arr) => {
  return arr.reduce((acc, cur) => (acc + cur), 0) - arr[30] - arr[31];
};

setInterval(function() {
  if (advertisingData.length != 0) {
    NRF.setAdvertising({
      0x2A3D: advertisingData
    }, {name: "HPMS"});
  }
}, 1000);

E.onInit(() => {
  NRF.setTxPower(4);
});

In fact, most of the time I spent trying to advertise was not even in Espruino, but in the Bluetooth SIG website https://www.bluetooth.com/ trying to find a suitable service or characteristic for the sensor.

Unfortunately https://www.bluetooth.com/specifications/assigned-numbers/environmental-sensing-service-characteristics/ environmental sensing specs doesn't have anything related to particulate matter (there's pollen, but that's not really the same) and many of the other characteristics are geared more towards fitness tracking, like heart rate or blood pressure. In fact, there isn't even a characteristic for lights nor buttons!

So I ended up with probably my favorite characteristic to use if I just need to expose some custom data: 0x2A3D org.bluetooth.characteristic.string :P

Chdcking with the nrfconnect app confirms that I'm getting advertised pm values as expected.

What can we do now that we have advertised data at regular intervals? We track them of course.

The creators of Espruino also has a project called EspruinoHub https://github.com/espruino/EspruinoHub that I've been using for some home automation projects that I should be writing about soon. EspruinoHub is a BLE => MQTT bridge and I have a couple of Tasmota devices on some sonoffs listening to MQTT messages originally generated by some bluetooth beacons and bridged by a raspberry-pi where everything runs.

I can reuse this installation for observing the advertising data and plotting them onto graphs.

The project page would have more information, but basically I get to watch a specific topic (which is the bluetooth device's address) and the characteristic, and parse the data so it can be charted by node-red-dashboard.

The identify function looks like this:

return {
    payload: [
        {
            topic: "pm2.5",
            payload: msg.payload[0],
        },
        {
            topic: "pm1.0",
            payload: msg.payload[1],
        }
    ]
}

 and the chartify function looks like this:

return {
    topic: msg.payload.topic,
    payload: msg.payload.payload,
}

 I left the project running overnight while connected to a USB charger, and it looks like everything got logged. Next I'll have to figure out how to mount it and possibly have some power sources that ideally makes the whole thing autonomous, or at least less of a maintenance hassle (I don't want to keep checking the battery charge or charging the batteries every few days).

Discussions