diff --git a/.circleci/config.yml b/.circleci/config.yml index bf67440..058d8c8 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 90c75b6..373d152 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 a503979..8067640 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 5b1506c..e178441 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 0000000..280f9a5 --- /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 +}