| package mounttablelib |
| |
| import ( |
| "strings" |
| "sync" |
| "time" |
| |
| "v.io/v23/context" |
| |
| "github.com/paypal/gatt" |
| "github.com/paypal/gatt/linux/cmd" |
| ) |
| |
| var mounttableServiceUUID = gatt.MustParseUUID("f7d47ad1-c344-4ad1-b43a-5be7570d3ffd") |
| var addressesUUID = gatt.MustParseUUID("32719e70-5da5-4d40-9a4a-866f491b2f2d") |
| |
| func newMountTableService(addresses []string) *gatt.Service { |
| addressString := strings.Join(addresses, "@@@@") |
| s := gatt.NewService(mounttableServiceUUID) |
| s.AddCharacteristic(addressesUUID).HandleReadFunc( |
| func(rsp gatt.ResponseWriter, req *gatt.ReadRequest) { |
| if req.Offset > len(addressString) { |
| return |
| } |
| end := req.Offset + req.Cap |
| if end > len(addressString) { |
| end = len(addressString) |
| } |
| rsp.Write([]byte(addressString[req.Offset:end])) |
| }) |
| 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) |
| ) |
| |
| // 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 gattServerOptions = []gatt.Option{ |
| gatt.LnxMaxConnections(100), |
| 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 bleCacheEntry struct { |
| id string |
| name string |
| endpoints []string |
| lastSeen time.Time |
| } |
| type bleNeighborHood struct { |
| mu sync.Mutex |
| neighborsIdCache map[string]*bleCacheEntry |
| neighborsNameCache map[string]*bleCacheEntry |
| name string |
| ctx *context.T |
| serverDevice gatt.Device |
| clientDevice gatt.Device |
| } |
| |
| func newBleNeighborhood(ctx *context.T, name string, eps []string) (*bleNeighborHood, error) { |
| b := &bleNeighborHood{ |
| neighborsIdCache: make(map[string]*bleCacheEntry), |
| neighborsNameCache: make(map[string]*bleCacheEntry), |
| name: name, |
| ctx: ctx, |
| } |
| return b, b.startBLEService(eps) |
| } |
| |
| func (b *bleNeighborHood) startBLEService(eps []string) error { |
| d, err := gatt.NewDevice(gattServerOptions...) |
| if err != nil { |
| return err |
| } |
| d.Handle( |
| gatt.CentralConnected(func(c gatt.Central) { b.ctx.VI(0).Infof("Connect: %v", c.ID()) }), |
| gatt.CentralDisconnected(func(c gatt.Central) { b.ctx.VI(0).Infof("Disconnected: %v", c.ID()) }), |
| ) |
| |
| onStateChanged := func(d gatt.Device, s gatt.State) { |
| b.ctx.VI(0).Infof("State: %s", s) |
| switch s { |
| case gatt.StatePoweredOn: |
| d.AddService(newGapService(b.name)) |
| d.AddService(newGattService()) |
| |
| s1 := newMountTableService(eps) |
| d.AddService(s1) |
| d.AdvertiseNameAndServices(b.name, []gatt.UUID{s1.UUID()}) |
| default: |
| } |
| } |
| |
| d.Init(onStateChanged) |
| b.serverDevice = d |
| return nil |
| } |
| |
| var gattClientOptions = []gatt.Option{ |
| gatt.LnxMaxConnections(1), |
| gatt.LnxDeviceID(-1, true), |
| } |
| |
| func (b *bleNeighborHood) scanForNeighbors() { |
| onStateChanged := func(d gatt.Device, s gatt.State) { |
| switch s { |
| case gatt.StatePoweredOn: |
| // We should limit it to only mounttable service, but this |
| // isn't implemented in the library |
| d.Scan([]gatt.UUID{}, false) |
| default: |
| d.StopScanning() |
| } |
| } |
| |
| onPeriphDiscovered := func(p gatt.Peripheral, a *gatt.Advertisement, rssi int) { |
| // TODO(bjornick): Handle error |
| p.Device().Connect(p) |
| } |
| |
| onPeriphConnected := func(p gatt.Peripheral, err error) { |
| defer p.Device().CancelConnection(p) |
| if err := p.SetMTU(500); err != nil { |
| b.ctx.Errorf("Failed to set MTU, err: %s", err) |
| return |
| } |
| |
| ss, err := p.DiscoverServices(nil) |
| |
| if err != nil { |
| b.ctx.Errorf("Failed to discover services, err: %s", err) |
| return |
| } |
| |
| for _, s := range ss { |
| if !s.UUID().Equal(mounttableServiceUUID) { |
| b.ctx.Infof("Skipping non-mt service %s", s.Name()) |
| continue |
| } |
| |
| cs, err := p.DiscoverCharacteristics(nil, s) |
| if err != nil { |
| b.ctx.Errorf("Failed to discover characteristics: %s", err) |
| continue |
| } |
| |
| for _, c := range cs { |
| if !c.UUID().Equal(addressesUUID) { |
| b.ctx.Infof("Skipping non-address characteristic: %s", c.Name()) |
| continue |
| } |
| eps, err := p.ReadCharacteristicBlob(c) |
| if err != nil { |
| b.ctx.Errorf("Failed to read the addresses: %v", err) |
| continue |
| } |
| b.saveAddress(p.ID(), p.Name(), string(eps)) |
| } |
| |
| } |
| } |
| var err error |
| b.clientDevice, err = gatt.NewDevice(gattClientOptions...) |
| if err != nil { |
| b.ctx.Errorf("Failed to open device, err: %s\n", err) |
| return |
| } |
| b.clientDevice.Handle( |
| gatt.PeripheralDiscovered(onPeriphDiscovered), |
| gatt.PeripheralConnected(onPeriphConnected), |
| ) |
| |
| b.clientDevice.Init(onStateChanged) |
| } |
| |
| func (b *bleNeighborHood) saveAddress(id string, name string, eps string) { |
| b.mu.Lock() |
| defer b.mu.Unlock() |
| b.ctx.VI(0).Infof("Saving endpoints (%s) for (%s, %s)", eps, id, name) |
| entry, found := b.neighborsIdCache[id] |
| if !found { |
| entry = &bleCacheEntry{ |
| id: id, |
| name: name, |
| } |
| b.neighborsIdCache[id] = entry |
| b.neighborsNameCache[name] = entry |
| } |
| |
| if entry.name != name { |
| b.ctx.Errorf("Name of the neighbor changed. was %s, now %s", entry.name, name) |
| return |
| } |
| entry.endpoints = strings.Split(eps, "@@@") |
| entry.lastSeen = time.Now() |
| } |