diff --git a/adapter.go b/adapter.go index 20d36c3..c4990a4 100644 --- a/adapter.go +++ b/adapter.go @@ -1,5 +1,17 @@ package bluetooth +// AdapterState represents the state of the adaptor. +type AdapterState int + +const ( + // AdapterStatePoweredOff is the state of the adaptor when it is powered off. + AdapterStatePoweredOff = AdapterState(iota) + // AdapterStatePoweredOn is the state of the adaptor when it is powered on. + AdapterStatePoweredOn + // AdapterStateUnknown is the state of the adaptor when it is unknown. + AdapterStateUnknown +) + // BLEAdapter is the shared interface that all platform-specific Adapter types must implement. type BLEAdapter interface { Connect(address Address, params ConnectionParams) (Device, error) diff --git a/adapter_darwin.go b/adapter_darwin.go index 1b52b87..c4b35c1 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -26,7 +26,8 @@ type Adapter struct { // used to allow multiple callers to call Connect concurrently. connectMap sync.Map - connectHandler func(device Device, connected bool) + connectHandler func(device Device, connected bool) + stateChangeHandler func(newState AdapterState) } // DefaultAdapter is the default adapter on the system. @@ -37,9 +38,8 @@ var DefaultAdapter = &Adapter{ pm: cbgo.NewPeripheralManager(nil), connectMap: sync.Map{}, - connectHandler: func(device Device, connected bool) { - return - }, + connectHandler: func(device Device, connected bool) {}, + stateChangeHandler: func(newState AdapterState) {}, } // Enable configures the BLE stack. It must be called before any @@ -121,6 +121,24 @@ func (a *Adapter) Reset() error { return nil } +// SetStateChangeHandler sets a handler function to be called whenever the adaptor's +// state changes. +func (a *Adapter) SetStateChangeHandler(c func(newState AdapterState)) { + a.stateChangeHandler = c +} + +// State returns the current state of the adapter. +func (a *Adapter) State() AdapterState { + switch a.cm.State() { + case cbgo.ManagerStatePoweredOn: + return AdapterStatePoweredOn + case cbgo.ManagerStatePoweredOff: + return AdapterStatePoweredOff + default: + return AdapterStateUnknown + } +} + // CentralManager delegate functions type centralManagerDelegate struct { @@ -132,18 +150,25 @@ type centralManagerDelegate struct { // CentralManagerDidUpdateState when central manager state updated. func (cmd *centralManagerDelegate) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { var event error + var newState AdapterState switch cmgr.State() { case cbgo.ManagerStatePoweredOn: + newState = AdapterStatePoweredOn event = nil case cbgo.ManagerStatePoweredOff: + newState = AdapterStatePoweredOff event = errors.New("bluetooth is powered off") case cbgo.ManagerStateUnsupported: + newState = AdapterStatePoweredOff event = errors.New("bluetooth is not supported on this device") case cbgo.ManagerStateUnauthorized: + newState = AdapterStatePoweredOff event = errors.New("bluetooth is not authorized for this app") default: + cmd.a.stateChangeHandler(AdapterStateUnknown) return } + cmd.a.stateChangeHandler(newState) // Non-blocking send: poweredChan may be nil after Enable // completes, or already buffered. select { diff --git a/adapter_linux.go b/adapter_linux.go index a62d6d4..4d43a65 100644 --- a/adapter_linux.go +++ b/adapter_linux.go @@ -25,7 +25,8 @@ type Adapter struct { address string defaultAdvertisement *Advertisement - connectHandler func(device Device, connected bool) + connectHandler func(device Device, connected bool) + stateChangeHandler func(newState AdapterState) } // NewAdapter creates a new Adapter with the given ID. @@ -33,8 +34,9 @@ type Adapter struct { // Make sure to call Enable() before using it to initialize the adapter. func NewAdapter(id string) *Adapter { return &Adapter{ - id: id, - connectHandler: func(device Device, connected bool) {}, + id: id, + connectHandler: func(device Device, connected bool) {}, + stateChangeHandler: func(newState AdapterState) {}, } } @@ -63,6 +65,9 @@ func (a *Adapter) Enable() (err error) { } addr.Store(&a.address) + if err := a.watchForStateChange(); err != nil { + return err + } return nil } @@ -87,3 +92,75 @@ func (a *Adapter) Address() (MACAddress, error) { } return MACAddress{MAC: mac}, nil } + +// SetStateChangeHandler sets a handler function to be called whenever the adaptor's +// state changes. +func (a *Adapter) SetStateChangeHandler(c func(newState AdapterState)) { + a.stateChangeHandler = c +} + +// State returns the current state of the adapter. +func (a *Adapter) State() AdapterState { + if a.adapter == nil { + return AdapterStateUnknown + } + + prop, err := a.adapter.GetProperty("org.bluez.Adapter1.Powered") + if err != nil { + return AdapterStateUnknown + } + powered, ok := prop.Value().(bool) + if !ok { + return AdapterStateUnknown + } + if powered { + return AdapterStatePoweredOn + } + return AdapterStatePoweredOff +} + +// watchForStateChange watches for D-Bus PropertiesChanged signals from the +// BlueZ adapter interface and reports Powered/Unpowered transitions to the +// registered state-change handler. +// +// We can watch for extra adapter properties here, see +// https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/org.bluez.Adapter.rst +// for the full list. +func (a *Adapter) watchForStateChange() error { + matchOptions := []dbus.MatchOption{ + dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), + dbus.WithMatchMember("PropertiesChanged"), + dbus.WithMatchArg(dbusPropertiesChangedInterfaceName, "org.bluez.Adapter1"), + } + if err := a.bus.AddMatchSignal(matchOptions...); err != nil { + return err + } + + signal := make(chan *dbus.Signal) + a.bus.Signal(signal) + + go func() { + for sig := range signal { + if sig.Name != dbusSignalPropertiesChanged { + continue + } + // Only react to property changes on the adapter interface. + if interfaceName, ok := sig.Body[dbusPropertiesChangedInterfaceName].(string); !ok || interfaceName != "org.bluez.Adapter1" { + continue + } + changes, ok := sig.Body[dbusPropertiesChangedDictionary].(map[string]dbus.Variant) + if !ok { + continue + } + if powered, ok := changes["Powered"].Value().(bool); ok { + if powered { + a.stateChangeHandler(AdapterStatePoweredOn) + } else { + a.stateChangeHandler(AdapterStatePoweredOff) + } + } + } + }() + + return nil +}