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