Maker of open-source software and hardware.

Building a Bluetooth LE temperature sensor

For my hydroponics project, I would like to measure the ambient temperature. I've done this in the past as a WiFi temperature sensor, but this time round I wanted to use my Pixl.js board with Bluetooth LE connectivity.

Here is the code to read data from a DS18B20 temperature sensor, display a temperature graph on the Pixl.js screen, and also make it available via Bluetooth LE as an Environmental Sensing Service.

const PIN_SENSOR = A1;  // Pin temperature sensor is connected to

let ow, sensor, history;
let avrSum = 0, avrNum = 0;
let lastTemp = 0;

// Draw the temperature and graph to the screem
function draw() {
  g.clear();
  g.setFontVector(30);
  g.setFontAlign(0,-1);
  g.drawString(lastTemp.toFixed(1), 64,5);
  g.setFontBitmap();
  g.drawString("Temperature", 64, 0);
  require("graph").drawLine(g, history, {
    axes : true,
    gridy : 5,
    x:0, y:37,
    width:128,
    height:27
  });
  g.flip();
}

// Called when we get a temperature reading
function onTemp(temp) {
  if (temp === null) return; // bad reading
  avrSum += temp;
  avrNum++;
  lastTemp = temp;

  // send on BLE
  const th = Math.round(lastTemp * 100);
  NRF.updateServices({
    0x181A : { // environmental_sensing
      0x2A6E : { // temperature
        value : [th&255,th>>8],
        readable: true,
        notify: true
      }
    }
  });

  draw();
}

// take temp reading and update graph
var readingInterval = setInterval(function() {
  if (sensor) sensor.getTemp(onTemp);
}, 10000);

// save average to history
var histInterval = setInterval(function() {
  history.set(new Float32Array(history.buffer,4));
  history[history.length-1] = avrSum / avrNum;
  avrSum = 0;
  avrNum = 0;
}, 60000); // 1 minute

// At startup, set up OneWire
function onInit() {
  try {
    ow = new OneWire(PIN_SENSOR);
    sensor = require("DS18B20").connect(ow);
    history = new Float32Array(128);
    avrSum=0;
    avrNum=0;

    NRF.setServices({
      0x181A: { // environmental_sensing
        0x2A6E: { // temperature
          notify: true,
          readable: true,
          value : [0x00, 0x00, 0x7F, 0xFF, 0xFF],
        }
      }
    }, { advertise: [ '181A' ] });
  } catch (e) {
    console.log('Error:', e);
  }

This code is based on the lovely Pixl.js Freezer Alarm example.

I then wanted to be able to connect to the sensor via Web Bluetooth in the browser, which led to this web app:

document.addEventListener('DOMContentLoaded', event => {
  const button = document.getElementById('connectButton')
  let device

  button.addEventListener('click', async (e) => {
    try {
      if (button.innerHTML === 'Disconnect') {
        device.gatt.disconnect()
        button.innerHTML = 'Connect'
        return
      }
      document.getElementById('p1').innerHTML = 'Connecting'
      console.log('Requesting Bluetooth Device...')
      device = await navigator.bluetooth.requestDevice({
        filters: [{
          namePrefix: 'Pixl.js'
        }],
        optionalServices: ['environmental_sensing']
      })

      device.addEventListener('gattserverdisconnected', onDisconnected)

      console.log('Connecting to GATT Server...')
      const server = await device.gatt.connect()
      button.innerHTML = 'Disconnect'

      console.log('Getting Environmental Sensing Service...')
      const service = await server.getPrimaryService('environmental_sensing')

      console.log('Getting Temperature Characteristic...')
      const characteristic = await service.getCharacteristic('temperature')

      console.log('Reading temperature...')
      const value = await characteristic.readValue()
      const temp = value.getUint16(0, true) / 100

      console.log('Temperature is ' + temp)
      document.getElementById('p1').innerHTML = temp

      await characteristic.startNotifications()

      console.log('Notifications started')
      characteristic.addEventListener('characteristicvaluechanged', handleNotifications)
    } catch (error) {
      console.log('Argh! ' + error)
      document.getElementById('p1').innerHTML = error
      button.innerHTML = 'Connect'
    }
  })

  function handleNotifications (event) {
    const value = event.target.value
    const temp = value.getUint16(0, true) / 100

    console.log('Temperature is now ' + temp)
    document.getElementById('p1').innerHTML = temp
  }

  function onDisconnected () {
    console.log('Disconnected.')
    button.innerHTML = 'Connect'
    document.getElementById('p1').innerHTML = 'Disconnected'
  }
})

The index.html file looks like this:

<html>
  <head>
    <title>Hydroloop</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="main.js"></script>
    <link rel="stylesheet" type="text/css" href="main.css">
  </head>
  <body>
    <button id="connectButton" class="buttons">Connect</button>
    <p id="p1" class="text"></p>
  </body>
</html>

One Web Bluetooth discrepancy that I discovered between desktop Chrome and Chrome on Android, is that desktop Chrome will still read a value from a service, even if the characteristic is not set to be readable. Chrome on Android, on the other hand, will throw an error. That's why you see both readable: true and notify: true in the code above.

This works great! Temperature readings in the browser are automatically updated, and it even shows you when you get disconnected from the sensor.


I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting https://100daystooffload.com.

#100DaysToOffload #day62 #hydroponics