macos: completed initial implementation
Signed-off-by: Ron Evans <ron@hybridgroup.com>
This commit is contained in:
parent
27cc0b725a
commit
5f44bb4a96
5 changed files with 188 additions and 45 deletions
|
@ -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
|
||||||
|
|
|
@ -9,7 +9,7 @@ This package attempts to build a cross-platform Bluetooth Low Energy module for
|
||||||
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ |
|
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ |
|
||||||
| 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: |
|
||||||
|
|
|
@ -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,7 +16,9 @@ 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.
|
||||||
|
@ -21,15 +27,28 @@ type Adapter struct {
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <-cancelChan:
|
case <-a.scanChan:
|
||||||
// stop scanning here?
|
close(a.scanChan)
|
||||||
|
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
44
gattc_darwin.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue