ref/discovery: Implement a ble plugin for discovery.
Change-Id: I45f7f0fda6e399e10f533d6ac72488365e7828f3
diff --git a/lib/discovery/plugins/ble/advertisement.go b/lib/discovery/plugins/ble/advertisement.go
new file mode 100644
index 0000000..04c87b9
--- /dev/null
+++ b/lib/discovery/plugins/ble/advertisement.go
@@ -0,0 +1,91 @@
+// 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
+
+import (
+ "strings"
+
+ "fmt"
+ "net/url"
+
+ vdiscovery "v.io/v23/discovery"
+ "v.io/x/ref/lib/discovery"
+
+ "github.com/pborman/uuid"
+)
+
+type bleAdv struct {
+ serviceUUID uuid.UUID
+ instanceID []byte
+ attrs map[string][]byte
+}
+
+var (
+ // This uuids are v4 uuid generated out of band. These constants need
+ // to be accessible in all the languages that have a ble implementation
+
+ // The attribute uuid for the unique service id
+ instanceUUID = "f6445c7f-73fd-4b8d-98d0-c4e02b087844"
+
+ // The attribute uuid for the interface name
+ interfaceNameUUID = "d4789810-4db0-40d8-9658-92f8e304d578"
+
+ addrUUID = "f123fb0e-770f-4e46-b8ad-aee4185ab5a1"
+)
+
+func newAdvertisment(adv discovery.Advertisement) bleAdv {
+ cleanAddrs := make([]string, len(adv.Addrs))
+ for i, v := range adv.Addrs {
+ cleanAddrs[i] = url.QueryEscape(v)
+ }
+ attrs := map[string][]byte{
+ instanceUUID: adv.InstanceUuid,
+ interfaceNameUUID: []byte(adv.InterfaceName),
+ addrUUID: []byte(strings.Join(cleanAddrs, "&")),
+ }
+
+ for k, v := range adv.Attrs {
+ hexUUID := discovery.NewAttributeUUID(k).String()
+ attrs[hexUUID] = []byte(k + "=" + v)
+ }
+ return bleAdv{
+ instanceID: adv.InstanceUuid,
+ serviceUUID: adv.ServiceUuid,
+ attrs: attrs,
+ }
+}
+
+func (a *bleAdv) toDiscoveryAdvertisement() (*discovery.Advertisement, error) {
+ out := &discovery.Advertisement{
+ Service: vdiscovery.Service{
+ Attrs: vdiscovery.Attributes{},
+ InterfaceName: string(a.attrs[interfaceNameUUID]),
+ InstanceUuid: a.instanceID,
+ },
+ ServiceUuid: a.serviceUUID,
+ }
+ out.Addrs = strings.Split(string(a.attrs[addrUUID]), "&")
+ var err error
+ for i, v := range out.Addrs {
+ out.Addrs[i], err = url.QueryUnescape(v)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ for k, v := range a.attrs {
+ if k == instanceUUID || k == interfaceNameUUID || k == addrUUID {
+ continue
+ }
+ parts := strings.SplitN(string(v), "=", 2)
+ if len(parts) != 2 {
+ return nil, fmt.Errorf("incorrectly formatted value, %s", v)
+ }
+ out.Attrs[parts[0]] = parts[1]
+
+ }
+
+ return out, nil
+}
diff --git a/lib/discovery/plugins/ble/advertisement_test.go b/lib/discovery/plugins/ble/advertisement_test.go
new file mode 100644
index 0000000..5b4025c
--- /dev/null
+++ b/lib/discovery/plugins/ble/advertisement_test.go
@@ -0,0 +1,37 @@
+// 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
+
+import (
+ "github.com/pborman/uuid"
+ "reflect"
+ "testing"
+ vdiscovery "v.io/v23/discovery"
+ "v.io/x/ref/lib/discovery"
+)
+
+func TestConvertingBackAndForth(t *testing.T) {
+ v23Adv := discovery.Advertisement{
+ Service: vdiscovery.Service{
+ Addrs: []string{"localhost:1000", "example.com:540"},
+ InstanceUuid: []byte(uuid.NewUUID()),
+ Attrs: map[string]string{
+ "key1": "value1",
+ "key2": "value2",
+ },
+ },
+ ServiceUuid: uuid.NewUUID(),
+ }
+
+ adv := newAdvertisment(v23Adv)
+ out, err := adv.toDiscoveryAdvertisement()
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+
+ if !reflect.DeepEqual(&v23Adv, out) {
+ t.Errorf("input does not equal output: %v, %v", v23Adv, out)
+ }
+}
diff --git a/lib/discovery/plugins/ble/neighborhood.go b/lib/discovery/plugins/ble/neighborhood.go
new file mode 100644
index 0000000..c909855
--- /dev/null
+++ b/lib/discovery/plugins/ble/neighborhood.go
@@ -0,0 +1,472 @@
+// 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.handleChange(uuid.Parse(id), adv, nil)
+ }
+ }
+ }
+ }
+ 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(uuid uuid.UUID) (chan *discovery.Advertisement, int64) {
+ ch := make(chan *discovery.Advertisement)
+ s := &scanner{
+ uuid: uuid,
+ ch: ch,
+ }
+ b.mu.Lock()
+ id := b.nextScanId
+ b.nextScanId++
+ b.scannersById[id] = s
+ key := uuid.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())
+ // TODO(bjornick): Don't scan unless there is a scanner running.
+ 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
+ }
+ 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, oldAdv := range oldAdvs {
+ newValue := services[id]
+ if !reflect.DeepEqual(oldAdv, newValue) {
+ uid := uuid.Parse(id)
+ for _, s := range b.scannersByService[id] {
+ s.handleChange(uid, oldAdv, newValue)
+ }
+ }
+ }
+
+ for id, newAdv := range newEntry.advertisements {
+ if _, ok := oldAdvs[id]; !ok {
+ uid := uuid.Parse(id)
+ for _, s := range b.scannersByService[id] {
+ s.handleChange(uid, nil, newAdv)
+ }
+ }
+ }
+}
+
+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.AppendManufacturerData(manufacturerId, hasher.Sum(nil))
+ adv.AppendName(b.name)
+ return adv
+}
diff --git a/lib/discovery/plugins/ble/plugin.go b/lib/discovery/plugins/ble/plugin.go
new file mode 100644
index 0000000..6c7d36b
--- /dev/null
+++ b/lib/discovery/plugins/ble/plugin.go
@@ -0,0 +1,60 @@
+// 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.
+
+// For now, this plugin only works on Linux machines.
+// TODO(bjornick): Make this work on Mac and Android.
+
+package ble
+
+import (
+ "v.io/v23/context"
+ "v.io/x/ref/lib/discovery"
+
+ "github.com/pborman/uuid"
+)
+
+type blePlugin struct {
+ b *bleNeighborhood
+ trigger *discovery.Trigger
+}
+
+func (b *blePlugin) Advertise(ctx *context.T, ad *discovery.Advertisement) error {
+ b.b.addAdvertisement(newAdvertisment(*ad))
+ b.trigger.Add(func() {
+ b.b.removeService(ad.InstanceUuid)
+ }, ctx.Done())
+ return nil
+}
+
+func (b *blePlugin) Scan(ctx *context.T, serviceUuid uuid.UUID, scan chan<- *discovery.Advertisement) error {
+ ch, id := b.b.addScanner(serviceUuid)
+ drain := func() {
+ for range ch {
+ }
+ }
+ go func() {
+ defer func() {
+ b.b.removeScanner(id)
+ go drain()
+ }()
+ L:
+ for {
+ select {
+ case <-ctx.Done():
+ break L
+ case a := <-ch:
+ scan <- a
+ }
+ }
+ }()
+ return nil
+}
+
+func NewPlugin(name string) (discovery.Plugin, error) {
+ b, err := newBleNeighborhood(name)
+ if err != nil {
+ return nil, err
+ }
+ return &blePlugin{b: b, trigger: discovery.NewTrigger()}, nil
+}
diff --git a/lib/discovery/plugins/ble/scanner.go b/lib/discovery/plugins/ble/scanner.go
new file mode 100644
index 0000000..9b379a1
--- /dev/null
+++ b/lib/discovery/plugins/ble/scanner.go
@@ -0,0 +1,53 @@
+// 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
+
+import (
+ "log"
+ "sync"
+
+ "v.io/x/ref/lib/discovery"
+
+ "github.com/pborman/uuid"
+)
+
+type scanner struct {
+ mu sync.Mutex
+ uuid uuid.UUID
+ ch chan *discovery.Advertisement
+ done bool
+}
+
+func (s *scanner) handleChange(id uuid.UUID, oldAdv *bleAdv, newAdv *bleAdv) error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if s.done {
+ return nil
+ }
+ if oldAdv != nil {
+ a, err := oldAdv.toDiscoveryAdvertisement()
+ if err != nil {
+ log.Println("failed to convert advertisement:", err)
+ }
+ a.Lost = true
+ s.ch <- a
+ }
+
+ if newAdv != nil {
+ a, err := newAdv.toDiscoveryAdvertisement()
+ if err != nil {
+ return err
+ }
+ s.ch <- a
+ }
+ return nil
+}
+
+func (s *scanner) stop() {
+ s.mu.Lock()
+ s.done = true
+ close(s.ch)
+ s.mu.Unlock()
+}
diff --git a/lib/discovery/plugins/ble/services_darwin.go b/lib/discovery/plugins/ble/services_darwin.go
new file mode 100644
index 0000000..98a2031
--- /dev/null
+++ b/lib/discovery/plugins/ble/services_darwin.go
@@ -0,0 +1,15 @@
+// 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
+
+import "github.com/paypal/gatt"
+
+var gattOptions = []gatt.Option{
+ gatt.MacDeviceRole(gatt.CentralManager),
+}
+
+func addDefaultServices(string) map[string]*gatt.Service {
+ return map[string]*gatt.Service{}
+}
diff --git a/lib/discovery/plugins/ble/services_linux.go b/lib/discovery/plugins/ble/services_linux.go
new file mode 100644
index 0000000..f6987bd
--- /dev/null
+++ b/lib/discovery/plugins/ble/services_linux.go
@@ -0,0 +1,72 @@
+// 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
+
+import (
+ "github.com/paypal/gatt"
+ "github.com/paypal/gatt/linux/cmd"
+)
+
+var (
+ // These constants are taken from:
+ // https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx
+ attrDeviceNameUUID = gatt.UUID16(0x2A00)
+ attrAppearanceUUID = gatt.UUID16(0x2A01)
+ attrPeripheralPrivacyUUID = gatt.UUID16(0x2A02)
+ attrReconnectionAddrUUID = gatt.UUID16(0x2A03)
+ attrPreferredParamsUUID = gatt.UUID16(0x2A04)
+
+ attrServiceChangedUUID = gatt.UUID16(0x2A05)
+
+ // https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml
+ 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)
+ // Disable peripheral privacy
+ s.AddCharacteristic(attrPeripheralPrivacyUUID).SetValue([]byte{0x00})
+
+ // Make up some value for a required field:
+ // https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx
+ s.AddCharacteristic(attrReconnectionAddrUUID).SetValue([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
+ // Preferred params are:
+ // min connection interval: 0x0006 * 1.25ms (7.5 ms)
+ // max connection interval: 0x0006 * 1.25ms (7.5 ms)
+ // slave latency: 0
+ // connection supervision timeout multiplier: 0x07d0 (2000)
+ s.AddCharacteristic(attrPreferredParamsUUID).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,
+ }),
+}
+
+func addDefaultServices(name string) map[string]*gatt.Service {
+ s1 := newGapService(name)
+ s2 := newGattService()
+ return map[string]*gatt.Service{
+ s1.UUID().String(): s1,
+ s2.UUID().String(): s2,
+ }
+}
diff --git a/lib/discovery/uuid.go b/lib/discovery/uuid.go
index 221ef5a..e18457b 100644
--- a/lib/discovery/uuid.go
+++ b/lib/discovery/uuid.go
@@ -12,6 +12,9 @@
// UUID of Vanadium namespace.
// Generated from UUID5("00000000-0000-0000-0000-000000000000", "v.io").
v23UUID uuid.UUID = uuid.UUID{0x3d, 0xd1, 0xd5, 0xa8, 0x2e, 0xef, 0x58, 0x16, 0xa7, 0x20, 0xf8, 0x8b, 0x9b, 0xcf, 0x6e, 0xe4}
+
+ // Generated from UUID5("00000000-0000-0000-0000-000000000000", "v.io/attrs").
+ v23AttrUUID uuid.UUID = uuid.UUID{0x94, 0x2b, 0x61, 0x64, 0x12, 0x79, 0x5e, 0xb6, 0xb6, 0x43, 0xc9, 0x0c, 0x4c, 0xcc, 0x8a, 0x72}
)
// NewServiceUUID returns a version 5 UUID for the given interface name.
@@ -24,3 +27,8 @@
func NewInstanceUUID() uuid.UUID {
return uuid.NewRandom()
}
+
+// NewAttributeUUID returns a version 5 UUID for the given key.
+func NewAttributeUUID(key string) uuid.UUID {
+ return uuid.NewSHA1(v23AttrUUID, []byte(key))
+}
diff --git a/runtime/internal/naming/namespace/mount.go b/runtime/internal/naming/namespace/mount.go
index c431560..f181b08 100644
--- a/runtime/internal/naming/namespace/mount.go
+++ b/runtime/internal/naming/namespace/mount.go
@@ -44,7 +44,7 @@
}
}
}
-
+
me, err := ns.ResolveToMountTable(ctx, name, opts...)
if err == nil {
copts := append(getCallOpts(opts), options.Preresolved{me})