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:
Ayke van Laethem 2020-06-15 18:11:53 +02:00
parent 15b3e8e3e2
commit 602e656a6b
No known key found for this signature in database
GPG Key ID: E97FF5335DFDFDED
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
}

@ -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
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:
}
// 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 {
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
}
// 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)
}
return nil
// 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

@ -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

@ -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=