From 738e140bfb96098f62ff08488a539013cae21a13 Mon Sep 17 00:00:00 2001 From: Arsen Musayelyan Date: Thu, 16 Dec 2021 21:30:29 -0800 Subject: [PATCH] Create custom BlueZ agent --- btsetup.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++- infinitime.go | 97 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 155 insertions(+), 33 deletions(-) diff --git a/btsetup.go b/btsetup.go index 9fce824..3d33516 100644 --- a/btsetup.go +++ b/btsetup.go @@ -1,27 +1,116 @@ package infinitime import ( + "github.com/godbus/dbus/v5" bt "github.com/muka/go-bluetooth/api" "github.com/muka/go-bluetooth/bluez/profile/adapter" + "github.com/muka/go-bluetooth/bluez/profile/agent" + "github.com/muka/go-bluetooth/hw/linux/btmgmt" ) var defaultAdapter *adapter.Adapter1 +var itdAgent *Agent func Init() { + conn, err := dbus.SystemBus() + if err != nil { + panic(err) + } + + ag := &Agent{} + err = agent.ExposeAgent(conn, ag, agent.CapKeyboardDisplay, true) + if err != nil { + panic(err) + } + // Get bluez default adapter da, err := bt.GetDefaultAdapter() if err != nil { panic(err) } - da.SetPowered(true) + daMgmt := btmgmt.NewBtMgmt(bt.GetDefaultAdapterID()) + daMgmt.SetPowered(true) defaultAdapter = da + itdAgent = ag } func Exit() error { if defaultAdapter != nil { defaultAdapter.Close() } + agent.RemoveAgent(itdAgent) return bt.Exit() } + +var errAuthFailed = dbus.NewError("org.bluez.Error.AuthenticationFailed", nil) + +// Agent implements the agent.Agent1Client interface. +// It only requires RequestPasskey as that is all InfiniTime +// will use. +type Agent struct { + ReqPasskey func() (uint32, error) +} + +// Release returns nil +func (*Agent) Release() *dbus.Error { + return nil +} + +// RequestPinCode returns an empty string and nil +func (*Agent) RequestPinCode(device dbus.ObjectPath) (pincode string, err *dbus.Error) { + return "", nil +} + +// DisplayPinCode returns nil +func (*Agent) DisplayPinCode(device dbus.ObjectPath, pincode string) *dbus.Error { + return nil +} + +// RequestPasskey runs Agent.ReqPasskey and returns the result +func (a *Agent) RequestPasskey(device dbus.ObjectPath) (uint32, *dbus.Error) { + if a.ReqPasskey == nil { + return 0, errAuthFailed + } + passkey, err := a.ReqPasskey() + if err != nil { + return 0, errAuthFailed + } + return passkey, nil +} + +// DisplayPasskey returns nil +func (*Agent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, entered uint16) *dbus.Error { + return nil +} + +// RequestConfirmation returns nil +func (*Agent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error { + return nil +} + +// RequestAuthorization returns nil +func (*Agent) RequestAuthorization(device dbus.ObjectPath) *dbus.Error { + return nil +} + +// AuthorizeService returns nil +func (*Agent) AuthorizeService(device dbus.ObjectPath, uuid string) *dbus.Error { + return nil +} + +// Cancel returns nil +func (*Agent) Cancel() *dbus.Error { + return nil +} + +// Path returns "/dev/arsenm/infinitime/Agent" +func (*Agent) Path() dbus.ObjectPath { + return "/dev/arsenm/infinitime/Agent" +} + +// Interface returns "org.bluez.Agent1" +func (*Agent) Interface() string { + return "org.bluez.Agent1" +} diff --git a/infinitime.go b/infinitime.go index 409ab79..42278f7 100644 --- a/infinitime.go +++ b/infinitime.go @@ -59,12 +59,14 @@ var ( ErrNotConnected = errors.New("not connected") ErrCharNotAvail = errors.New("required characteristic is not available") ErrNoTimelineHeader = errors.New("events must contain the timeline header") + ErrPairTimeout = errors.New("reached timeout while pairing") ) type Options struct { AttemptReconnect bool WhitelistEnabled bool Whitelist []string + OnReqPasskey func() (uint32, error) } var DefaultOptions = &Options{ @@ -94,6 +96,8 @@ func Connect(opts *Options) (*Device, error) { } dev.opts = opts dev.onReconnect = func() {} + setOnPasskeyReq(opts.OnReqPasskey) + // Watch device properties devEvtCh, err := dev.device.WatchProperties() if err != nil { @@ -121,14 +125,29 @@ func Connect(opts *Options) (*Device, error) { dev.device.Properties.Connected = false // While not connected for !dev.device.Properties.Connected { - // Attempt to connect via bluetooth address - reConnDev, err := ConnectByAddress(dev.device.Properties.Address) + reConnDev := dev + + paired, err := reConnDev.device.GetPaired() if err != nil { - // Decrement disconnect event number - disconnEvtNum-- - // Skip rest of loop 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 @@ -154,6 +173,7 @@ func (i *Device) OnReconnect(f func()) { // 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 @@ -203,6 +223,7 @@ func contains(ss []string, s string) bool { // 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 @@ -237,10 +258,17 @@ func pair(opts *Options) (*Device, error) { return nil, ErrNotFound } - // Pair device - out.device.Pair() + // Connect to device + err = out.device.Connect() + if err != nil { + return nil, err + } - out.device.Properties.Connected = true + // Pair device + err = out.pairTimeout() + if err != nil { + return nil, err + } // Set connected to true out.device.Properties.Connected = true @@ -254,32 +282,33 @@ func pair(opts *Options) (*Device, error) { return out, nil } -// ConnectByAddress tries to connect to an InifiniTime at -// the specified InfiniTime address -func ConnectByAddress(addr string) (*Device, error) { - var err error - // Create new device - out := &Device{} - // Get device from bluetooth address - out.device, err = defaultAdapter.GetDeviceByAddress(addr) - if err != nil { - return nil, err +// setOnPasskeyReq sets the callback for a passkey request. +// It ensures the function will never be nil. +func setOnPasskeyReq(onReqPasskey func() (uint32, error)) { + itdAgent.ReqPasskey = onReqPasskey + if itdAgent.ReqPasskey == nil { + itdAgent.ReqPasskey = func() (uint32, error) { + return 0, nil + } } +} - // Connect to device - err = out.device.Connect() - if err != nil { - return nil, err +// 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 + } + return ErrPairTimeout } - - out.device.Properties.Connected = true - - // Resolve characteristics - err = out.resolveChars() - if err != nil { - return nil, err - } - return out, nil } // resolveChars attempts to set all required @@ -719,7 +748,11 @@ func (i *Device) AddWeatherEvent(event interface{}) error { } func (i *Device) checkStatus(char *gatt.GattCharacteristic1) error { - if !i.device.Properties.Connected { + connected, err := i.device.GetConnected() + if err != nil { + return err + } + if !connected { return ErrNotConnected } if char == nil {