2

Here's the ScanUtility.py file that the BeaconScanner.py file uses to find and list the ble beacons.

#This is a working prototype. DO NOT USE IT IN LIVE PROJECTS


import sys
import struct
import bluetooth._bluetooth as bluez

OGF_LE_CTL=0x08
OCF_LE_SET_SCAN_ENABLE=0x000C

def hci_enable_le_scan(sock):
    hci_toggle_le_scan(sock, 0x01)

def hci_disable_le_scan(sock):
    hci_toggle_le_scan(sock, 0x00)

def hci_toggle_le_scan(sock, enable):
    cmd_pkt = struct.pack("<BB", enable, 0x00)
    bluez.hci_send_cmd(sock, OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE, cmd_pkt)

def packetToString(packet):
    """
    Returns the string representation of a raw HCI packet.
    """
    if sys.version_info > (3, 0):
    return ''.join('%02x' % struct.unpack("B", bytes([x]))[0] for x in packet)
    else:
    return ''.join('%02x' % struct.unpack("B", x)[0] for x in packet)

def parse_events(sock, loop_count=100):
    old_filter = sock.getsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, 14)
    flt = bluez.hci_filter_new()
    bluez.hci_filter_all_events(flt)
    bluez.hci_filter_set_ptype(flt, bluez.HCI_EVENT_PKT)
    sock.setsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, flt )
    results = []
    for i in range(0, loop_count):
    packet = sock.recv(255)
    ptype, event, plen = struct.unpack("BBB", packet[:3])
    packetOffset = 0
    dataString = packetToString(packet)
    """
    If the bluetooth device is an beacon then show the beacon.
    """
    #print (dataString)
    if dataString[34:50] == '0303aafe1516aafe' or '0303AAFE1116AAFE':
        """
        Selects parts of the bluetooth packets.
        """
        broadcastType = dataString[50:52]
        if broadcastType == '00' :
        type = "Eddystone UID"
        namespace = dataString[54:74].upper()
        instance = dataString[74:86].upper()
        resultsArray = [
        {"type": type, "namespace": namespace, "instance": instance}]
        return resultsArray

        elif broadcastType == '10':
        type = "Eddystone URL"
        urlprefix = dataString[54:56]
        if urlprefix == '00':
            prefix = 'http://www.'
        elif urlprefix == '01':
            prefix = 'https://www.'
        elif urlprefix == '02':
            prefix = 'http://'
        elif urlprefix == '03':
            prefix = 'https://'
        hexUrl = dataString[56:][:-2]
        url = prefix + hexUrl.decode("hex")
        rssi, = struct.unpack("b", packet[packetOffset -1])
        resultsArray = [{"type": type, "url": url}]
        return resultsArray

        elif broadcastType == '20':
        type = "Eddystone TLM"
        resultsArray = [{"type": type}]
        return resultsArray

        elif broadcastType == '30':
        type = "Eddystone EID"
        resultsArray = [{"type": type}]
        return resultsArray

        elif broadcastType == '40':
        type = "Eddystone RESERVED"
        resultsArray = [{"type": type}]
        return resultsArray

    if dataString[38:46] == '4c000215':
        """
        Selects parts of the bluetooth packets.
        """
        type = "iBeacon"
        uuid = dataString[46:54] + "-" + dataString[54:58] + "-" + dataString[58:62] + "-" + dataString[62:66] + "-" + dataString[66:78]
        major = dataString[78:82]
        minor = dataString[82:86]
        majorVal = int("".join(major.split()[::-1]), 16)
        minorVal = int("".join(minor.split()[::-1]), 16)
        """
        Organises Mac Address to display properly
        """
        scrambledAddress = dataString[14:26]
        fixStructure = iter("".join(reversed([scrambledAddress[i:i+2] for i in range(0, len(scrambledAddress), 2)])))
        macAddress = ':'.join(a+b for a,b in zip(fixStructure, fixStructure))
        rssi, = struct.unpack("b", packet[packetOffset -1])

        resultsArray = [{"type": type, "uuid": uuid, "major": majorVal, "minor": minorVal, "rssi": rssi, "macAddress": macAddress}]

        return resultsArray

    return results

The orginal Beaconscanner.py file works as it should by listing the beacons.

