BLE Beacon Scanner#

BLE beacons send small packets of information such as a URL. The sample code listens for Eddystone TLM messages and reports the temperature send by the device.

For testing I am using a BC011 from Bluecharm Beacons.

%connect ble
%rsync
Connected to ble @ serial:///dev/ttyUSB0
Directories match

from struct import unpack

import bluetooth
import binascii
import time


class EddystoneTLM:
    
    def __init__(self, ble, addresses=[]):
        self._scanning = False
        self._addresses = [ binascii.unhexlify(x) for x in addresses ]
        self._ble = ble
        self._ble.active(True)
        self._ble.irq(self._irq)

    def _irq(self, event, data):
        if event == 5:
            # scan result
            addr_type, addr, adv_type, rssi, adv_data = data
            if addr in self._addresses:
                self._scan_callback(self._decode(adv_data, rssi))
                # stop scanning
                self._ble.gap_scan(None)
                self._scanning = False
        elif event == 6:
            # scan done
            self._scanning = False

    def _decode(self, adv_data, rssi):
        adv_data = bytes(adv_data)   # memoryview -> bytes
        eddytlm = b'\x02\x01\x06\x03\x03\xaa\xfe\x11\x16\xaa\xfe \x00'
        ibeacon = b'\x02\x01\x06\x1a\xffL\x00\x02\x15'
        if eddytlm in adv_data:
            return {
                'packet' : 'tlm',
                'bat'    : unpack('>H', adv_data[13:15])[0],         # battery voltage [mV]
                'temp'   : unpack('>h', adv_data[15:17])[0] // 256,  # temperature [C]
                'adv'    : unpack('>I', adv_data[17:21])[0],         # number of packets
                'sec'    : unpack('>I', adv_data[21:25])[0],         # time since turn on, [0.1s]
                'rssi'   : rssi
            }
        if ibeacon in adv_data:
            return {
                'packet' : 'iBeacon',
                'name'   : adv_data[9:25].decode(),
                'major'  : unpack('>H', adv_data[25:27])[0],
                'minor'  : unpack('>H', adv_data[27:29])[0],
                'rssi'   : adv_data[29]
            }
        print("unrecognized packet", binascii.hexlify(adv_data))
        return None

    def scan(self, callback=None):
        self._scanning = True
        self._scan_callback = callback
        self._ble.gap_scan(20000, 30000, 30000)
        
    @property
    def scanning(self):
        return self._scanning


def demo():
    ble = bluetooth.BLE()
    central = EddystoneTLM(ble, ['dd34020676d4'])
    
    n = 0
    
    def on_scan(data):
        nonlocal n
        print("[{:4d}] {}".format(n, data))
        n += 1

    # Wait for connection...
    while True:
        if not central.scanning:
            central.scan(callback=on_scan)
        time.sleep_ms(100)


if __name__ == "__main__":
    demo()
[   0] {'temp': 21, 'adv': 158327, 'sec': 10222614, 'rssi': -71, 'bat': 2968, 'packet': 'tlm'}
[   1] {'temp': 21, 'adv': 158329, 'sec': 10222814, 'rssi': -67, 'bat': 2968, 'packet': 'tlm'}
[   2] {'temp': 20, 'adv': 158330, 'sec': 10222914, 'rssi': -77, 'bat': 2968, 'packet': 'tlm'}
[   3] {'temp': 20, 'adv': 158331, 'sec': 10223014, 'rssi': -67, 'bat': 2969, 'packet': 'tlm'}
[   4] {'temp': 21, 'adv': 158332, 'sec': 10223114, 'rssi': -69, 'bat': 2969, 'packet': 'tlm'}
[   5] {'temp': 20, 'adv': 158333, 'sec': 10223214, 'rssi': -66, 'bat': 2970, 'packet': 'tlm'}
[   6] {'temp': 20, 'adv': 158334, 'sec': 10223314, 'rssi': -66, 'bat': 2970, 'packet': 'tlm'}
[   7] {'temp': 20, 'adv': 158335, 'sec': 10223414, 'rssi': -76, 'bat': 2977, 'packet': 'tlm'}
[   8] {'temp': 21, 'adv': 158336, 'sec': 10223514, 'rssi': -71, 'bat': 2974, 'packet': 'tlm'}
[   9] {'temp': 20, 'adv': 158337, 'sec': 10223614, 'rssi': -75, 'bat': 2974, 'packet': 'tlm'}
[  10] {'temp': 21, 'adv': 158338, 'sec': 10223714, 'rssi': -76, 'bat': 2970, 'packet': 'tlm'}
[  11] {'temp': 20, 'adv': 158339, 'sec': 10223814, 'rssi': -75, 'bat': 2970, 'packet': 'tlm'}
[  12] {'temp': 20, 'adv': 158340, 'sec': 10223914, 'rssi': -78, 'bat': 2971, 'packet': 'tlm'}
[  13] {'temp': 20, 'adv': 158341, 'sec': 10224015, 'rssi': -70, 'bat': 2971, 'packet': 'tlm'}
[  14] {'temp': 20, 'adv': 158343, 'sec': 10224215, 'rssi': -71, 'bat': 2971, 'packet': 'tlm'}
[  15] {'temp': 21, 'adv': 158345, 'sec': 10224415, 'rssi': -69, 'bat': 2968, 'packet': 'tlm'}
[  16] {'temp': 20, 'adv': 158346, 'sec': 10224515, 'rssi': -72, 'bat': 2968, 'packet': 'tlm'}
[  17] {'temp': 21, 'adv': 158347, 'sec': 10224615, 'rssi': -78, 'bat': 2968, 'packet': 'tlm'}
[  18] {'temp': 20, 'adv': 158348, 'sec': 10224715, 'rssi': -78, 'bat': 2968, 'packet': 'tlm'}
[  19] {'temp': 20, 'adv': 158349, 'sec': 10224815, 'rssi': -79, 'bat': 2969, 'packet': 'tlm'}
[  20] {'temp': 20, 'adv': 158350, 'sec': 10224915, 'rssi': -67, 'bat': 2969, 'packet': 'tlm'}
[  21] {'temp': 20, 'adv': 158352, 'sec': 10225115, 'rssi': -69, 'bat': 2969, 'packet': 'tlm'}
[  22] {'temp': 21, 'adv': 158353, 'sec': 10225215, 'rssi': -69, 'bat': 2972, 'packet': 'tlm'}
[  23] {'temp': 21, 'adv': 158354, 'sec': 10225315, 'rssi': -82, 'bat': 2976, 'packet': 'tlm'}
[  24] {'temp': 21, 'adv': 158356, 'sec': 10225515, 'rssi': -67, 'bat': 2972, 'packet': 'tlm'}
[  25] {'temp': 20, 'adv': 158358, 'sec': 10225715, 'rssi': -70, 'bat': 2971, 'packet': 'tlm'}
Interrupted