Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Scan For BLE iBeacon Devices With Golang On A Raspberry Pi Zero W

TwitterFacebookRedditLinkedInHacker News

Last year I had worked on an interesting project at my day job, Couchbase, where I scanned for BLE iBeacon devices from a few Raspberry Pi IoT devices and uploaded the information to a remote server for analysis. I wrote about this experiment in an article titled, Collecting iBeacon Data with Couchbase and Raspberry Pi IoT Devices.

My first attempt at scanning and analyzing iBeacon data was done with Java and Node.js. The Raspberry Pi devices were using Java, and the data was being offloaded to a Node.js server. The problem with this is that Java is too heavy for the low spec Internet of Things (IoT) devices.

Fast forward to round two of playing with iBeacon devices and a Raspberry Pi. We’re going to see how to use Golang to scan for BLE signals and parse the data to determine if they are iBeacon.

This time around, I’m going to be using a Raspberry Pi Zero W instead of a Raspberry Pi 3. These devices are a fraction of the size and a fraction of the price.

Before continuing, you need to have set up the Raspberry Pi Zero W with Raspbian. If you’re not sure how to do this, check out my tutorial titled, Use Your Raspberry Pi as a Headless System Without a Monitor. You can even take this tutorial a step further and learn how to automatically configure your WiFi on the Pi Zero W, here.

Develop a Golang Application on a Host Computer

Golang is great because you can install it pretty much anywhere and run applications built with it pretty much anywhere. However, to save us the burden of slow compile times on a Raspberry Pi Zero W, we’re going to do our development on a host machine like Mac or Windows and cross-compile it.

Assuming you’ve already installed and configured your computer for Go development, execute the following command:

go get github.com/paypal/gatt

The above command will download the Gatt (Generic Attribute Profile) library created by PayPal. The Gatt library will do all the heavy lifting when it comes to scanning for Bluetooth Low Energy devices. Most of our work will be in parsing.

Within your $GOPATH, create a new project with a main.go and a ibeacon.go file. We’ll do the parsing in our ibeacon.go file and the scanning in our main.go file.

Let’s start by creating our parsing logic. Open the project’s ibeacon.go file and include the following:

package main

import (
	"encoding/binary"
	"encoding/hex"
	"errors"
	"strings"
)

type iBeacon struct {
	uuid  string
	major uint16
	minor uint16
}

func NewiBeacon(data []byte) (*iBeacon, error) {
	if len(data) < 25 || binary.BigEndian.Uint32(data) != 0x4c000215 {
		return nil, errors.New("Not an iBeacon")
	}
	beacon := new(iBeacon)
	beacon.uuid = strings.ToUpper(hex.EncodeToString(data[4:8]) + "-" + hex.EncodeToString(data[8:10]) + "-" + hex.EncodeToString(data[10:12]) + "-" + hex.EncodeToString(data[12:14]) + "-" + hex.EncodeToString(data[14:20]))
	beacon.major = binary.BigEndian.Uint16(data[20:22])
	beacon.minor = binary.BigEndian.Uint16(data[22:24])
	return beacon, nil
}

We know our iBeacon will have a UUID as well as a major and minor code. For this reason, we create a data structure for it. Within the NewiBeacon function, we accept manufacturer data and return a parsed iBeacon instance.

The manufacturer data will be passed in from the Gatt scanning. However, once we have the data, we can start parsing it. There are many other types of devices that will be picked up with Gatt, but we can narrow down what is and isn’t an iBeacon.

if len(data) < 25 || binary.BigEndian.Uint32(data) != 0x4c000215 {
    return nil, errors.New("Not an iBeacon")
}

An iBeacon will have twenty-five bytes and be of a particular value. If these are not true, we’re not working with an iBeacon, so we should throw an error.

beacon := new(iBeacon)
beacon.uuid = strings.ToUpper(hex.EncodeToString(data[4:8]) + "-" + hex.EncodeToString(data[8:10]) + "-" + hex.EncodeToString(data[10:12]) + "-" + hex.EncodeToString(data[12:14]) + "-" + hex.EncodeToString(data[14:20]))
beacon.major = binary.BigEndian.Uint16(data[20:22])
beacon.minor = binary.BigEndian.Uint16(data[22:24])

To parse our data, we need to look at certain bytes. The UUID bytes need to be converted to hexadecimal and the major and minor codes need to be converted to Uint16 values.

If you wanted to track Eddystone beacons and similar, you’d alter things slightly. That is beyond the scope of this tutorial though.

Now open your project’s main.go file and include the following:

package main

import (
	"fmt"
	"log"

	"github.com/paypal/gatt"
	"github.com/paypal/gatt/examples/option"
)

func onStateChanged(device gatt.Device, s gatt.State) {
	switch s {
	case gatt.StatePoweredOn:
		fmt.Println("Scanning for iBeacon Broadcasts...")
		device.Scan([]gatt.UUID{}, true)
		return
	default:
		device.StopScanning()
	}
}

