2022-02-22 13:04:20 +03:00
|
|
|
//go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
|
2021-04-14 18:23:12 +03:00
|
|
|
// +build softdevice,s132v6 softdevice,s140v6 softdevice,s140v7
|
2020-06-11 17:52:04 +03:00
|
|
|
|
|
|
|
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"
|
|
|
|
)
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
const (
|
|
|
|
maxDefaultServicesToDiscover = 6
|
|
|
|
maxDefaultCharacteristicsToDiscover = 8
|
|
|
|
)
|
|
|
|
|
2020-06-11 17:52:04 +03:00
|
|
|
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
|
2020-09-13 21:21:38 +03:00
|
|
|
uuid C.ble_uuid_t
|
2020-06-11 17:52:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// DeviceService is a BLE service on a connected peripheral device. It is only
|
|
|
|
// valid as long as the device remains connected.
|
|
|
|
type DeviceService struct {
|
2020-09-13 21:21:38 +03:00
|
|
|
uuid shortUUID
|
|
|
|
|
2020-06-11 17:52:04 +03:00
|
|
|
connectionHandle uint16
|
|
|
|
startHandle uint16
|
|
|
|
endHandle uint16
|
|
|
|
}
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
// UUID returns the UUID for this DeviceService.
|
|
|
|
func (s *DeviceService) UUID() UUID {
|
|
|
|
return s.uuid.UUID()
|
|
|
|
}
|
|
|
|
|
2020-06-11 17:52:04 +03:00
|
|
|
// 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.
|
|
|
|
//
|
2020-09-13 21:21:38 +03:00
|
|
|
// Passing a nil slice of UUIDs will return a complete list of
|
2020-06-11 17:52:04 +03:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
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)
|
|
|
|
}
|
2020-06-11 17:52:04 +03:00
|
|
|
}
|
2020-09-13 21:21:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2020-06-11 17:52:04 +03:00
|
|
|
discoveringService.state.Set(1)
|
2020-09-13 21:21:38 +03:00
|
|
|
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)
|
|
|
|
}
|
2020-06-11 17:52:04 +03:00
|
|
|
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.
|
2020-09-13 21:21:38 +03:00
|
|
|
startHandle = discoveringService.startHandle.Get()
|
2020-06-11 17:52:04 +03:00
|
|
|
endHandle := discoveringService.endHandle.Get()
|
2020-09-13 21:21:38 +03:00
|
|
|
suuid = discoveringService.uuid
|
2020-06-11 17:52:04 +03:00
|
|
|
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.
|
2020-09-13 21:21:38 +03:00
|
|
|
svc := DeviceService{
|
2022-04-13 19:02:17 +03:00
|
|
|
uuid: shortUUID(suuid),
|
2020-06-11 17:52:04 +03:00
|
|
|
connectionHandle: d.connectionHandle,
|
|
|
|
startHandle: startHandle,
|
|
|
|
endHandle: endHandle,
|
|
|
|
}
|
2020-09-13 21:21:38 +03:00
|
|
|
services = append(services, svc)
|
|
|
|
|
|
|
|
numFound++
|
|
|
|
if numFound >= sz {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// last entry
|
|
|
|
if endHandle == 0xffff {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// start with the next handle
|
|
|
|
startHandle = endHandle + 1
|
2020-06-11 17:52:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2020-09-13 21:21:38 +03:00
|
|
|
uuid shortUUID
|
|
|
|
|
2020-06-11 17:52:04 +03:00
|
|
|
connectionHandle uint16
|
|
|
|
valueHandle uint16
|
|
|
|
cccdHandle uint16
|
|
|
|
permissions CharacteristicPermissions
|
|
|
|
}
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
// UUID returns the UUID for this DeviceCharacteristic.
|
|
|
|
func (c *DeviceCharacteristic) UUID() UUID {
|
|
|
|
return c.uuid.UUID()
|
|
|
|
}
|
|
|
|
|
2020-06-11 17:52:04 +03:00
|
|
|
// 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.
|
|
|
|
//
|
2020-09-13 21:21:38 +03:00
|
|
|
// Passing a nil slice of UUIDs will return a complete
|
2020-06-11 17:52:04 +03:00
|
|
|
// list of characteristics.
|
|
|
|
func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
|
|
|
|
if discoveringCharacteristic.handle_value.Get() != 0 {
|
|
|
|
return nil, errAlreadyDiscovering
|
|
|
|
}
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
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)
|
|
|
|
}
|
2020-06-11 17:52:04 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Request characteristics one by one, until all are found.
|
|
|
|
numFound := 0
|
|
|
|
startHandle := s.startHandle
|
2020-09-13 21:21:38 +03:00
|
|
|
|
|
|
|
for startHandle < s.endHandle {
|
2020-06-11 17:52:04 +03:00
|
|
|
// Discover the next characteristic in this service.
|
|
|
|
handles := C.ble_gattc_handle_range_t{
|
|
|
|
start_handle: startHandle,
|
2020-09-13 21:21:38 +03:00
|
|
|
end_handle: startHandle + 1,
|
2020-06-11 17:52:04 +03:00
|
|
|
}
|
2020-09-13 21:21:38 +03:00
|
|
|
|
2020-06-11 17:52:04 +03:00
|
|
|
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)
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
// was it last characteristic?
|
|
|
|
if foundCharacteristicHandle == 0xffff {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2020-06-11 17:52:04 +03:00
|
|
|
// Start the next request from the handle right after this one.
|
|
|
|
startHandle = foundCharacteristicHandle + 1
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2022-04-13 19:02:17 +03:00
|
|
|
dc := DeviceCharacteristic{uuid: shortUUID(discoveringCharacteristic.uuid)}
|
2020-09-13 21:21:38 +03:00
|
|
|
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")
|
2020-06-11 17:52:04 +03:00
|
|
|
}
|
2020-09-13 21:21:38 +03:00
|
|
|
foundDescriptorHandle := discoveringCharacteristic.handle_value.Get()
|
|
|
|
discoveringCharacteristic.handle_value.Set(0)
|
|
|
|
|
|
|
|
dc.cccdHandle = foundDescriptorHandle
|
|
|
|
}
|
|
|
|
|
|
|
|
characteristics = append(characteristics, dc)
|
|
|
|
numFound++
|
|
|
|
if numFound >= sz {
|
|
|
|
break
|
2020-06-11 17:52:04 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
if len(uuids) > 0 && numFound != len(uuids) {
|
2020-06-11 17:52:04 +03:00
|
|
|
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)
|
|
|
|
}
|
2020-10-22 20:04:47 +03:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2022-05-12 12:49:03 +03:00
|
|
|
|
|
|
|
// GetMTU returns the MTU for the characteristic.
|
|
|
|
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
|
|
|
|
return uint16(C.BLE_GATT_ATT_MTU_DEFAULT), nil
|
|
|
|
}
|