blob: b992ccda7b59f55c14ea77db7bf949b763e051d2 [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin,cgo
// Package corebluetooth provides an implementation of ble.Driver using the CoreBluetooth Objective-C API
//
// The bridge rules between the two are as follows:
// THREADS:
// Everything in obj-c runs single threaded on a dedicated Grand Central Dispatch queue (read: thread).
// Obj-C is responsible for getting itself on that thread in calls from Go.
// Go is responsible for getting _off_ the obj-c queue via goroutines when calling out to the rest of
// the stack.
// MEMORY:
// Callers retain ownership of their memory -- callee must copy right away.
// The exception to this is when memory is returned either via the function or via a double pointer
// (in the case of errorOut following Obj-C semantics of returning BOOL matched with passing of NSError **).
// In this case ownership is transfered and callee must free.
package corebluetooth
import (
"errors"
"strings"
"sync"
"unsafe"
"v.io/v23/context"
"v.io/x/lib/vlog"
"v.io/x/ref/lib/discovery/plugins/ble"
)
/*
#cgo CFLAGS: -x objective-c -fobjc-arc -DCBLOG_LEVEL=CBLOG_LEVEL_ERROR
#cgo LDFLAGS: -framework Foundation -framework CoreBluetooth
#import <CoreBluetooth/CoreBluetooth.h>
#import "CBDriver.h"
static int objcBOOL2int(BOOL b) {
return (int)b;
}
*/
import "C"
type (
// CoreBluetoothDriver provides an abstraction for an underlying mechanism to discover
// near-by Vanadium services through Bluetooth Low Energy (BLE) with CoreBluetooth.
//
// See Driver for more documentation.
CoreBluetoothDriver struct {
ctx *context.T
scanHandler ble.ScanHandler
mu sync.Mutex
}
OnDiscovered struct {
UUID string
Characteristics map[string][]byte
RSSI int
}
)
var (
driverMu sync.Mutex
driver *CoreBluetoothDriver
)
func New(ctx *context.T) (*CoreBluetoothDriver, error) {
if ctx == nil {
return nil, errors.New("context cannot be nil")
}
driverMu.Lock()
defer driverMu.Unlock()
if driver != nil {
return nil, errors.New("only one corebluetooth driver can be created at a time; call .Clean() instead")
}
driver = &CoreBluetoothDriver{ctx: ctx}
// Clean everything when the context is done.
go func() {
<-ctx.Done()
Clean()
}()
return driver, nil
}
// Clean shuts down any existing scans/advertisements, releases the BLE hardware, removes the
// objective-c singleton from memory, and releases the global driver in this package. It is
// necessary before New may be called.
func Clean() {
driverMu.Lock()
if driver != nil {
driver.StopScan()
}
// This is thread safe in objc
C.v23_cbdriver_clean()
driver = nil
driverMu.Unlock()
}
func (d *CoreBluetoothDriver) NumServicesAdvertising() int {
return int(C.v23_cbdriver_advertisingServiceCount())
}
// AddService implements v.io/x/lib/discovery/plugins/ble.Driver.AddService
func (d *CoreBluetoothDriver) AddService(uuid string, characteristics map[string][]byte) error {
// Convert args to C
entries := C.malloc(C.size_t(len(characteristics)) * C.sizeof_CBDriverCharacteristicMapEntry)
// See CGO Wiki on how we can use this weird looking technique to get a go slice out of a c array
// https://github.com/golang/go/wiki/cgo
entriesSlice := (*[1 << 30]C.CBDriverCharacteristicMapEntry)(unsafe.Pointer(entries))[:len(characteristics):len(characteristics)]
i := 0
for characteristicUuid, data := range characteristics {
var entry C.CBDriverCharacteristicMapEntry
entry.uuid = C.CString(characteristicUuid)
entry.data = unsafe.Pointer(&data[0])
entry.dataLength = C.int(len(data))
entriesSlice[i] = entry
i++
}
defer func() {
for _, entry := range entriesSlice {
C.free(unsafe.Pointer(entry.uuid))
}
C.free(unsafe.Pointer(entries))
}()
// Call objective-c
var errorOut *C.char = nil
// This is thread-safe in obj-c
if err := objcBOOL2Error(C.v23_cbdriver_addService(C.CString(uuid), (*C.CBDriverCharacteristicMapEntry)(entries), C.int(len(characteristics)), &errorOut), &errorOut); err != nil {
return err
}
// Success
d.ctx.Info("Added service ", uuid)
return nil
}
// RemoveService implements v.io/x/lib/discovery/plugins/ble.Driver.RemoveService
func (d *CoreBluetoothDriver) RemoveService(uuid string) {
cUuid := C.CString(uuid)
// This is thread-safe in obj-c
C.v23_cbdriver_removeService(cUuid)
C.free(unsafe.Pointer(cUuid))
}
// StartScan implements v.io/x/lib/discovery/plugins/ble.Driver.StartService
func (d *CoreBluetoothDriver) StartScan(uuids []string, baseUuid, maskUuid string, handler ble.ScanHandler) error {
// Convert args to C
cUuids := C.malloc(C.sizeof_size_t * C.size_t(len(uuids)))
// See CGO Wiki on how we can use this weird looking technique to get a go slice out of a c array
// https://github.com/golang/go/wiki/cgo
cUuidsSlice := (*[1 << 30]*C.char)(unsafe.Pointer(cUuids))[:len(uuids):len(uuids)]
for i, uuid := range uuids {
cUuidsSlice[i] = C.CString(uuid)
}
cBaseUuid := C.CString(baseUuid)
cMaskUuid := C.CString(maskUuid)
defer func() {
for _, cUuid := range cUuidsSlice {
C.free(unsafe.Pointer(cUuid))
}
C.free(unsafe.Pointer(cUuids))
C.free(unsafe.Pointer(cBaseUuid))
C.free(unsafe.Pointer(cMaskUuid))
}()
d.mu.Lock()
defer d.mu.Unlock()
if d.scanHandler != nil {
return errors.New("scan already in progress")
}
// Kick start handler
d.scanHandler = handler
// Call Objective-C
var errorOut *C.char = nil
if err := objcBOOL2Error(C.v23_cbdriver_startScan((**C.char)(cUuids), C.int(len(uuids)), cBaseUuid, cMaskUuid, &errorOut), &errorOut); err != nil {
d.scanHandler = nil
return err
}
// Success
return nil
}
//export v23_corebluetooth_scan_handler_on_discovered
func v23_corebluetooth_scan_handler_on_discovered(cUuid *C.char, cEntries *C.CBDriverCharacteristicMapEntry, entriesLength C.int, rssi C.int) {
uuid := strings.ToLower(C.GoString(cUuid))
characteristics := map[string][]byte{}
if cEntries != nil && entriesLength > 0 {
// See CGO Wiki on how we can use this weird looking technique to get a go slice out of a c array
// https://github.com/golang/go/wiki/cgo
entries := (*[1 << 30]C.CBDriverCharacteristicMapEntry)(unsafe.Pointer(cEntries))[:int(entriesLength):int(entriesLength)]
for _, entry := range entries {
characteristicUuid := strings.ToLower(C.GoString(entry.uuid))
data := C.GoBytes(entry.data, entry.dataLength)
characteristics[characteristicUuid] = data
}
}
driverMu.Lock()
defer driverMu.Unlock()
if driver == nil {
vlog.Error("got onDiscovered event from CoreBluetooth but missing driver -- dropping")
return
}
driver.mu.Lock()
// Callbacks should happen off Swift threads and instead on a go routine.
// We use a local variable to avoid closure on driver itself since we have it currently locked.
if sh := driver.scanHandler; sh != nil {
go func() {
sh.OnDiscovered(uuid, characteristics, int(rssi))
}()
}
driver.mu.Unlock()
}
// StopScan implements v.io/x/lib/discovery/plugins/ble.Driver.StopScan
func (d *CoreBluetoothDriver) StopScan() {
// This call is thread-safe in obj-c
C.v23_cbdriver_stopScan()
d.mu.Lock()
d.scanHandler = nil
d.mu.Unlock()
}
// DebugString implements v.io/x/lib/discovery/plugins/ble.Driver.DebugString by
// returning the current state of the CoreBluetooth driver in a string description
func (d *CoreBluetoothDriver) DebugString() string {
cstr := C.v23_cbdriver_debug_string()
str := C.GoString(cstr)
C.free(unsafe.Pointer(cstr))
return str
}
// Callback from Obj-C
//export v23_corebluetooth_go_log
func v23_corebluetooth_go_log(message *C.char) {
msg := C.GoString(message)
// Run asynchronously to prevent deadlocks where us calling functions like stopScan log
// while already retaining this lock.
go func() {
driverMu.Lock()
if driver != nil {
driver.ctx.Info(msg)
} else {
vlog.Info(msg)
}
driverMu.Unlock()
}()
}
// Callback from Obj-C
//export v23_corebluetooth_go_log_error
func v23_corebluetooth_go_log_error(message *C.char) {
msg := C.GoString(message)
// Run asynchronously to prevent deadlocks where us calling functions like stopScan log
// while already retaining this lock.
go func() {
driverMu.Lock()
if driver != nil {
driver.ctx.Error(msg)
} else {
vlog.Error(msg)
}
driverMu.Unlock()
}()
}
func objcBOOL2Error(b C.BOOL, errStr **C.char) error {
// Any non-zero means true for Obj-C BOOL
if int(C.objcBOOL2int(b)) != 0 {
return nil
}
err := C.GoString(*errStr)
C.free(unsafe.Pointer(*errStr))
return errors.New(err)
}