Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

linux & macos: adapter power state handling #188

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +14 to +15
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually possible for the state to be unknown, and under what circumstances can this happen?

If possible, I'd rather avoid this condition as it's just another special case.

If there are only two states, we can just use a boolean (enabled bool as a parameter for example).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They vary between OS's.

The MacOS implementation does define an unknown state along with a few others . On the linux side the Powered state that this PR watches only has on or off.

The Unknown was just an attempt to do 'something' with the extra ones that Core Bluetooth throws at us. I wasn't sure if the other states are merely transient and the adapter will always eventually fall into an on | off state or if there is a chance the states like CBManagerStateUnsupported may be a permanent latch.

)

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.
Expand Down
43 changes: 39 additions & 4 deletions adapter_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand All @@ -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 {
Expand All @@ -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.
Expand Down
75 changes: 74 additions & 1 deletion adapter_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
}