ble(client): add scan timout (#1051)
This commit is contained in:
parent
d2b01b99e0
commit
9430005b82
|
@ -26,7 +26,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
bleAdaptor := bleclient.NewAdaptor(os.Args[1])
|
||||
bleAdaptor := bleclient.NewAdaptor(os.Args[1], bleclient.WithScanTimeout(30*time.Second))
|
||||
battery := ble.NewBatteryDriver(bleAdaptor)
|
||||
|
||||
work := func() {
|
||||
|
|
|
@ -18,6 +18,7 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gobot.io/x/gobot/v2"
|
||||
"gobot.io/x/gobot/v2/drivers/ble"
|
||||
|
@ -25,7 +26,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
bleAdaptor := bleclient.NewAdaptor(os.Args[1])
|
||||
bleAdaptor := bleclient.NewAdaptor(os.Args[1], bleclient.WithScanTimeout(30*time.Second), bleclient.WithDebug())
|
||||
info := ble.NewDeviceInformationDriver(bleAdaptor)
|
||||
|
||||
work := func() {
|
||||
|
|
|
@ -18,6 +18,7 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gobot.io/x/gobot/v2"
|
||||
"gobot.io/x/gobot/v2/drivers/ble"
|
||||
|
@ -25,7 +26,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
bleAdaptor := bleclient.NewAdaptor(os.Args[1])
|
||||
bleAdaptor := bleclient.NewAdaptor(os.Args[1], bleclient.WithScanTimeout(30*time.Second), bleclient.WithDebug())
|
||||
access := ble.NewGenericAccessDriver(bleAdaptor)
|
||||
|
||||
work := func() {
|
||||
|
|
|
@ -11,112 +11,168 @@ import (
|
|||
"gobot.io/x/gobot/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
currentAdapter *bluetooth.Adapter
|
||||
bleMutex sync.Mutex
|
||||
)
|
||||
type configuration struct {
|
||||
scanTimeout time.Duration
|
||||
sleepAfterDisconnect time.Duration
|
||||
debug bool
|
||||
}
|
||||
|
||||
// Adaptor represents a client connection to a BLE Peripheral
|
||||
// Adaptor represents a Client Connection to a BLE Peripheral
|
||||
type Adaptor struct {
|
||||
name string
|
||||
address string
|
||||
AdapterName string
|
||||
name string
|
||||
identifier string
|
||||
cfg *configuration
|
||||
|
||||
addr bluetooth.Address
|
||||
adpt *bluetooth.Adapter
|
||||
device *bluetooth.Device
|
||||
characteristics map[string]bluetooth.DeviceCharacteristic
|
||||
btAdpt *btAdapter
|
||||
btDevice *btDevice
|
||||
characteristics map[string]bluetoothExtCharacteristicer
|
||||
|
||||
connected bool
|
||||
withoutResponses bool
|
||||
connected bool
|
||||
rssi int
|
||||
|
||||
btAdptCreator btAdptCreatorFunc
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
|
||||
// NewAdaptor returns a new Bluetooth LE client adaptor given an address
|
||||
func NewAdaptor(address string) *Adaptor {
|
||||
return &Adaptor{
|
||||
name: gobot.DefaultName("BLEClient"),
|
||||
address: address,
|
||||
AdapterName: "default",
|
||||
connected: false,
|
||||
withoutResponses: false,
|
||||
characteristics: make(map[string]bluetooth.DeviceCharacteristic),
|
||||
// NewAdaptor returns a new Adaptor given an identifier. The identifier can be the address or the name.
|
||||
//
|
||||
// Supported options:
|
||||
//
|
||||
// "WithAdaptorDebug"
|
||||
// "WithAdaptorScanTimeout"
|
||||
func NewAdaptor(identifier string, opts ...optionApplier) *Adaptor {
|
||||
cfg := configuration{
|
||||
scanTimeout: 10 * time.Minute,
|
||||
sleepAfterDisconnect: 500 * time.Millisecond,
|
||||
}
|
||||
|
||||
a := Adaptor{
|
||||
name: gobot.DefaultName("BLEClient"),
|
||||
identifier: identifier,
|
||||
cfg: &cfg,
|
||||
characteristics: make(map[string]bluetoothExtCharacteristicer),
|
||||
btAdptCreator: newBtAdapter,
|
||||
mutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o.apply(a.cfg)
|
||||
}
|
||||
|
||||
return &a
|
||||
}
|
||||
|
||||
// Name returns the name for the adaptor
|
||||
func (a *Adaptor) Name() string { return a.name }
|
||||
// WithDebug switch on some debug messages.
|
||||
func WithDebug() debugOption {
|
||||
return debugOption(true)
|
||||
}
|
||||
|
||||
// WithScanTimeout substitute the default scan timeout of 10 min.
|
||||
func WithScanTimeout(timeout time.Duration) scanTimeoutOption {
|
||||
return scanTimeoutOption(timeout)
|
||||
}
|
||||
|
||||
// Name returns the name for the adaptor and after the connection is done, the name of the device
|
||||
func (a *Adaptor) Name() string {
|
||||
if a.btDevice != nil {
|
||||
return a.btDevice.name()
|
||||
}
|
||||
return a.name
|
||||
}
|
||||
|
||||
// SetName sets the name for the adaptor
|
||||
func (a *Adaptor) SetName(n string) { a.name = n }
|
||||
|
||||
// Address returns the Bluetooth LE address for the adaptor
|
||||
func (a *Adaptor) Address() string { return a.address }
|
||||
// Address returns the Bluetooth LE address of the device if connected, otherwise the identifier
|
||||
func (a *Adaptor) Address() string {
|
||||
if a.btDevice != nil {
|
||||
return a.btDevice.address()
|
||||
}
|
||||
|
||||
return a.identifier
|
||||
}
|
||||
|
||||
// RSSI returns the Bluetooth LE RSSI value at the moment of connecting the adaptor
|
||||
func (a *Adaptor) RSSI() int { return a.rssi }
|
||||
|
||||
// WithoutResponses sets if the adaptor should expect responses after
|
||||
// writing characteristics for this device
|
||||
func (a *Adaptor) WithoutResponses(use bool) { a.withoutResponses = use }
|
||||
// writing characteristics for this device (has no effect at the moment).
|
||||
func (a *Adaptor) WithoutResponses(bool) {}
|
||||
|
||||
// Connect initiates a connection to the BLE peripheral. Returns true on successful connection.
|
||||
// Connect initiates a connection to the BLE peripheral.
|
||||
func (a *Adaptor) Connect() error {
|
||||
bleMutex.Lock()
|
||||
defer bleMutex.Unlock()
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
var err error
|
||||
// enable adaptor
|
||||
a.adpt, err = getBLEAdapter(a.AdapterName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't get adapter %s: %w", a.AdapterName, err)
|
||||
|
||||
if a.cfg.debug {
|
||||
fmt.Println("[Connect]: enable adaptor...")
|
||||
}
|
||||
|
||||
// handle address
|
||||
a.addr.Set(a.Address())
|
||||
|
||||
// scan for the address
|
||||
ch := make(chan bluetooth.ScanResult, 1)
|
||||
err = a.adpt.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) {
|
||||
if result.Address.String() == a.Address() {
|
||||
if err := a.adpt.StopScan(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
a.SetName(result.LocalName())
|
||||
ch <- result
|
||||
// for re-connect, the adapter is already known
|
||||
if a.btAdpt == nil {
|
||||
a.btAdpt = a.btAdptCreator(bluetooth.DefaultAdapter, a.cfg.debug)
|
||||
if err := a.btAdpt.enable(); err != nil {
|
||||
return fmt.Errorf("can't get adapter default: %w", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if a.cfg.debug {
|
||||
fmt.Printf("[Connect]: scan %s for the identifier '%s'...\n", a.cfg.scanTimeout, a.identifier)
|
||||
}
|
||||
|
||||
result, err := a.btAdpt.scan(a.identifier, a.cfg.scanTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// wait to connect to peripheral device
|
||||
result := <-ch
|
||||
a.device, err = a.adpt.Connect(result.Address, bluetooth.ConnectionParams{})
|
||||
if a.cfg.debug {
|
||||
fmt.Printf("[Connect]: connect to peripheral device with address %s...\n", result.Address)
|
||||
}
|
||||
|
||||
dev, err := a.btAdpt.connect(result.Address, result.LocalName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get all services/characteristics
|
||||
srvcs, err := a.device.DiscoverServices(nil)
|
||||
a.rssi = int(result.RSSI)
|
||||
a.btDevice = dev
|
||||
|
||||
if a.cfg.debug {
|
||||
fmt.Println("[Connect]: get all services/characteristics...")
|
||||
}
|
||||
services, err := a.btDevice.discoverServices(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, srvc := range srvcs {
|
||||
chars, err := srvc.DiscoverCharacteristics(nil)
|
||||
for _, service := range services {
|
||||
if a.cfg.debug {
|
||||
fmt.Printf("[Connect]: service found: %s\n", service)
|
||||
}
|
||||
chars, err := service.DiscoverCharacteristics(nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
for _, char := range chars {
|
||||
a.characteristics[char.UUID().String()] = char
|
||||
if a.cfg.debug {
|
||||
fmt.Printf("[Connect]: characteristic found: %s\n", char)
|
||||
}
|
||||
c := char // to prevent implicit memory aliasing in for loop, before go 1.22
|
||||
a.characteristics[char.UUID().String()] = &c
|
||||
}
|
||||
}
|
||||
|
||||
if a.cfg.debug {
|
||||
fmt.Println("[Connect]: connected")
|
||||
}
|
||||
a.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 (a *Adaptor) Reconnect() error {
|
||||
if a.connected {
|
||||
if err := a.Disconnect(); err != nil {
|
||||
|
@ -126,10 +182,17 @@ func (a *Adaptor) Reconnect() error {
|
|||
return a.Connect()
|
||||
}
|
||||
|
||||
// Disconnect terminates the connection to the BLE peripheral. Returns true on successful disconnect.
|
||||
// Disconnect terminates the connection to the BLE peripheral.
|
||||
func (a *Adaptor) Disconnect() error {
|
||||
err := a.device.Disconnect()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if a.cfg.debug {
|
||||
fmt.Println("[Disconnect]: disconnect...")
|
||||
}
|
||||
err := a.btDevice.disconnect()
|
||||
time.Sleep(a.cfg.sleepAfterDisconnect)
|
||||
a.connected = false
|
||||
if a.cfg.debug {
|
||||
fmt.Println("[Disconnect]: disconnected")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -138,74 +201,59 @@ func (a *Adaptor) Finalize() error {
|
|||
return a.Disconnect()
|
||||
}
|
||||
|
||||
// ReadCharacteristic returns bytes from the BLE device for the
|
||||
// requested characteristic uuid
|
||||
// ReadCharacteristic returns bytes from the BLE device for the requested characteristic UUID.
|
||||
// The UUID can be given as 16-bit or 128-bit (with or without dashes) value.
|
||||
func (a *Adaptor) ReadCharacteristic(cUUID string) ([]byte, error) {
|
||||
if !a.connected {
|
||||
return nil, fmt.Errorf("Cannot read from BLE device until connected")
|
||||
return nil, fmt.Errorf("cannot read from BLE device until connected")
|
||||
}
|
||||
|
||||
cUUID = convertUUID(cUUID)
|
||||
|
||||
if char, ok := a.characteristics[cUUID]; ok {
|
||||
buf := make([]byte, 255)
|
||||
n, err := char.Read(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:n], nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unknown characteristic: %s", cUUID)
|
||||
}
|
||||
|
||||
// WriteCharacteristic writes bytes to the BLE device for the
|
||||
// requested service and characteristic
|
||||
func (a *Adaptor) WriteCharacteristic(cUUID string, data []byte) error {
|
||||
if !a.connected {
|
||||
return fmt.Errorf("Cannot write to BLE device until connected")
|
||||
}
|
||||
|
||||
cUUID = convertUUID(cUUID)
|
||||
|
||||
if char, ok := a.characteristics[cUUID]; ok {
|
||||
_, err := char.WriteWithoutResponse(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Unknown characteristic: %s", cUUID)
|
||||
}
|
||||
|
||||
// Subscribe subscribes to notifications from the BLE device for the
|
||||
// requested service and characteristic
|
||||
func (a *Adaptor) Subscribe(cUUID string, f func([]byte)) error {
|
||||
if !a.connected {
|
||||
return fmt.Errorf("Cannot subscribe to BLE device until connected")
|
||||
}
|
||||
|
||||
cUUID = convertUUID(cUUID)
|
||||
|
||||
if char, ok := a.characteristics[cUUID]; ok {
|
||||
return char.EnableNotifications(f)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
currentAdapter = bluetooth.DefaultAdapter
|
||||
err := currentAdapter.Enable()
|
||||
cUUID, err := convertUUID(cUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return currentAdapter, nil
|
||||
if chara, ok := a.characteristics[cUUID]; ok {
|
||||
return readFromCharacteristic(chara)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown characteristic: %s", cUUID)
|
||||
}
|
||||
|
||||
// WriteCharacteristic writes bytes to the BLE device for the requested characteristic UUID.
|
||||
// The UUID can be given as 16-bit or 128-bit (with or without dashes) value.
|
||||
func (a *Adaptor) WriteCharacteristic(cUUID string, data []byte) error {
|
||||
if !a.connected {
|
||||
return fmt.Errorf("cannot write to BLE device until connected")
|
||||
}
|
||||
|
||||
cUUID, err := convertUUID(cUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chara, ok := a.characteristics[cUUID]; ok {
|
||||
return writeToCharacteristicWithoutResponse(chara, data)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown characteristic: %s", cUUID)
|
||||
}
|
||||
|
||||
// Subscribe subscribes to notifications from the BLE device for the requested characteristic UUID.
|
||||
// The UUID can be given as 16-bit or 128-bit (with or without dashes) value.
|
||||
func (a *Adaptor) Subscribe(cUUID string, f func(data []byte)) error {
|
||||
if !a.connected {
|
||||
return fmt.Errorf("cannot subscribe to BLE device until connected")
|
||||
}
|
||||
|
||||
cUUID, err := convertUUID(cUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chara, ok := a.characteristics[cUUID]; ok {
|
||||
return enableNotificationsForCharacteristic(chara, f)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown characteristic: %s", cUUID)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package bleclient
|
||||
|
||||
import "time"
|
||||
|
||||
// optionApplier needs to be implemented by each configurable option type
|
||||
type optionApplier interface {
|
||||
apply(cfg *configuration)
|
||||
}
|
||||
|
||||
// debugOption is the type for applying the debug switch on or off.
|
||||
type debugOption bool
|
||||
|
||||
// scanTimeoutOption is the type for applying another timeout than the default 10 min.
|
||||
type scanTimeoutOption time.Duration
|
||||
|
||||
func (o debugOption) String() string {
|
||||
return "debug option for BLE client adaptors"
|
||||
}
|
||||
|
||||
func (o scanTimeoutOption) String() string {
|
||||
return "scan timeout option for BLE client adaptors"
|
||||
}
|
||||
|
||||
func (o debugOption) apply(cfg *configuration) {
|
||||
cfg.debug = bool(o)
|
||||
}
|
||||
|
||||
func (o scanTimeoutOption) apply(cfg *configuration) {
|
||||
cfg.scanTimeout = time.Duration(o)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package bleclient
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWithDebug(t *testing.T) {
|
||||
// This is a general test, that options are applied by using the WithDebug() option.
|
||||
// All other configuration options can also be tested by With..(val).apply(cfg).
|
||||
// arrange & act
|
||||
a := NewAdaptor("address", WithDebug())
|
||||
// assert
|
||||
assert.True(t, a.cfg.debug)
|
||||
}
|
||||
|
||||
func TestWithScanTimeout(t *testing.T) {
|
||||
// arrange
|
||||
newTimeout := 2 * time.Second
|
||||
cfg := &configuration{scanTimeout: 10 * time.Second}
|
||||
// act
|
||||
WithScanTimeout(newTimeout).apply(cfg)
|
||||
// assert
|
||||
assert.Equal(t, newTimeout, cfg.scanTimeout)
|
||||
}
|
|
@ -3,8 +3,10 @@ package bleclient
|
|||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gobot.io/x/gobot/v2"
|
||||
)
|
||||
|
@ -25,3 +27,381 @@ func TestName(t *testing.T) {
|
|||
a.SetName("awesome")
|
||||
assert.Equal(t, "awesome", a.Name())
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
const (
|
||||
scanTimeout = 5 * time.Millisecond
|
||||
deviceName = "hello"
|
||||
deviceAddress = "11:22:44:AA:BB:CC"
|
||||
rssi = 56
|
||||
)
|
||||
tests := map[string]struct {
|
||||
identifier string
|
||||
extAdapter *btTestAdapter
|
||||
extDevice *btTestDevice
|
||||
wantAddress string
|
||||
wantName string
|
||||
wantErr string
|
||||
}{
|
||||
"connect_by_address": {
|
||||
identifier: deviceAddress,
|
||||
extAdapter: &btTestAdapter{
|
||||
deviceAddress: deviceAddress,
|
||||
rssi: rssi,
|
||||
payload: &btTestPayload{name: deviceName},
|
||||
},
|
||||
extDevice: &btTestDevice{},
|
||||
wantAddress: deviceAddress,
|
||||
wantName: deviceName,
|
||||
},
|
||||
"connect_by_name": {
|
||||
identifier: deviceName,
|
||||
extAdapter: &btTestAdapter{
|
||||
deviceAddress: deviceAddress,
|
||||
rssi: rssi,
|
||||
payload: &btTestPayload{name: deviceName},
|
||||
},
|
||||
extDevice: &btTestDevice{},
|
||||
wantAddress: deviceAddress,
|
||||
wantName: deviceName,
|
||||
},
|
||||
"error_enable": {
|
||||
extAdapter: &btTestAdapter{
|
||||
simulateEnableErr: true,
|
||||
},
|
||||
wantName: "BLEClient",
|
||||
wantErr: "can't get adapter default: adapter enable error",
|
||||
},
|
||||
"error_scan": {
|
||||
extAdapter: &btTestAdapter{
|
||||
simulateScanErr: true,
|
||||
},
|
||||
wantName: "BLEClient",
|
||||
wantErr: "scan error",
|
||||
},
|
||||
"error_stop_scan": {
|
||||
extAdapter: &btTestAdapter{
|
||||
deviceAddress: deviceAddress,
|
||||
payload: &btTestPayload{},
|
||||
simulateStopScanErr: true,
|
||||
},
|
||||
wantName: "BLEClient",
|
||||
wantErr: "stop scan error",
|
||||
},
|
||||
"error_timeout_long_delay": {
|
||||
extAdapter: &btTestAdapter{
|
||||
deviceAddress: deviceAddress,
|
||||
payload: &btTestPayload{},
|
||||
scanDelay: 2 * scanTimeout,
|
||||
},
|
||||
wantName: "BLEClient",
|
||||
wantErr: "scan timeout (5ms) elapsed",
|
||||
},
|
||||
"error_timeout_bad_identifier": {
|
||||
identifier: "bad_identifier",
|
||||
extAdapter: &btTestAdapter{
|
||||
deviceAddress: deviceAddress,
|
||||
payload: &btTestPayload{},
|
||||
},
|
||||
wantAddress: "bad_identifier",
|
||||
wantName: "BLEClient",
|
||||
wantErr: "scan timeout (5ms) elapsed",
|
||||
},
|
||||
"error_connect": {
|
||||
extAdapter: &btTestAdapter{
|
||||
deviceAddress: deviceAddress,
|
||||
payload: &btTestPayload{},
|
||||
simulateConnectErr: true,
|
||||
},
|
||||
wantName: "BLEClient",
|
||||
wantErr: "adapter connect error",
|
||||
},
|
||||
"error_discovery_services": {
|
||||
identifier: "disco_err",
|
||||
extAdapter: &btTestAdapter{
|
||||
deviceAddress: deviceAddress,
|
||||
payload: &btTestPayload{name: "disco_err"},
|
||||
},
|
||||
extDevice: &btTestDevice{
|
||||
simulateDiscoverServicesErr: true,
|
||||
},
|
||||
wantAddress: deviceAddress,
|
||||
wantName: "disco_err",
|
||||
wantErr: "device discover services error",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := NewAdaptor(tc.identifier)
|
||||
btdc := func(_ bluetoothExtDevicer, address, name string) *btDevice {
|
||||
return &btDevice{extDevice: tc.extDevice, devAddress: address, devName: name}
|
||||
}
|
||||
btac := func(bluetoothExtAdapterer, bool) *btAdapter {
|
||||
return &btAdapter{extAdapter: tc.extAdapter, btDeviceCreator: btdc}
|
||||
}
|
||||
a.btAdptCreator = btac
|
||||
a.cfg.scanTimeout = scanTimeout // to speed up test
|
||||
// act
|
||||
err := a.Connect()
|
||||
// assert
|
||||
if tc.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.wantName, a.Name())
|
||||
assert.Equal(t, tc.wantAddress, a.Address())
|
||||
assert.Equal(t, rssi, a.RSSI())
|
||||
assert.True(t, a.connected)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.wantErr)
|
||||
assert.Contains(t, a.Name(), tc.wantName)
|
||||
assert.Equal(t, tc.wantAddress, a.Address())
|
||||
assert.False(t, a.connected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconnect(t *testing.T) {
|
||||
const (
|
||||
scanTimeout = 5 * time.Millisecond
|
||||
deviceName = "hello"
|
||||
deviceAddress = "11:22:44:AA:BB:CC"
|
||||
rssi = 56
|
||||
)
|
||||
tests := map[string]struct {
|
||||
extAdapter *btTestAdapter
|
||||
extDevice *btTestDevice
|
||||
wasConnected bool
|
||||
wantErr string
|
||||
}{
|
||||
"reconnect_not_connected": {
|
||||
extAdapter: &btTestAdapter{
|
||||
deviceAddress: deviceAddress,
|
||||
rssi: rssi,
|
||||
payload: &btTestPayload{name: deviceName},
|
||||
},
|
||||
extDevice: &btTestDevice{},
|
||||
},
|
||||
"reconnect_was_connected": {
|
||||
extAdapter: &btTestAdapter{
|
||||
deviceAddress: deviceAddress,
|
||||
rssi: rssi,
|
||||
payload: &btTestPayload{name: deviceName},
|
||||
},
|
||||
extDevice: &btTestDevice{},
|
||||
wasConnected: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := NewAdaptor(deviceAddress)
|
||||
btdc := func(_ bluetoothExtDevicer, address, name string) *btDevice {
|
||||
return &btDevice{extDevice: tc.extDevice, devAddress: address, devName: name}
|
||||
}
|
||||
a.btAdpt = &btAdapter{extAdapter: tc.extAdapter, btDeviceCreator: btdc}
|
||||
a.cfg.scanTimeout = scanTimeout // to speed up test in case of errors
|
||||
a.cfg.sleepAfterDisconnect = 0 // to speed up test
|
||||
if tc.wasConnected {
|
||||
a.btDevice = btdc(nil, "", "")
|
||||
a.connected = tc.wasConnected
|
||||
}
|
||||
// act
|
||||
err := a.Reconnect()
|
||||
// assert
|
||||
if tc.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rssi, a.RSSI())
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.wantErr)
|
||||
}
|
||||
assert.True(t, a.connected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalize(t *testing.T) {
|
||||
// this also tests Disconnect()
|
||||
tests := map[string]struct {
|
||||
extDevice *btTestDevice
|
||||
wantErr string
|
||||
}{
|
||||
"disconnect": {
|
||||
extDevice: &btTestDevice{},
|
||||
},
|
||||
"error_disconnect": {
|
||||
extDevice: &btTestDevice{
|
||||
simulateDisconnectErr: true,
|
||||
},
|
||||
wantErr: "device disconnect error",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := NewAdaptor("")
|
||||
a.cfg.sleepAfterDisconnect = 0 // to speed up test
|
||||
a.btDevice = &btDevice{extDevice: tc.extDevice}
|
||||
// act
|
||||
err := a.Finalize()
|
||||
// assert
|
||||
if tc.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.wantErr)
|
||||
}
|
||||
assert.False(t, a.connected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCharacteristic(t *testing.T) {
|
||||
const uuid = "00001234-0000-1000-8000-00805f9b34fb"
|
||||
tests := map[string]struct {
|
||||
inUUID string
|
||||
chara *btTestChara
|
||||
notConnected bool
|
||||
want []byte
|
||||
wantErr string
|
||||
}{
|
||||
"read_ok": {
|
||||
inUUID: uuid,
|
||||
chara: &btTestChara{readData: []byte{1, 2, 3}},
|
||||
want: []byte{1, 2, 3},
|
||||
},
|
||||
"error_not_connected": {
|
||||
notConnected: true,
|
||||
wantErr: "cannot read from BLE device until connected",
|
||||
},
|
||||
"error_bad_chara": {
|
||||
inUUID: "gag1",
|
||||
wantErr: "'gag1' is not a valid 16-bit Bluetooth UUID",
|
||||
},
|
||||
"error_unknown_chara": {
|
||||
inUUID: uuid,
|
||||
wantErr: "unknown characteristic: 00001234-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := NewAdaptor("")
|
||||
if tc.chara != nil {
|
||||
a.characteristics[uuid] = tc.chara
|
||||
}
|
||||
a.connected = !tc.notConnected
|
||||
// act
|
||||
got, err := a.ReadCharacteristic(tc.inUUID)
|
||||
// assert
|
||||
if tc.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.wantErr)
|
||||
}
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCharacteristic(t *testing.T) {
|
||||
const uuid = "00004321-0000-1000-8000-00805f9b34fb"
|
||||
tests := map[string]struct {
|
||||
inUUID string
|
||||
inData []byte
|
||||
notConnected bool
|
||||
chara *btTestChara
|
||||
want []byte
|
||||
wantErr string
|
||||
}{
|
||||
"write_ok": {
|
||||
inUUID: uuid,
|
||||
inData: []byte{3, 2, 1},
|
||||
chara: &btTestChara{},
|
||||
want: []byte{3, 2, 1},
|
||||
},
|
||||
"error_not_connected": {
|
||||
notConnected: true,
|
||||
wantErr: "cannot write to BLE device until connected",
|
||||
},
|
||||
"error_bad_chara": {
|
||||
inUUID: "gag2",
|
||||
wantErr: "'gag2' is not a valid 16-bit Bluetooth UUID",
|
||||
},
|
||||
"error_unknown_chara": {
|
||||
inUUID: uuid,
|
||||
wantErr: "unknown characteristic: 00004321-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := NewAdaptor("")
|
||||
if tc.chara != nil {
|
||||
a.characteristics[uuid] = tc.chara
|
||||
}
|
||||
a.connected = !tc.notConnected
|
||||
// act
|
||||
err := a.WriteCharacteristic(tc.inUUID, tc.inData)
|
||||
// assert
|
||||
if tc.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, tc.chara.writtenData)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscribe(t *testing.T) {
|
||||
const uuid = "00004321-0000-1000-8000-00805f9b34fb"
|
||||
tests := map[string]struct {
|
||||
inUUID string
|
||||
notConnected bool
|
||||
chara *btTestChara
|
||||
want []byte
|
||||
wantErr string
|
||||
}{
|
||||
"subscribe_ok": {
|
||||
inUUID: uuid,
|
||||
chara: &btTestChara{},
|
||||
want: []byte{3, 4, 5},
|
||||
},
|
||||
"error_not_connected": {
|
||||
notConnected: true,
|
||||
wantErr: "cannot subscribe to BLE device until connected",
|
||||
},
|
||||
"error_bad_chara": {
|
||||
inUUID: "gag2",
|
||||
wantErr: "'gag2' is not a valid 16-bit Bluetooth UUID",
|
||||
},
|
||||
"error_unknown_chara": {
|
||||
inUUID: uuid,
|
||||
wantErr: "unknown characteristic: 00004321-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := NewAdaptor("")
|
||||
if tc.chara != nil {
|
||||
a.characteristics[uuid] = tc.chara
|
||||
}
|
||||
a.connected = !tc.notConnected
|
||||
var got []byte
|
||||
notificationFunc := func(data []byte) {
|
||||
got = append(got, data...)
|
||||
}
|
||||
// act
|
||||
err := a.Subscribe(tc.inUUID, notificationFunc)
|
||||
// assert
|
||||
if tc.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
tc.chara.notificationFunc([]byte{3, 4, 5})
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.wantErr)
|
||||
}
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
package bleclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/bluetooth"
|
||||
)
|
||||
|
||||
// bluetoothExtDevicer is the interface usually implemented by bluetooth.Device
|
||||
type bluetoothExtDevicer interface {
|
||||
DiscoverServices(uuids []bluetooth.UUID) ([]bluetooth.DeviceService, error)
|
||||
Disconnect() error
|
||||
}
|
||||
|
||||
// bluetoothExtAdapterer is the interface usually implemented by bluetooth.Adapter
|
||||
type bluetoothExtAdapterer interface {
|
||||
Enable() error
|
||||
Scan(callback func(*bluetooth.Adapter, bluetooth.ScanResult)) error
|
||||
StopScan() error
|
||||
Connect(address bluetooth.Address, params bluetooth.ConnectionParams) (*bluetooth.Device, error)
|
||||
}
|
||||
|
||||
type bluetoothExtCharacteristicer interface {
|
||||
Read(data []byte) (int, error)
|
||||
WriteWithoutResponse(p []byte) (n int, err error)
|
||||
EnableNotifications(callback func(buf []byte)) error
|
||||
}
|
||||
|
||||
// btAdptCreatorFunc is just a convenience type, used in the BLE client to ensure testability
|
||||
type btAdptCreatorFunc func(bluetoothExtAdapterer, bool) *btAdapter
|
||||
|
||||
// btAdapter is the wrapper for an external adapter implementation
|
||||
type btAdapter struct {
|
||||
extAdapter bluetoothExtAdapterer
|
||||
btDeviceCreator func(bluetoothExtDevicer, string, string) *btDevice
|
||||
debug bool
|
||||
}
|
||||
|
||||
// newBtAdapter creates a new wrapper around the given external implementation
|
||||
func newBtAdapter(a bluetoothExtAdapterer, debug bool) *btAdapter {
|
||||
bta := btAdapter{
|
||||
extAdapter: a,
|
||||
btDeviceCreator: newBtDevice,
|
||||
debug: debug,
|
||||
}
|
||||
|
||||
return &bta
|
||||
}
|
||||
|
||||
// Enable configures the BLE stack. It must be called before any Bluetooth-related calls (unless otherwise indicated).
|
||||
// It pass through the function of the external implementation.
|
||||
func (bta *btAdapter) enable() error {
|
||||
return bta.extAdapter.Enable()
|
||||
}
|
||||
|
||||
// StopScan stops any in-progress scan. It can be called from within a Scan callback to stop the current scan.
|
||||
// If no scan is in progress, an error will be returned.
|
||||
func (bta *btAdapter) stopScan() error {
|
||||
return bta.extAdapter.StopScan()
|
||||
}
|
||||
|
||||
// Connect starts a connection attempt to the given peripheral device address.
|
||||
//
|
||||
// On Linux and Windows, the IsRandom part of the address is ignored.
|
||||
func (bta *btAdapter) connect(address bluetooth.Address, devName string) (*btDevice, error) {
|
||||
extDev, err := bta.extAdapter.Connect(address, bluetooth.ConnectionParams{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bta.btDeviceCreator(extDev, address.String(), devName), nil
|
||||
}
|
||||
|
||||
// Scan starts a BLE scan for the given identifier (address or name).
|
||||
func (bta *btAdapter) scan(identifier string, scanTimeout time.Duration) (*bluetooth.ScanResult, error) {
|
||||
resultChan := make(chan bluetooth.ScanResult, 1)
|
||||
errChan := make(chan error)
|
||||
|
||||
go func() {
|
||||
callback := func(_ *bluetooth.Adapter, result bluetooth.ScanResult) {
|
||||
if bta.debug {
|
||||
fmt.Printf("[scan result]: address: '%s', rssi: %d, name: '%s', manufacturer: %v\n",
|
||||
result.Address, result.RSSI, result.LocalName(), result.ManufacturerData())
|
||||
}
|
||||
if result.Address.String() == identifier || result.LocalName() == identifier {
|
||||
resultChan <- result
|
||||
}
|
||||
}
|
||||
err := bta.extAdapter.Scan(callback)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if err := bta.stopScan(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
case err := <-errChan:
|
||||
return nil, err
|
||||
case <-time.After(scanTimeout):
|
||||
_ = bta.stopScan()
|
||||
return nil, fmt.Errorf("scan timeout (%s) elapsed", scanTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// btDevice is the wrapper for an external device implementation
|
||||
type btDevice struct {
|
||||
extDevice bluetoothExtDevicer
|
||||
devAddress string
|
||||
devName string
|
||||
}
|
||||
|
||||
// newBtDevice creates a new wrapper around the given external implementation
|
||||
func newBtDevice(d bluetoothExtDevicer, address, name string) *btDevice {
|
||||
return &btDevice{extDevice: d, devAddress: address, devName: name}
|
||||
}
|
||||
|
||||
func (btd *btDevice) name() string { return btd.devName }
|
||||
|
||||
func (btd *btDevice) address() string { return btd.devAddress }
|
||||
|
||||
func (btd *btDevice) discoverServices(uuids []bluetooth.UUID) ([]bluetooth.DeviceService, error) {
|
||||
return btd.extDevice.DiscoverServices(uuids)
|
||||
}
|
||||
|
||||
// Disconnect from the BLE device. This method is non-blocking and does not wait until the connection is fully gone.
|
||||
func (btd *btDevice) disconnect() error {
|
||||
return btd.extDevice.Disconnect()
|
||||
}
|
||||
|
||||
func readFromCharacteristic(chara bluetoothExtCharacteristicer) ([]byte, error) {
|
||||
buf := make([]byte, 255)
|
||||
n, err := chara.Read(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:n], nil
|
||||
}
|
||||
|
||||
func writeToCharacteristicWithoutResponse(chara bluetoothExtCharacteristicer, data []byte) error {
|
||||
if _, err := chara.WriteWithoutResponse(data); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func enableNotificationsForCharacteristic(chara bluetoothExtCharacteristicer, f func(data []byte)) error {
|
||||
return chara.EnableNotifications(f)
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package bleclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/bluetooth"
|
||||
)
|
||||
|
||||
type btTestAdapter struct {
|
||||
deviceAddress string
|
||||
rssi int16
|
||||
scanDelay time.Duration
|
||||
payload *btTestPayload
|
||||
simulateEnableErr bool
|
||||
simulateScanErr bool
|
||||
simulateStopScanErr bool
|
||||
simulateConnectErr bool
|
||||
}
|
||||
|
||||
func (bta *btTestAdapter) Enable() error {
|
||||
if bta.simulateEnableErr {
|
||||
return fmt.Errorf("adapter enable error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bta *btTestAdapter) Scan(callback func(*bluetooth.Adapter, bluetooth.ScanResult)) error {
|
||||
if bta.simulateScanErr {
|
||||
return fmt.Errorf("adapter scan error")
|
||||
}
|
||||
|
||||
devAddr, err := bluetooth.ParseMAC(bta.deviceAddress)
|
||||
if err != nil {
|
||||
// normally this error should not happen in test
|
||||
return err
|
||||
}
|
||||
time.Sleep(bta.scanDelay)
|
||||
|
||||
a := bluetooth.Address{MACAddress: bluetooth.MACAddress{MAC: devAddr}}
|
||||
r := bluetooth.ScanResult{Address: a, RSSI: bta.rssi, AdvertisementPayload: bta.payload}
|
||||
callback(nil, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bta *btTestAdapter) StopScan() error {
|
||||
if bta.simulateStopScanErr {
|
||||
return fmt.Errorf("adapter stop scan error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bta *btTestAdapter) Connect(_ bluetooth.Address, _ bluetooth.ConnectionParams) (*bluetooth.Device, error) {
|
||||
if bta.simulateConnectErr {
|
||||
return nil, fmt.Errorf("adapter connect error")
|
||||
}
|
||||
|
||||
//nolint:nilnil // for this test we can not return a *bluetooth.Device
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type btTestPayload struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (ptp *btTestPayload) LocalName() string { return ptp.name }
|
||||
|
||||
func (*btTestPayload) HasServiceUUID(bluetooth.UUID) bool { return true }
|
||||
|
||||
func (*btTestPayload) Bytes() []byte { return nil }
|
||||
|
||||
func (*btTestPayload) ManufacturerData() map[uint16][]byte { return nil }
|
||||
|
||||
type btTestDevice struct {
|
||||
simulateDiscoverServicesErr bool
|
||||
simulateDisconnectErr bool
|
||||
}
|
||||
|
||||
func (btd *btTestDevice) DiscoverServices(_ []bluetooth.UUID) ([]bluetooth.DeviceService, error) {
|
||||
if btd.simulateDiscoverServicesErr {
|
||||
return nil, fmt.Errorf("device discover services error")
|
||||
}
|
||||
|
||||
// for this test we can not return any []bluetooth.DeviceService
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (btd *btTestDevice) Disconnect() error {
|
||||
if btd.simulateDisconnectErr {
|
||||
return fmt.Errorf("device disconnect error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type btTestChara struct {
|
||||
readData []byte
|
||||
writtenData []byte
|
||||
notificationFunc func(buf []byte)
|
||||
}
|
||||
|
||||
func (btc *btTestChara) Read(data []byte) (int, error) {
|
||||
copy(data, btc.readData)
|
||||
return len(btc.readData), nil
|
||||
}
|
||||
|
||||
func (btc *btTestChara) WriteWithoutResponse(data []byte) (int, error) {
|
||||
btc.writtenData = append(btc.writtenData, data...)
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (btc *btTestChara) EnableNotifications(callback func(buf []byte)) error {
|
||||
btc.notificationFunc = callback
|
||||
return nil
|
||||
}
|
|
@ -3,28 +3,38 @@ package bleclient
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"tinygo.org/x/bluetooth"
|
||||
)
|
||||
|
||||
func convertUUID(cUUID string) string {
|
||||
// convertUUID creates a common 128 bit UUID xxxxyyyy-0000-1000-8000-00805f9b34fb from a short 16 bit UUID by replacing
|
||||
// the yyyy fields. If the given ID is still an arbitrary long one but without dashes, the dashes will be added.
|
||||
// Additionally some simple checks for the resulting UUID will be done.
|
||||
func convertUUID(cUUID string) (string, error) {
|
||||
var uuid string
|
||||
switch len(cUUID) {
|
||||
case 4:
|
||||
// convert to full uuid from "22bb"
|
||||
uid, e := strconv.ParseUint("0x"+cUUID, 0, 16)
|
||||
if e != nil {
|
||||
return ""
|
||||
uid, err := strconv.ParseUint(cUUID, 16, 16)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("'%s' is not a valid 16-bit Bluetooth UUID: %v", cUUID, err)
|
||||
}
|
||||
|
||||
uuid := bluetooth.New16BitUUID(uint16(uid))
|
||||
return uuid.String()
|
||||
|
||||
return bluetooth.New16BitUUID(uint16(uid)).String(), nil
|
||||
case 32:
|
||||
// convert "22bb746f2bbd75542d6f726568705327"
|
||||
// to "22bb746f-2bbd-7554-2d6f-726568705327"
|
||||
return fmt.Sprintf("%s-%s-%s-%s-%s", cUUID[:8], cUUID[8:12], cUUID[12:16], cUUID[16:20],
|
||||
cUUID[20:32])
|
||||
// convert "22bb746f2bbd75542d6f726568705327" to "22bb746f-2bbd-7554-2d6f-726568705327"
|
||||
uuid = fmt.Sprintf("%s-%s-%s-%s-%s", cUUID[:8], cUUID[8:12], cUUID[12:16], cUUID[16:20], cUUID[20:])
|
||||
case 36:
|
||||
uuid = cUUID
|
||||
}
|
||||
|
||||
return cUUID
|
||||
if uuid != "" {
|
||||
id := strings.ReplaceAll(uuid, "-", "")
|
||||
_, errHigh := strconv.ParseUint(id[:16], 16, 64)
|
||||
_, errLow := strconv.ParseUint(id[16:], 16, 64)
|
||||
if errHigh == nil && errLow == nil {
|
||||
return uuid, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("'%s' is not a valid 128-bit Bluetooth UUID", cUUID)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package bleclient
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_convertUUID(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
"32_bit": {
|
||||
input: "12345678-4321-1234-4321-123456789abc",
|
||||
want: "12345678-4321-1234-4321-123456789abc",
|
||||
},
|
||||
"16_bit": {
|
||||
input: "12f4",
|
||||
want: "000012f4-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
"32_bit_without_dashes": {
|
||||
input: "0123456789abcdef012345678abcdefc",
|
||||
want: "01234567-89ab-cdef-0123-45678abcdefc",
|
||||
},
|
||||
"error_bad_chacters_16bit": {
|
||||
input: "123g",
|
||||
wantErr: "'123g' is not a valid 16-bit Bluetooth UUID",
|
||||
},
|
||||
"error_bad_chacters_32bit": {
|
||||
input: "12345678-4321-1234-4321-123456789abg",
|
||||
wantErr: "'12345678-4321-1234-4321-123456789abg' is not a valid 128-bit Bluetooth UUID",
|
||||
},
|
||||
"error_too_long": {
|
||||
input: "12345678-4321-1234-4321-123456789abcd",
|
||||
wantErr: "'12345678-4321-1234-4321-123456789abcd' is not a valid 128-bit Bluetooth UUID",
|
||||
},
|
||||
"error_invalid": {
|
||||
input: "12345",
|
||||
wantErr: "'12345' is not a valid 128-bit Bluetooth UUID",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// act
|
||||
got, err := convertUUID(tc.input)
|
||||
// assert
|
||||
if tc.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.wantErr)
|
||||
}
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue