From e82981e3fc4b1142b01a9c7b8d7b9d737b3d79fb Mon Sep 17 00:00:00 2001 From: Arsen Musayelyan Date: Mon, 21 Feb 2022 02:46:20 -0800 Subject: [PATCH] Rewrite connect/reconnect code --- go.mod | 8 +- go.sum | 10 +- infinitime.go | 372 +++++++++++++++++++++++++------------------------- 3 files changed, 198 insertions(+), 192 deletions(-) diff --git a/go.mod b/go.mod index 7448154..dbc4ad5 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module go.arsenm.dev/infinitime go 1.16 require ( - github.com/fxamacker/cbor/v2 v2.3.0 - github.com/godbus/dbus/v5 v5.0.3 - github.com/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a - golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect + github.com/fxamacker/cbor/v2 v2.4.0 + github.com/godbus/dbus/v5 v5.0.6 + github.com/muka/go-bluetooth v0.0.0-20220219050759-674a63b8741a + golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect ) diff --git a/go.sum b/go.sum index 5b1b7bc..05eedb5 100644 --- a/go.sum +++ b/go.sum @@ -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/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.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/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/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/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/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a h1:KxRXeSWoBM5FCPAnSUYxt1qwEzmoH/K7upb4fiSDwdc= -github.com/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0= +github.com/muka/go-bluetooth v0.0.0-20220219050759-674a63b8741a h1:fnzS9RRQW8B5AgNCxkN0vJ/AoX+Xfqk3sAYon3iVrzA= +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/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk= 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-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-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/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= diff --git a/infinitime.go b/infinitime.go index 42278f7..a90fb2b 100644 --- a/infinitime.go +++ b/infinitime.go @@ -10,6 +10,7 @@ import ( "github.com/fxamacker/cbor/v2" 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/device" "github.com/muka/go-bluetooth/bluez/profile/gatt" @@ -33,7 +34,6 @@ const ( ) type Device struct { - opts *Options device *device.Device1 newAlertChar *gatt.GattCharacteristic1 notifEventChar *gatt.GattCharacteristic1 @@ -48,7 +48,6 @@ type Device struct { weatherDataChar *gatt.GattCharacteristic1 notifEventCh chan uint8 notifEventDone bool - onReconnect func() Music MusicCtrl DFU DFU } @@ -67,6 +66,7 @@ type Options struct { WhitelistEnabled bool Whitelist []string OnReqPasskey func() (uint32, error) + OnReconnect func() } var DefaultOptions = &Options{ @@ -79,207 +79,215 @@ var DefaultOptions = &Options{ // it will attempt to discover and pair one. // // 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) { if opts == nil { opts = DefaultOptions } - // Attempt to connect to paired device by name - dev, err := connectByName(opts) - // 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() {} + + // Set passkey request callback setOnPasskeyReq(opts.OnReqPasskey) - // Watch device properties - devEvtCh, err := dev.device.WatchProperties() + // Connect to bluetooth device + btDev, err := connect(opts, true) if err != nil { 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 - out := &Device{} - // Get devices from default adapter + out := &Device{device: btDev} + + // 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() if err != nil { return nil, err } + // For every device - for _, dev := range devs { - // If device name is InfiniTime - if dev.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 { + for _, listDev := range devs { + // If device name does not match, skip + if listDev.Properties.Name != BTName { continue } - // Create new device with discovered path - dev, err := device.NewDevice1(event.Path) + // If whitelist enabled and doesn't contain + // 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 { return nil, err } - // If device name is InfiniTime - if dev.Properties.Name == BTName { - if opts.WhitelistEnabled && !contains(opts.Whitelist, dev.Properties.Address) { + + // For every discovery event + for event := range discoverCh { + // If event type is not device added, skip + if event.Type != adapter.DeviceAdded { 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 } + // 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 { - return nil, ErrNotFound + // If this is the first connection and reconnect + // is enabled, start reconnect goroutine + if first && opts.AttemptReconnect { + go reconnect(opts, dev) } - // Connect to device - err = out.device.Connect() + // If this is not the first connection, a reonnect + // 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 { - return nil, err + panic(err) } - - // 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 + return bufferChannel(uPropCh) } // 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. -// It will time out after 20 seconds. -func (i *Device) pairTimeout() error { - errCh := make(chan error) - go func() { - 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 +// contains checks if s is contained within ss +func contains(ss []string, s string) bool { + for _, str := range ss { + if strings.EqualFold(str, s) { + return true } - return ErrPairTimeout } + return false } // resolveChars attempts to set all required