blob: 0cbaea83722cd09a30fd5d74b9efae8a1ff1ec3b [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package ble implements a ble protocol to support the Vanadium discovery api.
// The package exports a discovery.Plugin that can be used to use ble for discovery.
// The advertising packet of Vanadium device should contain a manufacturer data field
// with the manufacturer id of 1001. The first 8 bytes of the data is the hash of
// the services exported. If the hash has not changed, then it is expected that the
// services and the properties they contain has not changed either.
package ble
import (
"encoding/hex"
"hash/fnv"
"log"
"reflect"
"strings"
"sync"
"time"
"github.com/paypal/gatt"
"github.com/pborman/uuid"
"v.io/x/ref/lib/discovery"
)
const (
// TODO(bjornick): Make sure this is actually unique.
manufacturerId = uint16(1001)
ttl = time.Minute * 5
)
var (
// These constants are taken from:
// https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx
attrGAPUUID = gatt.UUID16(0x1800)
attrGATTUUID = gatt.UUID16(0x1801)
)
func newV23Service(uuid uuid.UUID, attrs map[string][]byte) *gatt.Service {
s := gatt.NewService(gatt.MustParseUUID(uuid.String()))
for k, v := range attrs {
s.AddCharacteristic(gatt.MustParseUUID(k)).SetValue(v)
}
return s
}
type bleCacheEntry struct {
id string
advertisements map[string]*bleAdv
hash string
lastSeen time.Time
}
type bleNeighborhood struct {
mu sync.Mutex
// key is the hex encoded hash of the bleCacheEntry
neighborsHashCache map[string]*bleCacheEntry
// key is the hex encoded ID of the device.
knownNeighbors map[string]*bleCacheEntry
// key is the hex-encoded instance id
services map[string]*gatt.Service
// Scanners out standing calls to Scan that need be serviced. Each time a
// new device appears or disappears, the scanner is notified of the event.
// The key is the unique scanner id for the scanner.
scannersById map[int64]*scanner
// scannersByService maps from human readable service uuid to a map of
// scanner id to scanner.
scannersByService map[string]map[int64]*scanner
// If both sides try to connect to each other at the same time, then only
// one will succeed and the other hangs forever. This means that the side
// that hangs won't ever start scanning or advertising again. To avoid this
// we timeout any connections that don't finish in under 4 seconds. This
// channel is closed when a connection has been made successfully, to notify
// the cancel goroutine that it doesn't need to do anything. This is a map
// from the hex encoded device id to the channel to close.
timeoutMap map[string]chan struct{}
// The hash that we use to avoid multiple connections are stored in the
// advertising data, so we need to store somewhere in the bleNeighorhood
// until we are ready to save the new device data. This map is
// the keeper of the data. This is a map from hex encoded device id to
// hex encoded hash.
pendingHashMap map[string]string
name string
device gatt.Device
stopped chan struct{}
nextScanId int64
}
func newBleNeighborhood(name string) (*bleNeighborhood, error) {
b := &bleNeighborhood{
neighborsHashCache: make(map[string]*bleCacheEntry),
knownNeighbors: make(map[string]*bleCacheEntry),
name: name,
services: make(map[string]*gatt.Service),
scannersById: make(map[int64]*scanner),
scannersByService: make(map[string]map[int64]*scanner),
timeoutMap: make(map[string]chan struct{}),
pendingHashMap: make(map[string]string),
stopped: make(chan struct{}),
}
if err := b.startBLEService(); err != nil {
return nil, err
}
go b.checkTTL()
return b, nil
}
func (b *bleNeighborhood) checkTTL() {
for {
select {
case <-b.stopped:
return
case <-time.After(time.Minute):
b.mu.Lock()
now := time.Now()
for k, entry := range b.neighborsHashCache {
if entry.lastSeen.Add(ttl).Before(now) {
delete(b.neighborsHashCache, k)
delete(b.knownNeighbors, entry.id)
for id, adv := range entry.advertisements {
for _, scanner := range b.scannersById {
scanner.handleLost(uuid.Parse(id), adv)
}
}
}
}
b.mu.Unlock()
}
}
}
func (b *bleNeighborhood) addAdvertisement(adv bleAdv) {
b.mu.Lock()
b.services[hex.EncodeToString(adv.instanceID)] = newV23Service(adv.serviceUUID, adv.attrs)
v := make([]*gatt.Service, len(b.services))
i := 0
for _, s := range b.services {
v[i] = s
i++
}
b.mu.Unlock()
b.device.SetServices(v)
}
func (b *bleNeighborhood) removeService(id []byte) {
b.mu.Lock()
delete(b.services, hex.EncodeToString(id))
v := make([]*gatt.Service, 0, len(b.services))
for _, s := range b.services {
v = append(v, s)
}
b.mu.Unlock()
b.device.SetServices(v)
}
func (b *bleNeighborhood) addScanner(uid discovery.Uuid) (chan *discovery.Advertisement, int64) {
ch := make(chan *discovery.Advertisement)
s := &scanner{
uuid: uuid.UUID(uid),
ch: ch,
}
b.mu.Lock()
id := b.nextScanId
b.nextScanId++
b.scannersById[id] = s
key := uuid.UUID(uid).String()
m, found := b.scannersByService[key]
if !found {
m = map[int64]*scanner{}
b.scannersByService[key] = m
}
m[id] = s
b.mu.Unlock()
return ch, id
}
func (b *bleNeighborhood) removeScanner(id int64) {
b.mu.Lock()
scanner, found := b.scannersById[id]
if found {
scanner.stop()
}
delete(b.scannersById, id)
key := scanner.uuid.String()
delete(b.scannersByService[key], id)
b.mu.Unlock()
}
func (b *bleNeighborhood) Stop() error {
close(b.stopped)
b.device.StopAdvertising()
b.device.StopScanning()
return nil
}
func (b *bleNeighborhood) advertiseAndScan() {
select {
case <-b.stopped:
return
default:
}
b.device.Advertise(b.computeAdvertisement())
b.mu.Lock()
hasScanner := len(b.scannersById) > 0
b.mu.Unlock()
// TODO(bjornick): Don't scan unless there is a scanner running.
if hasScanner {
b.device.Scan([]gatt.UUID{}, true)
}
}
// seenHash returns whether or not we have seen the hash <h> before.
func (b *bleNeighborhood) seenHash(id string, h []byte) bool {
b.mu.Lock()
defer b.mu.Unlock()
key := hex.EncodeToString(h)
entry, ok := b.neighborsHashCache[key]
if !ok {
b.pendingHashMap[id] = key
return false
}
if entry.id != id {
// This can happen either because two different devices chose the same
// endpoint and name, or that one device changed its mac address. It
// seems more likely that the latter happened
// TODO(bjornick): Deal with the first case.
entry.id = id
}
entry.lastSeen = time.Now()
return true
}
// getVanadiumHash returns the hash of the vanadium device, if this advertisement
// is from a vanadium device.
func getVanadiumHash(a *gatt.Advertisement) ([]byte, bool) {
md := a.ManufacturerData
// The manufacturer data for other vanadium devices contains the hash in
// the first 8 bytes of the data portion of the packet. Since we can't tell
// gatt to only call us for advertisements from a particular manufacturer, we
// have to decode the manufacturer field to:
// 1) figure out if this is a vanadium device
// 2) find the hash of the data.
// The formnat of the manufacturer data is:
// 2-bytes for the manufacturer id (in little endian)
// 1-byte for the length of the data segment
// <the actual data>
if len(md) < 2 {
return nil, false
}
if md[0] != uint8(0xe9) || md[1] != uint8(0x03) {
return nil, false
}
return md[3:], true
}
// gattUUIDtoUUID converts a gatt.UUID to uuid.UUID.
func gattUUIDtoUUID(u gatt.UUID) (uuid.UUID, error) {
// We can't just do uuid.Parse(u.String()), because the uuid code expects
// the '-' to be in the string, but gatt.UUID.String() basically does
// hex.EncodeToString. Instead we have decode the bytes with the hex
// decoder and just cast it to a uuid.UUID.
bytes, err := hex.DecodeString(u.String())
return uuid.UUID(bytes), err
}
func (b *bleNeighborhood) getAllServices(p gatt.Peripheral) {
b.mu.Lock()
h := b.pendingHashMap[p.ID()]
delete(b.pendingHashMap, p.ID())
b.mu.Unlock()
defer func() {
b.mu.Lock()
ch := b.timeoutMap[p.ID()]
delete(b.timeoutMap, p.ID())
b.mu.Unlock()
if ch != nil {
close(ch)
}
}()
/*
if err := p.SetMTU(500); err != nil {
log.Errorf("Failed to set MTU, err: %s", err)
return
}
*/
ss, err := p.DiscoverServices(nil)
if err != nil {
log.Printf("Failed to discover services, err: %s\n", err)
return
}
services := map[string]*bleAdv{}
for _, s := range ss {
if s.UUID().Equal(attrGAPUUID) {
continue
}
cs, err := p.DiscoverCharacteristics(nil, s)
if err != nil {
log.Printf("Failed to discover characteristics: %s\n", err)
continue
}
charMap := map[string][]byte{}
for _, c := range cs {
if s.UUID().Equal(attrGATTUUID) {
continue
}
u, err := gattUUIDtoUUID(c.UUID())
if err != nil {
log.Printf("malformed uuid:%v\n", c.UUID().String())
continue
}
key := u.String()
v, err := p.ReadLongCharacteristic(c)
if err != nil {
log.Printf("Failed to read the characteristc (%s): %v\n", key, err)
continue
}
charMap[key] = v
}
uid, err := gattUUIDtoUUID(s.UUID())
if err != nil {
log.Printf("Failed to decode string: %v\n", err)
}
services[uid.String()] = &bleAdv{
serviceUUID: uid,
attrs: charMap,
instanceID: charMap[strings.Replace(InstanceUUID, "-", "", -1)],
}
}
b.saveDevice(h, p.ID(), services)
}
func (b *bleNeighborhood) startBLEService() error {
d, err := gatt.NewDevice(gattOptions...)
if err != nil {
return err
}
onPeriphDiscovered := func(p gatt.Peripheral, a *gatt.Advertisement, rssi int) {
h, v := getVanadiumHash(a)
if v && !b.seenHash(p.ID(), h) {
log.Println("trying to connect to ", p.Name())
// We stop the scanning and advertising so we can connect to the new device.
// If one device is changing too frequently we might never find all the devices,
// since we restart the scan every time we finish connecting, but hopefully
// that is rare.
p.Device().StopScanning()
p.Device().StopAdvertising()
p.Device().Connect(p)
b.mu.Lock()
cancel := make(chan struct{}, 1)
b.timeoutMap[p.ID()] = cancel
b.mu.Unlock()
go func() {
select {
case <-time.After(4 * time.Second):
case <-cancel:
}
b.mu.Lock()
if b.timeoutMap[p.ID()] == cancel {
delete(b.timeoutMap, p.ID())
}
b.mu.Unlock()
p.Device().CancelConnection(p)
b.advertiseAndScan()
}()
}
}
onPeriphConnected := func(p gatt.Peripheral, err error) {
if err != nil {
log.Println("Failed to connect:", err)
return
}
b.getAllServices(p)
}
onStateChanged := func(d gatt.Device, s gatt.State) {
log.Printf("State: %s\n", s)
switch s {
case gatt.StatePoweredOn:
defaultServices := addDefaultServices(b.name)
for k, v := range defaultServices {
b.services[k] = v
d.AddService(v)
}
b.advertiseAndScan()
default:
d.StopScanning()
d.StopAdvertising()
}
}
d.Handle(
gatt.CentralConnected(func(c gatt.Central) { log.Printf("Connect: %v\n", c.ID()) }),
gatt.CentralDisconnected(func(c gatt.Central) { log.Printf("Disconnected: %v\n", c.ID()) }),
gatt.PeripheralDiscovered(onPeriphDiscovered),
gatt.PeripheralConnected(onPeriphConnected),
)
d.Init(onStateChanged)
b.device = d
return nil
}
func (b *bleNeighborhood) saveDevice(hash string, id string, services map[string]*bleAdv) {
b.mu.Lock()
defer b.mu.Unlock()
if _, found := b.neighborsHashCache[hash]; found {
log.Printf("Skipping a new save for the same hash (%s)\n",
hash)
return
}
var oldAdvs map[string]*bleAdv
if oldEntry, ok := b.knownNeighbors[id]; ok {
oldAdvs = oldEntry.advertisements
}
newEntry := &bleCacheEntry{
id: id,
hash: hash,
advertisements: services,
lastSeen: time.Now(),
}
b.neighborsHashCache[hash] = newEntry
b.knownNeighbors[id] = newEntry
for id, newAdv := range services {
oldAdv := oldAdvs[id]
if !reflect.DeepEqual(oldAdv, newAdv) {
uid := uuid.Parse(id)
for _, s := range b.scannersByService[id] {
s.handleUpdate(uid, oldAdv, newAdv)
}
}
}
for id, oldAdv := range oldAdvs {
if _, exist := services[id]; !exist {
uid := uuid.Parse(id)
for _, s := range b.scannersByService[id] {
s.handleLost(uid, oldAdv)
}
}
}
}
func (b *bleNeighborhood) computeAdvertisement() *gatt.AdvPacket {
// The hash is:
// Hash(Hash(name),Hash(b.endpoints))
hasher := fnv.New64()
w := func(field string) {
tmp := fnv.New64()
tmp.Write([]byte(field))
hasher.Write(tmp.Sum(nil))
}
w(b.name)
for k, _ := range b.services {
w(k)
}
adv := &gatt.AdvPacket{}
adv.AppendFlags(0x06)
adv.AppendManufacturerData(manufacturerId, hasher.Sum(nil))
adv.AppendName(b.name)
return adv
}