diff --git a/gap.go b/gap.go index 5ccfb86..6bec575 100644 --- a/gap.go +++ b/gap.go @@ -8,6 +8,7 @@ import ( var ( errScanning = errors.New("bluetooth: a scan is already in progress") errNotScanning = errors.New("bluetooth: there is no scan in progress") + errScanStopped = errors.New("bluetooth: scan was stopped unexpectedly") errAdvertisementPacketTooBig = errors.New("bluetooth: advertisement packet overflows") errNotYetImplmented = errors.New("bluetooth: not implemented") ) diff --git a/gap_linux.go b/gap_linux.go index a03b5e1..dcb774d 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -14,6 +14,7 @@ import ( var errAdvertisementNotStarted = errors.New("bluetooth: advertisement is not started") var errAdvertisementAlreadyStarted = errors.New("bluetooth: advertisement is already started") +var errAdaptorNotPowered = errors.New("bluetooth: adaptor is not powered") // Unique ID per advertisement (to generate a unique object path). var advertisementID uint64 @@ -141,6 +142,27 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { return errScanning } + signal := make(chan *dbus.Signal) + a.bus.Signal(signal) + defer a.bus.RemoveSignal(signal) + + propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")} + a.bus.AddMatchSignal(propertiesChangedMatchOptions...) + defer a.bus.RemoveMatchSignal(propertiesChangedMatchOptions...) + + newObjectMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager")} + a.bus.AddMatchSignal(newObjectMatchOptions...) + defer a.bus.RemoveMatchSignal(newObjectMatchOptions...) + + // Check if the adapter is powered on. + powered, err := a.adapter.GetProperty("org.bluez.Adapter1.Powered") + if err != nil { + return err + } + if !powered.Value().(bool) { + return errAdaptorNotPowered + } + // 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. @@ -149,25 +171,13 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { // This appears to be necessary to receive any BLE discovery results at all. defer a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0) - err := a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0, map[string]interface{}{ + err = a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0, map[string]interface{}{ "Transport": "le", }).Err if err != nil { return err } - signal := make(chan *dbus.Signal) - a.bus.Signal(signal) - defer a.bus.RemoveSignal(signal) - - propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")} - a.bus.AddMatchSignal(propertiesChangedMatchOptions...) - defer a.bus.RemoveMatchSignal(propertiesChangedMatchOptions...) - - newObjectMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager")} - a.bus.AddMatchSignal(newObjectMatchOptions...) - defer a.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 @@ -199,10 +209,9 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { } // Instruct BlueZ to start discovering. - err = a.adapter.Call("org.bluez.Adapter1.StartDiscovery", 0).Err - if err != nil { - return err - } + // NOTE: We must call Go here, not Call, because it can block if adapter is + // powered off, or was recently powered off. + startDiscovery := a.adapter.Go("org.bluez.Adapter1.StartDiscovery", 0, nil) for { // Check whether the scan is stopped. This is necessary to avoid a race @@ -216,6 +225,12 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { } select { + case <-startDiscovery.Done: + if startDiscovery.Err != nil { + close(cancelChan) + a.scanCancelChan = nil + return startDiscovery.Err + } case sig := <-signal: // This channel receives anything that we watch for, so we'll have // to check for signals that are relevant to us. @@ -231,20 +246,38 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { callback(a, makeScanResult(rawprops)) 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) - device, ok := devices[sig.Path] - if !ok { - // This shouldn't happen, but protect against it just in - // case. + switch interfaceName { + case "org.bluez.Adapter1": + // check power state + changes := sig.Body[1].(map[string]dbus.Variant) + if powered, ok := changes["Powered"]; ok && !powered.Value().(bool) { + // adapter is powered off, stop the scan + close(cancelChan) + a.scanCancelChan = nil + return errAdaptorNotPowered + } else if discovering, ok := changes["Discovering"]; ok && !discovering.Value().(bool) { + // adapter stopped discovering unexpectedly (e.g. due to external event) + close(cancelChan) + a.scanCancelChan = nil + return errScanStopped + } + + case "org.bluez.Device1": + changes := sig.Body[1].(map[string]dbus.Variant) + device, ok := devices[sig.Path] + if !ok { + // This shouldn't happen, but protect against it just in + // case. + continue + } + for k, v := range changes { + device[k] = v + } + callback(a, makeScanResult(device)) + + default: continue } - for k, v := range changes { - device[k] = v - } - callback(a, makeScanResult(device)) } case <-cancelChan: continue @@ -354,6 +387,14 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err a.bus.AddMatchSignal(propertiesChangedMatchOptions...) defer a.bus.RemoveMatchSignal(propertiesChangedMatchOptions...) + powered, err := a.adapter.GetProperty("org.bluez.Adapter1.Powered") + if err != nil { + return Device{}, err + } + if !powered.Value().(bool) { + return Device{}, errAdaptorNotPowered + } + // Read whether this device is already connected. connected, err := device.device.GetProperty("org.bluez.Device1.Connected") if err != nil { @@ -375,20 +416,34 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err switch sig.Name { case "org.freedesktop.DBus.Properties.PropertiesChanged": interfaceName := sig.Body[0].(string) - if interfaceName != "org.bluez.Device1" { - continue - } - if sig.Path != device.device.Path() { - continue - } - changes := sig.Body[1].(map[string]dbus.Variant) - if connected, ok := changes["Connected"].Value().(bool); ok && connected { - close(connectChan) + switch interfaceName { + case "org.bluez.Adapter1": + // check power state + changes := sig.Body[1].(map[string]dbus.Variant) + for k, v := range changes { + if k == "Powered" && !v.Value().(bool) { + // adapter is powered off, stop the scan + err = errAdaptorNotPowered + close(connectChan) + } + } + case "org.bluez.Device1": + if sig.Path != device.device.Path() { + continue + } + changes := sig.Body[1].(map[string]dbus.Variant) + if connected, ok := changes["Connected"].Value().(bool); ok && connected { + close(connectChan) + } } } } }() <-connectChan + + if err != nil { + return Device{}, err + } } if a.connectHandler != nil {