Rewrite connect/reconnect code

This commit is contained in:
Elara 2022-02-21 02:46:20 -08:00
parent 738e140bfb
commit e82981e3fc
3 changed files with 198 additions and 192 deletions

8
go.mod
View File

@ -3,8 +3,8 @@ module go.arsenm.dev/infinitime
go 1.16 go 1.16
require ( require (
github.com/fxamacker/cbor/v2 v2.3.0 github.com/fxamacker/cbor/v2 v2.4.0
github.com/godbus/dbus/v5 v5.0.3 github.com/godbus/dbus/v5 v5.0.6
github.com/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a github.com/muka/go-bluetooth v0.0.0-20220219050759-674a63b8741a
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
) )

10
go.sum
View File

@ -5,15 +5,19 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fxamacker/cbor/v2 v2.3.0 h1:aM45YGMctNakddNNAezPxDUpv38j44Abh+hifNuqXik= github.com/fxamacker/cbor/v2 v2.3.0 h1:aM45YGMctNakddNNAezPxDUpv38j44Abh+hifNuqXik=
github.com/fxamacker/cbor/v2 v2.3.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/cbor/v2 v2.3.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a h1:KxRXeSWoBM5FCPAnSUYxt1qwEzmoH/K7upb4fiSDwdc= github.com/muka/go-bluetooth v0.0.0-20220219050759-674a63b8741a h1:fnzS9RRQW8B5AgNCxkN0vJ/AoX+Xfqk3sAYon3iVrzA=
github.com/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0= github.com/muka/go-bluetooth v0.0.0-20220219050759-674a63b8741a/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk= github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -45,6 +49,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=

View File

