Introduction: Monitor and Record Temperature With Bluetooth LE and RaspberryPi

This instructable is about how to put together a multi-node temperature monitoring system with Bluetooth LE sensor bug from Blue Radios (BLEHome) and RaspberryPi 3B

Thanks to the development of the Bluetooth LE standard, there is now readily available low power wireless sensors on the market for very low cost and can run on a single coin cell for months at a time. One of these sensor I picked up is from Blue Radio called Sensor Bugs. Costing about $25 on Amazon, it is a Bluetooth LE device with Temperature sensor, light sensor and accerometer all build into a small unit that can communicate wirelessly.

This is a perfect match for Raspberry Pi 3B, which has build in support for Bluetooth LE radio.

Step 1: Setup Raspberry Pi

First step is to get a working Raspberry Pi setup. Follow the instruction from Raspberry Pi’s website, load Raspbian on a SD card, insert into Raspberry Pi and boot it up.

I setup my system with Raspbian Stretch Lite (No GUI) version Nov 2017.
Setup WiFi if needed, I prefer to adjust the time zone to the current time zone instead of UTC. You can do this though the command:
$ sudo dpkg-reconfigure tzdata

Rest of the instruction assume the setup is done though command line interface.

Step 2: Setting Up MySQL on Raspberry Pi

It it useful to have a database installed locally to store all the captured data. Installing MySQL on Raspberry Pi is super easy.
It is also not difficult to modify the script to connect to a SQL server externally, you can skip this step if you wish to use an SQL server on the network.

There are many instructions on the net, I suggest this:
https://www.stewright.me/2014/06/tutorial-install-...


Once the SQL server is installed, you can use MySQL CLI client to create user, database and table.

To enter MySQL CLI, use the command:

$ sudo mysql -uroot-p

First, create a local user to insert captured data:
> CREATE USER ‘datasrc’@‘localhost’ IDENTIFYED BY ‘datasrc000’ ;

Next, create a database and table:
> CREATE DATABASE SensorBug;

Setting up the user permission:
> GRANT ALL PRIVILEGES ON SensorBug.* TO 'datasrc'@'localhost';

Now add a new table to the database. For this example, I’m going to add a table with the following columns: DATE, TIME, ADDRESS, LOCATION, TEMPERATURE and ACCEROMETER

  • DATE/TIME - This is the date and time the data is recorded
  • ADDRESS - This is the MAC of the SensorBug the message is captured from
  • LOCATION - A human readable string to indicate where the sensor is located
  • TEMPERATURE - This is the recorded temperature
  • ACCELE - This is the value of the accelerometer output, useful for recording sensor position (if enabled)


The command that does this is:
> USE SensorBug;
> CREATE TABLE data (date DATE, time TIME, address TINYTEXT, location TINYTEXT, temperature FLOAT, accele INT);

Now the database is ready, we can move on to setting up the sensorBugs.

Step 3: Setting Up the SensorBugs

The sensor bugs are pretty neat little devices. Unfortunately, the manufacturer only provided IOS app for programming it. Never the less, it is still possible to work with it if you only have an Android device.

First step, pair the device with a phone.
Without pairing the device, the SensorBug will not advertise data. I tried to see if I can do this directly with RaspberryPi, unfortunately, it seems like the Bluetooth LE driver on RaspberryPi is still experimental and contain bugs to prevent it from pairing with Bluetooth LE devices. Future version of the blueZ driver might fix this, but as the current writing, there is no way to pair the SensorBug with RaspberryPi.

Fortunately, we don’t need to pair the device to capture its advertised data. The only thing we need is a phone to configure the SensorBug. By default, the SensorBug will start advertise temperature data at 1s interval once it is paired with a device. For capture temperature data, that’s all that’s needed. If you plan to expand to use the position or light sensor, than configuration the device will be needed. For starter, we’ll pair the device and disconnect. This will be good enough for temperature capture purpose.

Start by pressing both buttons on the SensorBug. The blue/green LED will blink, which indicate it is powered on.
Press one of the button, the green LED should light up, indicate the power is on. If the green LED isn’t lit, press both button to try turn on the device again.
Press and hold one of the button till the blue LED start to blink. This will put the device into pair mode.
Go into Bluetooth configuration menu on the phone and look for the SensorBug device. Once it shows up, select it to pair with the device.

That’s it, now the SensorBug is powered and advertising the temperature data

Step 4: Installing the Bluetooth LE Python Wrapper

Next we need to install the library for python to talk to Bluetooth LE stack.

The instruction can be found here:
https://github.com/IanHarvey/bluepy

For Python 2.7, it is as simple as entering the following commands:

   $ sudo apt-get install python-pip libglib2.0-dev<br>   $ sudo pip install bluepy

Step 5: Scan and Find Out the Address of the SensorBug

