From d2c99ce14be7eddcb927554bdeb15146e62942a9 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 20 Feb 2025 12:45:55 +0100 Subject: [PATCH 1/5] linux: handle adaptor power state to return an error if the adaptor is powered off while scanning Signed-off-by: deadprogram --- gap_linux.go | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/gap_linux.go b/gap_linux.go index a03b5e1..06788ab 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 @@ -231,20 +232,34 @@ 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) + for k, v := range changes { + if k == "Powered" && !v.Value().(bool) { + // adapter is powered off, stop the scan + close(cancelChan) + return errAdaptorNotPowered + } + } + + 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 From d7d29da5be97f37a26c2ac64165fc5c415025118 Mon Sep 17 00:00:00 2001 From: Lenart Kos Date: Tue, 25 Feb 2025 16:38:47 +0100 Subject: [PATCH 2/5] linux: handle adaptor power state to return an error if the adaptor is powered off while connecting --- gap_linux.go | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/gap_linux.go b/gap_linux.go index 06788ab..6e6cfdf 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -390,20 +390,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 { From 905b208897bed2717da0fcd66b5bd5825fcdbfdd Mon Sep 17 00:00:00 2001 From: Lenart Kos Date: Tue, 25 Feb 2025 16:50:06 +0100 Subject: [PATCH 3/5] linux: close scan in progress channel on adapter power off --- gap_linux.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gap_linux.go b/gap_linux.go index 6e6cfdf..d97e9c6 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -240,6 +240,8 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { if k == "Powered" && !v.Value().(bool) { // adapter is powered off, stop the scan close(cancelChan) + close(a.scanCancelChan) + a.scanCancelChan = nil return errAdaptorNotPowered } } From 6a081beb4919534d5427c4c714c64cec7799527f Mon Sep 17 00:00:00 2001 From: Lenart Kos Date: Tue, 25 Feb 2025 18:49:35 +0100 Subject: [PATCH 4/5] linux: call startDiscovery async as it can block if adapter powered off --- gap.go | 1 + gap_linux.go | 66 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 42 insertions(+), 25 deletions(-) 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 d97e9c6..2a0ee55 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -142,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. @@ -150,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 @@ -200,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 @@ -217,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. @@ -236,14 +250,16 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { 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 - close(cancelChan) - close(a.scanCancelChan) - a.scanCancelChan = nil - return errAdaptorNotPowered - } + 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": From 542455003a392ed5b91e42780d787074d3545896 Mon Sep 17 00:00:00 2001 From: Lenart Kos Date: Tue, 25 Feb 2025 18:58:50 +0100 Subject: [PATCH 5/5] linux: check adapter powered state before connecting --- gap_linux.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gap_linux.go b/gap_linux.go index 2a0ee55..dcb774d 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -387,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 {