@ -10,6 +10,7 @@ import (
"github.com/fxamacker/cbor/v2" "github.com/fxamacker/cbor/v2"
bt "github.com/muka/go-bluetooth/api" bt "github.com/muka/go-bluetooth/api"
"github.com/muka/go-bluetooth/bluez"
"github.com/muka/go-bluetooth/bluez/profile/adapter" "github.com/muka/go-bluetooth/bluez/profile/adapter"
"github.com/muka/go-bluetooth/bluez/profile/device" "github.com/muka/go-bluetooth/bluez/profile/device"
"github.com/muka/go-bluetooth/bluez/profile/gatt" "github.com/muka/go-bluetooth/bluez/profile/gatt"
@ -33,7 +34,6 @@ const (
) )
type Device struct { type Device struct {
opts *Options
device *device.Device1 device *device.Device1
newAlertChar *gatt.GattCharacteristic1 newAlertChar *gatt.GattCharacteristic1
notifEventChar *gatt.GattCharacteristic1 notifEventChar *gatt.GattCharacteristic1
@ -48,7 +48,6 @@ type Device struct {
weatherDataChar *gatt.GattCharacteristic1 weatherDataChar *gatt.GattCharacteristic1
notifEventCh chan uint8 notifEventCh chan uint8
notifEventDone bool notifEventDone bool
onReconnect func()
Music MusicCtrl Music MusicCtrl
DFU DFU DFU DFU
} }
@ -67,6 +66,7 @@ type Options struct {
WhitelistEnabled bool WhitelistEnabled bool
Whitelist []string Whitelist []string
OnReqPasskey func() (uint32, error) OnReqPasskey func() (uint32, error)
OnReconnect func()
} }
var DefaultOptions = &Options{ var DefaultOptions = &Options{
@ -79,207 +79,215 @@ var DefaultOptions = &Options{
// it will attempt to discover and pair one. // it will attempt to discover and pair one.
// //
// It will also attempt to reconnect to the device // It will also attempt to reconnect to the device
// if it disconnects. // if it disconnects and that is enabled in the options.
func Connect(opts *Options) (*Device, error) { func Connect(opts *Options) (*Device, error) {
if opts == nil { if opts == nil {
opts = DefaultOptions opts = DefaultOptions
} }
// Attempt to connect to paired device by name
dev, err := connectByName(opts) // Set passkey request callback
// If such device does not exist
if errors.Is(err, ErrNoDevices) {
// Attempt to pair device
dev, err = pair(opts)
}
if err != nil {
return nil, err
}
dev.opts = opts
dev.onReconnect = func() {}
setOnPasskeyReq(opts.OnReqPasskey) setOnPasskeyReq(opts.OnReqPasskey)
// Watch device properties // Connect to bluetooth device
devEvtCh, err := dev.device.WatchProperties() btDev, err := connect(opts, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// If AttemptReconnect enabled
if dev.opts.AttemptReconnect {
go func() {
disconnEvtNum := 0
// For every event
for evt := range devEvtCh {
// If device disconnected
if evt.Name == "Connected" && evt.Value == false {
// Increment disconnect event number
disconnEvtNum++
// If more than one disconnect event
if disconnEvtNum > 1 {
// Decrement disconnect event number
disconnEvtNum--
// Skip loop
continue
}
// Set connected to false
dev.device.Properties.Connected = false
// While not connected
for !dev.device.Properties.Connected {
reConnDev := dev
paired, err := reConnDev.device.GetPaired()
if err != nil {
continue
}
if !paired {
err = reConnDev.pairTimeout()
if err != nil {
continue
}
} else {
// Attempt to connect via bluetooth address
reConnDev, err = connectByName(opts)
if err != nil {
// Decrement disconnect event number
disconnEvtNum--
// Skip rest of loop
continue
}
}
// Store onReconn callback
onReconn := dev.onReconnect
// Set device to new device
*dev = *reConnDev
// Run on reconnect callback
onReconn()
// Assign callback to new device
dev.onReconnect = onReconn
}
// Decrement disconnect event number
disconnEvtNum--
}
}
}()
}
return dev, nil
}
// OnReconnect sets the callback that runs on reconnect
func (i *Device) OnReconnect(f func()) {
i.onReconnect = f
}
// Connect connects to a paired InfiniTime device
func connectByName(opts *Options) (*Device, error) {
setOnPasskeyReq(opts.OnReqPasskey)
// Create new device // Create new device
out := &Device{} out := &Device{device: btDev}
// Get devices from default adapter
// Resolve characteristics
err = out.resolveChars()
if err != nil {
return nil, err
}
return out, nil
}
// connect connects to the InfiniTime bluez device
func connect(opts *Options, first bool) (dev *device.Device1, err error) {
// Get devices
devs, err := defaultAdapter.GetDevices() devs, err := defaultAdapter.GetDevices()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// For every device // For every device
for _, dev := range devs { for _, listDev := range devs {
// If device name is InfiniTime // If device name does not match, skip
if dev.Properties.Name == BTName { if listDev.Properties.Name != BTName {
if opts.WhitelistEnabled && !contains(opts.Whitelist, dev.Properties.Address) {
continue
}
// Set outout device to discovered device
out.device = dev
break
}
}
if out.device == nil {
return nil, ErrNoDevices
}
// Connect to device
err = out.device.Connect()
if err != nil {
return nil, err
}
out.device.Properties.Connected = true
// Resolve characteristics
err = out.resolveChars()
if err != nil {
return nil, err
}
return out, nil
}
func contains(ss []string, s string) bool {
for _, str := range ss {
if strings.EqualFold(str, s) {
return true
}
}
return false
}
// Pair attempts to discover and pair an InfiniTime device
func pair(opts *Options) (*Device, error) {
setOnPasskeyReq(opts.OnReqPasskey)
// Create new device
out := &Device{}
// Start bluetooth discovery
// Ignore the cancel function as it blocks forever
discovery, _, err := bt.Discover(defaultAdapter, &adapter.DiscoveryFilter{Transport: "le"})
if err != nil {
return nil, err
}
// For every discovery event
for event := range discovery {
// If device removed, skip event
if event.Type == adapter.DeviceRemoved {
continue continue
} }
// Create new device with discovered path // If whitelist enabled and doesn't contain
dev, err := device.NewDevice1(event.Path) // device, skip
if opts.WhitelistEnabled &&
!contains(opts.Whitelist, listDev.Properties.Address) {
continue
}
// Set device
dev = listDev
break
}
// If device not set
if dev == nil {
// Discover devices on adapter
discoverCh, cancel, err := bt.Discover(defaultAdapter, &adapter.DiscoveryFilter{Transport: "le"})
if err != nil { if err != nil {
return nil, err return nil, err
} }
// If device name is InfiniTime
if dev.Properties.Name == BTName { // For every discovery event
if opts.WhitelistEnabled && !contains(opts.Whitelist, dev.Properties.Address) { for event := range discoverCh {
// If event type is not device added, skip
if event.Type != adapter.DeviceAdded {
continue continue
} }
// Set output device
out.device = dev // Create new device from event path
discovered, err := device.NewDevice1(event.Path)
if err != nil {
return nil, err
}
// If device name does not match, skip
if discovered.Properties.Name != BTName {
continue
}
// If whitelist enabled and doesn't contain
// device, skip
if opts.WhitelistEnabled &&
!contains(opts.Whitelist, discovered.Properties.Address) {
continue
}
// Set device
dev = discovered
break break
} }
// Stop discovery
cancel()
}
// If device is still not set, return error
if dev == nil {
return nil, ErrNoDevices
}
// Create variable to track if reconnect
// was required
reconnRequired := false
// If device is not connected
if !dev.Properties.Connected {
// Connect to device
err = dev.Connect()
if err != nil {
return nil, err
}
// Set reconnect required to true
reconnRequired = true
}
// If device is not paired
if !dev.Properties.Paired {
// Pair device
err = dev.Pair()
if err != nil {
return nil, err
}
} }
if out.device == nil { // If this is the first connection and reconnect
return nil, ErrNotFound // is enabled, start reconnect goroutine
if first && opts.AttemptReconnect {
go reconnect(opts, dev)
} }
// Connect to device // If this is not the first connection, a reonnect
err = out.device.Connect() // was required, and the OnReconnect callback exists,
// run it
if !first && reconnRequired && opts.OnReconnect != nil {
opts.OnReconnect()
}
return dev, nil
}
// reconnect reconnects to a device if it disconnects
func reconnect(opts *Options, dev *device.Device1) {
// Watch device properties
propCh := watchProps(dev)
// Create variables to store time of last disconnect
// and amount of diconnects
lastDisconnect := time.Unix(0, 0)
amtDisconnects := 0
for event := range propCh {
// If event name is not Connected and value is not false, skip
if event.Name != "Connected" && event.Value != false {
continue
}
// Store seconds since last disconnect
secsSince := time.Since(lastDisconnect).Seconds()
// If over 3 seconds have passed, reset disconnect count
if secsSince > 3 {
amtDisconnects = 0
}
// If less than 3 seconds have past and more than 6
// disconnects have occurred, remove the device and reset
if secsSince <= 3 && amtDisconnects >= 6 {
defaultAdapter.RemoveDevice(dev.Path())
lastDisconnect = time.Unix(0, 0)
amtDisconnects = 0
}
// Set disconnect variables
lastDisconnect = time.Now()
amtDisconnects++
for i := 0; i < 6; i++ {
// If three tries failed, remove device
if i == 3 {
defaultAdapter.RemoveDevice(dev.Path())
}
// Connect to device
newDev, err := connect(opts, false)
if err != nil {
time.Sleep(time.Second)
continue
}
// Replace device with new device
*dev = *newDev
break
}
}
}
// bufferChannel writes all events on propCh to a new, buffered channel
func bufferChannel(propCh chan *bluez.PropertyChanged) <-chan *bluez.PropertyChanged {
out := make(chan *bluez.PropertyChanged, 10)
go func() {
for event := range propCh {
out <- event
}
}()
return out
}
// watchProps returns a buffered channel for the device properties
func watchProps(dev *device.Device1) <-chan *bluez.PropertyChanged {
uPropCh, err := dev.WatchProperties()
if err != nil { if err != nil {
return nil, err panic(err)
} }
return bufferChannel(uPropCh)
// Pair device
err = out.pairTimeout()
if err != nil {
return nil, err
}
// Set connected to true
out.device.Properties.Connected = true
// Resolve characteristics
err = out.resolveChars()
if err != nil {
return nil, err
}
return out, nil
} }
// setOnPasskeyReq sets the callback for a passkey request. // setOnPasskeyReq sets the callback for a passkey request.
@ -293,22 +301,14 @@ func setOnPasskeyReq(onReqPasskey func() (uint32, error)) {
} }
} }
// pairTimeout tries to pair with the device. // contains checks if s is contained within ss
// It will time out after 20 seconds. func contains(ss []string, s string) bool {
func (i *Device) pairTimeout() error { for _, str := range ss {
errCh := make(chan error) if strings.EqualFold(str, s) {
go func() { return true
errCh <- i.device.Pair()
}()
select {
case err := <-errCh:
return err
case <-time.After(20 * time.Second):
if err := i.device.CancelPairing(); err != nil {
return err
} }
return ErrPairTimeout
} }
return false
} }
// resolveChars attempts to set all required // resolveChars attempts to set all required