import ScanUtility
import bluetooth._bluetooth as bluez

#Set bluetooth device. Default 0.
dev_id = 0
try:
    sock = bluez.hci_open_dev(dev_id)
    print ("\n *** Looking for BLE Beacons ***\n")
    print ("\n *** CTRL-C to Cancel ***\n")
except:
    print ("Error accessing bluetooth")

ScanUtility.hci_enable_le_scan(sock)
#Scans for iBeacons
try:
    while True:
        returnedList = ScanUtility.parse_events(sock, 10)
        for item in returnedList:
            print(item)
            print("")
except KeyboardInterrupt:
    pass

Here's the modified BeaconScanner.py file which should print "Works" if the scanner finds the wanted beacon by it's mac address.

import ScanUtility
import bluetooth._bluetooth as bluez

#Set bluetooth device. Default 0.
dev_id = 0
try:
    sock = bluez.hci_open_dev(dev_id)
    print ("\n *** Looking for BLE Beacons ***\n")
    print ("\n *** CTRL-C to Cancel ***\n")
except:
    print ("Error accessing bluetooth")

ScanUtility.hci_enable_le_scan(sock)
#Scans for iBeacons
try:
    while True:
        returnedList = ScanUtility.parse_events(sock, 10)
        for macAddress in returnedList:
            if macAddress == "e2:e3:23:d1:b0:54":
            print("Works")
            else:
            print("Nope")
except KeyboardInterrupt:
    pass

The modified file however always prints "Nope". I think the "macAddress" part in the if statement can't be used to identify the beacons. What have to be changed in the code so the beacon can be identified by it's mac address in the if statement?

ukBaz
  • 6,985
  • 2
  • 8
  • 31

2 Answers2

0

According to the source of ScanUtility.py, it seems like the function returns a list of one dict which is a bit odd. You should query the dict as follow:

for item in returnedList:
    try:
        if item['macAddress'] == "e2:e3:23:d1:b0:54":
            print("Works")
        else:
            print("Nope")
    except KeyError:
        print('MAC Address is missing')

Note that I have added a try/except statement to deal with cases where the macAddress key is not present in your dict. This works only if your dict is not a subclass of defaultdict.

alfajet
  • 389
  • 1
  • 14
0

Searching for a beacon by its mac address is not always a good solution as it is possible a beacon is using a random private addresses.

Are you sure that the mac address you are looking for is ever broadcast?

Your code also only seems to return the mac address for iBeacons.

The other thing I noticed about this code is that it bypasses the bluetoothd running on your system by doing direct calls to the hci socket. This is generally not a good idea and requires the python script to be run with root priveliages.

A way to avoid this is use the BlueZ D-Bus API. An example of this is included below and prints out beacon data plus a message when it sees the mac address of interest. This code requires pydbus and gi.repository

import argparse
from gi.repository import GLib
from pydbus import SystemBus
import uuid

DEVICE_INTERFACE = 'org.bluez.Device1'

remove_list = set()


def stop_scan():
    """Stop device discovery and quit event loop"""
    adapter.StopDiscovery()
    mainloop.quit()


def clean_beacons():
    """
    BlueZ D-Bus API does not show duplicates. This is a
    workaround that removes devices that have been found
    during discovery
    """
    not_found = set()
    for rm_dev in remove_list:
        try:
            adapter.RemoveDevice(rm_dev)
        except GLib.Error as err:
            not_found.add(rm_dev)
    for lost in not_found:
        remove_list.remove(lost)


def process_eddystone(data):
    """Print Eddystone data in human readable format"""
    _url_prefix_scheme = ['http://www.', 'https://www.',
                          'http://', 'https://', ]
    _url_encoding = ['.com/', '.org/', '.edu/', '.net/', '.info/',
                     '.biz/', '.gov/', '.com', '.org', '.edu',
                     '.net', '.info', '.biz', '.gov']
    tx_pwr = int.from_bytes([data[1]], 'big', signed=True)
    # Eddystone UID Beacon format
    if data[0] == 0x00:
        namespace_id = int.from_bytes(data[2:12], 'big')
        instance_id = int.from_bytes(data[12:18], 'big')
        print(f'\t\tEddystone UID: {namespace_id} - {instance_id} \u2197 {tx_pwr}')
    # Eddystone URL beacon format
    elif data[0] == 0x10:
        prefix = data[2]
        encoded_url = data[3:]
        full_url = _url_prefix_scheme[prefix]
        for letter in encoded_url:
            if letter < len(_url_encoding):
                full_url += _url_encoding[letter]
            else:
                full_url += chr(letter)
        print(f'\t\tEddystone URL: {full_url} \u2197 {tx_pwr}')


