From 105d50ae1408f884445e2f44af0b76a70e102e4e Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Thu, 27 Aug 2020 22:22:27 +0200 Subject: [PATCH 01/12] macos: starting point for adding macOS support Signed-off-by: Ron Evans --- Makefile | 4 ++++ README.md | 22 +++++++++++----------- adapter_darwin.go | 23 +++++++++++++++++++++++ gap_darwin.go | 14 ++++++++++++++ go.mod | 3 +++ go.sum | 9 +++++++++ macbt/cmdelegate.go | 27 +++++++++++++++++++++++++++ macbt/macbt.go | 2 ++ macbt/pmdelegate.go | 35 +++++++++++++++++++++++++++++++++++ 9 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 adapter_darwin.go create mode 100644 gap_darwin.go create mode 100644 macbt/cmdelegate.go create mode 100644 macbt/macbt.go create mode 100644 macbt/pmdelegate.go diff --git a/Makefile b/Makefile index 9cde3886..0315e015 100644 --- a/Makefile +++ b/Makefile @@ -35,3 +35,7 @@ smoketest-linux: smoketest-windows: # Test on Windows. GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -o /tmp/go-build-discard ./examples/scanner + +smoketest-macos: + # Test on macos. + GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/scanner diff --git a/README.md b/README.md index 47b4b0ba..bbeb5fa6 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,17 @@ This package attempts to build a cross-platform Bluetooth Low Energy module for Go. It currently supports the following systems: -| | Windows | Linux | Nordic chips | -| -------------------------------- | ------------------ | ------------------ | ------------------ | -| API used | WinRT | BlueZ (over D-Bus) | SoftDevice | -| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Connect to peripheral | :x: | :heavy_check_mark: | :heavy_check_mark: | -| Write peripheral characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | -| Receive notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | -| Advertisement | :x: | :heavy_check_mark: | :heavy_check_mark: | -| Local services | :x: | :heavy_check_mark: | :heavy_check_mark: | -| Local characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | -| Send notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | +| | Windows | Linux | Nordic chips | macOS | +| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | +| API used | WinRT | BlueZ (over D-Bus) | SoftDevice | CoreBluetooth | +| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Connect to peripheral | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Write peripheral characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Receive notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Advertisement | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Local services | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Local characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Send notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | ## Baremetal support diff --git a/adapter_darwin.go b/adapter_darwin.go new file mode 100644 index 00000000..237935af --- /dev/null +++ b/adapter_darwin.go @@ -0,0 +1,23 @@ +package bluetooth + +import ( + "github.com/JuulLabs-OSS/cbgo" + "github.com/tinygo-org/bluetooth/macbt" +) + +type Adapter struct { + cm cbgo.CentralManager + cmd macbt.CMDelegate +} + +// DefaultAdapter is the default adapter on the system. +// +// Make sure to call Enable() before using it to initialize the adapter. +var DefaultAdapter = &Adapter{} + +// Enable configures the BLE stack. It must be called before any +// Bluetooth-related calls (unless otherwise indicated). +func (a *Adapter) Enable() error { + a.cm = cbgo.NewCentralManager(nil) + return nil +} diff --git a/gap_darwin.go b/gap_darwin.go new file mode 100644 index 00000000..4e829c35 --- /dev/null +++ b/gap_darwin.go @@ -0,0 +1,14 @@ +package bluetooth + +// 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) { + return nil +} + +// 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 { + return nil +} diff --git a/go.mod b/go.mod index cbd04ec6..4fdf6bae 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module github.com/tinygo-org/bluetooth go 1.14 require ( + github.com/JuulLabs-OSS/cbgo v0.0.2 github.com/go-ole/go-ole v1.2.4 github.com/godbus/dbus/v5 v5.0.3 github.com/muka/go-bluetooth v0.0.0-20200619025933-f6113f7141c5 + github.com/sirupsen/logrus v1.6.0 // indirect golang.org/x/crypto v0.0.0-20200602180216-279210d13fed + golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 // indirect ) diff --git a/go.sum b/go.sum index 018679d7..26739ae8 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/JuulLabs-OSS/cbgo v0.0.2 h1:gCDyT0+EPuI8GOFyvAksFcVD2vF4CXBAVwT6uVnD9oo= +github.com/JuulLabs-OSS/cbgo v0.0.2/go.mod h1:L4YtGP+gnyD84w7+jN66ncspFRfOYB5aj9QSXaFHmBA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -8,7 +10,10 @@ github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -19,6 +24,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -32,6 +39,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/macbt/cmdelegate.go b/macbt/cmdelegate.go new file mode 100644 index 00000000..f5fc0cfa --- /dev/null +++ b/macbt/cmdelegate.go @@ -0,0 +1,27 @@ +// Implements the CentralManagerDelegate interface. CoreBluetooth +// communicates events asynchronously via callbacks. This file implements a +// synchronous interface by translating these callbacks into channel +// operations. + +package macbt + +import ( + "github.com/JuulLabs-OSS/cbgo" +) + +// CMDelegate to handle callbacks from CoreBluetooth. +type CMDelegate struct { +} + +func (d *CMDelegate) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { +} + +func (d *CMDelegate) DidDiscoverPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, + advFields cbgo.AdvFields, rssi int) { +} + +func (d *CMDelegate) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) { +} + +func (d *CMDelegate) DidDisconnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, err error) { +} diff --git a/macbt/macbt.go b/macbt/macbt.go new file mode 100644 index 00000000..bdb76765 --- /dev/null +++ b/macbt/macbt.go @@ -0,0 +1,2 @@ +// Package macbt is wrapper to CoreBluetooth. +package macbt diff --git a/macbt/pmdelegate.go b/macbt/pmdelegate.go new file mode 100644 index 00000000..bac02213 --- /dev/null +++ b/macbt/pmdelegate.go @@ -0,0 +1,35 @@ +// Implements the PeripheralManagerDelegate interface. +// CoreBluetooth communicates events asynchronously via callbacks. This file +// implements a synchronous interface by translating these callbacks into +// channel operations. + +package macbt + +import ( + "github.com/JuulLabs-OSS/cbgo" +) + +// PMDelegate to handle callbacks from CoreBluetooth. +type PMDelegate struct { +} + +func (d *PMDelegate) PeripheralManagerDidUpdateState(pmgr cbgo.PeripheralManager) { +} + +func (d *PMDelegate) DidAddService(pmgr cbgo.PeripheralManager, svc cbgo.Service, err error) { +} + +func (d *PMDelegate) DidStartAdvertising(pmgr cbgo.PeripheralManager, err error) { +} + +func (d *PMDelegate) DidReceiveReadRequest(pmgr cbgo.PeripheralManager, cbreq cbgo.ATTRequest) { +} + +func (d *PMDelegate) DidReceiveWriteRequests(pmgr cbgo.PeripheralManager, cbreqs []cbgo.ATTRequest) { +} + +func (d *PMDelegate) CentralDidSubscribe(pmgr cbgo.PeripheralManager, cent cbgo.Central, cbchr cbgo.Characteristic) { +} + +func (d *PMDelegate) CentralDidUnsubscribe(pmgr cbgo.PeripheralManager, cent cbgo.Central, chr cbgo.Characteristic) { +} From 2791776a66a24ff84940d424be05cf049431237e Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Fri, 28 Aug 2020 08:35:13 +0200 Subject: [PATCH 02/12] macos: able to retrieve some of the info when scanning Signed-off-by: Ron Evans --- adapter_darwin.go | 97 ++++++++++++++++++++++++++++++++++++++++++--- gap_darwin.go | 47 +++++++++++++++++++++- macbt/cmdelegate.go | 27 ------------- macbt/macbt.go | 2 - macbt/pmdelegate.go | 35 ---------------- 5 files changed, 138 insertions(+), 70 deletions(-) delete mode 100644 macbt/cmdelegate.go delete mode 100644 macbt/macbt.go delete mode 100644 macbt/pmdelegate.go diff --git a/adapter_darwin.go b/adapter_darwin.go index 237935af..7a0e5c01 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -2,22 +2,109 @@ package bluetooth import ( "github.com/JuulLabs-OSS/cbgo" - "github.com/tinygo-org/bluetooth/macbt" ) type Adapter struct { - cm cbgo.CentralManager - cmd macbt.CMDelegate + cbgo.CentralManagerDelegateBase + cbgo.PeripheralManagerDelegateBase + + cm cbgo.CentralManager + pm cbgo.PeripheralManager + + peripheralFoundHandler func(*Adapter, ScanResult) + cancelChan chan struct{} } // DefaultAdapter is the default adapter on the system. // // Make sure to call Enable() before using it to initialize the adapter. -var DefaultAdapter = &Adapter{} +var DefaultAdapter = &Adapter{ + cm: cbgo.NewCentralManager(nil), + pm: cbgo.NewPeripheralManager(nil), +} // Enable configures the BLE stack. It must be called before any // Bluetooth-related calls (unless otherwise indicated). func (a *Adapter) Enable() error { - a.cm = cbgo.NewCentralManager(nil) + a.cm.SetDelegate(a) + // TODO: wait until powered + a.pm.SetDelegate(a) + // TODO: wait until powered + return nil } + +// CentralManager delegate functions + +func (a *Adapter) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { +} + +// DidDiscoverPeripheral when peripheral is discovered. +func (a *Adapter) DidDiscoverPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, + advFields cbgo.AdvFields, rssi int) { + if a.peripheralFoundHandler != nil { + sr := makeScanResult(prph, advFields, rssi) + a.peripheralFoundHandler(a, sr) + } +} + +// DidConnectPeripheral when peripheral is connected. +func (a *Adapter) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) { +} + +// DidDisconnectPeripheral when peripheral is disconnected. +func (a *Adapter) DidDisconnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, err error) { +} + +// PeripheralManager delegate functions + +// PeripheralManagerDidUpdateState when state updated. +func (a *Adapter) PeripheralManagerDidUpdateState(pmgr cbgo.PeripheralManager) { +} + +// DidAddService when service added. +func (a *Adapter) DidAddService(pmgr cbgo.PeripheralManager, svc cbgo.Service, err error) { +} + +// DidStartAdvertising when advertising starts. +func (a *Adapter) DidStartAdvertising(pmgr cbgo.PeripheralManager, err error) { +} + +// DidReceiveReadRequest when read request received. +func (a *Adapter) DidReceiveReadRequest(pmgr cbgo.PeripheralManager, cbreq cbgo.ATTRequest) { +} + +// DidReceiveWriteRequests when write requests received. +func (a *Adapter) DidReceiveWriteRequests(pmgr cbgo.PeripheralManager, cbreqs []cbgo.ATTRequest) { +} + +// CentralDidSubscribe when central subscribed. +func (a *Adapter) CentralDidSubscribe(pmgr cbgo.PeripheralManager, cent cbgo.Central, cbchr cbgo.Characteristic) { +} + +// CentralDidUnsubscribe when central unsubscribed. +func (a *Adapter) CentralDidUnsubscribe(pmgr cbgo.PeripheralManager, cent cbgo.Central, chr cbgo.Characteristic) { +} + +// makeScanResult creates a ScanResult when peripheral is found. +func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) ScanResult { + // TODO: figure out the peripheral info. + + // TODO: create a list of serviceUUIDs. + + return ScanResult{ + RSSI: int16(rssi), + Address: Address{ + // TODO: fill in this info + //MAC: prph.Identifier(), + //IsRandom: prph.Identifier == "random", + }, + AdvertisementPayload: &advertisementFields{ + AdvertisementFields{ + LocalName: advFields.LocalName, + // TODO: fill in this info + //ServiceUUIDs: serviceUUIDs, + }, + }, + } +} diff --git a/gap_darwin.go b/gap_darwin.go index 4e829c35..8e30a04b 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -1,14 +1,59 @@ package bluetooth +import ( + "errors" + + "github.com/JuulLabs-OSS/cbgo" +) + // 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) { - return nil + if callback == nil { + return errors.New("must provide callback to Scan function") + } + + if a.cancelChan != nil { + 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. + cancelChan := make(chan struct{}) + a.cancelChan = cancelChan + + a.cm.Scan(nil, &cbgo.CentralManagerScanOpts{ + AllowDuplicates: true, + }) + + for { + // 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 <-cancelChan: + // stop scanning here? + return nil + default: + } + } } // 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 { + if a.cancelChan != nil { + return errors.New("already calling Scan function") + } + + a.cm.StopScan() + close(a.cancelChan) + a.cancelChan = nil + return nil } diff --git a/macbt/cmdelegate.go b/macbt/cmdelegate.go deleted file mode 100644 index f5fc0cfa..00000000 --- a/macbt/cmdelegate.go +++ /dev/null @@ -1,27 +0,0 @@ -// Implements the CentralManagerDelegate interface. CoreBluetooth -// communicates events asynchronously via callbacks. This file implements a -// synchronous interface by translating these callbacks into channel -// operations. - -package macbt - -import ( - "github.com/JuulLabs-OSS/cbgo" -) - -// CMDelegate to handle callbacks from CoreBluetooth. -type CMDelegate struct { -} - -func (d *CMDelegate) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { -} - -func (d *CMDelegate) DidDiscoverPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, - advFields cbgo.AdvFields, rssi int) { -} - -func (d *CMDelegate) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) { -} - -func (d *CMDelegate) DidDisconnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, err error) { -} diff --git a/macbt/macbt.go b/macbt/macbt.go deleted file mode 100644 index bdb76765..00000000 --- a/macbt/macbt.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package macbt is wrapper to CoreBluetooth. -package macbt diff --git a/macbt/pmdelegate.go b/macbt/pmdelegate.go deleted file mode 100644 index bac02213..00000000 --- a/macbt/pmdelegate.go +++ /dev/null @@ -1,35 +0,0 @@ -// Implements the PeripheralManagerDelegate interface. -// CoreBluetooth communicates events asynchronously via callbacks. This file -// implements a synchronous interface by translating these callbacks into -// channel operations. - -package macbt - -import ( - "github.com/JuulLabs-OSS/cbgo" -) - -// PMDelegate to handle callbacks from CoreBluetooth. -type PMDelegate struct { -} - -func (d *PMDelegate) PeripheralManagerDidUpdateState(pmgr cbgo.PeripheralManager) { -} - -func (d *PMDelegate) DidAddService(pmgr cbgo.PeripheralManager, svc cbgo.Service, err error) { -} - -func (d *PMDelegate) DidStartAdvertising(pmgr cbgo.PeripheralManager, err error) { -} - -func (d *PMDelegate) DidReceiveReadRequest(pmgr cbgo.PeripheralManager, cbreq cbgo.ATTRequest) { -} - -func (d *PMDelegate) DidReceiveWriteRequests(pmgr cbgo.PeripheralManager, cbreqs []cbgo.ATTRequest) { -} - -func (d *PMDelegate) CentralDidSubscribe(pmgr cbgo.PeripheralManager, cent cbgo.Central, cbchr cbgo.Characteristic) { -} - -func (d *PMDelegate) CentralDidUnsubscribe(pmgr cbgo.PeripheralManager, cent cbgo.Central, chr cbgo.Characteristic) { -} From 7a1e1f652b8a5ae556e71eeed6be9dd99ac0476d Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Fri, 28 Aug 2020 12:40:03 +0200 Subject: [PATCH 03/12] all: use Addresser interface to handle fact that macOS uses UUID instead of MAC as the BLE address for a peripheral Signed-off-by: Ron Evans --- adapter_darwin.go | 10 ++++++---- adapter_nrf528xx.go | 4 ++-- gap.go | 17 +++++++++++------ gap_darwin.go | 24 ++++++++++++++++++++++++ gap_linux.go | 30 +++++++++++++++++++++++++++--- gap_nrf51.go | 23 +++++++++++++++++++++++ gap_nrf528xx.go | 32 ++++++++++++++++++++++++++++---- gap_windows.go | 23 +++++++++++++++++++++++ 8 files changed, 144 insertions(+), 19 deletions(-) diff --git a/adapter_darwin.go b/adapter_darwin.go index 7a0e5c01..a5039797 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -36,6 +36,7 @@ func (a *Adapter) Enable() error { // CentralManager delegate functions +// CentralManagerDidUpdateState when central manager state updated. func (a *Adapter) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { } @@ -88,15 +89,16 @@ func (a *Adapter) CentralDidUnsubscribe(pmgr cbgo.PeripheralManager, cent cbgo.C // makeScanResult creates a ScanResult when peripheral is found. func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) ScanResult { - // TODO: figure out the peripheral info. + var u [16]byte + copy(u[:], prph.Identifier()) + uuid := NewUUID(u) // TODO: create a list of serviceUUIDs. return ScanResult{ - RSSI: int16(rssi), + RSSI: int16(rssi), Address: Address{ - // TODO: fill in this info - //MAC: prph.Identifier(), + UUID: uuid, //IsRandom: prph.Identifier == "random", }, AdvertisementPayload: &advertisementFields{ diff --git a/adapter_nrf528xx.go b/adapter_nrf528xx.go index 9fd3a30d..c7a8602f 100644 --- a/adapter_nrf528xx.go +++ b/adapter_nrf528xx.go @@ -102,8 +102,8 @@ func handleEvent() { // callback. scanReportBuffer.len = byte(advReport.data.len) globalScanResult.RSSI = int16(advReport.rssi) - globalScanResult.Address.MAC = advReport.peer_addr.addr - globalScanResult.Address.IsRandom = advReport.peer_addr.bitfield_addr_type() != 0 + globalScanResult.Address.Set(advReport.peer_addr.addr) + globalScanResult.Address.SetRandom(advReport.peer_addr.bitfield_addr_type() != 0) globalScanResult.AdvertisementPayload = &scanReportBuffer // Signal to the main thread that there was a scan report. // Scanning will be resumed (from the main thread) once the scan diff --git a/gap.go b/gap.go index bd15c318..092afeed 100644 --- a/gap.go +++ b/gap.go @@ -41,11 +41,14 @@ func NewDuration(interval time.Duration) Duration { // Connection is a numeric identifier that indicates a connection handle. type Connection uint16 -// Address contains a Bluetooth address, which is a MAC address plus some extra +// Addresser contains a Bluetooth address, which is a MAC address plus some extra // information. -type Address struct { - // The MAC address of a Bluetooth device. - MAC +type Addresser interface { + // String of the address + String() string + + // Set the address + Set(val interface{}) // Is this address a random address? // Bluetooth addresses are roughly split in two kinds: public @@ -54,14 +57,16 @@ type Address struct { // random. Sometimes, it contains a hash. // For more information: // https://www.novelbits.io/bluetooth-address-privacy-ble/ - IsRandom bool + // Set the address + SetRandom(bool) + IsRandom() bool } // ScanResult contains information from when an advertisement packet was // received. It is passed as a parameter to the callback of the Scan method. type ScanResult struct { // Bluetooth address of the scanned device. - Address Address + Address Addresser // RSSI the last time a packet from this device has been received. RSSI int16 diff --git a/gap_darwin.go b/gap_darwin.go index 8e30a04b..5b1506c9 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -6,6 +6,30 @@ import ( "github.com/JuulLabs-OSS/cbgo" ) +// Address contains a Bluetooth address, which is a MAC address plus some extra +// information. +type Address struct { + // UUID if this is macOS. + UUID + + isRandom bool +} + +// IsRandom if the address is randomly created. +func (ad Address) IsRandom() bool { + return ad.isRandom +} + +// SetRandom if is a random address. +func (ad Address) SetRandom(val bool) { + ad.isRandom = val +} + +// Set the address +func (ad Address) Set(val interface{}) { + ad.UUID = val.(UUID) +} + // 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) { diff --git a/gap_linux.go b/gap_linux.go index e3b73116..8b2e145a 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -11,6 +11,29 @@ import ( "github.com/muka/go-bluetooth/bluez/profile/device" ) +// Address contains a Bluetooth address, which is a MAC address plus some extra +// information. +type Address struct { + // The MAC address of a Bluetooth device. + MAC + isRandom bool +} + +// IsRandom if the address is randomly created. +func (ad Address) IsRandom() bool { + return ad.isRandom +} + +// SetRandom if is a random address. +func (ad Address) SetRandom(val bool) { + ad.isRandom = val +} + +// Set the address +func (ad Address) Set(val interface{}) { + ad.MAC = val.(MAC) +} + // Advertisement encapsulates a single advertisement instance. type Advertisement struct { adapter *Adapter @@ -218,7 +241,7 @@ func makeScanResult(props *device.Device1Properties) ScanResult { RSSI: props.RSSI, Address: Address{ MAC: addr, - IsRandom: props.AddressType == "random", + isRandom: props.AddressType == "random", }, AdvertisementPayload: &advertisementFields{ AdvertisementFields{ @@ -237,8 +260,9 @@ type Device struct { // Connect starts a connection attempt to the given peripheral device address. // // On Linux and Windows, the IsRandom part of the address is ignored. -func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, error) { - devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(address.MAC.String(), ":", "_", -1)) +func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device, error) { + adr := address.(Address) + devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(adr.MAC.String(), ":", "_", -1)) dev, err := device.NewDevice1(devicePath) if err != nil { return nil, err diff --git a/gap_nrf51.go b/gap_nrf51.go index d6b1bbf3..1b42674c 100644 --- a/gap_nrf51.go +++ b/gap_nrf51.go @@ -16,6 +16,29 @@ import ( "time" ) +// Address contains a Bluetooth address, which is a MAC address plus some extra +// information. +type Address struct { + // The MAC address of a Bluetooth device. + MAC + isRandom bool +} + +// IsRandom if the address is randomly created. +func (ad Address) IsRandom() bool { + return ad.isRandom +} + +// SetRandom if is a random address. +func (ad Address) SetRandom(val bool) { + ad.isRandom = val +} + +// Set the address +func (ad Address) Set(val interface{}) { + ad.MAC = val.(MAC) +} + // Advertisement encapsulates a single advertisement instance. type Advertisement struct { interval Duration diff --git a/gap_nrf528xx.go b/gap_nrf528xx.go index 8a9c373d..704e06bc 100644 --- a/gap_nrf528xx.go +++ b/gap_nrf528xx.go @@ -27,6 +27,29 @@ var ( globalScanResult ScanResult ) +// Address contains a Bluetooth address, which is a MAC address plus some extra +// information. +type Address struct { + // The MAC address of a Bluetooth device. + MAC + isRandom bool +} + +// IsRandom if the address is randomly created. +func (ad Address) IsRandom() bool { + return ad.isRandom +} + +// SetRandom if is a random address. +func (ad Address) SetRandom(val bool) { + ad.isRandom = val +} + +// Set the address +func (ad Address) Set(val interface{}) { + ad.MAC = val.(MAC) +} + // Advertisement encapsulates a single advertisement instance. type Advertisement struct { handle uint8 @@ -169,12 +192,13 @@ var connectionAttempt struct { // connection attempt at once and that the address parameter must have the // IsRandom bit set correctly. This bit is set correctly for scan results, so // you can reuse that address directly. -func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, error) { +func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device, error) { + adr := address.(Address) // Construct an address object as used in the SoftDevice. var addr C.ble_gap_addr_t - addr.addr = address.MAC - if address.IsRandom { - switch address.MAC[5] >> 6 { + addr.addr = adr.MAC + if address.IsRandom() { + switch adr.MAC[5] >> 6 { case 0b11: addr.set_bitfield_addr_type(C.BLE_GAP_ADDR_TYPE_RANDOM_STATIC) case 0b01: diff --git a/gap_windows.go b/gap_windows.go index 4322bd11..a0dd2772 100644 --- a/gap_windows.go +++ b/gap_windows.go @@ -4,6 +4,29 @@ import ( "github.com/tinygo-org/bluetooth/winbt" ) +// Address contains a Bluetooth address, which is a MAC address plus some extra +// information. +type Address struct { + // The MAC address of a Bluetooth device. + MAC + isRandom bool +} + +// IsRandom if the address is randomly created. +func (ad Address) IsRandom() bool { + return ad.isRandom +} + +// SetRandom if is a random address. +func (ad Address) SetRandom(val bool) { + ad.isRandom = val +} + +// Set the address +func (ad Address) Set(val interface{}) { + ad.MAC = val.(MAC) +} + // 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) { From 35de6ef6787dd75742f3db49427085d792daae28 Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Fri, 28 Aug 2020 12:46:36 +0200 Subject: [PATCH 04/12] windows: use Addresser interface for the MAC address for a peripheral Signed-off-by: Ron Evans --- gap_windows.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gap_windows.go b/gap_windows.go index a0dd2772..6f19c0ec 100644 --- a/gap_windows.go +++ b/gap_windows.go @@ -47,8 +47,9 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) { var result ScanResult result.RSSI = args.RawSignalStrengthInDBm() addr := args.BluetoothAddress() - for i := range result.Address.MAC { - result.Address.MAC[i] = byte(addr) + adr := result.Address.(Address) + for i := range adr.MAC { + adr.MAC[i] = byte(addr) addr >>= 8 } // Note: the IsRandom bit is never set. From a5ff736f62279dbb17a6255670811e3f3f7bdf93 Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Fri, 28 Aug 2020 12:53:30 +0200 Subject: [PATCH 05/12] docs: show that we can now scan on macOS Signed-off-by: Ron Evans --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bbeb5fa6..90c75b65 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This package attempts to build a cross-platform Bluetooth Low Energy module for | | Windows | Linux | Nordic chips | macOS | | -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | | API used | WinRT | BlueZ (over D-Bus) | SoftDevice | CoreBluetooth | -| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Connect to peripheral | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | | Write peripheral characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | | Receive notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | From 7cc099d704d09dba0b489745f25f590e6af8c800 Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Fri, 28 Aug 2020 13:31:17 +0200 Subject: [PATCH 06/12] macos: completed initial implementation Signed-off-by: Ron Evans --- .circleci/config.yml | 23 +++++++++- README.md | 4 +- adapter_darwin.go | 56 +++++++++++++++++------ gap_darwin.go | 106 +++++++++++++++++++++++++++++++------------ gattc_darwin.go | 44 ++++++++++++++++++ 5 files changed, 188 insertions(+), 45 deletions(-) create mode 100644 gattc_darwin.go diff --git a/.circleci/config.yml b/.circleci/config.yml index bf67440e..058d8c85 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,4 @@ -version: 2 +version: 2.1 jobs: build: @@ -22,3 +22,24 @@ jobs: - run: name: "Run Windows smoke tests" command: make smoketest-windows + build-macos: + macos: + xcode: "10.1.0" + steps: + - checkout + - run: + name: "Install dependencies" + command: | + curl https://dl.google.com/go/go1.14.darwin-amd64.tar.gz -o go1.14.darwin-amd64.tar.gz + sudo tar -C /usr/local -xzf go1.14.darwin-amd64.tar.gz + ln -s /usr/local/go/bin/go /usr/local/bin/go + - run: go version + - run: + name: "Run macOS smoke tests" + command: make smoketest-macos + +workflows: + test-all: + jobs: + - build + - build-macos diff --git a/README.md b/README.md index 90c75b65..373d1528 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ This package attempts to build a cross-platform Bluetooth Low Energy module for | | Windows | Linux | Nordic chips | macOS | | -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | | API used | WinRT | BlueZ (over D-Bus) | SoftDevice | CoreBluetooth | -| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Connect to peripheral | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Connect to peripheral | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Write peripheral characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | | Receive notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | | Advertisement | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | diff --git a/adapter_darwin.go b/adapter_darwin.go index a5039797..80676406 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -1,9 +1,13 @@ package bluetooth import ( + "errors" + "time" + "github.com/JuulLabs-OSS/cbgo" ) +// Adapter is a connection to BLE devices. type Adapter struct { cbgo.CentralManagerDelegateBase cbgo.PeripheralManagerDelegateBase @@ -12,24 +16,39 @@ type Adapter struct { pm cbgo.PeripheralManager peripheralFoundHandler func(*Adapter, ScanResult) - cancelChan chan struct{} + scanChan chan error + poweredChan chan error + connectChan chan cbgo.Peripheral } // DefaultAdapter is the default adapter on the system. // // Make sure to call Enable() before using it to initialize the adapter. var DefaultAdapter = &Adapter{ - cm: cbgo.NewCentralManager(nil), - pm: cbgo.NewPeripheralManager(nil), + cm: cbgo.NewCentralManager(nil), + pm: cbgo.NewPeripheralManager(nil), + connectChan: make(chan cbgo.Peripheral), } // Enable configures the BLE stack. It must be called before any // Bluetooth-related calls (unless otherwise indicated). func (a *Adapter) Enable() error { + if a.poweredChan != nil { + return errors.New("already calling Enable function") + } + + // wait until powered + a.poweredChan = make(chan error) a.cm.SetDelegate(a) - // TODO: wait until powered - a.pm.SetDelegate(a) - // TODO: wait until powered + select { + case <-a.poweredChan: + case <-time.NewTimer(10 * time.Second).C: + return errors.New("timeout enabling CentralManager") + } + a.poweredChan = nil + + // wait until powered? + //a.pm.SetDelegate(a) return nil } @@ -38,6 +57,12 @@ func (a *Adapter) Enable() error { // CentralManagerDidUpdateState when central manager state updated. func (a *Adapter) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { + // powered on? + if cmgr.State() == cbgo.ManagerStatePoweredOn { + close(a.poweredChan) + } + + // TODO: handle other state changes. } // DidDiscoverPeripheral when peripheral is discovered. @@ -51,6 +76,8 @@ func (a *Adapter) DidDiscoverPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peri // DidConnectPeripheral when peripheral is connected. func (a *Adapter) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) { + // Unblock now that we're connected. + a.connectChan <- prph } // DidDisconnectPeripheral when peripheral is disconnected. @@ -89,23 +116,24 @@ func (a *Adapter) CentralDidUnsubscribe(pmgr cbgo.PeripheralManager, cent cbgo.C // makeScanResult creates a ScanResult when peripheral is found. func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) ScanResult { - var u [16]byte - copy(u[:], prph.Identifier()) - uuid := NewUUID(u) + uuid, _ := ParseUUID(prph.Identifier().String()) - // TODO: create a list of serviceUUIDs. + var serviceUUIDs []UUID + for _, u := range advFields.ServiceUUIDs { + parsedUUID, _ := ParseUUID(u.String()) + serviceUUIDs = append(serviceUUIDs, parsedUUID) + } + // It is never a random address on macOS. return ScanResult{ RSSI: int16(rssi), Address: Address{ UUID: uuid, - //IsRandom: prph.Identifier == "random", }, AdvertisementPayload: &advertisementFields{ AdvertisementFields{ - LocalName: advFields.LocalName, - // TODO: fill in this info - //ServiceUUIDs: serviceUUIDs, + LocalName: advFields.LocalName, + ServiceUUIDs: serviceUUIDs, }, }, } diff --git a/gap_darwin.go b/gap_darwin.go index 5b1506c9..e178441e 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -2,27 +2,26 @@ package bluetooth import ( "errors" + "fmt" + "time" "github.com/JuulLabs-OSS/cbgo" ) -// Address contains a Bluetooth address, which is a MAC address plus some extra -// information. +// Address contains a Bluetooth address, which on macOS instead of a MAC address +// is instead a UUID. type Address struct { - // UUID if this is macOS. + // UUID since this is macOS. UUID - - isRandom bool } -// IsRandom if the address is randomly created. +// IsRandom ignored on macOS. func (ad Address) IsRandom() bool { - return ad.isRandom + return false } -// SetRandom if is a random address. +// SetRandom ignored on macOS. func (ad Address) SetRandom(val bool) { - ad.isRandom = val } // Set the address @@ -37,7 +36,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) { return errors.New("must provide callback to Scan function") } - if a.cancelChan != nil { + if a.scanChan != nil { return errors.New("already calling Scan function") } @@ -46,24 +45,21 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) { // 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. - cancelChan := make(chan struct{}) - a.cancelChan = cancelChan + a.scanChan = make(chan error) a.cm.Scan(nil, &cbgo.CentralManagerScanOpts{ - AllowDuplicates: true, + AllowDuplicates: false, }) - for { - // 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 <-cancelChan: - // stop scanning here? - return nil - default: - } + // 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 } } @@ -71,13 +67,67 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) { // callback to stop the current scan. If no scan is in progress, an error will // be returned. func (a *Adapter) StopScan() error { - if a.cancelChan != nil { - return errors.New("already calling Scan function") + if a.scanChan == nil { + return errors.New("not calling Scan function") } + a.scanChan <- nil a.cm.StopScan() - close(a.cancelChan) - a.cancelChan = nil return nil } + +// Device is a connection to a remote peripheral. +type Device struct { + cbgo.PeripheralDelegateBase + + cm cbgo.CentralManager +} + +// Connect starts a connection attempt to the given peripheral device address. +func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device, error) { + adr := address.(Address) + uuid, err := cbgo.ParseUUID(adr.UUID.String()) + if err != nil { + return nil, err + } + prphs := a.cm.RetrievePeripheralsWithIdentifiers([]cbgo.UUID{uuid}) + if len(prphs) == 0 { + return nil, fmt.Errorf("Connect failed: no peer with address: %s", adr.UUID.String()) + } + a.cm.Connect(prphs[0], nil) + + // wait on channel for connect + select { + case p := <-a.connectChan: + d := &Device{ + cm: a.cm, + } + + p.SetDelegate(d) + return d, nil + case <-time.NewTimer(10 * time.Second).C: + return nil, errors.New("timeout on Connect") + } +} + +// Peripheral delegate functions + +func (d *Device) DidDiscoverServices(prph cbgo.Peripheral, err error) { +} +func (d *Device) DidDiscoverCharacteristics(prph cbgo.Peripheral, svc cbgo.Service, err error) { +} +func (d *Device) DidDiscoverDescriptors(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { +} +func (d *Device) DidUpdateValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { +} +func (d *Device) DidUpdateValueForDescriptor(prph cbgo.Peripheral, dsc cbgo.Descriptor, err error) { +} +func (d *Device) DidWriteValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { +} +func (d *Device) DidWriteValueForDescriptor(prph cbgo.Peripheral, dsc cbgo.Descriptor, err error) { +} +func (d *Device) DidUpdateNotificationState(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { +} +func (d *Device) DidReadRSSI(prph cbgo.Peripheral, rssi int, err error) { +} diff --git a/gattc_darwin.go b/gattc_darwin.go new file mode 100644 index 00000000..280f9a59 --- /dev/null +++ b/gattc_darwin.go @@ -0,0 +1,44 @@ +package bluetooth + +// DiscoverServices starts a service discovery procedure. Pass a list of service +// UUIDs you are interested in to this function. Either a slice of all services +// is returned (of the same length as the requested UUIDs and in the same +// order), or if some services could not be discovered an error is returned. +func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { + return nil, nil +} + +// DeviceService is a BLE service on a connected peripheral device. +type DeviceService struct { +} + +// DiscoverCharacteristics discovers characteristics in this service. Pass a +// list of characteristic UUIDs you are interested in to this function. Either a +// list of all requested services is returned, or if some services could not be +// discovered an error is returned. If there is no error, the characteristics +// slice has the same length as the UUID slice with characteristics in the same +// order in the slice as in the requested UUID list. +func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) { + return nil, nil +} + +// DeviceCharacteristic is a BLE characteristic on a connected peripheral +// device. +type DeviceCharacteristic struct { +} + +// WriteWithoutResponse replaces the characteristic value with a new value. The +// call will return before all data has been written. A limited number of such +// writes can be in flight at any given time. This call is also known as a +// "write command" (as opposed to a write request). +func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) { + return 0, nil +} + +// EnableNotifications enables notifications in the Client Characteristic +// Configuration Descriptor (CCCD). This means that most peripherals will send a +// notification with a new value every time the value of the characteristic +// changes. +func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error { + return nil +} From 0a7f0038dc95fc52a427c5afd3d79ac263f1557e Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Sat, 29 Aug 2020 11:35:26 +0200 Subject: [PATCH 07/12] macos: able to discover services and characteristics for a device Signed-off-by: Ron Evans --- README.md | 2 +- adapter_darwin.go | 34 ------------------- gap_darwin.go | 43 +++++++++++++++--------- gattc_darwin.go | 83 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 108 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 373d1528..13222cc0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This package attempts to build a cross-platform Bluetooth Low Energy module for | API used | WinRT | BlueZ (over D-Bus) | SoftDevice | CoreBluetooth | | Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Connect to peripheral | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Write peripheral characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Write peripheral characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Receive notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | | Advertisement | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | | Local services | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | diff --git a/adapter_darwin.go b/adapter_darwin.go index 80676406..3f22b77f 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -80,40 +80,6 @@ func (a *Adapter) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Perip a.connectChan <- prph } -// DidDisconnectPeripheral when peripheral is disconnected. -func (a *Adapter) DidDisconnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, err error) { -} - -// PeripheralManager delegate functions - -// PeripheralManagerDidUpdateState when state updated. -func (a *Adapter) PeripheralManagerDidUpdateState(pmgr cbgo.PeripheralManager) { -} - -// DidAddService when service added. -func (a *Adapter) DidAddService(pmgr cbgo.PeripheralManager, svc cbgo.Service, err error) { -} - -// DidStartAdvertising when advertising starts. -func (a *Adapter) DidStartAdvertising(pmgr cbgo.PeripheralManager, err error) { -} - -// DidReceiveReadRequest when read request received. -func (a *Adapter) DidReceiveReadRequest(pmgr cbgo.PeripheralManager, cbreq cbgo.ATTRequest) { -} - -// DidReceiveWriteRequests when write requests received. -func (a *Adapter) DidReceiveWriteRequests(pmgr cbgo.PeripheralManager, cbreqs []cbgo.ATTRequest) { -} - -// CentralDidSubscribe when central subscribed. -func (a *Adapter) CentralDidSubscribe(pmgr cbgo.PeripheralManager, cent cbgo.Central, cbchr cbgo.Characteristic) { -} - -// CentralDidUnsubscribe when central unsubscribed. -func (a *Adapter) CentralDidUnsubscribe(pmgr cbgo.PeripheralManager, cent cbgo.Central, chr cbgo.Characteristic) { -} - // makeScanResult creates a ScanResult when peripheral is found. func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) ScanResult { uuid, _ := ParseUUID(prph.Identifier().String()) diff --git a/gap_darwin.go b/gap_darwin.go index e178441e..9e1da797 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -81,7 +81,11 @@ func (a *Adapter) StopScan() error { type Device struct { cbgo.PeripheralDelegateBase - cm cbgo.CentralManager + cm cbgo.CentralManager + prph cbgo.Peripheral + + servicesChan chan error + charsChan chan error } // Connect starts a connection attempt to the given peripheral device address. @@ -101,7 +105,10 @@ func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device, select { case p := <-a.connectChan: d := &Device{ - cm: a.cm, + cm: a.cm, + prph: p, + servicesChan: make(chan error), + charsChan: make(chan error), } p.SetDelegate(d) @@ -111,23 +118,27 @@ func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device, } } +// Peripheral returns the Device's cbgo.Peripheral +func (d *Device) Peripheral() (prph cbgo.Peripheral) { + return d.prph +} + +// CharsChan returns the Device's charsChan channel used for +// blocking on discovering the characteristics for a service. +func (d *Device) CharsChan() chan error { + return d.charsChan +} + // Peripheral delegate functions +// DidDiscoverServices is called when the services for a Peripheral +// have been discovered. func (d *Device) DidDiscoverServices(prph cbgo.Peripheral, err error) { + d.servicesChan <- nil } + +// DidDiscoverCharacteristics is called when the characteristics for a Service +// for a Peripheral have been discovered. func (d *Device) DidDiscoverCharacteristics(prph cbgo.Peripheral, svc cbgo.Service, err error) { -} -func (d *Device) DidDiscoverDescriptors(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { -} -func (d *Device) DidUpdateValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { -} -func (d *Device) DidUpdateValueForDescriptor(prph cbgo.Peripheral, dsc cbgo.Descriptor, err error) { -} -func (d *Device) DidWriteValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { -} -func (d *Device) DidWriteValueForDescriptor(prph cbgo.Peripheral, dsc cbgo.Descriptor, err error) { -} -func (d *Device) DidUpdateNotificationState(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { -} -func (d *Device) DidReadRSSI(prph cbgo.Peripheral, rssi int, err error) { + d.charsChan <- nil } diff --git a/gattc_darwin.go b/gattc_darwin.go index 280f9a59..2f345627 100644 --- a/gattc_darwin.go +++ b/gattc_darwin.go @@ -1,15 +1,56 @@ package bluetooth +import ( + "errors" + "time" + + "github.com/JuulLabs-OSS/cbgo" +) + // DiscoverServices starts a service discovery procedure. Pass a list of service // UUIDs you are interested in to this function. Either a slice of all services // is returned (of the same length as the requested UUIDs and in the same // order), or if some services could not be discovered an error is returned. func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { - return nil, nil + cbuuids := []cbgo.UUID{} + for _, u := range uuids { + uuid, _ := cbgo.ParseUUID(u.String()) + cbuuids = append(cbuuids, uuid) + } + + d.prph.DiscoverServices(cbuuids) + + // wait on channel for service discovery + select { + case <-d.servicesChan: + svcs := []DeviceService{} + for _, dsvc := range d.prph.Services() { + uuid, _ := ParseUUID(dsvc.UUID().String()) + svc := DeviceService{ + UUID: uuid, + device: d, + service: dsvc, + } + svcs = append(svcs, svc) + } + return svcs, nil + case <-time.NewTimer(10 * time.Second).C: + return nil, errors.New("timeout on DiscoverServices") + } } // DeviceService is a BLE service on a connected peripheral device. type DeviceService struct { + UUID + + device *Device + + service cbgo.Service +} + +// Device returns the Device for this service. +func (s *DeviceService) Device() *Device { + return s.device } // DiscoverCharacteristics discovers characteristics in this service. Pass a @@ -19,12 +60,42 @@ type DeviceService struct { // slice has the same length as the UUID slice with characteristics in the same // order in the slice as in the requested UUID list. func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) { - return nil, nil + cbuuids := []cbgo.UUID{} + for _, u := range uuids { + uuid, _ := cbgo.ParseUUID(u.String()) + cbuuids = append(cbuuids, uuid) + } + + s.Device().Peripheral().DiscoverCharacteristics(cbuuids, s.service) + + // wait on channel for characteristic discovery + select { + case <-s.Device().CharsChan(): + chars := []DeviceCharacteristic{} + for _, dchar := range s.service.Characteristics() { + uuid, _ := ParseUUID(dchar.UUID().String()) + char := DeviceCharacteristic{ + UUID: uuid, + service: s, + characteristic: dchar, + } + chars = append(chars, char) + } + return chars, nil + case <-time.NewTimer(10 * time.Second).C: + return nil, errors.New("timeout on DiscoverCharacteristics") + } } // DeviceCharacteristic is a BLE characteristic on a connected peripheral // device. type DeviceCharacteristic struct { + UUID + + service *DeviceService + + characteristic cbgo.Characteristic + callback func(buf []byte) } // WriteWithoutResponse replaces the characteristic value with a new value. The @@ -32,7 +103,9 @@ type DeviceCharacteristic struct { // writes can be in flight at any given time. This call is also known as a // "write command" (as opposed to a write request). func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) { - return 0, nil + c.service.Device().Peripheral().WriteCharacteristic(p, c.characteristic, false) + + return len(p), nil } // EnableNotifications enables notifications in the Client Characteristic @@ -40,5 +113,9 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) // notification with a new value every time the value of the characteristic // changes. func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error { + if callback == nil { + return errors.New("must provide a callback for EnableNotifications") + } + return nil } From be4168e414e92c7eb46c32fb68a344f5e53c8308 Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Sat, 29 Aug 2020 14:43:11 +0200 Subject: [PATCH 08/12] gap: switch to use MACAddress struct when possible for shared implementation Signed-off-by: Ron Evans --- gap.go | 22 ++++++++++++++++++++++ gap_darwin.go | 3 +-- gap_linux.go | 33 ++++++++------------------------- gap_nrf51.go | 22 ++-------------------- gap_nrf528xx.go | 22 ++-------------------- gap_windows.go | 22 ++-------------------- 6 files changed, 37 insertions(+), 87 deletions(-) diff --git a/gap.go b/gap.go index 092afeed..50229b55 100644 --- a/gap.go +++ b/gap.go @@ -11,6 +11,28 @@ var ( errAdvertisementPacketTooBig = errors.New("bluetooth: advertisement packet overflows") ) +// MACAddress contains a Bluetooth address which is a MAC address. +type MACAddress struct { + // MAC address of the Bluetooth device. + MAC + isRandom bool +} + +// IsRandom if the address is randomly created. +func (mac MACAddress) IsRandom() bool { + return mac.isRandom +} + +// SetRandom if is a random address. +func (mac MACAddress) SetRandom(val bool) { + mac.isRandom = val +} + +// Set the address +func (mac MACAddress) Set(val interface{}) { + mac.MAC = val.(MAC) +} + // AdvertisementOptions configures an advertisement instance. More options may // be added over time. type AdvertisementOptions struct { diff --git a/gap_darwin.go b/gap_darwin.go index 9e1da797..5f195403 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -8,8 +8,7 @@ import ( "github.com/JuulLabs-OSS/cbgo" ) -// Address contains a Bluetooth address, which on macOS instead of a MAC address -// is instead a UUID. +// Address contains a Bluetooth address which on macOS is a UUID. type Address struct { // UUID since this is macOS. UUID diff --git a/gap_linux.go b/gap_linux.go index 8b2e145a..5a6a9129 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -11,27 +11,9 @@ import ( "github.com/muka/go-bluetooth/bluez/profile/device" ) -// Address contains a Bluetooth address, which is a MAC address plus some extra -// information. +// Address contains a Bluetooth MAC address. type Address struct { - // The MAC address of a Bluetooth device. - MAC - isRandom bool -} - -// IsRandom if the address is randomly created. -func (ad Address) IsRandom() bool { - return ad.isRandom -} - -// SetRandom if is a random address. -func (ad Address) SetRandom(val bool) { - ad.isRandom = val -} - -// Set the address -func (ad Address) Set(val interface{}) { - ad.MAC = val.(MAC) + MACAddress } // Advertisement encapsulates a single advertisement instance. @@ -237,12 +219,13 @@ func makeScanResult(props *device.Device1Properties) ScanResult { serviceUUIDs = append(serviceUUIDs, parsedUUID) } + a := Address{} + a.Set(addr) + a.SetRandom(props.AddressType == "random") + return ScanResult{ - RSSI: props.RSSI, - Address: Address{ - MAC: addr, - isRandom: props.AddressType == "random", - }, + RSSI: props.RSSI, + Address: a, AdvertisementPayload: &advertisementFields{ AdvertisementFields{ LocalName: props.Name, diff --git a/gap_nrf51.go b/gap_nrf51.go index 1b42674c..86da013a 100644 --- a/gap_nrf51.go +++ b/gap_nrf51.go @@ -16,27 +16,9 @@ import ( "time" ) -// Address contains a Bluetooth address, which is a MAC address plus some extra -// information. +// Address contains a Bluetooth MAC address. type Address struct { - // The MAC address of a Bluetooth device. - MAC - isRandom bool -} - -// IsRandom if the address is randomly created. -func (ad Address) IsRandom() bool { - return ad.isRandom -} - -// SetRandom if is a random address. -func (ad Address) SetRandom(val bool) { - ad.isRandom = val -} - -// Set the address -func (ad Address) Set(val interface{}) { - ad.MAC = val.(MAC) + MACAddress } // Advertisement encapsulates a single advertisement instance. diff --git a/gap_nrf528xx.go b/gap_nrf528xx.go index 704e06bc..b2a89338 100644 --- a/gap_nrf528xx.go +++ b/gap_nrf528xx.go @@ -27,27 +27,9 @@ var ( globalScanResult ScanResult ) -// Address contains a Bluetooth address, which is a MAC address plus some extra -// information. +// Address contains a Bluetooth MAC address. type Address struct { - // The MAC address of a Bluetooth device. - MAC - isRandom bool -} - -// IsRandom if the address is randomly created. -func (ad Address) IsRandom() bool { - return ad.isRandom -} - -// SetRandom if is a random address. -func (ad Address) SetRandom(val bool) { - ad.isRandom = val -} - -// Set the address -func (ad Address) Set(val interface{}) { - ad.MAC = val.(MAC) + MACAddress } // Advertisement encapsulates a single advertisement instance. diff --git a/gap_windows.go b/gap_windows.go index 6f19c0ec..dda3fbff 100644 --- a/gap_windows.go +++ b/gap_windows.go @@ -4,27 +4,9 @@ import ( "github.com/tinygo-org/bluetooth/winbt" ) -// Address contains a Bluetooth address, which is a MAC address plus some extra -// information. +// Address contains a Bluetooth MAC address. type Address struct { - // The MAC address of a Bluetooth device. - MAC - isRandom bool -} - -// IsRandom if the address is randomly created. -func (ad Address) IsRandom() bool { - return ad.isRandom -} - -// SetRandom if is a random address. -func (ad Address) SetRandom(val bool) { - ad.isRandom = val -} - -// Set the address -func (ad Address) Set(val interface{}) { - ad.MAC = val.(MAC) + MACAddress } // Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern From b5b4c6813dfc6271c0497b3cabe0d77b2e673c22 Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Sat, 29 Aug 2020 15:01:34 +0200 Subject: [PATCH 09/12] macos: remove unneeded functions to export internal implementaions. Signed-off-by: Ron Evans --- adapter_darwin.go | 39 ++++++++++++++++++++++++++++----------- gap_darwin.go | 35 +++++++++++++++++++---------------- gattc_darwin.go | 14 ++++++-------- 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/adapter_darwin.go b/adapter_darwin.go index 3f22b77f..b0fae8d6 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -9,8 +9,8 @@ import ( // Adapter is a connection to BLE devices. type Adapter struct { - cbgo.CentralManagerDelegateBase - cbgo.PeripheralManagerDelegateBase + cmd *centralManagerDelegate + pmd *peripheralManagerDelegate cm cbgo.CentralManager pm cbgo.PeripheralManager @@ -39,7 +39,9 @@ func (a *Adapter) Enable() error { // wait until powered a.poweredChan = make(chan error) - a.cm.SetDelegate(a) + + a.cmd = ¢ralManagerDelegate{a: a} + a.cm.SetDelegate(a.cmd) select { case <-a.poweredChan: case <-time.NewTimer(10 * time.Second).C: @@ -48,36 +50,43 @@ func (a *Adapter) Enable() error { a.poweredChan = nil // wait until powered? - //a.pm.SetDelegate(a) + a.pmd = &peripheralManagerDelegate{a: a} + a.pm.SetDelegate(a.pmd) return nil } // CentralManager delegate functions +type centralManagerDelegate struct { + cbgo.CentralManagerDelegateBase + + a *Adapter +} + // CentralManagerDidUpdateState when central manager state updated. -func (a *Adapter) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { +func (cmd *centralManagerDelegate) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { // powered on? if cmgr.State() == cbgo.ManagerStatePoweredOn { - close(a.poweredChan) + close(cmd.a.poweredChan) } // TODO: handle other state changes. } // DidDiscoverPeripheral when peripheral is discovered. -func (a *Adapter) DidDiscoverPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, +func (cmd *centralManagerDelegate) DidDiscoverPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) { - if a.peripheralFoundHandler != nil { + if cmd.a.peripheralFoundHandler != nil { sr := makeScanResult(prph, advFields, rssi) - a.peripheralFoundHandler(a, sr) + cmd.a.peripheralFoundHandler(cmd.a, sr) } } // DidConnectPeripheral when peripheral is connected. -func (a *Adapter) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) { +func (cmd *centralManagerDelegate) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) { // Unblock now that we're connected. - a.connectChan <- prph + cmd.a.connectChan <- prph } // makeScanResult creates a ScanResult when peripheral is found. @@ -104,3 +113,11 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc }, } } + +// PeripheralManager delegate functions + +type peripheralManagerDelegate struct { + cbgo.PeripheralManagerDelegateBase + + a *Adapter +} diff --git a/gap_darwin.go b/gap_darwin.go index 5f195403..039f3ff8 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -78,7 +78,7 @@ func (a *Adapter) StopScan() error { // Device is a connection to a remote peripheral. type Device struct { - cbgo.PeripheralDelegateBase + delegate *peripheralDelegate cm cbgo.CentralManager prph cbgo.Peripheral @@ -110,34 +110,37 @@ func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device, charsChan: make(chan error), } - p.SetDelegate(d) + d.delegate = &peripheralDelegate{d: d} + p.SetDelegate(d.delegate) + return d, nil case <-time.NewTimer(10 * time.Second).C: return nil, errors.New("timeout on Connect") } } -// Peripheral returns the Device's cbgo.Peripheral -func (d *Device) Peripheral() (prph cbgo.Peripheral) { - return d.prph -} +// Peripheral delegate functions -// CharsChan returns the Device's charsChan channel used for -// blocking on discovering the characteristics for a service. -func (d *Device) CharsChan() chan error { - return d.charsChan -} +type peripheralDelegate struct { + cbgo.PeripheralDelegateBase -// Peripheral delegate functions + d *Device +} // DidDiscoverServices is called when the services for a Peripheral // have been discovered. -func (d *Device) DidDiscoverServices(prph cbgo.Peripheral, err error) { - d.servicesChan <- nil +func (pd *peripheralDelegate) DidDiscoverServices(prph cbgo.Peripheral, err error) { + pd.d.servicesChan <- nil } // DidDiscoverCharacteristics is called when the characteristics for a Service // for a Peripheral have been discovered. -func (d *Device) DidDiscoverCharacteristics(prph cbgo.Peripheral, svc cbgo.Service, err error) { - d.charsChan <- nil +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 +// for a Peripheral receives a notification with a new value. +func (pd *peripheralDelegate) DidUpdateValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { + // TODO: implement this } diff --git a/gattc_darwin.go b/gattc_darwin.go index 2f345627..ad4a6857 100644 --- a/gattc_darwin.go +++ b/gattc_darwin.go @@ -48,11 +48,6 @@ type DeviceService struct { service cbgo.Service } -// Device returns the Device for this service. -func (s *DeviceService) Device() *Device { - return s.device -} - // DiscoverCharacteristics discovers characteristics in this service. Pass a // list of characteristic UUIDs you are interested in to this function. Either a // list of all requested services is returned, or if some services could not be @@ -66,11 +61,11 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter cbuuids = append(cbuuids, uuid) } - s.Device().Peripheral().DiscoverCharacteristics(cbuuids, s.service) + s.device.prph.DiscoverCharacteristics(cbuuids, s.service) // wait on channel for characteristic discovery select { - case <-s.Device().CharsChan(): + case <-s.device.charsChan: chars := []DeviceCharacteristic{} for _, dchar := range s.service.Characteristics() { uuid, _ := ParseUUID(dchar.UUID().String()) @@ -103,7 +98,7 @@ type DeviceCharacteristic struct { // writes can be in flight at any given time. This call is also known as a // "write command" (as opposed to a write request). func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) { - c.service.Device().Peripheral().WriteCharacteristic(p, c.characteristic, false) + c.service.device.prph.WriteCharacteristic(p, c.characteristic, false) return len(p), nil } @@ -117,5 +112,8 @@ func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) err return errors.New("must provide a callback for EnableNotifications") } + c.callback = callback + c.service.device.prph.SetNotify(true, c.characteristic) + return nil } From f789e840054ce08dd0a5aab56d17002afc8465f2 Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Sat, 29 Aug 2020 16:15:24 +0200 Subject: [PATCH 10/12] macos: added characteristic notifications Signed-off-by: Ron Evans --- README.md | 6 +++--- gap_darwin.go | 10 +++++++++- gattc_darwin.go | 8 ++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 13222cc0..643d9dca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Go Bluetooth -[![CircleCI](https://circleci.com/gh/tinygo-org/bluetooth/tree/master.svg?style=svg)](https://circleci.com/gh/tinygo-org/bluetooth/tree/master) +[![CircleCI](https://circleci.com/gh/tinygo-org/bluetooth/tree/dev.svg?style=svg)](https://circleci.com/gh/tinygo-org/bluetooth/tree/dev) [![GoDoc](https://godoc.org/github.com/tinygo-org/bluetooth?status.svg)](https://godoc.org/github.com/tinygo-org/bluetooth) This package attempts to build a cross-platform Bluetooth Low Energy module for Go. It currently supports the following systems: @@ -11,7 +11,7 @@ This package attempts to build a cross-platform Bluetooth Low Energy module for | Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Connect to peripheral | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Write peripheral characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Receive notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| Receive notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Advertisement | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | | Local services | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | | Local characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | @@ -52,7 +52,7 @@ Flashing will then need to be done a bit differently, using the CMSIS-DAP interf ## API stability -**The API is not stable!** Because many features are not yet implemented and some platforms (e.g. MacOS) are not yet supported, it's hard to say what a good API will be. Therefore, if you want stability you should pick a particular git commit and use that. Go modules can be useful for this purpose. +**The API is not stable!** Because many features are not yet implemented and some platforms (e.g. Windows and macOS) are not yet fully supported, it's hard to say what a good API will be. Therefore, if you want stability you should pick a particular git commit and use that. Go modules can be useful for this purpose. Some things that will probably change: diff --git a/gap_darwin.go b/gap_darwin.go index 039f3ff8..46413c91 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -85,6 +85,9 @@ type Device struct { servicesChan chan error charsChan chan error + + services map[UUID]*DeviceService + characteristics map[UUID]*DeviceCharacteristic } // Connect starts a connection attempt to the given peripheral device address. @@ -142,5 +145,10 @@ func (pd *peripheralDelegate) DidDiscoverCharacteristics(prph cbgo.Peripheral, s // DidUpdateValueForCharacteristic is called when the characteristic for a Service // for a Peripheral receives a notification with a new value. func (pd *peripheralDelegate) DidUpdateValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { - // TODO: implement this + uuid, _ := ParseUUID(chr.UUID().String()) + if char, ok := pd.d.characteristics[uuid]; ok { + if char != nil && char.callback != nil { + go char.callback(chr.Value()) + } + } } diff --git a/gattc_darwin.go b/gattc_darwin.go index ad4a6857..6beba58c 100644 --- a/gattc_darwin.go +++ b/gattc_darwin.go @@ -20,6 +20,9 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { d.prph.DiscoverServices(cbuuids) + // clear cache of services + d.services = make(map[UUID]*DeviceService) + // wait on channel for service discovery select { case <-d.servicesChan: @@ -32,6 +35,7 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { service: dsvc, } svcs = append(svcs, svc) + d.services[svc.UUID] = &svc } return svcs, nil case <-time.NewTimer(10 * time.Second).C: @@ -63,6 +67,9 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter s.device.prph.DiscoverCharacteristics(cbuuids, s.service) + // clear cache of characteristics + s.device.characteristics = make(map[UUID]*DeviceCharacteristic) + // wait on channel for characteristic discovery select { case <-s.device.charsChan: @@ -75,6 +82,7 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter characteristic: dchar, } chars = append(chars, char) + s.device.characteristics[char.UUID] = &char } return chars, nil case <-time.NewTimer(10 * time.Second).C: From c245754636f8b8781b96eedacebe4d85dfda3b7e Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 30 Aug 2020 14:53:17 +0200 Subject: [PATCH 11/12] gap: correct use of Address on Linux platform Signed-off-by: deadprogram --- gap.go | 4 +++- gap_linux.go | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gap.go b/gap.go index 50229b55..8f1adc15 100644 --- a/gap.go +++ b/gap.go @@ -15,6 +15,7 @@ var ( type MACAddress struct { // MAC address of the Bluetooth device. MAC + isRandom bool } @@ -30,7 +31,8 @@ func (mac MACAddress) SetRandom(val bool) { // Set the address func (mac MACAddress) Set(val interface{}) { - mac.MAC = val.(MAC) + m := val.(MAC) + mac.MAC = m } // AdvertisementOptions configures an advertisement instance. More options may diff --git a/gap_linux.go b/gap_linux.go index 5a6a9129..f1359f2d 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -219,8 +219,7 @@ func makeScanResult(props *device.Device1Properties) ScanResult { serviceUUIDs = append(serviceUUIDs, parsedUUID) } - a := Address{} - a.Set(addr) + a := &Address{MACAddress{MAC: addr}} a.SetRandom(props.AddressType == "random") return ScanResult{ From b80b9184eaae5b92a1dead82468e903b8f91cbf4 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 2 Sep 2020 08:34:58 +0200 Subject: [PATCH 12/12] docs: better explanation of peripheral UUID on macOS Signed-off-by: deadprogram --- adapter_darwin.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/adapter_darwin.go b/adapter_darwin.go index b0fae8d6..a19ce03d 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -99,7 +99,8 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc serviceUUIDs = append(serviceUUIDs, parsedUUID) } - // It is never a random address on macOS. + // Peripheral UUID is randomized on macOS, which means to + // different centrals it will appear to have a different UUID. return ScanResult{ RSSI: int16(rssi), Address: Address{