func onPeripheralDiscovered(p gatt.Peripheral, a *gatt.Advertisement, rssi int) {
	b, err := NewiBeacon(a.ManufacturerData)
	if err == nil {
		fmt.Println("UUID: ", b.uuid)
		fmt.Println("Major: ", b.major)
		fmt.Println("Minor: ", b.minor)
		fmt.Println("RSSI: ", rssi)
	}
}

func main() {
	device, err := gatt.NewDevice(option.DefaultClientOptions...)
	if err != nil {
		log.Fatalf("Failed to open device, err: %s\n", err)
		return
	}
	device.Handle(gatt.PeripheralDiscovered(onPeripheralDiscovered))
	device.Init(onStateChanged)
	select {}
}

A lot of the above code was taken from the example project within the Gatt repository. We’ll break it down anyways for clarity.

Starting with the main function:

func main() {
	device, err := gatt.NewDevice(option.DefaultClientOptions...)
	if err != nil {
		log.Fatalf("Failed to open device, err: %s\n", err)
		return
	}
	device.Handle(gatt.PeripheralDiscovered(onPeripheralDiscovered))
	device.Init(onStateChanged)
	select {}
}

In the above code, we’re configuring our Bluetooth scanning device, registering a handler for when a peripheral (iBeacon) is discovered, and initializing the device for scanning.

func onStateChanged(device gatt.Device, s gatt.State) {
	switch s {
	case gatt.StatePoweredOn:
		fmt.Println("Scanning for iBeacon Broadcasts...")
		device.Scan([]gatt.UUID{}, true)
		return
	default:
		device.StopScanning()
	}
}

When the bluetooth state is powered on and ready, we start scanning for all peripherals, allowing duplicates. Remember, iBeacons typically broadcast numerous times per second. If we don’t allow duplicates, not all our discoveries will register.

Fast forward to the most important part of our scanning code, the onPeripheralDiscovered function:

func onPeripheralDiscovered(p gatt.Peripheral, a *gatt.Advertisement, rssi int) {
	b, err := NewiBeacon(a.ManufacturerData)
	if err == nil {
		fmt.Println("UUID: ", b.uuid)
		fmt.Println("Major: ", b.major)
		fmt.Println("Minor: ", b.minor)
		fmt.Println("RSSI: ", rssi)
	}
}

When a BLE device is picked up in our scan, we pass the manufacturer data into our parser. If it is a valid iBeacon, we print out the parsed data.

If you want to upload this data, you could run some HTTP requests instead of printing the data to the screen.

Configuring the Raspberry Pi Zero W for Bluetooth Scanning

Per the Gatt instructions, Gatt needs complete control of your bluetooth. For this reason, we need to disable functionality within the OS.

Sign into your Raspberry Pi Zero W and execute the following commands:

sudo hciconfig hci0 down
sudo service bluetooth stop

The above commands will bring down the bluetooth device and stop the built-in bluetooth server. Don’t worry, if you restart your Pi, they will come back on. You can also start them manually. If you want a more permanent solution, you’ll need to alter the startup scripts.

Building and Deploying the Golang Application

With the code ready to go, we need to compile the application to be used on the Raspberry Pi Zero W. If you’re using a Mac or Windows computer and you try to run this application, there is a pretty good chance it will not work. I have tested this on my Raspberry Pi Zero W and it works flawlessly.

Remember that article I wrote titled, Cross Compiling Golang Applications For Use on a Raspberry Pi? We’re going to follow the same steps to get a compatible ARMv6 build.

From the command line, execute the following:

env GOOS=linux GOARCH=arm GOARM=5 go build

When the build completes, you should have a binary file within your project directory.

Hopefully you can SSH into your Raspberry Pi Zero W. If you can, issue a simple SCP command to transfer the binary from your host computer to your IoT device:

scp ./name-of-binary-file pi@raspberrypi.local:~/

Make sure the change the filename, path, and hostname to whatever yours are. If you’d rather transfer the binary another way, do whatever works for you.

To run the application on your Pi, execute the following:

sudo ./name-of-binary-file

You’ll need to use sudo otherwise your application won’t have access to your BLE functionality. Based on how we wrote our Go application, scanning should happen until we tell it to stop.

Conclusion

You just saw how to scan for BLE iBeacon devices from a Raspberry Pi Zero W using Golang and the open source Gatt library. This guide isn’t limited to just the Pi Zero W. It should work fine on the standard Raspberry Pi or any other computer that has BLE support.

If you want to take this to the next level, try to send the scanned data to a remote web server with HTTP. You can learn about this in a tutorial I wrote titled, Consume RESTful API Endpoints within a Golang Application.

A video version of this article can be seen below.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.