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
@ -13,7 +13,7 @@ import (
|
|||||||
type Adapter struct {
|
type Adapter struct {
|
||||||
adapter *adapter.Adapter1
|
adapter *adapter.Adapter1
|
||||||
id string
|
id string
|
||||||
cancelScan func()
|
cancelChan chan struct{}
|
||||||
defaultAdvertisement *Advertisement
|
defaultAdvertisement *Advertisement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
180
gap_linux.go
180
gap_linux.go
@ -3,8 +3,8 @@
|
|||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
"github.com/muka/go-bluetooth/api"
|
"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/advertising"
|
||||||
"github.com/muka/go-bluetooth/bluez/profile/device"
|
"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
|
// possible some events are missed and perhaps even possible that some events
|
||||||
// are duplicated.
|
// are duplicated.
|
||||||
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
|
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
|
||||||
if a.cancelScan != nil {
|
if a.cancelChan != nil {
|
||||||
return errScanning
|
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.
|
// This appears to be necessary to receive any BLE discovery results at all.
|
||||||
defer a.adapter.SetDiscoveryFilter(nil)
|
defer a.adapter.SetDiscoveryFilter(nil)
|
||||||
err := a.adapter.SetDiscoveryFilter(map[string]interface{}{
|
err := a.adapter.SetDiscoveryFilter(map[string]interface{}{
|
||||||
@ -81,122 +87,142 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
|
|||||||
return err
|
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.
|
// Instruct BlueZ to start discovering.
|
||||||
err = a.adapter.StartDiscovery()
|
err = a.adapter.StartDiscovery()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for newly found devices.
|
for {
|
||||||
discoveryChan, cancelChan, err := a.adapter.OnDeviceDiscovered()
|
// Check whether the scan is stopped. This is necessary to avoid a race
|
||||||
if err != nil {
|
// condition between the signal channel and the cancelScan channel when
|
||||||
return err
|
// the callback calls StopScan() (no new callbacks may be called after
|
||||||
}
|
// StopScan is called).
|
||||||
a.cancelScan = cancelChan
|
select {
|
||||||
|
case <-cancelChan:
|
||||||
// Obtain a list of cached devices to watch.
|
a.adapter.StopDiscovery()
|
||||||
// BlueZ won't show advertisement data as it is discovered. Instead, it
|
return nil
|
||||||
// caches all the data and only produces events for changes. Worse: it
|
default:
|
||||||
// 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.
|
select {
|
||||||
dev, err := device.NewDevice1(result.Path)
|
case sig := <-signal:
|
||||||
if err != nil || dev == nil {
|
// 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
|
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
|
// 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
|
// callback to stop the current scan. If no scan is in progress, an error will
|
||||||
// be returned.
|
// be returned.
|
||||||
func (a *Adapter) StopScan() error {
|
func (a *Adapter) StopScan() error {
|
||||||
if a.cancelScan == nil {
|
if a.cancelChan == nil {
|
||||||
return errNotScanning
|
return errNotScanning
|
||||||
}
|
}
|
||||||
a.adapter.StopDiscovery()
|
close(a.cancelChan)
|
||||||
cancel := a.cancelScan
|
a.cancelChan = nil
|
||||||
a.cancelScan = nil
|
|
||||||
cancel()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeScanResult creates a ScanResult from a Device1 object.
|
// 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.
|
// Assume the Address property is well-formed.
|
||||||
addr, _ := ParseMAC(dev.Properties.Address)
|
addr, _ := ParseMAC(props.Address)
|
||||||
|
|
||||||
// Create a list of UUIDs.
|
// Create a list of UUIDs.
|
||||||
var serviceUUIDs []UUID
|
var serviceUUIDs []UUID
|
||||||
for _, uuid := range dev.Properties.UUIDs {
|
for _, uuid := range props.UUIDs {
|
||||||
// Assume the UUID is well-formed.
|
// Assume the UUID is well-formed.
|
||||||
parsedUUID, _ := ParseUUID(uuid)
|
parsedUUID, _ := ParseUUID(uuid)
|
||||||
serviceUUIDs = append(serviceUUIDs, parsedUUID)
|
serviceUUIDs = append(serviceUUIDs, parsedUUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ScanResult{
|
return ScanResult{
|
||||||
RSSI: dev.Properties.RSSI,
|
RSSI: props.RSSI,
|
||||||
Address: Address{
|
Address: Address{
|
||||||
MAC: addr,
|
MAC: addr,
|
||||||
IsRandom: dev.Properties.AddressType == "random",
|
IsRandom: props.AddressType == "random",
|
||||||
},
|
},
|
||||||
AdvertisementPayload: &advertisementFields{
|
AdvertisementPayload: &advertisementFields{
|
||||||
AdvertisementFields{
|
AdvertisementFields{
|
||||||
LocalName: dev.Properties.Name,
|
LocalName: props.Name,
|
||||||
ServiceUUIDs: serviceUUIDs,
|
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 (
|
require (
|
||||||
github.com/go-ole/go-ole v1.2.4
|
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
|
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/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 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
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/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
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.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/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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
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/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-20200619025933-f6113f7141c5 h1:xnTS/7y0g28W2SJeWNLMYTiTOmfW2P/YdPByoQnPvVo=
|
||||||
github.com/muka/go-bluetooth v0.0.0-20200518110738-ed2c87e2f9fa/go.mod h1:9Y4iuJfFe4N3afRt1qKpHU7vKUqyaWm/wJk2QEk6hgM=
|
github.com/muka/go-bluetooth v0.0.0-20200619025933-f6113f7141c5/go.mod h1:yV39+EVOWdnoTe75NyKdo9iuyI3Slyh4t7eQvElUbWE=
|
||||||
github.com/muka/go-bluetooth v0.0.0-20200601103727-d7408229e514 h1:0pId7zm3QkmG0qPQnNZZS2RN4Y8ll8BWzGo3CfpcXMk=
|
github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk=
|
||||||
github.com/muka/go-bluetooth v0.0.0-20200601103727-d7408229e514/go.mod h1:9Y4iuJfFe4N3afRt1qKpHU7vKUqyaWm/wJk2QEk6hgM=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
|
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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
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-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 h1:g4KENRiCMEx58Q7/ecwfT0N2o8z35Fnbsjig/Alf2T4=
|
||||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
Loading…
Reference in New Issue
Block a user