diff --git a/adapter.go b/adapter.go index f3f4528c..a6533316 100644 --- a/adapter.go +++ b/adapter.go @@ -3,6 +3,31 @@ package bluetooth // Set this to true to print debug messages, for example for unknown events. const debug = false +// 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 +) + +func (as *AdapterState) String() string { + switch *as { + case AdapterStatePoweredOff: + return "PoweredOff" + case AdapterStatePoweredOn: + return "PoweredOn" + case AdapterStateUnknown: + return "Unknown" + default: + return "Unknown" + } +} + // SetConnectHandler sets a handler function to be called whenever the adaptor connects // or disconnects. You must call this before you call adaptor.Connect() for centrals // or adaptor.Start() for peripherals in order for it to work. diff --git a/adapter_darwin.go b/adapter_darwin.go index 06c7ffc9..1fe0ab69 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -24,7 +24,8 @@ type Adapter struct { // used to allow multiple callers to call Connect concurrently. connectMap sync.Map - connectHandler func(device Address, connected bool) + connectHandler func(device Address, connected bool) + stateChangeHandler func(newState AdapterState) } // DefaultAdapter is the default adapter on the system. @@ -38,6 +39,9 @@ var DefaultAdapter = &Adapter{ connectHandler: func(device Address, connected bool) { return }, + stateChangeHandler: func(newState AdapterState) { + return + }, } // Enable configures the BLE stack. It must be called before any @@ -65,6 +69,8 @@ func (a *Adapter) Enable() error { for len(a.poweredChan) > 0 { <-a.poweredChan } + // we are done with this chan so lets remove it, this lets Enable() be reentrant if needed + a.poweredChan = nil // wait until powered? a.pmd = &peripheralManagerDelegate{a: a} @@ -73,6 +79,24 @@ func (a *Adapter) Enable() 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 { @@ -83,9 +107,20 @@ type centralManagerDelegate struct { // CentralManagerDidUpdateState when central manager state updated. func (cmd *centralManagerDelegate) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { - // powered on? - if cmgr.State() == cbgo.ManagerStatePoweredOn { - cmd.a.poweredChan <- nil + + switch cmgr.State() { + case cbgo.ManagerStatePoweredOn: + // if we are waiting for a PoweredOn signal then send it + if cmd.a.poweredChan != nil { + cmd.a.poweredChan <- nil + } + cmd.a.stateChangeHandler(AdapterStatePoweredOn) + + case cbgo.ManagerStatePoweredOff: + cmd.a.stateChangeHandler(AdapterStatePoweredOff) + + default: + cmd.a.stateChangeHandler(AdapterStateUnknown) } // TODO: handle other state changes. diff --git a/adapter_linux.go b/adapter_linux.go index 8ff92600..3eda4b01 100644 --- a/adapter_linux.go +++ b/adapter_linux.go @@ -6,9 +6,11 @@ package bluetooth import ( + "context" "errors" "github.com/muka/go-bluetooth/api" + "github.com/muka/go-bluetooth/bluez" "github.com/muka/go-bluetooth/bluez/profile/adapter" ) @@ -18,7 +20,12 @@ type Adapter struct { cancelChan chan struct{} defaultAdvertisement *Advertisement - connectHandler func(device Address, connected bool) + ctx context.Context // context for our event watcher, canceled on power off event + cancel context.CancelFunc // cancel function to halt our event watcher context + propchanged chan *bluez.PropertyChanged // channel that adapter property changes will show up on + + connectHandler func(device Address, connected bool) + stateChangeHandler func(newState AdapterState) } // DefaultAdapter is the default adapter on the system. On Linux, it is the @@ -29,6 +36,9 @@ var DefaultAdapter = &Adapter{ connectHandler: func(device Address, connected bool) { return }, + stateChangeHandler: func(newState AdapterState) { + return + }, } // Enable configures the BLE stack. It must be called before any @@ -40,6 +50,8 @@ func (a *Adapter) Enable() (err error) { return } a.id, err = a.adapter.GetAdapterID() + a.ctx, a.cancel = context.WithCancel(context.Background()) + a.watchForStateChange() } return nil } @@ -54,3 +66,64 @@ 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 + } + + powered, err := a.adapter.GetPowered() + if err != nil { + return AdapterStateUnknown + } + if powered { + return AdapterStatePoweredOn + } + return AdapterStatePoweredOff +} + +// watchForConnect watches for a signal from the bluez adapter interface that indicates a Powered/Unpowered event. +// +// We can add extra signals to watch for here, +// see https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/adapter-api.txt, for a full list +func (a *Adapter) watchForStateChange() error { + var err error + a.propchanged, err = a.adapter.WatchProperties() + if err != nil { + return err + } + + go func() { + for { + select { + case changed := <-a.propchanged: + // we will receive a nil if bluez.UnwatchProperties(a, ch) is called, if so we can stop watching + if changed == nil { + a.cancel() + return + } + switch changed.Name { + case "Powered": + if changed.Value.(bool) { + a.stateChangeHandler(AdapterStatePoweredOn) + } else { + a.stateChangeHandler(AdapterStatePoweredOff) + } + } + + continue + case <-a.ctx.Done(): + return + } + } + }() + + return nil +}