Skip to content

Commit

Permalink
ble(client): add scan timout
Browse files Browse the repository at this point in the history
  • Loading branch information
gen2thomas committed Jan 23, 2024
1 parent 915d0c8 commit b67a662
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 68 deletions.
2 changes: 1 addition & 1 deletion platforms/adaptors/pwmpinsadaptoroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package adaptors

import "time"

// pwmPinOptionApplier needs to be implemented by each configurable option type
// PwmPinsOptionApplier needs to be implemented by each configurable option type
type PwmPinsOptionApplier interface {
apply(cfg *pwmPinsConfiguration)
}
Expand Down
201 changes: 137 additions & 64 deletions platforms/ble/ble_client_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ import (
"gobot.io/x/gobot/v2"
)

var (
currentAdapter *bluetooth.Adapter
bleMutex sync.Mutex
)
type bluetoothDevice interface {
DiscoverServices(uuids []bluetooth.UUID) ([]bluetooth.DeviceService, error)
Disconnect() error
}

type bluetoothAdapter interface {
Enable() error
Scan(callback func(*bluetooth.Adapter, bluetooth.ScanResult)) error
StopScan() error
Connect(address bluetooth.Address, params bluetooth.ConnectionParams) (*bluetooth.Device, error)
}

// BLEConnector is the interface that a BLE ClientAdaptor must implement
type BLEConnector interface {
Expand All @@ -30,31 +37,61 @@ type BLEConnector interface {
WithoutResponses(use bool)
}

type bleClientAdaptorConfiguration struct {
scanTimeout time.Duration
debug bool
}

// ClientAdaptor represents a Client Connection to a BLE Peripheral
type ClientAdaptor struct {
name string
address string
AdapterName string
name string
address string
clientCfg *bleClientAdaptorConfiguration

addr bluetooth.Address
adpt *bluetooth.Adapter
device *bluetooth.Device
// addr bluetooth.Address
btAdpt bluetoothAdapter // *bluetooth.Adapter
btDevice bluetoothDevice // *bluetooth.Device
characteristics map[string]bluetooth.DeviceCharacteristic

connected bool
withoutResponses bool
connected bool
rssi int

mutex *sync.Mutex
}

// NewClientAdaptor returns a new ClientAdaptor given an address
func NewClientAdaptor(address string) *ClientAdaptor {
return &ClientAdaptor{
name: gobot.DefaultName("BLEClient"),
address: address,
AdapterName: "default",
connected: false,
withoutResponses: false,
characteristics: make(map[string]bluetooth.DeviceCharacteristic),
//
// Supported options:
//
// "WithClientAdaptorScanTimeout"
func NewClientAdaptor(address string, opts ...bleClientAdaptorOptionApplier) *ClientAdaptor {
cfg := bleClientAdaptorConfiguration{
scanTimeout: 10 * time.Minute,
}

b := ClientAdaptor{
name: gobot.DefaultName("BLEClient"),
address: address,
clientCfg: &cfg,
characteristics: make(map[string]bluetooth.DeviceCharacteristic),
mutex: &sync.Mutex{},
}

for _, o := range opts {
o.apply(b.clientCfg)
}

return &b
}

// WithClientAdaptorDebug switch on some debug messages.
func WithClientAdaptorDebug() bleClientAdaptorDebugOption {
return bleClientAdaptorDebugOption(true)
}

// WithClientAdaptorScanTimeout substitute the default scan timeout of 10 min.
func WithClientAdaptorScanTimeout(timeout time.Duration) bleClientAdaptorScanTimeoutOption {
return bleClientAdaptorScanTimeoutOption(timeout)
}

// Name returns the name for the adaptor
Expand All @@ -66,71 +103,97 @@ func (b *ClientAdaptor) SetName(n string) { b.name = n }
// Address returns the Bluetooth LE address for the adaptor
func (b *ClientAdaptor) Address() string { return b.address }

// RSSI returns the Bluetooth LE RSSI value at the moment of connecting the adaptor
func (b *ClientAdaptor) RSSI() int { return b.rssi }

// WithoutResponses sets if the adaptor should expect responses after
// writing characteristics for this device
func (b *ClientAdaptor) WithoutResponses(use bool) { b.withoutResponses = use }
// writing characteristics for this device (has no effect at the moment).
func (b *ClientAdaptor) WithoutResponses(bool) {}

// Connect initiates a connection to the BLE peripheral. Returns true on successful connection.
// Connect initiates a connection to the BLE peripheral.
func (b *ClientAdaptor) Connect() error {
bleMutex.Lock()
defer bleMutex.Unlock()
b.mutex.Lock()
defer b.mutex.Unlock()

var err error
// enable adaptor
b.adpt, err = getBLEAdapter(b.AdapterName)
if err != nil {
return fmt.Errorf("can't get adapter %s: %w", b.AdapterName, err)
}

// handle address
b.addr.Set(b.Address())
if b.clientCfg.debug {
fmt.Println("[Connect]: enable adaptor...")
}

// scan for the address
ch := make(chan bluetooth.ScanResult, 1)
err = b.adpt.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) {
if result.Address.String() == b.Address() {
if err := b.adpt.StopScan(); err != nil {
panic(err)
}
b.SetName(result.LocalName())
ch <- result
// for re-connect, the adapter is already known
if b.btAdpt == nil {
b.btAdpt = bluetooth.DefaultAdapter
if err := b.btAdpt.Enable(); err != nil {
return fmt.Errorf("can't get adapter default: %w", err)
}
})
}

if err != nil {
return err
if b.clientCfg.debug {
fmt.Println("[Connect]: handle address...")
}
// b.addr.Set(b.address)

// wait to connect to peripheral device
result := <-ch
b.device, err = b.adpt.Connect(result.Address, bluetooth.ConnectionParams{})
if err != nil {
if b.clientCfg.debug {
fmt.Println("[Connect]: scan for the address...")
}

resultChan, errChan := goClientAdaptorScanForAddress(b.btAdpt, b.address)

if b.clientCfg.debug {
fmt.Printf("[Connect]: wait %s to connect to peripheral device...\n", b.clientCfg.scanTimeout)
}
select {
case result := <-resultChan:
if err := b.btAdpt.StopScan(); err != nil {
return err
}
b.SetName(result.LocalName())
b.rssi = int(result.RSSI)
b.btDevice, err = b.btAdpt.Connect(result.Address, bluetooth.ConnectionParams{})
if err != nil {
return err
}
case err := <-errChan:
return err
case <-time.After(b.clientCfg.scanTimeout):
_ = b.btAdpt.StopScan()
return fmt.Errorf("scan timeout (%s) elapsed", b.clientCfg.scanTimeout)
}

// get all services/characteristics
srvcs, err := b.device.DiscoverServices(nil)
if b.clientCfg.debug {
fmt.Println("[Connect]: get all services/characteristics...")
}
srvcs, err := b.btDevice.DiscoverServices(nil)
if err != nil {
return err
}
for _, srvc := range srvcs {
if b.clientCfg.debug {
fmt.Printf("[Connect]: service found: %s\n", srvc)
}
chars, err := srvc.DiscoverCharacteristics(nil)
if err != nil {
log.Println(err)
continue
}
for _, char := range chars {
if b.clientCfg.debug {
fmt.Printf("[Connect]: characteristic found: %s\n", char)
}
b.characteristics[char.UUID().String()] = char
}
}

if b.clientCfg.debug {
fmt.Println("[Connect]: connected")
}
b.connected = true
return nil
}

// Reconnect attempts to reconnect to the BLE peripheral. If it has an active connection
// it will first close that connection and then establish a new connection.
// Returns true on Successful reconnection
func (b *ClientAdaptor) Reconnect() error {
if b.connected {
if err := b.Disconnect(); err != nil {
Expand All @@ -140,10 +203,17 @@ func (b *ClientAdaptor) Reconnect() error {
return b.Connect()
}

// Disconnect terminates the connection to the BLE peripheral. Returns true on successful disconnect.
// Disconnect terminates the connection to the BLE peripheral.
func (b *ClientAdaptor) Disconnect() error {
err := b.device.Disconnect()
if b.clientCfg.debug {
fmt.Println("[Disconnect]: disconnect...")
}
err := b.btDevice.Disconnect()
time.Sleep(500 * time.Millisecond)
b.connected = false
if b.clientCfg.debug {
fmt.Println("[Disconnect]: disconnected")
}
return err
}

Expand Down Expand Up @@ -212,17 +282,20 @@ func (b *ClientAdaptor) Subscribe(cUUID string, f func([]byte, error)) error {
return fmt.Errorf("Unknown characteristic: %s", cUUID)
}

// getBLEAdapter is singleton for bluetooth adapter connection
func getBLEAdapter(impl string) (*bluetooth.Adapter, error) { //nolint:unparam // TODO: impl is unused, maybe an error
if currentAdapter != nil {
return currentAdapter, nil
}
func goClientAdaptorScanForAddress(btAdpt bluetoothAdapter, address string) (chan bluetooth.ScanResult, chan error) {
resultChan := make(chan bluetooth.ScanResult, 1)
errChan := make(chan error)

currentAdapter = bluetooth.DefaultAdapter
err := currentAdapter.Enable()
if err != nil {
return nil, err
}
go func() {
err := btAdpt.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) {
if result.Address.String() == address {
resultChan <- result
}
})
if err != nil {
errChan <- err
}
}()

return currentAdapter, nil
return resultChan, errChan
}
30 changes: 30 additions & 0 deletions platforms/ble/ble_client_adaptor_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ble

import "time"

// bleClientAdaptorOptionApplier needs to be implemented by each configurable option type
type bleClientAdaptorOptionApplier interface {
apply(cfg *bleClientAdaptorConfiguration)
}

// bleClientAdaptorDebug is the type for applying the debug switch on or off.
type bleClientAdaptorDebugOption bool

// bleClientAdaptorScanTimeoutOption is the type for applying another timeout than the default 10 min.
type bleClientAdaptorScanTimeoutOption time.Duration

func (o bleClientAdaptorDebugOption) String() string {
return "debug option for BLE client adaptors"
}

func (o bleClientAdaptorScanTimeoutOption) String() string {
return "scan timeout option for BLE client adaptors"
}

func (o bleClientAdaptorDebugOption) apply(cfg *bleClientAdaptorConfiguration) {
cfg.debug = bool(o)
}

func (o bleClientAdaptorScanTimeoutOption) apply(cfg *bleClientAdaptorConfiguration) {
cfg.scanTimeout = time.Duration(o)
}
27 changes: 27 additions & 0 deletions platforms/ble/ble_client_adaptor_options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ble

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestWithClientAdaptorDebug(t *testing.T) {
// This is a general test, that options are applied by using the TestWithClientAdaptorDebug() option.
// All other configuration options can also be tested by With..(val).apply(cfg).
// arrange & act
a := NewClientAdaptor("address", WithClientAdaptorDebug())
// assert
assert.True(t, a.clientCfg.debug)
}

func TestWithClientAdaptorScanTimeout(t *testing.T) {
// arrange
newTimeout := 2 * time.Second
cfg := &bleClientAdaptorConfiguration{scanTimeout: 10 * time.Second}
// act
WithClientAdaptorScanTimeout(newTimeout).apply(cfg)
// assert
assert.Equal(t, newTimeout, cfg.scanTimeout)
}
Loading

0 comments on commit b67a662

Please sign in to comment.