155 lines
4.7 KiB
Go
155 lines
4.7 KiB
Go
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)
|
|
}
|