To find out the SensorBug MAC address, use this command:
$ sudo hcitool lescan

You should see output like:

EC:FE:7E:10:B1:92 (unknown)

If you have a lot of bluetooth LE devices around, it might be hard to find out which one you are talking to. You can try bluetoothctl which give more details:

<p>$ sudo bluetoothctl<br>[bluetooth]# scan on
[NEW] Device EC:FE:7E:10:B1:92 SensorBug10B192
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Key: 0x0085
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x02
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x00
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x3c
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x25
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x09
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x41
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x02
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x02
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x43
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x0b
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x01
[CHG] Device EC:FE:7E:10:B1:92 ManufacturerData Value: 0x6f</p>

Record the MAC address, this will need to be entered into the python script to filter out unwanted Bluetooth LE devices

Step 6: Add the Python Script

A copy of the Python script is available from:

https://drive.google.com/open?id=10vOeEAbS7mi_eXn_...

Here is the same file, take care of the indent when copying:

Also, update the MAC address in the python file to match the sensor address obtained from the scan result.

<p># This program is free software: you can redistribute it and/or modify</p><p># it under the terms of the GNU General Public License as published by</p><p># the Free Software Foundation, either version 3 of the License, or</p><p># (at your option) any later version.</p><p>#</p><p># This program is distributed in the hope that it will be useful,</p><p># but WITHOUT ANY WARRANTY; without even the implied warranty of</p><p># MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the</p><p># GNU General Public License for more details.</p><p>#</p><p># You should have received a copy of the GNU General Public License</p><p># along with this program.  If not, see .</p><p># bscan.py - Simple bluetooth LE scanner and data extractor</p><p>from bluepy.btle import Scanner, DefaultDelegate</p><p>import time</p><p>import pymysql</p><p>import struct</p><p>hostname = 'localhost'</p><p>username = 'datasrc'</p><p>password = 'datasrc000'</p><p>database = 'SensorBug'</p><p>#Enter the MAC address of the sensor from the lescan</p><p>SENSOR_ADDRESS = ["ec:fe:7e:10:b9:92", "ec:fe:7e:10:b9:93"]</p><p>SENSOR_LOCATION = ["Garage", "Exterior"]</p><p>class DecodeErrorException(Exception):</p><p>     def __init__(self, value):</p><p>         self.value = value</p><p>     def __str__(self):</p><p>         return repr(self.value)</p><p>class ScanDelegate(DefaultDelegate):</p><p>    def __init__(self):</p><p>        DefaultDelegate.__init__(self)</p><p>    def handleDiscovery(self, dev, isNewDev, isNewData):</p><p>        if isNewDev:</p><p>            print "Discovered device", dev.addr</p><p>        elif isNewData:</p><p>            print "Received new data from", dev.addr</p><p>def doQueryInsert (conn, addr, loc, temp, accero):</p><p>    #blesensor table is date, time, addr, location, temp, accero</p><p>    cur = conn.cursor()</p><p>    dostr = 'INSERT INTO data VALUES (CURRENT_DATE(), NOW(), %s, %s, %s, %s);'</p><p>    cur.execute (dostr, (addr, loc, temp, accero))</p><p>    conn.commit()</p><p>scanner = Scanner().withDelegate(ScanDelegate())</p><p>myConnection = pymysql.connect (host=hostname, user=username, passwd=password, db=database)</p><p>ManuDataHex = []</p><p>ReadLoop = True</p><p>try:</p><p>    while (ReadLoop):</p><p>        devices = scanner.scan(2.0)</p><p>        ManuData = ""</p><p>        for dev in devices:</p><p>            entry = 0</p><p>            AcceroData = 0 </p><p>            AcceroType = 0 </p><p>            TempData = 0</p><p>            for saddr in SENSOR_ADDRESS:</p><p>                entry += 1</p><p>                if (dev.addr == saddr):</p><p>                    print "Device %s (%s), RSSI=%d dB" % (dev.addr, dev.addrType, dev.rssi)</p><p>                    CurrentDevAddr = saddr</p><p>                    CurrentDevLoc = SENSOR_LOCATION[entry-1]</p><p>                    for (adtype, desc, value) in dev.getScanData():</p><p>                        print "  %s = %s" % (desc, value)</p><p>                        if (desc == "Manufacturer"):</p><p>                            ManuData = value</p><p>                    if (ManuData == ""):</p><p>                        print "No data received, end decoding"</p><p>                        continue</p><p>                    #print ManuData</p><p>                    for i, j in zip (ManuData[::2], ManuData[1::2]):</p><p>                        ManuDataHex.append(int(i+j, 16))</p><p>                    #Start decoding the raw Manufacturer data</p><p>                    if ((ManuDataHex[0] == 0x85) and (ManuDataHex[1] == 0x00)):</p><p>                        print "Header byte 0x0085 found"</p><p>                    else:</p><p>                        print "Header byte 0x0085 not found, decoding stop"</p><p>                        continue</p><p>                    #Skip Major/Minor </p><p>                    #Index 5 is 0x3c, indicate battery level and config #</p><p>                    if (ManuDataHex[4] == 0x3c):</p><p>                        BatteryLevel = ManuDataHex[5]</p><p>                        ConfigCounter = ManuDataHex[6]</p><p>                    idx = 7</p><p>                    #print "TotalLen: " + str(len(ManuDataHex))</p><p>                    while (idx < len(ManuDataHex)):</p><p>                        #print "Idx: " + str(idx)</p><p>                        #print "Data: " + hex(ManuDataHex[idx]) </p><p>                        if (ManuDataHex[idx] == 0x41):</p><p>                            #Accerometer data</p><p>                            idx += 1</p><p>                            AcceleroType = ManuDataHex[idx]</p><p>                            AcceleroData = ManuDataHex[idx+1]</p><p>                            idx += 2</p><p>                        elif (ManuDataHex[idx] == 0x43):</p><p>                            #Temperature data</p><p>                            idx += 1</p><p>                            TempData = ManuDataHex[idx]</p><p>                            TempData += ManuDataHex[idx+1] * 0x100</p><p>                            TempData = TempData * 0.0625</p><p>                            idx += 2</p><p>                        else:</p><p>                            idx += 1</p><p>                    print "Device Address: " + CurrentDevAddr </p><p>                    print "Device Location: " + CurrentDevLoc </p><p>                    print "Battery Level: " + str(BatteryLevel) + "%"</p><p>                    print "Config Counter: " + str(ConfigCounter)</p><p>                    print "Accelero Data: " + hex(AcceleroType) + " " + hex(AcceleroData)</p><p>                    print "Temp Data: " + str(TempData)</p><p>                    doQueryInsert(myConnection, CurrentDevAddr, CurrentDevLoc, TempData, AcceleroData) </p><p>                    ReadLoop = False</p><p>except DecodeErrorException:</p><p>    pass</p>

