2020-08-27 23:22:27 +03:00
|
|
|
package bluetooth
|
|
|
|
|
2020-08-28 09:35:13 +03:00
|
|
|
import (
|
|
|
|
"errors"
|
2020-08-28 14:31:17 +03:00
|
|
|
"fmt"
|
|
|
|
"time"
|
2020-08-28 09:35:13 +03:00
|
|
|
|
2022-08-30 20:06:18 +03:00
|
|
|
"github.com/tinygo-org/cbgo"
|
2020-08-28 09:35:13 +03:00
|
|
|
)
|
|
|
|
|
2023-05-03 06:55:14 +03:00
|
|
|
// default connection timeout
|
|
|
|
const defaultConnectionTimeout time.Duration = 10 * time.Second
|
|
|
|
|
2020-08-29 15:43:11 +03:00
|
|
|
// Address contains a Bluetooth address which on macOS is a UUID.
|
2020-08-28 13:40:03 +03:00
|
|
|
type Address struct {
|
2020-08-28 14:31:17 +03:00
|
|
|
// UUID since this is macOS.
|
2020-08-28 13:40:03 +03:00
|
|
|
UUID
|
|
|
|
}
|
|
|
|
|
2020-08-28 14:31:17 +03:00
|
|
|
// IsRandom ignored on macOS.
|
2020-08-28 13:40:03 +03:00
|
|
|
func (ad Address) IsRandom() bool {
|
2020-08-28 14:31:17 +03:00
|
|
|
return false
|
2020-08-28 13:40:03 +03:00
|
|
|
}
|
|
|
|
|
2020-08-28 14:31:17 +03:00
|
|
|
// SetRandom ignored on macOS.
|
2023-04-27 17:54:43 +03:00
|
|
|
func (ad *Address) SetRandom(val bool) {
|
2020-08-28 13:40:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set the address
|
2023-04-27 17:54:43 +03:00
|
|
|
func (ad *Address) Set(val string) {
|
2020-09-20 12:28:39 +03:00
|
|
|
uuid, err := ParseUUID(val)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ad.UUID = uuid
|
2020-08-28 13:40:03 +03:00
|
|
|
}
|
|
|
|
|
2020-08-27 23:22:27 +03:00
|
|
|
// Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern
|
|
|
|
// is to cancel the scan when a particular device has been found.
|
|
|
|
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
|
2020-08-28 09:35:13 +03:00
|
|
|
if callback == nil {
|
|
|
|
return errors.New("must provide callback to Scan function")
|
|
|
|
}
|
|
|
|
|
2020-08-28 14:31:17 +03:00
|
|
|
if a.scanChan != nil {
|
2020-08-28 09:35:13 +03:00
|
|
|
return errors.New("already calling Scan function")
|
|
|
|
}
|
|
|
|
|
|
|
|
a.peripheralFoundHandler = callback
|
|
|
|
|
|
|
|
// Channel that will be closed when the scan is stopped.
|
|
|
|
// Detecting whether the scan is stopped can be done by doing a non-blocking
|
|
|
|
// read from it. If it succeeds, the scan is stopped.
|
2020-08-28 14:31:17 +03:00
|
|
|
a.scanChan = make(chan error)
|
2020-08-28 09:35:13 +03:00
|
|
|
|
|
|
|
a.cm.Scan(nil, &cbgo.CentralManagerScanOpts{
|
2020-08-28 14:31:17 +03:00
|
|
|
AllowDuplicates: false,
|
2020-08-28 09:35:13 +03:00
|
|
|
})
|
|
|
|
|
2020-08-28 14:31:17 +03:00
|
|
|
// Check whether the scan is stopped. This is necessary to avoid a race
|
|
|
|
// condition between the signal channel and the cancelScan channel when
|
|
|
|
// the callback calls StopScan() (no new callbacks may be called after
|
|
|
|
// StopScan is called).
|
|
|
|
select {
|
|
|
|
case <-a.scanChan:
|
|
|
|
close(a.scanChan)
|
|
|
|
a.scanChan = nil
|
|
|
|
return nil
|
2020-08-28 09:35:13 +03:00
|
|
|
}
|
2020-08-27 23:22:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 (a *Adapter) StopScan() error {
|
2020-08-28 14:31:17 +03:00
|
|
|
if a.scanChan == nil {
|
|
|
|
return errors.New("not calling Scan function")
|
2020-08-28 09:35:13 +03:00
|
|
|
}
|
|
|
|
|
2020-08-28 14:31:17 +03:00
|
|
|
a.scanChan <- nil
|
2020-08-28 09:35:13 +03:00
|
|
|
a.cm.StopScan()
|
|
|
|
|
2020-08-27 23:22:27 +03:00
|
|
|
return nil
|
|
|
|
}
|
2020-08-28 14:31:17 +03:00
|
|
|
|
|
|
|
// Device is a connection to a remote peripheral.
|
|
|
|
type Device struct {
|
2023-12-25 16:28:56 +03:00
|
|
|
*deviceInternal
|
|
|
|
}
|
|
|
|
|
|
|
|
type deviceInternal struct {
|
2020-08-29 16:01:34 +03:00
|
|
|
delegate *peripheralDelegate
|
2020-08-28 14:31:17 +03:00
|
|
|
|
2020-08-29 12:35:26 +03:00
|
|
|
cm cbgo.CentralManager
|
|
|
|
prph cbgo.Peripheral
|
|
|
|
|
|
|
|
servicesChan chan error
|
|
|
|
charsChan chan error
|
2020-08-29 17:15:24 +03:00
|
|
|
|
2023-06-06 06:49:10 +03:00
|
|
|
services map[UUID]DeviceService
|
2020-08-28 14:31:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Connect starts a connection attempt to the given peripheral device address.
|
2023-12-25 16:28:56 +03:00
|
|
|
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
|
2023-04-25 03:12:05 +03:00
|
|
|
uuid, err := cbgo.ParseUUID(address.UUID.String())
|
2020-08-28 14:31:17 +03:00
|
|
|
if err != nil {
|
2023-12-25 16:28:56 +03:00
|
|
|
return Device{}, err
|
2020-08-28 14:31:17 +03:00
|
|
|
}
|
|
|
|
prphs := a.cm.RetrievePeripheralsWithIdentifiers([]cbgo.UUID{uuid})
|
|
|
|
if len(prphs) == 0 {
|
2023-12-25 16:28:56 +03:00
|
|
|
return Device{}, fmt.Errorf("Connect failed: no peer with address: %s", address.UUID.String())
|
2020-08-28 14:31:17 +03:00
|
|
|
}
|
2021-03-17 02:49:19 +03:00
|
|
|
|
2023-05-03 06:55:14 +03:00
|
|
|
timeout := defaultConnectionTimeout
|
|
|
|
if params.ConnectionTimeout != 0 {
|
|
|
|
timeout = time.Duration(int64(params.ConnectionTimeout)*625) * time.Microsecond
|
|
|
|
}
|
|
|
|
|
2021-03-17 02:49:19 +03:00
|
|
|
id := prphs[0].Identifier().String()
|
|
|
|
prphCh := make(chan cbgo.Peripheral)
|
|
|
|
|
|
|
|
a.connectMap.Store(id, prphCh)
|
|
|
|
defer a.connectMap.Delete(id)
|
|
|
|
|
2020-08-28 14:31:17 +03:00
|
|
|
a.cm.Connect(prphs[0], nil)
|
2023-05-03 06:55:14 +03:00
|
|
|
timeoutTimer := time.NewTimer(timeout)
|
|
|
|
var connectionError error
|
|
|
|
|
|
|
|
for {
|
|
|
|
// wait on channel for connect
|
|
|
|
select {
|
|
|
|
case p := <-prphCh:
|
|
|
|
|
|
|
|
// check if we have received a disconnected peripheral
|
|
|
|
if p.State() == cbgo.PeripheralStateDisconnected {
|
2023-12-25 16:28:56 +03:00
|
|
|
return Device{}, connectionError
|
2023-05-03 06:55:14 +03:00
|
|
|
}
|
|
|
|
|
2023-12-25 16:28:56 +03:00
|
|
|
d := Device{
|
|
|
|
&deviceInternal{
|
|
|
|
cm: a.cm,
|
|
|
|
prph: p,
|
|
|
|
servicesChan: make(chan error),
|
|
|
|
charsChan: make(chan error),
|
|
|
|
},
|
2023-05-03 06:55:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
d.delegate = &peripheralDelegate{d: d}
|
|
|
|
p.SetDelegate(d.delegate)
|
|
|
|
|
|
|
|
a.connectHandler(address, true)
|
|
|
|
|
|
|
|
return d, nil
|
2023-05-04 11:45:58 +03:00
|
|
|
|
2023-05-03 06:55:14 +03:00
|
|
|
case <-timeoutTimer.C:
|
2023-05-04 11:45:58 +03:00
|
|
|
// we need to cancel the connection if we have timed out ourselves
|
|
|
|
a.cm.CancelConnect(prphs[0])
|
|
|
|
|
|
|
|
// record an error to use when the disconnect comes through later.
|
2023-05-03 06:55:14 +03:00
|
|
|
connectionError = errors.New("timeout on Connect")
|
2023-05-04 11:45:58 +03:00
|
|
|
|
|
|
|
// we are not ready to return yet, we need to wait for the disconnect event to come through
|
|
|
|
// so continue on from this case and wait for something to show up on prphCh
|
2023-05-03 06:55:14 +03:00
|
|
|
continue
|
2020-08-28 14:31:17 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-24 18:07:29 +03:00
|
|
|
// Disconnect from the BLE device. This method is non-blocking and does not
|
|
|
|
// wait until the connection is fully gone.
|
2023-12-25 16:28:56 +03:00
|
|
|
func (d Device) Disconnect() error {
|
2020-09-22 17:55:44 +03:00
|
|
|
d.cm.CancelConnect(d.prph)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-29 16:01:34 +03:00
|
|
|
// Peripheral delegate functions
|
2020-08-29 12:35:26 +03:00
|
|
|
|
2020-08-29 16:01:34 +03:00
|
|
|
type peripheralDelegate struct {
|
|
|
|
cbgo.PeripheralDelegateBase
|
2020-08-29 12:35:26 +03:00
|
|
|
|
2023-12-25 16:28:56 +03:00
|
|
|
d Device
|
2020-08-29 16:01:34 +03:00
|
|
|
}
|
2020-08-28 14:31:17 +03:00
|
|
|
|
2020-08-29 12:35:26 +03:00
|
|
|
// DidDiscoverServices is called when the services for a Peripheral
|
|
|
|
// have been discovered.
|
2020-08-29 16:01:34 +03:00
|
|
|
func (pd *peripheralDelegate) DidDiscoverServices(prph cbgo.Peripheral, err error) {
|
|
|
|
pd.d.servicesChan <- nil
|
2020-08-28 14:31:17 +03:00
|
|
|
}
|
2020-08-29 12:35:26 +03:00
|
|
|
|
|
|
|
// DidDiscoverCharacteristics is called when the characteristics for a Service
|
|
|
|
// for a Peripheral have been discovered.
|
2020-08-29 16:01:34 +03:00
|
|
|
func (pd *peripheralDelegate) DidDiscoverCharacteristics(prph cbgo.Peripheral, svc cbgo.Service, err error) {
|
|
|
|
pd.d.charsChan <- nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DidUpdateValueForCharacteristic is called when the characteristic for a Service
|
2020-10-22 20:04:47 +03:00
|
|
|
// for a Peripheral receives a notification with a new value,
|
|
|
|
// or receives a value for a read request.
|
2020-08-29 16:01:34 +03:00
|
|
|
func (pd *peripheralDelegate) DidUpdateValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) {
|
2020-08-29 17:15:24 +03:00
|
|
|
uuid, _ := ParseUUID(chr.UUID().String())
|
2023-06-06 06:06:15 +03:00
|
|
|
svcuuid, _ := ParseUUID(chr.Service().UUID().String())
|
|
|
|
|
|
|
|
if svc, ok := pd.d.services[svcuuid]; ok {
|
2023-06-06 07:30:20 +03:00
|
|
|
for _, char := range svc.characteristics {
|
|
|
|
|
|
|
|
if char.characteristic == chr && uuid == char.UUID() { // compare pointers
|
|
|
|
if err == nil && char.callback != nil {
|
|
|
|
go char.callback(chr.Value())
|
|
|
|
}
|
2020-10-22 20:04:47 +03:00
|
|
|
|
2023-06-06 07:30:20 +03:00
|
|
|
if char.readChan != nil {
|
|
|
|
char.readChan <- err
|
|
|
|
}
|
2023-06-06 06:06:15 +03:00
|
|
|
}
|
2023-06-06 07:30:20 +03:00
|
|
|
|
2020-10-22 20:04:47 +03:00
|
|
|
}
|
2023-06-06 07:30:20 +03:00
|
|
|
|
2020-08-29 17:15:24 +03:00
|
|
|
}
|
2020-08-28 14:31:17 +03:00
|
|
|
}
|
2024-01-05 16:03:30 +03:00
|
|
|
|
|
|
|
// DidWriteValueForCharacteristic is called after the characteristic for a Service
|
|
|
|
// for a Peripheral trigger a write with response. It contains the returned error or nil.
|
|
|
|
func (pd *peripheralDelegate) DidWriteValueForCharacteristic(_ cbgo.Peripheral, chr cbgo.Characteristic, err error) {
|
|
|
|
uuid, _ := ParseUUID(chr.UUID().String())
|
|
|
|
svcuuid, _ := ParseUUID(chr.Service().UUID().String())
|
|
|
|
|
|
|
|
if svc, ok := pd.d.services[svcuuid]; ok {
|
|
|
|
for _, char := range svc.characteristics {
|
|
|
|
if char.characteristic == chr && uuid == char.UUID() { // compare pointers
|
|
|
|
if char.writeChan != nil {
|
|
|
|
char.writeChan <- err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|