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