blob: e9eb10f439de91f1442f7a35acc5a714637e1e9c [file] [log] [blame]
// Copyright 2016 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 (
"sync"
"time"
"v.io/v23/context"
"v.io/v23/discovery"
idiscovery "v.io/x/ref/lib/discovery"
)
type scanner struct {
ctx *context.T
driver Driver
rescan chan struct{}
done chan struct{}
wg sync.WaitGroup
mu sync.Mutex
listeners map[string][]chan<- *idiscovery.AdInfo // GUARDED_BY(mu)
scanRecords map[discovery.AdId]*scanRecord // GUARDED_BY(mu)
// TTL for scanned advertisement. If we do not see the advertisement again
// during that period, we send a "Lost" notification.
ttl time.Duration
}
type scanRecord struct {
interfaceName string
expiry time.Time
}
func (s *scanner) addListener(interfaceName string) chan *idiscovery.AdInfo {
// TODO(jhahn): Revisit the buffer size. A channel with one buffer should be
// enough to avoid deadlock, but more buffers are added for smooth operation.
ch := make(chan *idiscovery.AdInfo, 10)
s.mu.Lock()
listeners := append(s.listeners[interfaceName], ch)
s.listeners[interfaceName] = listeners
s.mu.Unlock()
select {
case s.rescan <- struct{}{}:
default:
}
return ch
}
func (s *scanner) removeListener(interfaceName string, ch chan *idiscovery.AdInfo) {
go func() {
for range ch {
}
}()
s.mu.Lock()
listeners := s.listeners[interfaceName]
for i, listener := range listeners {
if listener == ch {
n := len(listeners) - 1
listeners[i], listeners[n] = listeners[n], nil
listeners = listeners[:n]
break
}
}
if len(listeners) > 0 {
s.listeners[interfaceName] = listeners
} else {
delete(s.listeners, interfaceName)
}
s.mu.Unlock()
select {
case s.rescan <- struct{}{}:
default:
}
close(ch)
}
func (s *scanner) shutdown() {
close(s.done)
s.wg.Wait()
}
func (s *scanner) scanLoop() {
defer s.wg.Done()
var gcWg sync.WaitGroup
defer gcWg.Wait()
isScanning := false
stopScan := func() {
if isScanning {
s.driver.StopScan()
}
isScanning = false
}
defer stopScan()
refreshInterval := s.ttl / 2
var refresh <-chan time.Time
for {
select {
case <-refresh:
case <-s.rescan:
case <-s.done:
return
}
stopScan()
s.mu.Lock()
if len(s.listeners) == 0 {
s.scanRecords = make(map[discovery.AdId]*scanRecord)
s.mu.Unlock()
refresh = nil
continue
}
uuids := make([]string, 0, len(s.listeners)*2)
if _, ok := s.listeners[""]; !ok {
for interfaceName, _ := range s.listeners {
uuid := newServiceUuid(interfaceName)
uuids = append(uuids, uuid.String())
toggleServiceUuid(uuid)
uuids = append(uuids, uuid.String())
}
}
s.mu.Unlock()
if err := s.driver.StartScan(uuids, vanadiumUuidBase, vanadiumUuidMask, s); err != nil {
s.ctx.Error(err)
} else {
isScanning = true
}
gcWg.Add(1)
go s.gc(&gcWg)
refresh = time.After(refreshInterval)
}
}
func (s *scanner) OnDiscovered(uuid string, characteristics map[string][]byte, rssi int) {
// TODO(jhahn): Add rssi to adinfo.
unpacked := unpackFromCharacteristics(characteristics)
for _, encoded := range unpacked {
adinfo, err := decodeAdInfo(encoded)
if err != nil {
s.ctx.Error(err)
continue
}
s.mu.Lock()
s.scanRecords[adinfo.Ad.Id] = &scanRecord{adinfo.Ad.InterfaceName, time.Now().Add(s.ttl)}
for _, ch := range append(s.listeners[adinfo.Ad.InterfaceName], s.listeners[""]...) {
select {
case ch <- adinfo:
case <-s.done:
s.mu.Unlock()
return
}
}
s.mu.Unlock()
}
}
func (s *scanner) gc(wg *sync.WaitGroup) {
defer wg.Done()
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for id, rec := range s.scanRecords {
if rec.expiry.After(now) {
continue
}
delete(s.scanRecords, id)
adinfo := &idiscovery.AdInfo{Ad: discovery.Advertisement{Id: id}, Lost: true}
for _, ch := range append(s.listeners[rec.interfaceName], s.listeners[""]...) {
select {
case ch <- adinfo:
case <-s.done:
return
}
}
}
}
func newScanner(ctx *context.T, driver Driver, ttl time.Duration) *scanner {
s := &scanner{
ctx: ctx,
driver: driver,
rescan: make(chan struct{}, 1),
done: make(chan struct{}),
listeners: make(map[string][]chan<- *idiscovery.AdInfo),
scanRecords: make(map[discovery.AdId]*scanRecord),
ttl: ttl,
}
s.wg.Add(1)
go s.scanLoop()
return s
}