macos: completed initial implementation

Signed-off-by: Ron Evans <ron@hybridgroup.com>
This commit is contained in:
Ron Evans 2020-08-28 13:31:17 +02:00
parent 27cc0b725a
commit 5f44bb4a96
5 changed files with 188 additions and 45 deletions

View file

@ -1,4 +1,4 @@
version: 2 version: 2.1
jobs: jobs:
build: build:
@ -22,3 +22,24 @@ jobs:
- run: - run:
name: "Run Windows smoke tests" name: "Run Windows smoke tests"
command: make smoketest-windows 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

View file

@ -8,8 +8,8 @@ This package attempts to build a cross-platform Bluetooth Low Energy module for
| | Windows | Linux | Nordic chips | macOS | | | Windows | Linux | Nordic chips | macOS |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | | -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ |
| API used | WinRT | BlueZ (over D-Bus) | SoftDevice | CoreBluetooth | | API used | WinRT | BlueZ (over D-Bus) | SoftDevice | CoreBluetooth |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Connect to peripheral | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: | | 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: | :x: |
| Receive notifications | :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: | | Advertisement | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: |

View file

@ -1,9 +1,13 @@
package bluetooth package bluetooth
import ( import (
"errors"
"time"
"github.com/JuulLabs-OSS/cbgo" "github.com/JuulLabs-OSS/cbgo"
) )
// Adapter is a connection to BLE devices.
type Adapter struct { type Adapter struct {
cbgo.CentralManagerDelegateBase cbgo.CentralManagerDelegateBase
cbgo.PeripheralManagerDelegateBase cbgo.PeripheralManagerDelegateBase
@ -12,24 +16,39 @@ type Adapter struct {
pm cbgo.PeripheralManager pm cbgo.PeripheralManager
peripheralFoundHandler func(*Adapter, ScanResult) 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. // DefaultAdapter is the default adapter on the system.
// //
// Make sure to call Enable() before using it to initialize the adapter. // Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{ var DefaultAdapter = &Adapter{
cm: cbgo.NewCentralManager(nil), cm: cbgo.NewCentralManager(nil),
pm: cbgo.NewPeripheralManager(nil), pm: cbgo.NewPeripheralManager(nil),
connectChan: make(chan cbgo.Peripheral),
} }
// Enable configures the BLE stack. It must be called before any // Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated). // Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() error { 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) a.cm.SetDelegate(a)
// TODO: wait until powered select {
a.pm.SetDelegate(a) case <-a.poweredChan:
// TODO: wait until powered 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 return nil
} }
@ -38,6 +57,12 @@ func (a *Adapter) Enable() error {
// CentralManagerDidUpdateState when central manager state updated. // CentralManagerDidUpdateState when central manager state updated.
func (a *Adapter) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { 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. // DidDiscoverPeripheral when peripheral is discovered.
@ -51,6 +76,8 @@ func (a *Adapter) DidDiscoverPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peri
// DidConnectPeripheral when peripheral is connected. // DidConnectPeripheral when peripheral is connected.
func (a *Adapter) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) { func (a *Adapter) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) {
// Unblock now that we're connected.
a.connectChan <- prph
} }
// DidDisconnectPeripheral when peripheral is disconnected. // 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. // makeScanResult creates a ScanResult when peripheral is found.
func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) ScanResult { func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) ScanResult {
var u [16]byte uuid, _ := ParseUUID(prph.Identifier().String())
copy(u[:], prph.Identifier())
uuid := NewUUID(u)
// 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{ return ScanResult{
RSSI: int16(rssi), RSSI: int16(rssi),
Address: Address{ Address: Address{
UUID: uuid, UUID: uuid,
//IsRandom: prph.Identifier == "random",
}, },
AdvertisementPayload: &advertisementFields{ AdvertisementPayload: &advertisementFields{
AdvertisementFields{ AdvertisementFields{
LocalName: advFields.LocalName, LocalName: advFields.LocalName,
// TODO: fill in this info ServiceUUIDs: serviceUUIDs,
//ServiceUUIDs: serviceUUIDs,
}, },
}, },
} }

View file

@ -2,27 +2,26 @@ package bluetooth
import ( import (
"errors" "errors"
"fmt"
"time"
"github.com/JuulLabs-OSS/cbgo" "github.com/JuulLabs-OSS/cbgo"
) )
// Address contains a Bluetooth address, which is a MAC address plus some extra // Address contains a Bluetooth address, which on macOS instead of a MAC address
// information. // is instead a UUID.
type Address struct { type Address struct {
// UUID if this is macOS. // UUID since this is macOS.
UUID UUID
isRandom bool
} }
// IsRandom if the address is randomly created. // IsRandom ignored on macOS.
func (ad Address) IsRandom() bool { 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) { func (ad Address) SetRandom(val bool) {
ad.isRandom = val
} }
// Set the address // 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") return errors.New("must provide callback to Scan function")
} }
if a.cancelChan != nil { if a.scanChan != nil {
return errors.New("already calling Scan function") 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. // Channel that will be closed when the scan is stopped.
// Detecting whether the scan is stopped can be done by doing a non-blocking // Detecting whether the scan is stopped can be done by doing a non-blocking
// read from it. If it succeeds, the scan is stopped. // read from it. If it succeeds, the scan is stopped.
cancelChan := make(chan struct{}) a.scanChan = make(chan error)
a.cancelChan = cancelChan
a.cm.Scan(nil, &cbgo.CentralManagerScanOpts{ a.cm.Scan(nil, &cbgo.CentralManagerScanOpts{
AllowDuplicates: true, AllowDuplicates: false,
}) })
for { // Check whether the scan is stopped. This is necessary to avoid a race
// Check whether the scan is stopped. This is necessary to avoid a race // condition between the signal channel and the cancelScan channel when
// condition between the signal channel and the cancelScan channel when // the callback calls StopScan() (no new callbacks may be called after
// the callback calls StopScan() (no new callbacks may be called after // StopScan is called).
// StopScan is called). select {
select { case <-a.scanChan:
case <-cancelChan: close(a.scanChan)
// stop scanning here? a.scanChan = nil
return nil return nil
default:
}
} }
} }
@ -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 // callback to stop the current scan. If no scan is in progress, an error will
// be returned. // be returned.
func (a *Adapter) StopScan() error { func (a *Adapter) StopScan() error {
if a.cancelChan != nil { if a.scanChan == nil {
return errors.New("already calling Scan function") return errors.New("not calling Scan function")
} }
a.scanChan <- nil
a.cm.StopScan() a.cm.StopScan()
close(a.cancelChan)
a.cancelChan = nil
return 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) {
}

44
gattc_darwin.go Normal file
View file

@ -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
}