blob: 478043dbd4efb532c8917a63d1876f0fe7794353 [file] [log] [blame]
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()
}