Bluetooth Sensors: Pt. 2

Bluetooth Sensors: Part 2

Ok, I’m getting back to this. The past week has been rather hectic, hosting a 4th of July BBQ with family, and then a short trip to Toronto.

Today’s Goal

Today I am going to add the ability to enable/disable the Bluetooth adapter via the DBus interface.

RFKill

The Linux distribution I defined using Yocto uses RFKill to control access to wireless interfaces. This isn’t really necessary, and I considered simply removing it from my distro, but it seems that the muka library already has some support for dealing with rfkill.

Looking at api/switch.go in muka/go-bluetooth, I see the TurnOnBluetooth() function–that looks promising! Let’s start there. I am going to make an assumption here that this program can tell rfkill to enable Bluetooth once, and that nobody is going to futz with it during runtime. If I were doing this in a production system, I’d probably make this more specific (e.g. enable the specific device I want, when I want it).

package main                                                                                                                                                                       [0/36]
import (
        "github.com/godbus/dbus"
        "github.com/muka/go-bluetooth/api"
        "github.com/muka/go-bluetooth/bluez/profile"
        "github.com/muka/go-bluetooth/emitter"
        "log"
)

func main() {
        err := api.TurnOnBluetooth()
        if err != nil {
                log.Fatalf("Failed to enable bluetooth (%s)", err)
        }

        manager, err := api.NewManager()
        if err != nil {
                log.Fatalf("Failed to get new Manager: %s", err)
        }

        defer api.Exit()

        err = api.On("adapter", emitter.NewCallback(func(ev emitter.Event) {
                adapterEvent := ev.GetData().(api.AdapterEvent)
                log.Printf("Adapter: %s, Path: %s, Status: %d", adapterEvent.Name, adapterEvent.Path, adapterEvent.Status)
                if adapterEvent.Status == api.DeviceAdded {
                        log.Printf("ADDED\n")
                }
                foundAdapter(adapterEvent.Name)
        }))

        err = manager.RefreshState()
        if err != nil {
                log.Fatalf("Failed to refresh Manager state: %s", err)
        }

        // Install the listener for new devices.
        select {}
}

func foundAdapter(adapterId string) {
        adapter := profile.NewAdapter1(adapterId)

        // Make sure that the adapter is powered on. Note that because
        // we made a call to api.TurnOnBluetooth() in main, we expect that
        // rfkill has enabled the device.
        if !adapter.Properties.Powered {
                err := adapter.SetProperty("Powered", dbus.MakeVariant(true))
                if err != nil {
                        log.Fatalf("Failed to set Powered property: %s", err)
                }
        }
}

Scanning for devices

Ok, we have the adapter turned on! Now the next step is getting it to see other devices in the area. To do this, we have to tell the adapter to start scanning. Fortunately, this is simply done by calling the StartDiscovery() method of the adapter. This will start the adapter scanning for nearby devices. The BlueZ documentation states that This process will start creating Device objects as new devices are discovered..

Behind the scenes, the new devices will be detectable on DBus by monitoring for the InterfacesAdded signal, where the new org.bluez.Device1 is described.

Now it is important to note that while new devices will be created and broadcast, we need to also account for devices that are already being tracked by BlueZ. This is done by a call a call to api.GetDevices() which returns a slice of the cached devices.

I’ve modified the foundAdapter function to listen for discovered devices by adding the following:

	err = api.On("discovery", emitter.NewCallback(func (ev emitter.Event) {
		discoveryEvent := ev.GetData().(api.DiscoveredDeviceEvent)
		if discoveryEvent.Status == api.DeviceAdded {
			showDeviceInfo(discoveryEvent.Device)
		}
	}))

and adding the showDeviceInfo function:

func showDeviceInfo(dev *api.Device) {
	if dev == nil {
		return
	}

	props, err := dev.GetProperties()
	if err != nil {
		log.Printf("Failed to read device properties: %s", err)
		return
	}

	log.Printf("Name: %s, Addr: %s, RSSI: %d", props.Name, props.Address, props.RSSI)
}

With these changes, I am now able to get a complete and running list of devices as they appear. Note that this currently does nothing to handle devices that are removed from the list of BlueZ managed devices; for the time being, I don’t particularly care about when devices disappear.

Identifying services

Ok, so we can see Bluetooth devices–how do we find the ones we’re looking for? The naive approach would be to look for specific Names (e.g. “My Sensor”). Another way would be to look for the specific MAC Addresses of the devices that I manage myself. However, the Right Way(tm) to do what I’m trying to do is to look for available GATT services.

GATT (see the Bluetooth site for an overview) stands for Generic Attributes, and is a Bluetooth Low Energy (BLE) concept, used for defining communication between a client (e.g. our Linux machine) and a server (e.g. our temperature and humidity sensors) in a relatively simple way.

Summary

Today I figured out how to use the muka library to enable the Bluetooth radios via rfkill, and added code for listening for new devices. I also ensured that I didn’t miss available devices by manually calling the GetDevices() function of the API.

Next Steps

Next time I will need to figure out my GATT strategy. It’s been awhile since I’ve worked with this; if I remember correctly, there are standard GATT attributes for things like Temperature and Humidity sensors. I may try to re-use those, but I’m not sure what will happen if other devices in my vicinity (e.g. phones) also have those attributes. I will probably consider creating my own custom UUIDs for my readings.