BLE Beacon Experiment #2

So, now that I have a base platform for BLE beacons, I wanted to create a basic utility for scanning for beacons and presenting the results usefully. Ideally, this could evolve into the program that would run on a Raspberry Pi to receive and record sensor measurements. Based on advice from a friend, I started diving into the BlueZ library. My goal was to write a C++ program that would scan for advertisements, read beacons, and present the latest beacon received.

First of all, there is pretty much no documentation. There's a few (poorly documented) StackOverflow examples, but not much usable. With that in mind. The documentation that is provided with the source tree covers parts of the DBus API. I set off to write something that would interact with the lower levels.

The first thing in such a program is to include the right headers. Through some trial and error, I found that I needed the following for BlueZ:

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>

I also set up a struct to organise the different values I'd need:

const int default_scan_time = 1;

struct {
        int              adapter;
        int              socket;
        int              flags;
        int              max_resp;
        int              num_resp;
        int              scan_time;
        inquiry_info    *info;
} inquiry;

The next thing is to actually open the adapter:

static bool
setup_socket(void)
{
        inquiry.adapter = hci_get_route(NULL);
        inquiry.socket = hci_open_dev(inquiry.adapter);
        if (inquiry.socket < 0) {
                cerr << "Failed to open socket." << endl;
                return false;
        }

        // Flush the "known-device" cache on each scan.
        inquiry.flags = IREQ_CACHE_FLUSH;
        inquiry.max_resp = 128;
        inquiry.info = static_cast<inquiry_info *>(
         malloc(inquiry.max_resp * sizeof(inquiry_info))
        );

        if (inquiry.info == NULL) {
                cerr << "Failed to allocate memory for inquiries." << endl;
                return false;
        }

        return true;
}

Two things to note here: the device ID (e.g. from hci_get_route) is separate from the socket. It's difficult to tell which one is actually required in a given call (hint: it would help if there were comments).

I got the IREQ_CACHE_FLUSH flag from an SO answer. The author actually described this, and it's as commented above: it tells the program to start with a blank slate on every scan. The program also creates some memory to store information about the devices it's seen. Once this returns, the program has a socket open to the default BLE adapter.

Next up, the program needs to set up the scan. This is the part I really struggled with because you need to pass a bunch of parameters and whatnot. I mostly figured these out by fuzzing a lot and trying to look up the relevant parts in Getting Started with BLE. Setting the scan parameters was really problematic; I had to keep resetting my adapter to get it to work again. What are these parameters? They are, as best as I can figure out,

  • scan type: this is an active v. passive scan. In a passive scan, the radio is in receive-only mode and just listens for advertising packets. In active mode, the server requests scan response packets from clients.
  • interval: this is related to the window parameter. This is the interval between scans; i.e. in a passive scan, the radio will wait for (interval * 0.625ms) between scans.
  • window: the window is how long the radio is scanning for. It's also in units of 0.625ms. In other words, the radio scans for window, then waits interval, then starts scanning again and repeats this process.
  • own type: I don't know what this means, yet.
  • filter: I'm also not sure how filtering works. As seen below, I set this to 0 to hopefully disable filtering.
  • to: Not entirely sure. Maybe the number of interval+window scans to do?

I still don't always understand when the device ID v. the socket should be used. That being said, I was thinking when the parameter calls for a dev_id, it's asking for the value stored in inquiry.adapter and when it calls for a dd, it's asking for the value stored in inquiry.socket. This is based entirely on this (hopefully consistent) naming convention:

int hci_open_dev(int dev_id);
int hci_close_dev(int dd);  

However, despite a function signature calling for dev_id, both hci_le_set_scan_parameters and hci_le_set_scan_enable fail when given inquiry.adapter, and succeed when given inquiry.socket. So, in short, I don't really know.

Finally the program makes an inquiry into what devices around it are available.

static bool
start_scan(void)
{
        int     res;

    // int hci_le_set_scan_parameters(int dev_id,
        //     uint8_t type, uint16_t interval,
        //     uint16_t window, uint8_t own_type,
        //     uint8_t filter, int to);  
        res = hci_le_set_scan_parameters(inquiry.socket,
            0, 160, 160, 0, 0, 1000);
        if (res < 0) {
                cerr << "hci_le_set_scan_parameters returned " << res << endl;
                return false;
        }

        // filter_dup: filter duplicates, 1 to enable
        if ((res = hci_le_set_scan_enable(inquiry.adapter, 1, 0, 1000)) < 0) {
                cerr << "hci_le_set_scan_enable returned " << res << endl;
                return false;
        }

        inquiry.num_resp = hci_inquiry(inquiry.adapter, inquiry.scan_time,
            inquiry.max_resp, NULL, &(inquiry.info), inquiry.flags);
        return inquiry.num_resp >= 0;
}

The rest of the program is just overhead for making the program display something useful.

static void
show_info(void)
{
        char    s[256];

        for (int i = 0; i < inquiry.num_resp; i++) {
                ba2str(&(inquiry.info[i].bdaddr), s);
                cout << s << endl;
        }
}

static void
usage(ostream &out, const char *prog)
{
        out << "Usage: " << endl;
        out << prog << " [-h] [-t n]" << endl;
        out << "        -h      Print this help message." << endl;
        out << "        -t n    Scan for n seconds." << endl;
}

int
main(int argc, char *argv[])
{
        int     opt;

        inquiry.scan_time = default_scan_time;
        while ((opt = getopt(argc, argv, "ht:")) != -1) {
                switch (opt) {
                case 'h':
                        usage(cout, argv[0]);
                        exit(0);
                case 't':
                        inquiry.scan_time = stoi(optarg);
                        break;
                default:
                        usage(cerr, argv[0]);
                        exit(1);
                }
        }

        cout << "Setting up socket... " << flush;
        if (!setup_socket()) {
                cerr << "FAILED" << endl;
        }
        cout << "OK" << endl;

        cout << "Scanning for " << inquiry.scan_time << "s... " << flush;
        if (!start_scan()) {
                cerr << "FAILED" << endl;
                exit(1);
        }
        cout << "OK" << endl;

        show_info();

        hci_close_dev(inquiry.socket);
}

I run this, and I can see some stuff:

$ sudo ./beacon-scanner -t 10
Setting up socket... OK
Scanning for 10s... OK
4B:21:01:27:DA:D9

Interestingly enough, when I run this on my XPS-13 at home, the inquiry scan hangs.

After running into these issues, I figured I'd give the DBus API a shot.

This was a huge mistake.

The C++ libraries are of poor quality (one wouldn't even compile when I just included the header), there's little information on building them, the documentation is worse than BlueZ (which is an impressive feat), and it's left me not really wanting to use it.

Just before starting my bedtime routine (giving myself time off the computer etc), I found this library. I'll have to give it a shot before giving DBus another go.


Tags: , , ,