bb8767730c
This SoftDevice is used by default on the BBC micro:bit v2 so it's a good idea to add support here. Unfortunately this SoftDevice does not support scanning and connecting to other devices. This means that I unfortunately had to duplicate the event handler. I managed to refactor most other code to avoid duplicating much more. (This is when macros would have been useful in Go...)
453 lines
13 KiB
Go
453 lines
13 KiB
Go
// +build softdevice,s132v6 softdevice,s140v6 softdevice,s140v7
|
|
|
|
package bluetooth
|
|
|
|
/*
|
|
// Define SoftDevice functions as regular function declarations (not inline
|
|
// static functions).
|
|
#define SVCALL_AS_NORMAL_FUNCTION
|
|
|
|
#include "ble_gattc.h"
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"device/arm"
|
|
"errors"
|
|
"runtime/volatile"
|
|
)
|
|
|
|
const (
|
|
maxDefaultServicesToDiscover = 6
|
|
maxDefaultCharacteristicsToDiscover = 8
|
|
)
|
|
|
|
var (
|
|
errAlreadyDiscovering = errors.New("bluetooth: already discovering a service or characteristic")
|
|
errNotFound = errors.New("bluetooth: not found")
|
|
errNoNotify = errors.New("bluetooth: no notify permission")
|
|
)
|
|
|
|
// A global used while discovering services, to communicate between the main
|
|
// program and the event handler.
|
|
var discoveringService struct {
|
|
state volatile.Register8 // 0 means nothing happening, 1 means in progress, 2 means found something
|
|
startHandle volatile.Register16
|
|
endHandle volatile.Register16
|
|
uuid C.ble_uuid_t
|
|
}
|
|
|
|
// DeviceService is a BLE service on a connected peripheral device. It is only
|
|
// valid as long as the device remains connected.
|
|
type DeviceService struct {
|
|
uuid shortUUID
|
|
|
|
connectionHandle uint16
|
|
startHandle uint16
|
|
endHandle uint16
|
|
}
|
|
|
|
// UUID returns the UUID for this DeviceService.
|
|
func (s *DeviceService) UUID() UUID {
|
|
return s.uuid.UUID()
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// Passing a nil slice of UUIDs will return a complete list of
|
|
// services.
|
|
//
|
|
// On the Nordic SoftDevice, only one service discovery procedure may be done at
|
|
// a time.
|
|
func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
|
|
if discoveringService.state.Get() != 0 {
|
|
// Not concurrency safe, but should catch most concurrency misuses.
|
|
return nil, errAlreadyDiscovering
|
|
}
|
|
|
|
sz := maxDefaultServicesToDiscover
|
|
if len(uuids) > 0 {
|
|
sz = len(uuids)
|
|
}
|
|
services := make([]DeviceService, 0, sz)
|
|
|
|
var shortUUIDs []C.ble_uuid_t
|
|
|
|
// Make a map of UUIDs in SoftDevice short form, for easier comparing.
|
|
if len(uuids) > 0 {
|
|
shortUUIDs = make([]C.ble_uuid_t, sz)
|
|
for i, uuid := range uuids {
|
|
var errCode uint32
|
|
shortUUIDs[i], errCode = uuid.shortUUID()
|
|
if errCode != 0 {
|
|
return nil, Error(errCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
numFound := 0
|
|
|
|
var startHandle uint16 = 1
|
|
|
|
for i := 0; i < sz; i++ {
|
|
var suuid C.ble_uuid_t
|
|
if len(uuids) > 0 {
|
|
suuid = shortUUIDs[i]
|
|
}
|
|
|
|
// Start discovery of this service.
|
|
discoveringService.state.Set(1)
|
|
var errCode uint32
|
|
if len(uuids) > 0 {
|
|
errCode = C.sd_ble_gattc_primary_services_discover(d.connectionHandle, startHandle, &suuid)
|
|
} else {
|
|
// calling with nil searches for all primary services.
|
|
// TODO: need a way to set suuid from the returned data
|
|
errCode = C.sd_ble_gattc_primary_services_discover(d.connectionHandle, startHandle, nil)
|
|
}
|
|
if errCode != 0 {
|
|
discoveringService.state.Set(0)
|
|
return nil, Error(errCode)
|
|
}
|
|
|
|
// Wait until it is discovered.
|
|
// TODO: use some sort of condition variable once the scheduler supports
|
|
// them.
|
|
for discoveringService.state.Get() == 1 {
|
|
// still waiting...
|
|
arm.Asm("wfe")
|
|
}
|
|
// Retrieve values, and mark the global as unused.
|
|
startHandle = discoveringService.startHandle.Get()
|
|
endHandle := discoveringService.endHandle.Get()
|
|
suuid = discoveringService.uuid
|
|
discoveringService.state.Set(0)
|
|
|
|
if startHandle == 0 {
|
|
// The event handler will set the start handle to zero if the
|
|
// service was not found.
|
|
return nil, errNotFound
|
|
}
|
|
|
|
// Store the discovered service.
|
|
svc := DeviceService{
|
|
uuid: suuid,
|
|
connectionHandle: d.connectionHandle,
|
|
startHandle: startHandle,
|
|
endHandle: endHandle,
|
|
}
|
|
services = append(services, svc)
|
|
|
|
numFound++
|
|
if numFound >= sz {
|
|
break
|
|
}
|
|
|
|
// last entry
|
|
if endHandle == 0xffff {
|
|
break
|
|
}
|
|
|
|
// start with the next handle
|
|
startHandle = endHandle + 1
|
|
}
|
|
|
|
return services, nil
|
|
}
|
|
|
|
// DeviceCharacteristic is a BLE characteristic on a connected peripheral
|
|
// device. It is only valid as long as the device remains connected.
|
|
type DeviceCharacteristic struct {
|
|
uuid shortUUID
|
|
|
|
connectionHandle uint16
|
|
valueHandle uint16
|
|
cccdHandle uint16
|
|
permissions CharacteristicPermissions
|
|
}
|
|
|
|
// UUID returns the UUID for this DeviceCharacteristic.
|
|
func (c *DeviceCharacteristic) UUID() UUID {
|
|
return c.uuid.UUID()
|
|
}
|
|
|
|
// A global used to pass information from the event handler back to the
|
|
// DiscoverCharacteristics function below.
|
|
var discoveringCharacteristic struct {
|
|
uuid C.ble_uuid_t
|
|
char_props C.ble_gatt_char_props_t
|
|
handle_value volatile.Register16
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// Passing a nil slice of UUIDs will return a complete
|
|
// list of characteristics.
|
|
func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
|
|
if discoveringCharacteristic.handle_value.Get() != 0 {
|
|
return nil, errAlreadyDiscovering
|
|
}
|
|
|
|
sz := maxDefaultCharacteristicsToDiscover
|
|
if len(uuids) > 0 {
|
|
sz = len(uuids)
|
|
}
|
|
characteristics := make([]DeviceCharacteristic, 0, sz)
|
|
|
|
var shortUUIDs []C.ble_uuid_t
|
|
|
|
// Make a map of UUIDs in SoftDevice short form, for easier comparing.
|
|
if len(uuids) > 0 {
|
|
shortUUIDs = make([]C.ble_uuid_t, sz)
|
|
for i, uuid := range uuids {
|
|
var errCode uint32
|
|
shortUUIDs[i], errCode = uuid.shortUUID()
|
|
if errCode != 0 {
|
|
return nil, Error(errCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Request characteristics one by one, until all are found.
|
|
numFound := 0
|
|
startHandle := s.startHandle
|
|
|
|
for startHandle < s.endHandle {
|
|
// Discover the next characteristic in this service.
|
|
handles := C.ble_gattc_handle_range_t{
|
|
start_handle: startHandle,
|
|
end_handle: startHandle + 1,
|
|
}
|
|
|
|
errCode := C.sd_ble_gattc_characteristics_discover(s.connectionHandle, &handles)
|
|
if errCode != 0 {
|
|
return nil, Error(errCode)
|
|
}
|
|
|
|
// Wait until it is discovered.
|
|
// TODO: use some sort of condition variable once the scheduler supports
|
|
// them.
|
|
for discoveringCharacteristic.handle_value.Get() == 0 {
|
|
arm.Asm("wfe")
|
|
}
|
|
foundCharacteristicHandle := discoveringCharacteristic.handle_value.Get()
|
|
discoveringCharacteristic.handle_value.Set(0)
|
|
|
|
// was it last characteristic?
|
|
if foundCharacteristicHandle == 0xffff {
|
|
break
|
|
}
|
|
|
|
// Start the next request from the handle right after this one.
|
|
startHandle = foundCharacteristicHandle + 1
|
|
|
|
// not one of the characteristics we are looking for
|
|
if len(shortUUIDs) > 0 && !shortUUID(discoveringCharacteristic.uuid).IsIn(shortUUIDs) {
|
|
continue
|
|
}
|
|
|
|
// Found a characteristic.
|
|
permissions := CharacteristicPermissions(0)
|
|
rawPermissions := discoveringCharacteristic.char_props
|
|
if rawPermissions.bitfield_broadcast() != 0 {
|
|
permissions |= CharacteristicBroadcastPermission
|
|
}
|
|
if rawPermissions.bitfield_read() != 0 {
|
|
permissions |= CharacteristicReadPermission
|
|
}
|
|
if rawPermissions.bitfield_write_wo_resp() != 0 {
|
|
permissions |= CharacteristicWriteWithoutResponsePermission
|
|
}
|
|
if rawPermissions.bitfield_write() != 0 {
|
|
permissions |= CharacteristicWritePermission
|
|
}
|
|
if rawPermissions.bitfield_notify() != 0 {
|
|
permissions |= CharacteristicNotifyPermission
|
|
}
|
|
if rawPermissions.bitfield_indicate() != 0 {
|
|
permissions |= CharacteristicIndicatePermission
|
|
}
|
|
|
|
dc := DeviceCharacteristic{uuid: discoveringCharacteristic.uuid}
|
|
dc.permissions = permissions
|
|
dc.valueHandle = foundCharacteristicHandle
|
|
|
|
if permissions&CharacteristicNotifyPermission != 0 {
|
|
// This characteristic has the notify permission, so most
|
|
// likely it also supports notifications.
|
|
errCode := C.sd_ble_gattc_descriptors_discover(s.connectionHandle, &C.ble_gattc_handle_range_t{
|
|
start_handle: startHandle,
|
|
end_handle: startHandle + 1,
|
|
})
|
|
if errCode != 0 {
|
|
return nil, Error(errCode)
|
|
}
|
|
|
|
// Wait until the descriptor handle is found.
|
|
for discoveringCharacteristic.handle_value.Get() == 0 {
|
|
arm.Asm("wfe")
|
|
}
|
|
foundDescriptorHandle := discoveringCharacteristic.handle_value.Get()
|
|
discoveringCharacteristic.handle_value.Set(0)
|
|
|
|
dc.cccdHandle = foundDescriptorHandle
|
|
}
|
|
|
|
characteristics = append(characteristics, dc)
|
|
numFound++
|
|
if numFound >= sz {
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(uuids) > 0 && numFound != len(uuids) {
|
|
return nil, errNotFound
|
|
}
|
|
|
|
return characteristics, nil
|
|
}
|
|
|
|
// 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) {
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
errCode := C.sd_ble_gattc_write(c.connectionHandle, &C.ble_gattc_write_params_t{
|
|
write_op: C.BLE_GATT_OP_WRITE_CMD,
|
|
handle: c.valueHandle,
|
|
offset: 0,
|
|
len: uint16(len(p)),
|
|
p_value: &p[0],
|
|
})
|
|
if errCode != 0 {
|
|
return 0, Error(errCode)
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
type gattcNotificationCallback struct {
|
|
connectionHandle uint16
|
|
valueHandle uint16 // may be 0 if the slot is empty
|
|
callback func([]byte)
|
|
}
|
|
|
|
// List of notification callbacks for the current connection. Some slots may be
|
|
// empty, they are indicated with a zero valueHandle. They can be reused for new
|
|
// notification callbacks.
|
|
var gattcNotificationCallbacks []gattcNotificationCallback
|
|
|
|
// 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.
|
|
//
|
|
// Warning: when using the SoftDevice, the callback is called from an interrupt
|
|
// which means there are various limitations (such as not being able to allocate
|
|
// heap memory).
|
|
func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
|
|
if c.permissions&CharacteristicNotifyPermission == 0 {
|
|
return errNoNotify
|
|
}
|
|
|
|
// Try to insert the callback in the list.
|
|
updatedCallback := false
|
|
mask := DisableInterrupts()
|
|
for i, callbackInfo := range gattcNotificationCallbacks {
|
|
// Check for re-enabling an already enabled notification.
|
|
if callbackInfo.valueHandle == c.valueHandle {
|
|
gattcNotificationCallbacks[i].callback = callback
|
|
updatedCallback = true
|
|
break
|
|
}
|
|
}
|
|
if !updatedCallback {
|
|
for i, callbackInfo := range gattcNotificationCallbacks {
|
|
// Check for empty slots.
|
|
if callbackInfo.valueHandle == 0 {
|
|
gattcNotificationCallbacks[i] = gattcNotificationCallback{
|
|
connectionHandle: c.connectionHandle,
|
|
valueHandle: c.valueHandle,
|
|
callback: callback,
|
|
}
|
|
updatedCallback = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
RestoreInterrupts(mask)
|
|
|
|
// Add this callback to the list of callbacks, if it couldn't be inserted
|
|
// into the list.
|
|
if !updatedCallback {
|
|
// The append call is done outside of a cricital section to avoid GC in
|
|
// a critical section.
|
|
callbackList := append(gattcNotificationCallbacks, gattcNotificationCallback{
|
|
connectionHandle: c.connectionHandle,
|
|
valueHandle: c.valueHandle,
|
|
callback: callback,
|
|
})
|
|
mask := DisableInterrupts()
|
|
gattcNotificationCallbacks = callbackList
|
|
RestoreInterrupts(mask)
|
|
}
|
|
|
|
// Write to the CCCD to enable notifications. Don't wait for a response.
|
|
value := [2]byte{0x01, 0x00} // 0x0001 enables notifications (and disables indications)
|
|
errCode := C.sd_ble_gattc_write(c.connectionHandle, &C.ble_gattc_write_params_t{
|
|
write_op: C.BLE_GATT_OP_WRITE_CMD,
|
|
handle: c.cccdHandle,
|
|
offset: 0,
|
|
len: 2,
|
|
p_value: &value[0],
|
|
})
|
|
return makeError(errCode)
|
|
}
|
|
|
|
// A global used to pass information from the event handler back to the
|
|
// Read function below.
|
|
var readingCharacteristic struct {
|
|
handle_value volatile.Register16
|
|
offset uint16
|
|
length uint16
|
|
value []byte
|
|
}
|
|
|
|
// Read reads the current characteristic value up to MTU length.
|
|
// A future enhancement would be to be able to retrieve a longer
|
|
// value by making multiple calls.
|
|
func (c *DeviceCharacteristic) Read(data []byte) (n int, err error) {
|
|
// global will copy bytes from read operation into data slice
|
|
readingCharacteristic.value = data
|
|
|
|
errCode := C.sd_ble_gattc_read(c.connectionHandle, c.valueHandle, 0)
|
|
if errCode != 0 {
|
|
return 0, Error(errCode)
|
|
}
|
|
|
|
// wait for response with data
|
|
for readingCharacteristic.handle_value.Get() == 0 {
|
|
arm.Asm("wfe")
|
|
}
|
|
|
|
// how much data was read into buffer
|
|
n = int(readingCharacteristic.length)
|
|
|
|
// prepare for next read
|
|
readingCharacteristic.handle_value.Set(0)
|
|
readingCharacteristic.length = 0
|
|
|
|
return
|
|
}
|