Rewrite connect/reconnect code
This commit is contained in:
parent
738e140bfb
commit
e82981e3fc
8
go.mod
8
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
|
||||
)
|
||||
|
10
go.sum
10
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=
|
||||
|
372
infinitime.go
372
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
|
||||
|
Reference in New Issue
Block a user