linux: improve scanning
By using the D-Bus APIs directly, I managed to avoid a deadlock that I somehow couldn't work around with the go-bluetooth package.
This commit is contained in:
parent
15b3e8e3e2
commit
602e656a6b
4 changed files with 113 additions and 86 deletions
|
@ -13,7 +13,7 @@ import (
|
|||
type Adapter struct {
|
||||
adapter *adapter.Adapter1
|
||||
id string
|
||||
cancelScan func()
|
||||
cancelChan chan struct{}
|
||||
defaultAdvertisement *Advertisement
|
||||
}
|
||||
|
||||
|
|
190
gap_linux.go
190
gap_linux.go
|
@ -3,8 +3,8 @@
|
|||
package bluetooth
|
||||
|
||||
import (
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/muka/go-bluetooth/api"
|
||||
"github.com/muka/go-bluetooth/bluez/profile/adapter"
|
||||
"github.com/muka/go-bluetooth/bluez/profile/advertising"
|
||||
"github.com/muka/go-bluetooth/bluez/profile/device"
|
||||
)
|
||||
|
@ -68,10 +68,16 @@ func (a *Advertisement) Start() error {
|
|||
// possible some events are missed and perhaps even possible that some events
|
||||
// are duplicated.
|
||||
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
|
||||
if a.cancelScan != nil {
|
||||
if a.cancelChan != nil {
|
||||
return errScanning
|
||||
}
|
||||
|
||||
// Channel that will be closed when the scan is stopped.
|
||||
// Detecting whether the scan is stopped can be done by doing a non-blocking
|
||||
// read from it. If it succeeds, the scan is stopped.
|
||||
cancelChan := make(chan struct{})
|
||||
a.cancelChan = cancelChan
|
||||
|
||||
// This appears to be necessary to receive any BLE discovery results at all.
|
||||
defer a.adapter.SetDiscoveryFilter(nil)
|
||||
err := a.adapter.SetDiscoveryFilter(map[string]interface{}{
|
||||
|
@ -81,122 +87,142 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
|
|||
return err
|
||||
}
|
||||
|
||||
bus, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signal := make(chan *dbus.Signal)
|
||||
bus.Signal(signal)
|
||||
defer bus.RemoveSignal(signal)
|
||||
|
||||
propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")}
|
||||
bus.AddMatchSignal(propertiesChangedMatchOptions...)
|
||||
defer bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
|
||||
|
||||
newObjectMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager")}
|
||||
bus.AddMatchSignal(newObjectMatchOptions...)
|
||||
defer bus.RemoveMatchSignal(newObjectMatchOptions...)
|
||||
|
||||
// Go through all connected devices and present the connected devices as
|
||||
// scan results. Also save the properties so that the full list of
|
||||
// properties is known on a PropertiesChanged signal. We can't present the
|
||||
// list of cached devices as scan results as devices may be cached for a
|
||||
// long time, long after they have moved out of range.
|
||||
deviceList, err := a.adapter.GetDevices()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
devices := make(map[dbus.ObjectPath]*device.Device1Properties)
|
||||
for _, dev := range deviceList {
|
||||
if dev.Properties.Connected {
|
||||
callback(a, makeScanResult(dev.Properties))
|
||||
select {
|
||||
case <-cancelChan:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
}
|
||||
devices[dev.Path()] = dev.Properties
|
||||
}
|
||||
|
||||
// Instruct BlueZ to start discovering.
|
||||
err = a.adapter.StartDiscovery()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Listen for newly found devices.
|
||||
discoveryChan, cancelChan, err := a.adapter.OnDeviceDiscovered()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.cancelScan = cancelChan
|
||||
|
||||
// Obtain a list of cached devices to watch.
|
||||
// BlueZ won't show advertisement data as it is discovered. Instead, it
|
||||
// caches all the data and only produces events for changes. Worse: it
|
||||
// doesn't seem to remove cached devices for a long time (3 minutes?) so
|
||||
// simply reading the list of cached devices won't tell you what devices are
|
||||
// actually around right now.
|
||||
// Luckily, there is a workaround. When any value changes, you can be sure a
|
||||
// new advertisement packet has been received. The RSSI value changes almost
|
||||
// every time it seems so just watching property changes is enough to get a
|
||||
// near-accurate view of the current state of the world around the listening
|
||||
// device.
|
||||
devices, err := a.adapter.GetDevices()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, dev := range devices {
|
||||
a.startWatchingDevice(dev, callback)
|
||||
}
|
||||
|
||||
// Iterate through new devices as they become visible.
|
||||
for result := range discoveryChan {
|
||||
if result.Type != adapter.DeviceAdded {
|
||||
continue
|
||||
}
|
||||
|
||||
// We only got a DBus object path, so turn that into a Device1 object.
|
||||
dev, err := device.NewDevice1(result.Path)
|
||||
if err != nil || dev == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Signal to the API client that a new device has been found.
|
||||
callback(a, makeScanResult(dev))
|
||||
|
||||
// Start watching this new device for when there are property changes.
|
||||
a.startWatchingDevice(dev, callback)
|
||||
}
|
||||
|
||||
for {
|
||||
// Check whether the scan is stopped. This is necessary to avoid a race
|
||||
// condition between the signal channel and the cancelScan channel when
|
||||
// the callback calls StopScan() (no new callbacks may be called after
|
||||
// StopScan is called).
|
||||
select {
|
||||
case <-cancelChan:
|
||||
a.adapter.StopDiscovery()
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
select {
|
||||
case sig := <-signal:
|
||||
// This channel receives anything that we watch for, so we'll have
|
||||
// to check for signals that are relevant to us.
|
||||
switch sig.Name {
|
||||
case "org.freedesktop.DBus.ObjectManager.InterfacesAdded":
|
||||
objectPath := sig.Body[0].(dbus.ObjectPath)
|
||||
interfaces := sig.Body[1].(map[string]map[string]dbus.Variant)
|
||||
rawprops, ok := interfaces["org.bluez.Device1"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var props *device.Device1Properties
|
||||
props, _ = props.FromDBusMap(rawprops)
|
||||
devices[objectPath] = props
|
||||
callback(a, makeScanResult(props))
|
||||
case "org.freedesktop.DBus.Properties.PropertiesChanged":
|
||||
interfaceName := sig.Body[0].(string)
|
||||
if interfaceName != "org.bluez.Device1" {
|
||||
continue
|
||||
}
|
||||
changes := sig.Body[1].(map[string]dbus.Variant)
|
||||
props := devices[sig.Path]
|
||||
for field, val := range changes {
|
||||
switch field {
|
||||
case "RSSI":
|
||||
props.RSSI = val.Value().(int16)
|
||||
case "Name":
|
||||
props.Name = val.Value().(string)
|
||||
case "UUIDs":
|
||||
props.UUIDs = val.Value().([]string)
|
||||
}
|
||||
}
|
||||
callback(a, makeScanResult(props))
|
||||
}
|
||||
case <-cancelChan:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// unreachable
|
||||
}
|
||||
|
||||
// StopScan stops any in-progress scan. It can be called from within a Scan
|
||||
// callback to stop the current scan. If no scan is in progress, an error will
|
||||
// be returned.
|
||||
func (a *Adapter) StopScan() error {
|
||||
if a.cancelScan == nil {
|
||||
if a.cancelChan == nil {
|
||||
return errNotScanning
|
||||
}
|
||||
a.adapter.StopDiscovery()
|
||||
cancel := a.cancelScan
|
||||
a.cancelScan = nil
|
||||
cancel()
|
||||
close(a.cancelChan)
|
||||
a.cancelChan = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeScanResult creates a ScanResult from a Device1 object.
|
||||
func makeScanResult(dev *device.Device1) ScanResult {
|
||||
func makeScanResult(props *device.Device1Properties) ScanResult {
|
||||
// Assume the Address property is well-formed.
|
||||
addr, _ := ParseMAC(dev.Properties.Address)
|
||||
addr, _ := ParseMAC(props.Address)
|
||||
|
||||
// Create a list of UUIDs.
|
||||
var serviceUUIDs []UUID
|
||||
for _, uuid := range dev.Properties.UUIDs {
|
||||
for _, uuid := range props.UUIDs {
|
||||
// Assume the UUID is well-formed.
|
||||
parsedUUID, _ := ParseUUID(uuid)
|
||||
serviceUUIDs = append(serviceUUIDs, parsedUUID)
|
||||
}
|
||||
|
||||
return ScanResult{
|
||||
RSSI: dev.Properties.RSSI,
|
||||
RSSI: props.RSSI,
|
||||
Address: Address{
|
||||
MAC: addr,
|
||||
IsRandom: dev.Properties.AddressType == "random",
|
||||
IsRandom: props.AddressType == "random",
|
||||
},
|
||||
AdvertisementPayload: &advertisementFields{
|
||||
AdvertisementFields{
|
||||
LocalName: dev.Properties.Name,
|
||||
LocalName: props.Name,
|
||||
ServiceUUIDs: serviceUUIDs,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// startWatchingDevice starts watching for property changes in the device.
|
||||
// Errors are ignored (for example, if watching the device failed).
|
||||
// The dev object will be owned by the function and will be modified as
|
||||
// properties change.
|
||||
func (a *Adapter) startWatchingDevice(dev *device.Device1, callback func(*Adapter, ScanResult)) {
|
||||
ch, err := dev.WatchProperties()
|
||||
if err != nil {
|
||||
// Assume the device has disappeared or something.
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for change := range ch {
|
||||
// Update the device with the changed property.
|
||||
props, _ := dev.Properties.ToMap()
|
||||
props[change.Name] = change.Value
|
||||
dev.Properties, _ = dev.Properties.FromMap(props)
|
||||
|
||||
// Signal to the API client that a property changed, as if this was
|
||||
// an incoming BLE advertisement packet.
|
||||
callback(a, makeScanResult(dev))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
3
go.mod
3
go.mod
|
@ -4,6 +4,7 @@ go 1.14
|
|||
|
||||
require (
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/muka/go-bluetooth v0.0.0-20200601103727-d7408229e514
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/muka/go-bluetooth v0.0.0-20200619025933-f6113f7141c5
|
||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed
|
||||
)
|
||||
|
|
14
go.sum
14
go.sum
|
@ -4,17 +4,17 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
|||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/muka/go-bluetooth v0.0.0-20200518110738-ed2c87e2f9fa h1:umshakNHYKRzZ7nQElCl/ceO1UVxW36H0uMw5kej0OU=
|
||||
github.com/muka/go-bluetooth v0.0.0-20200518110738-ed2c87e2f9fa/go.mod h1:9Y4iuJfFe4N3afRt1qKpHU7vKUqyaWm/wJk2QEk6hgM=
|
||||
github.com/muka/go-bluetooth v0.0.0-20200601103727-d7408229e514 h1:0pId7zm3QkmG0qPQnNZZS2RN4Y8ll8BWzGo3CfpcXMk=
|
||||
github.com/muka/go-bluetooth v0.0.0-20200601103727-d7408229e514/go.mod h1:9Y4iuJfFe4N3afRt1qKpHU7vKUqyaWm/wJk2QEk6hgM=
|
||||
github.com/muka/go-bluetooth v0.0.0-20200619025933-f6113f7141c5 h1:xnTS/7y0g28W2SJeWNLMYTiTOmfW2P/YdPByoQnPvVo=
|
||||
github.com/muka/go-bluetooth v0.0.0-20200619025933-f6113f7141c5/go.mod h1:yV39+EVOWdnoTe75NyKdo9iuyI3Slyh4t7eQvElUbWE=
|
||||
github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
|
||||
|
@ -22,7 +22,7 @@ github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s
|
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/suapapa/go_eddystone v0.0.0-20190827074641-8d8c1bb79363/go.mod h1:O/oFfbntg0b1z5NM/IGoTMKYPO3lkzPSA53E+J99lDU=
|
||||
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed h1:g4KENRiCMEx58Q7/ecwfT0N2o8z35Fnbsjig/Alf2T4=
|
||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
|
Loading…
Reference in a new issue