blob: 5f0727cceb62c2076902f266abeebc25c289edfe [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 lib
import (
"bytes"
"encoding/binary"
"encoding/hex"
"hash/fnv"
"log"
"sync"
"time"
"mojom/v.io/x/ref/services/discovery/ble/ble"
"github.com/paypal/gatt"
"github.com/paypal/gatt/linux/cmd"
"reflect"
)
func newService(uuid string, serviceId []byte, attributes map[string]string) *gatt.Service {
s := gatt.NewService(gatt.MustParseUUID(uuid))
for u, v := range attributes {
s.AddCharacteristic(gatt.MustParseUUID(u)).SetValue([]byte(v))
}
s.AddCharacteristic(uniqueServiceId).SetValue(serviceId)
return s
}
var (
attrGAPUUID = gatt.UUID16(0x1800)
attrDeviceNameUUID = gatt.UUID16(0x2A00)
attrAppearanceUUID = gatt.UUID16(0x2A01)
attrPeripheralPrivacyUUID = gatt.UUID16(0x2A02)
attrReconnectionAddrUUID = gatt.UUID16(0x2A03)
attrPeferredParamsUUID = gatt.UUID16(0x2A04)
attrGATTUUID = gatt.UUID16(0x1801)
attrServiceChangedUUID = gatt.UUID16(0x2A05)
)
const (
manufacturerId = uint16(1001)
)
var uniqueServiceId gatt.UUID
func init() {
uniqueServiceId = gatt.MustParseUUID("f6445c7f-73fd-4b8d-98d0-c4e02b087844")
}
// https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml
var gapCharAppearanceGenericComputer = []byte{0x00, 0x80}
func newGapService(name string) *gatt.Service {
s := gatt.NewService(attrGAPUUID)
s.AddCharacteristic(attrDeviceNameUUID).SetValue([]byte(name))
s.AddCharacteristic(attrAppearanceUUID).SetValue(gapCharAppearanceGenericComputer)
s.AddCharacteristic(attrPeripheralPrivacyUUID).SetValue([]byte{0x00})
s.AddCharacteristic(attrReconnectionAddrUUID).SetValue([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
s.AddCharacteristic(attrPeferredParamsUUID).SetValue([]byte{0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd0, 0x07})
return s
}
func newGattService() *gatt.Service {
s := gatt.NewService(attrGATTUUID)
s.AddCharacteristic(attrServiceChangedUUID).HandleNotifyFunc(
func(r gatt.Request, n gatt.Notifier) {})
return s
}
var gattOptions = []gatt.Option{
gatt.LnxMaxConnections(1),
gatt.LnxDeviceID(-1, true),
gatt.LnxSetAdvertisingParameters(&cmd.LESetAdvertisingParameters{
// Set an advertising rate of 150ms. This value is multipled by
// 0.625ms to get the actual rate.
AdvertisingIntervalMin: 0x00f4,
AdvertisingIntervalMax: 0x00f4,
AdvertisingChannelMap: 0x7,
}),
}
type scanner struct {
mu sync.Mutex
uuid string
attributes map[string]string
ch chan *update
done bool
}
func (s *scanner) handleChange(id string, oldService *ble.Service, newService *ble.Service) {
s.mu.Lock()
defer s.mu.Unlock()
if s.done {
return
}
matches := s.matches(id, newService)
oldMatches := s.matches(id, oldService)
uuid, err := hex.DecodeString(id)
if err != nil {
log.Fatal("Failed to decode uuid:",id,",",err)
}
if oldMatches {
s.ch <- &update{
found: false,
adv: ble.Advertisement{
ServiceId: uuid,
Service: *oldService,
},
}
}
if matches {
s.ch <- &update{
found: true,
adv: ble.Advertisement{
ServiceId: uuid,
Service: *newService,
},
}
}
}
func (s *scanner) stop() {
s.mu.Lock()
s.done = true
s.mu.Unlock()
}
func attributeMatch(filter map[string]string, attr map[string]string) bool {
for k, v := range filter {
if attr[k] != v {
return false
}
}
return true
}
func (s *scanner) matches(id string, service *ble.Service) bool {
if service == nil {
return false
}
return (s.uuid == "" || id == s.uuid) && attributeMatch(s.attributes, service.Attributes)
}
type update struct {
found bool
adv ble.Advertisement
}
type bleCacheEntry struct {
id string
name string
services map[string]*ble.Service
hash string
lastSeen time.Time
}
type BleNeighborHood struct {
mu sync.Mutex
neighborsHashCache map[string]*bleCacheEntry
knownNeighbors map[string]*bleCacheEntry
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.
scanners 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.
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.
pendingHashMap map[string]string
name string
device gatt.Device
isStopped bool
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),
scanners: make(map[int64]*scanner),
timeoutMap: make(map[string]chan struct{}),
pendingHashMap: make(map[string]string),
}
if err := b.startBLEService(); err != nil {
return nil, err
}
return b, nil
}
func (b *BleNeighborHood) AddService(id string, service ble.Service) {
b.mu.Lock()
b.services[id] = newService(id, service.InstanceId, service.Attributes)
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) RemoveService(id string) {
b.mu.Lock()
delete(b.services, 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(uuid *[]byte, attr map[string]string, ch chan *update) int64 {
s := &scanner{
attributes: attr,
ch: ch,
}
if uuid != nil {
s.uuid = hex.EncodeToString(*uuid)
}
b.mu.Lock()
id := b.nextScanId
b.nextScanId++
b.scanners[id] = s
b.mu.Unlock()
return id
}
func (b *BleNeighborHood) removeScanner(id int64) {
b.mu.Lock()
scanner, found := b.scanners[id]
if found {
scanner.stop()
}
delete(b.scanners, id)
b.mu.Unlock()
}
func (b *BleNeighborHood) Stop() error {
b.mu.Lock()
b.isStopped = true
b.mu.Unlock()
b.device.StopAdvertising()
b.device.StopScanning()
return nil
}
func (b *BleNeighborHood) advertiseAndScan() {
b.mu.Lock()
isStopped := b.isStopped
b.mu.Unlock()
if isStopped {
log.Println("Quitting")
return
}
log.Println("starting advertisement and scanning")
b.device.Advertise(b.computeAdvertisement())
b.device.Scan([]gatt.UUID{}, false)
}
// seenHash returns
func (b *BleNeighborHood) seenHash(id string, h string) bool {
log.Println("Checking for existence of", h)
b.mu.Lock()
defer b.mu.Unlock()
entry, ok := b.neighborsHashCache[h]
if !ok {
b.pendingHashMap[id] = h
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()
log.Println("Skipping connect because hashes match")
return true
}
// shouldConnect returns true if a connection should be made to p to get an update on the
// state of the services on that device.
func (b *BleNeighborHood) shouldConnect(p gatt.Peripheral, a *gatt.Advertisement) bool {
md := a.ManufacturerData
// The manufuacture data for other vanadium devices have the format:
// 0xe9 0x03 <length> <hash>
if len(md) < 2 {
return false
}
if md[0] != uint8(0xe9) || md[1] != uint8(0x03) {
return false
}
hash := md[3:]
return !b.seenHash(p.ID(), hex.EncodeToString(hash))
}
func (b *BleNeighborHood) getAllServices(p gatt.Peripheral) {
log.Println("Connected to device")
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 {
log.Println("Closing channel")
close(ch)
}
p.Device().CancelConnection(p)
b.advertiseAndScan()
}()
/*
if err := p.SetMTU(500); err != nil {
log.Errorf("Failed to set MTU, err: %s", err)
return
}
*/
log.Println("Scanning for services")
ss, err := p.DiscoverServices(nil)
if err != nil {
log.Printf("Failed to discover services, err: %s\n", err)
return
}
services := map[string]*ble.Service{}
var name string
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 := make(map[string]string)
uniqueId := []byte{}
for _, c := range cs {
if s.UUID().Equal(attrGATTUUID) {
if !c.UUID().Equal(attrDeviceNameUUID) {
continue
}
v, err := p.ReadLongCharacteristic(c)
if err != nil {
log.Printf("Failed to read the name: %v\n", err)
continue
}
name = string(v)
continue
}
key := c.UUID().String()
v, err := p.ReadLongCharacteristic(c)
if err != nil {
log.Printf("Failed to read the characteristc (%s): %v\n", key, err)
continue
}
if c.UUID().Equal(uniqueServiceId) {
uniqueId = v
continue
}
charMap[key] = string(v)
}
services[s.UUID().String()] = &ble.Service{
Attributes: charMap,
InstanceId: uniqueId,
}
}
b.saveDevice(h, p.ID(), name, 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) {
log.Printf("Found a device (%s)!\n", p.Name())
if b.shouldConnect(p, a) {
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):
p.Device().CancelConnection(p)
b.advertiseAndScan()
case <-cancel:
}
}()
}
}
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:
d.AddService(newGapService(b.name))
d.AddService(newGattService())
b.advertiseAndScan()
default:
d.StopScanning()
}
}
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, name string, services map[string]*ble.Service) {
b.mu.Lock()
defer b.mu.Unlock()
_, found := b.neighborsHashCache[hash]
if found {
log.Printf("Skipping a new save for the same hash (%s) for %s\n",
hash, name)
return
}
oldServices := map[string]*ble.Service{}
if oldEntry, ok := b.knownNeighbors[id]; ok {
oldServices = oldEntry.services
}
newEntry := &bleCacheEntry{
id: id,
hash: hash,
name: name,
services: services,
lastSeen: time.Now(),
}
b.neighborsHashCache[hash] = newEntry
b.knownNeighbors[id] = newEntry
log.Println("Looking through", len(b.scanners), "scanners *****")
for _, s := range b.scanners {
for id, oldService := range oldServices {
newValue := services[id]
if !reflect.DeepEqual(oldService, newValue) {
s.handleChange(id, oldService, newValue)
}
}
for id, newService := range newEntry.services {
if _, ok := oldServices[id]; !ok {
s.handleChange(id, nil, newService)
}
}
}
}
func (b *BleNeighborHood) computeAdvertisement() *gatt.AdvPacket {
// The hash is:
// Hash(Hash(name),Hash(b.endpoints))
hasher := fnv.New64()
nameHasher := fnv.New64()
nameHasher.Write([]byte(b.name))
binary.Write(hasher, binary.BigEndian, nameHasher.Sum64())
for k, _ := range b.services {
innerHash := fnv.New64()
innerHash.Write([]byte(k))
binary.Write(hasher, binary.BigEndian, innerHash.Sum64())
}
var buf bytes.Buffer
binary.Write(&buf, binary.BigEndian, hasher.Sum64())
adv := &gatt.AdvPacket{}
adv.AppendManufacturerData(manufacturerId, buf.Bytes())
adv.AppendName(b.name)
return adv
}