Step 7: Test Out the Python Script

The script must be run in root, so:

<p>$ sudo python bscan.py<br>Discovered device ec:6e:7e:10:b1:92
Device ec:6e:7e:10:b1:92 (public), RSSI=-80 dB
  Flags = 06
  Incomplete 16b Services = 0a18
  Manufacturer = 850002003c25094102024309016f
Header byte 0x0085 found
Device Address: ec:6e:7e:10:b1:92
Device Location: Garage
Battery Level: 37%
Config Counter: 9
Accero Data: 0x2 0x2
Temp Data: 16.5625</p>

Step 8: Add the Python Scrip to the Crontab

The python script must be run in root, so if you want to capture the data automatically, it will need to be added to the root’s crontab. For this example, I run the script every 20 minutes

Use the command:

$ sudo crontab -e

# Edit this file to introduce tasks to be run by cron. # # Each task to run has to be defined through a single line # indicating with different fields when the task will be run # and what command to run for the task # # To define the time you can provide concrete values for # minute (m), hour (h), day of month (dom), month (mon), # and day of week (dow) or use '*' in these fields (for 'any').# # Notice that tasks will be started based on the cron's system # daemon's notion of time and timezones. # # Output of the crontab jobs (including errors) is sent through # email to the user the crontab file belongs to (unless redirected). # # For example, you can run a backup of all your user accounts # at 5 a.m every week with: # 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/ # # For more information see the manual pages of crontab(5) and cron(8) # # m h dom mon dow command 0 * * * * python /home/pi/bscan.py 20 * * * * python /home/pi/bscan.py 40 * * * * python /home/pi/bscan.py


That’s it. The python script will be run at regular interval and recode the output into the SQL database

Step 9: Extra: Configure the SensorBug for Position Sensing Output

It is possible to configure the SensorBug on Android for position sensing output
For position change sensing, so called Garage door.sensing, the SensorBug will detect if the device is standing upright or laying down flat.
When the device is flat, the value recorded is 0x20 while if the device is standing upright, the value is 0x02
It does not differentiate if the X or Y position is up, as long as Z axis is not up or down.

The easiest way to do this is to use LightBlue App. The SensorBug should show up in the scan menu.

Select the device you wish to configure, go to the GATT characteristics for Accelerometer configuration
UUID:9DC84838-7619-4F09-A1CE-DDCF63225B11

See image:

Write a new configuration string:

010d3f02020000002d00000002

Read back the configuration string to confirm the write.

This enables the accelerometer for position sensing.

Raspberry Pi Contest 2017

Participated in the
Raspberry Pi Contest 2017