def process_ibeacon(data, beacon_type='iBeacon'):
    """Print iBeacon data in human readable format"""
    beacon_uuid = uuid.UUID(bytes=bytes(data[2:18]))
    major = int.from_bytes(bytearray(data[18:20]), 'big', signed=False)
    minor = int.from_bytes(bytearray(data[20:22]), 'big', signed=False)
    tx_pwr = int.from_bytes([data[22]], 'big', signed=True)
    print(f'\t\t{beacon_type}: {beacon_uuid} - {major} - {minor} \u2197 {tx_pwr}')


def ble_16bit_match(uuid_16, srv_data):
    """Expand 16 bit UUID to full 128 bit UUID"""
    uuid_128 = f'0000{uuid_16}-0000-1000-8000-00805f9b34fb'
    return uuid_128 == list(srv_data.keys())[0]


def on_iface_added(owner, path, iface, signal, interfaces_and_properties):
    """
    Event handler for D-Bus interface added.
    Test to see if it is a new Bluetooth device
    """
    iface_path, iface_props = interfaces_and_properties
    if DEVICE_INTERFACE in iface_props:
        on_device_found(iface_path, iface_props[DEVICE_INTERFACE])


def on_device_found(device_path, device_props):
    """
    Handle new Bluetooth device being discover.
    If it is a beacon of type iBeacon, Eddystone, AltBeacon
    then process it
    """
    address = device_props.get('Address')
    address_type = device_props.get('AddressType')
    name = device_props.get('Name')
    alias = device_props.get('Alias')
    paired = device_props.get('Paired')
    trusted = device_props.get('Trusted')
    rssi = device_props.get('RSSI')
    service_data = device_props.get('ServiceData')
    manufacturer_data = device_props.get('ManufacturerData')
    if address.casefold() == 'e2:e3:23:d1:b0:54':
        print('Found mac address of interest')
    if service_data and ble_16bit_match('feaa', service_data):
        process_eddystone(service_data['0000feaa-0000-1000-8000-00805f9b34fb'])
        remove_list.add(device_path)
    elif manufacturer_data:
        for mfg_id in manufacturer_data:
            # iBeacon 0x004c
            if mfg_id == 0x004c and manufacturer_data[mfg_id][0] == 0x02:
                process_ibeacon(manufacturer_data[mfg_id])
                remove_list.add(device_path)
            # AltBeacon 0xacbe
            elif mfg_id == 0xffff and manufacturer_data[mfg_id][0:2] == [0xbe, 0xac]:
                process_ibeacon(manufacturer_data[mfg_id], beacon_type='AltBeacon')
                remove_list.add(device_path)
    clean_beacons()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--duration', type=int, default=0,
                        help='Duration of scan [0 for continuous]')
    args = parser.parse_args()
    bus = SystemBus()
    adapter = bus.get('org.bluez', '/org/bluez/hci0')

    bus.subscribe(iface='org.freedesktop.DBus.ObjectManager',
                  signal='InterfacesAdded',
                  signal_fired=on_iface_added)

    mainloop = GLib.MainLoop()


    if args.duration > 0:
        GLib.timeout_add_seconds(args.duration, stop_scan)
    adapter.SetDiscoveryFilter({'DuplicateData': GLib.Variant.new_boolean(True)})
    adapter.StartDiscovery()

    try:
        print('\n\tUse CTRL-C to stop discovery\n')
        mainloop.run()
    except KeyboardInterrupt:
        stop_scan()

Example invocation and output:

$ python3 beacon_scanner.py 

        Use CTRL-C to stop discovery

                iBeacon: 1e9fdc8c-96e0-4d68-b34a-3b635cec0489 - 5555 - 99 ↗ -65
                Eddystone URL: http://www.bluetooth.com/ ↗ -69
Found mac address of interest
ukBaz
  • 6,985
  • 2
  • 8
  • 31