diff --git a/adapter_linux.go b/adapter_linux.go index 37992e2..e5136c3 100644 --- a/adapter_linux.go +++ b/adapter_linux.go @@ -13,7 +13,7 @@ import ( type Adapter struct { adapter *adapter.Adapter1 id string - cancelScan func() + cancelChan chan struct{} defaultAdvertisement *Advertisement } diff --git a/gap_linux.go b/gap_linux.go index 0c6a7ec..0edb341 100644 --- a/gap_linux.go +++ b/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 + 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)) - } - }() -} diff --git a/go.mod b/go.mod index c90bc8a..cbd04ec 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index cd3aca7..018679d 100644 --- a/go.sum +++ b/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=