Bluetooth Sensors: Pt. 3

Today’s Goal

Today’s goal is to figure out my GATT strategy. I will make a way to discover the sensors' Bluetooth profiles and hopefully set up a quick demo of finding the sensors and reading an attribute.

One thing I’ve noticed since the last time I worked on this is that the Bluetooth radios only support one custom attribute, 0xffe0, which can transfer arbitrary data.

Just for the exercise of it, however, I am going to approach this as if I wanted to do this “The Right Way(tm)”.

Step 1: Look at the documentation

As I mentioned last time, there are Generic Attributes defined for many things.. Here we see that there are a few attributes of interest:

Temperature Measurement (0x2a1c)

This attribute seems to be trying to be very flexible; it allows for the payload to define a few things about the temperature measurement:

  1. Unit (Celsius or Fahrenheit)
  2. Timestamp of the measurement
  3. Temperature type, itself a Generic Attribute type.

The Temperature Type attribute indicates that the measurement types are generally biological (the values indicate things like Toe, Mouth, Finger, etc…). To me this means we probably won’t want to use this attribute.

Temperature (0x2a6e)

This attribute looks much simpler. It consists of one 16-bit integer that represents 0.01 degrees Celsius. It’s nice that it’s a fixed-point number, something that MCUs can easily work with.

Temperature Celsius (0x2a1f)

This field is very similar to the one above (Temperature), except the name is explicitly calling out the units. I like that. Again, the attribute consists of one signed 16-bit integer, although the fixed-point is slightly less well documented, but it seems to be an exponent value of -1, meaning the value is in tenths of a degree Celsius.

Temperature Fahrenheit (0x2a20)

Almost identical to the Temperature Celcius attribute, except the value represents a Fahrenheit temperature (obviously).

Let’s make a choice

Given these fields, I am going to choose Temperature Celsius. This is somewhat arbitrary, but I guess if I had to justify it, I’d say I like the Celsius units better than Fahrenheit, and I don’t see a need for 2 digits of decimal precision.

Step 2: Working with the HC-08 parts

I’ve played with these a little bit before, but I don’t really remember anything about them.

Talking to the HC-08

I’ve hooked up the HC-08 to the Samsung Interface Board that came with my Artik530. I connected the Ground and Power, and remembered to reverse the Tx/Rx pins when connecting to UART0_RX and UART0_TX. After some trial and error, I realized that UART0 appears in the Linux devices under /dev/ttyAMA4.

By default, the HC-08 chip uses 9600 8N1 serial port configuration. I don’t see any real need to change this for now. I also realized that there’s something funky going on; I can’t seem to get screen or picocom working. However, I do have luck setting up a cat /dev/ttyAMA4 and echo-ing commands to the device. Very curious.

In any case, I am able to issue basic AT commands to the device and can set its name. I downloaded the BLE Scanner app from the iOS App Store, and could immediately see the Bluetooth radio–so far so good! I see the name and the FFE0 attribute. As you might expect, I immediately tried to Read and Write that attribute. From the iPhone side, I have to make sure to send a newline (0x0a) or carriage return (0x0d) to flush the output buffer of the HC-08 radio. I tested both, and it seems that one or the other will work fine. I wonder if there is some TTY shenanigans going on?

I also verify that I am able to send strings from the HC-08 to the phone. In this case, I can omit the line-endings completely and the values are still sent fine. I also note that the Notify portion of the attribute seems to be working (when I type into the HC-08 serial port, the phone receives the text without having to manually poll the value). This should be useful!

Detecting the HC-08 from the Artik530

I am going to now detect devices that expose the 0xffe0 attribute. This means we’re heading back to Go!

In my latest code, I was able to print devices as I found them (being careful to also look at the BlueZ cached devices!). What I am going to do now is check those devices to see whether they support the GATT UUID that I am looking for (0xffe0).

I noticed that the muka Device1 class' Properties field contains a list of supported UUIDs. This is promising! To make sure this is what I want, I added a bit of code to print devices supported UUIDs on detection:

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

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

// We're going to look for the truncated UUID, which is a bit
// hacky
for _, uuid := range props.UUIDs {
	log.Printf("\tUUID: %s", uuid)
}

Running this code on device detection leads to a nice list of supported GATT UUIDs for every discovered device!

All that’s left now is to write a bit of code to detect when we’ve found the UUID we care about. Instead of a snippet, I’ll just post whole code I have now:

package main

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"
)

const CUSTOM_UUID = "ffe0"

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 {
			foundAdapter(adapterEvent.Name)
		}
	}))

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

	// Now get a list of the previously identified BlueZ devices and
	// print their information to the log.
	devices, err := api.GetDevices()
	if err != nil {
		log.Fatalf("Failed to get cached devices: %s", err)
	}

	log.Printf("======================================")
	log.Printf("Cached Devices:")
	log.Printf("======================================")
	for _, dev := range devices {
		showDeviceInfo(&dev)
	}
	log.Printf("======================================")

	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)
		}
	}

	// Now that the adapter is on, we can start scanning for Bluetooth
	// devices.
	err := adapter.StartDiscovery()
	if err != nil {
		log.Fatalf("Failed to start discovery: %s", err)
	}

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

func deviceIsSensor(dev *api.Device) bool {
	props, err := dev.GetProperties()
	if err != nil {
		log.Printf("Failed to read device properties: %s", err)
		return false
	}

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

	// We're going to look for the truncated UUID, which is a bit
	// hacky
	for _, uuid := range props.UUIDs {
		r := []rune(uuid)
		substring := string(r[4:8])
		if substring == CUSTOM_UUID {
			return true
		}
	}

	return false
}

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

	if deviceIsSensor(dev) {
		log.Printf("Found our sensor.")
	}
}

Next Steps

I need to go finish some thing around the house, so that is all for tonight. For next time, I’ll be trying to send data from the HC-08 radios to the Artik side. This will form the basis of the data collection side. From there, I’ll add proper data reading from the Si7021 chips and write that to the HC-08, and receive it on the Artik.

After that, I’ll look at hooking it up to something like Amazon IoT so that I can do interesting things with it.