Scanning for iBeacon advertisements in Go

January 10, 2024 0 By addshore

I spent some time this evening faffing around trying to make a Raspberry Pi turn into a device that could detect nearby iBeacons, filtered by signal strength to find the closest, to basically create a kind of iBeacon scanner that spat out the UUID, minor and major values.

Eventually, I gave up on the Raspberry Pi for various reasons, and moved to my Windows laptop, found a good library, a working example to build on and managed to create a scanner that outputs what you see below!

The details

Searching around, I came across https://github.com/tinygo-org/bluetooth, which included an example that got me scanning for Bluetooth devices right away.

Expanding on that example, and looking at the iBeacon spec on Wikipedia, I figured out that apparently device.ManufacturerData()[76] gives me byte 7 onwards.

So using this library and making use of device.ManufacturerData()[76]

  • Byte 1: SubType: 0x02 (Apple’s iBeacon type of Custom Manufacturer Data)
  • Byte 2: SubType Length: 0x15 (Of the rest of the iBeacon data; UUID + Major + Minor + TXPower)
  • Byte 3-18: Proximity UUID (Random or Public/Registered UUID of the specific beacon)
  • Byte 19-20: Major (User-Defined value)
  • Byte 21-22: Minor (User-Defined value)
  • Byte 23: Measured Power (8 bit Signed value, ranges from -128 to 127, use Two’s Complement to “convert” if necessary, Units: Measured Transmission Power in dBm @ 1 meters from beacon) (Set by user, not dynamic, can be used in conjunction with the received RSSI at a receiver to calculate rough distance to beacon)

Here is the modified code includes iBeacon data output.

package main

import (
	"encoding/binary"
	"fmt"
	"tinygo.org/x/bluetooth"
)

var adapter = bluetooth.DefaultAdapter

func main() {
	// Enable BLE interface.
	must("enable BLE stack", adapter.Enable())

	// Start scanning.
	println("scanning...")
	err := adapter.Scan(func(adapter *bluetooth.Adapter, device bluetooth.ScanResult) {
		println("-------------------------------------------------------------------")
		println("found nearby device advertisement:", device.Address.String(), device.RSSI, device.LocalName())
		if _, ok := device.ManufacturerData()[76]; !ok {
			println("not an iBeacon advertisement")
		}
		if byteArray, ok := device.ManufacturerData()[76]; ok {
			subType := byteArray[0]
			subTypeLength := byteArray[1]
			uuid := byteArray[2:18]
			major := binary.BigEndian.Uint16(byteArray[18:20])
			minor := binary.BigEndian.Uint16(byteArray[20:22])
			measuredPower := int8(byteArray[22])

			fmt.Printf("SubType: %02X\n", subType)
			fmt.Printf("SubType Length: %02X\n", subTypeLength)
			fmt.Printf("UUID: %X\n", uuid)
			fmt.Printf("Major: %d\n", major)
			fmt.Printf("Minor: %d\n", minor)
			fmt.Printf("Measured Power: %d dBm\n", measuredPower)
		}
	})
	must("start scan", err)
}

func must(action string, err error) {
	if err != nil {
		panic("failed to " + action + ": " + err.Error())
	}
}
Code language: JavaScript (javascript)