From 15b3e8e3e21e0d5857a8e246560ecc7d7be729fc Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Thu, 11 Jun 2020 16:52:04 +0200 Subject: [PATCH] nrf: add GATT client This is not entirely complete (some errors are not handled properly) but it's a start. --- Makefile | 2 + README.md | 19 ++- adapter_nrf528xx.go | 106 +++++++++++- adapter_s110.c | 1 + adapter_s132.c | 12 ++ adapter_s140.c | 12 ++ adapter_sd.go | 16 ++ examples/nusclient/main.go | 129 ++++++++++++++ gap.go | 14 ++ gap_nrf528xx.go | 101 +++++++++++ gattc_sd.go | 336 +++++++++++++++++++++++++++++++++++++ gatts_sd.go | 5 +- 12 files changed, 742 insertions(+), 11 deletions(-) create mode 100644 examples/nusclient/main.go create mode 100644 gattc_sd.go diff --git a/Makefile b/Makefile index ef09317..7495cf6 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ smoketest-tinygo: @md5sum test.hex $(TINYGO) build -o test.hex -size=short -target=reelboard-s140v7 ./examples/ledcolor @md5sum test.hex + $(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/nusclient + @md5sum test.hex $(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/nusserver @md5sum test.hex $(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/scanner diff --git a/README.md b/README.md index 8ee9504..d605226 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,17 @@ This package attempts to build a cross-platform Bluetooth Low Energy module for Go. It currently supports the following systems: -| | Windows | Linux | Nordic chips | -| --------------------- | ------------------ | ------------------ | ------------------ | -| API used | WinRT | BlueZ (over D-Bus) | SoftDevice | -| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Advertisement | :x: | :heavy_check_mark: | :heavy_check_mark: | -| Local services | :x: | :heavy_check_mark: | :heavy_check_mark: | -| Local characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | -| Send notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | +| | Windows | Linux | Nordic chips | +| -------------------------------- | ------------------ | ------------------ | ------------------ | +| API used | WinRT | BlueZ (over D-Bus) | SoftDevice | +| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Connect to peripheral | :x: | :x: | :heavy_check_mark: | +| Write peripheral characteristics | :x: | :x: | :heavy_check_mark: | +| Receive notifications | :x: | :x: | :heavy_check_mark: | +| Advertisement | :x: | :heavy_check_mark: | :heavy_check_mark: | +| Local services | :x: | :heavy_check_mark: | :heavy_check_mark: | +| Local characteristics | :x: | :heavy_check_mark: | :heavy_check_mark: | +| Send notifications | :x: | :heavy_check_mark: | :heavy_check_mark: | ## Baremetal support diff --git a/adapter_nrf528xx.go b/adapter_nrf528xx.go index 9de2d3a..5daad13 100644 --- a/adapter_nrf528xx.go +++ b/adapter_nrf528xx.go @@ -8,6 +8,7 @@ package bluetooth #define SVCALL_AS_NORMAL_FUNCTION #include "nrf_sdm.h" +#include "nrf_nvic.h" #include "ble.h" #include "ble_gap.h" @@ -49,8 +50,32 @@ func handleEvent() { gapEvent := eventBuf.evt.unionfield_gap_evt() switch id { case C.BLE_GAP_EVT_CONNECTED: - currentConnection.Reg = gapEvent.conn_handle + connectEvent := gapEvent.params.unionfield_connected() + switch connectEvent.role { + case C.BLE_GAP_ROLE_PERIPH: + if debug { + println("evt: connected in peripheral role") + } + currentConnection.Reg = gapEvent.conn_handle + case C.BLE_GAP_ROLE_CENTRAL: + if debug { + println("evt: connected in central role") + } + connectionAttempt.connectionHandle = gapEvent.conn_handle + connectionAttempt.state.Set(2) // connection was successful + } case C.BLE_GAP_EVT_DISCONNECTED: + if debug { + println("evt: disconnected") + } + // Clean up state for this connection. + for i, cb := range gattcNotificationCallbacks { + if cb.connectionHandle == currentConnection.Reg { + gattcNotificationCallbacks[i].valueHandle = 0 // 0 means invalid + } + } + currentConnection.Reg = C.BLE_CONN_HANDLE_INVALID + // Auto-restart advertisement if needed. if defaultAdvertisement.isAdvertising.Get() != 0 { // The advertisement was running but was automatically stopped // by the connection event. @@ -60,7 +85,6 @@ func handleEvent() { // necessary. C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT) } - currentConnection.Reg = C.BLE_CONN_HANDLE_INVALID case C.BLE_GAP_EVT_ADV_REPORT: advReport := gapEvent.params.unionfield_adv_report() if debug && &scanReportBuffer.data[0] != advReport.data.p_data { @@ -126,6 +150,84 @@ func handleEvent() { println("unknown GATTS event:", id, id-C.BLE_GATTS_EVT_BASE) } } + case id >= C.BLE_GATTC_EVT_BASE && id <= C.BLE_GATTC_EVT_LAST: + gattcEvent := eventBuf.evt.unionfield_gattc_evt() + switch id { + case C.BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP: + discoveryEvent := gattcEvent.params.unionfield_prim_srvc_disc_rsp() + if debug { + println("evt: discovered primary service", discoveryEvent.count) + } + discoveringService.state.Set(2) // signal there is a result + if discoveryEvent.count >= 1 { + // Theoretically there may be more, but as we're only using + // sd_ble_gattc_primary_services_discover, there should only be + // one discovered service. Use the first as a sensible fallback. + discoveringService.startHandle.Set(discoveryEvent.services[0].handle_range.start_handle) + discoveringService.endHandle.Set(discoveryEvent.services[0].handle_range.end_handle) + } else { + // No service found. + discoveringService.startHandle.Set(0) + } + case C.BLE_GATTC_EVT_CHAR_DISC_RSP: + discoveryEvent := gattcEvent.params.unionfield_char_disc_rsp() + if debug { + println("evt: discovered characteristics", discoveryEvent.count) + } + if discoveryEvent.count >= 1 { + // There may be more, but for ease of implementing we only + // handle the first. + discoveringCharacteristic.handle_value.Set(discoveryEvent.chars[0].handle_value) + discoveringCharacteristic.char_props = discoveryEvent.chars[0].char_props + discoveringCharacteristic.uuid = discoveryEvent.chars[0].uuid + } + case C.BLE_GATTC_EVT_DESC_DISC_RSP: + discoveryEvent := gattcEvent.params.unionfield_desc_disc_rsp() + if debug { + println("evt: discovered descriptors", discoveryEvent.count) + } + if discoveryEvent.count >= 1 { + // There may be more, but for ease of implementing we only + // handle the first. + uuid := discoveryEvent.descs[0].uuid + if uuid._type == C.BLE_UUID_TYPE_BLE && uuid.uuid == 0x2902 { + // Found a CCCD (Client Characteristic Configuration + // Descriptor), which has a 16-bit UUID with value 0x2902). + discoveringCharacteristic.handle_value.Set(discoveryEvent.descs[0].handle) + } else { + // Found something else? + // TODO: handle this properly by continuing the scan. For + // now, give up if we found something other than a CCCD. + if debug { + println(" found some other descriptor (unimplemented)") + } + } + } + case C.BLE_GATTC_EVT_HVX: + hvxEvent := gattcEvent.params.unionfield_hvx() + switch hvxEvent._type { + case C.BLE_GATT_HVX_NOTIFICATION: + if debug { + println("evt: notification", hvxEvent.handle) + } + // Find the callback and call it (if there is any). + for _, callbackInfo := range gattcNotificationCallbacks { + if callbackInfo.valueHandle == hvxEvent.handle && callbackInfo.connectionHandle == gattcEvent.conn_handle { + // Create a Go slice from the data, to pass to the + // callback. + data := (*[255]byte)(unsafe.Pointer(&hvxEvent.data[0]))[:hvxEvent.len:hvxEvent.len] + if callbackInfo.callback != nil { + callbackInfo.callback(data) + } + break + } + } + } + default: + if debug { + println("unknown GATTC event:", id, id-C.BLE_GATTC_EVT_BASE) + } + } default: if debug { println("unknown event:", id) diff --git a/adapter_s110.c b/adapter_s110.c index a1adeb7..6ccb607 100644 --- a/adapter_s110.c +++ b/adapter_s110.c @@ -7,4 +7,5 @@ #define static #include "s110_nrf51_8.0.0/s110_nrf51_8.0.0_API/include/nrf_sdm.h" +#include "s110_nrf51_8.0.0/s110_nrf51_8.0.0_API/include/nrf_soc.h" #include "s110_nrf51_8.0.0/s110_nrf51_8.0.0_API/include/ble.h" diff --git a/adapter_s132.c b/adapter_s132.c index c3607b1..e72b947 100644 --- a/adapter_s132.c +++ b/adapter_s132.c @@ -6,5 +6,17 @@ // Discard all 'static' attributes to define functions normally. #define static +// Get rid of all __STATIC_INLINE symbols. +// This is a bit less straightforward: we first need to include the header that +// defines it, and then redefine it. +#include "nrf.h" +#undef __STATIC_INLINE +#define __STATIC_INLINE + #include "s132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include/nrf_sdm.h" +#include "s132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include/nrf_nvic.h" #include "s132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include/ble.h" + +// Define nrf_nvic_state, which is used by sd_nvic_critical_region_enter and +// sd_nvic_critical_region_exit. +nrf_nvic_state_t nrf_nvic_state = {0}; diff --git a/adapter_s140.c b/adapter_s140.c index f3fc947..bcaf6c8 100644 --- a/adapter_s140.c +++ b/adapter_s140.c @@ -6,5 +6,17 @@ // Discard all 'static' attributes to define functions normally. #define static +// Get rid of all __STATIC_INLINE symbols. +// This is a bit less straightforward: we first need to include the header that +// defines it, and then redefine it. +#include "nrf.h" +#undef __STATIC_INLINE +#define __STATIC_INLINE + #include "s140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include/nrf_sdm.h" +#include "s140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include/nrf_nvic.h" #include "s140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include/ble.h" + +// Define nrf_nvic_state, which is used by sd_nvic_critical_region_enter and +// sd_nvic_critical_region_exit. +nrf_nvic_state_t nrf_nvic_state = {0}; diff --git a/adapter_sd.go b/adapter_sd.go index 2b27455..fda0d0d 100644 --- a/adapter_sd.go +++ b/adapter_sd.go @@ -96,3 +96,19 @@ func (a *Adapter) Enable() error { errCode = C.sd_ble_gap_ppcp_set(&gapConnParams) return makeError(errCode) } + +// DisableInterrupts must be used instead of disabling interrupts directly, to +// play well with the SoftDevice. Restore interrupts to the previous state with +// RestoreInterrupts. +func DisableInterrupts() uintptr { + var is_nested_critical_region uint8 + C.sd_nvic_critical_region_enter(&is_nested_critical_region) + return uintptr(is_nested_critical_region) +} + +// RestoreInterrupts restores interrupts to the state before calling +// DisableInterrupts. The mask parameter must be the value returned by +// DisableInterrupts. +func RestoreInterrupts(mask uintptr) { + C.sd_nvic_critical_region_exit(uint8(mask)) +} diff --git a/examples/nusclient/main.go b/examples/nusclient/main.go new file mode 100644 index 0000000..35d4e8f --- /dev/null +++ b/examples/nusclient/main.go @@ -0,0 +1,129 @@ +package main + +// This example implements a NUS (Nordic UART Service) client. See nusserver for +// details. + +import ( + "github.com/tinygo-org/bluetooth" + "github.com/tinygo-org/bluetooth/rawterm" +) + +var ( + serviceUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x01, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E}) + rxUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x02, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E}) + txUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x03, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E}) +) + +var adapter = bluetooth.DefaultAdapter + +func main() { + // Enable BLE interface. + err := adapter.Enable() + if err != nil { + println("could not enable the BLE stack:", err.Error()) + return + } + + // The address to connect to. Set during scanning and read afterwards. + var foundDevice bluetooth.ScanResult + + // Scan for NUS peripheral. + println("Scanning...") + err = adapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) { + if !result.AdvertisementPayload.HasServiceUUID(serviceUUID) { + return + } + foundDevice = result + + // Stop the scan. + err := adapter.StopScan() + if err != nil { + // Unlikely, but we can't recover from this. + println("failed to stop the scan:", err.Error()) + } + }) + if err != nil { + println("could not start a scan:", err.Error()) + return + } + + // Found a device: print this event. + if name := foundDevice.LocalName(); name == "" { + print("Connecting to ", foundDevice.Address.String(), "...") + println() + } else { + print("Connecting to ", name, " (", foundDevice.Address.String(), ")...") + println() + } + + // Found a NUS peripheral. Connect to it. + device, err := adapter.Connect(foundDevice.Address, bluetooth.ConnectionParams{}) + if err != nil { + println("Failed to connect:", err.Error()) + return + } + + // Connected. Look up the Nordic UART Service. + println("Discovering service...") + services, err := device.DiscoverServices([]bluetooth.UUID{serviceUUID}) + if err != nil { + println("Failed to discover the Nordic UART Service:", err.Error()) + return + } + service := services[0] + + // Get the two characteristics present in this service. + chars, err := service.DiscoverCharacteristics([]bluetooth.UUID{rxUUID, txUUID}) + if err != nil { + println("Failed to discover RX and TX characteristics:", err.Error()) + return + } + rx := chars[0] + tx := chars[1] + + // Enable notifications to receive incoming data. + err = tx.EnableNotifications(func(value []byte) { + for _, c := range value { + rawterm.Putchar(c) + } + }) + if err != nil { + println("Failed to enable TX notifications:", err.Error()) + return + } + + println("Connected. Exit console using Ctrl-X.") + rawterm.Configure() + defer rawterm.Restore() + var line []byte + for { + ch := rawterm.Getchar() + line = append(line, ch) + + // Send the current line to the central. + if ch == '\x18' { + // The user pressed Ctrl-X, exit the program. + break + } else if ch == '\n' { + sendbuf := line // copy buffer + // Reset the slice while keeping the buffer in place. + line = line[:0] + + // Send the sendbuf after breaking it up in pieces. + for len(sendbuf) != 0 { + // Chop off up to 20 bytes from the sendbuf. + partlen := 20 + if len(sendbuf) < 20 { + partlen = len(sendbuf) + } + part := sendbuf[:partlen] + sendbuf = sendbuf[partlen:] + // This performs a "write command" aka "write without response". + _, err := rx.WriteWithoutResponse(part) + if err != nil { + println("could not send:", err.Error()) + } + } + } + } +} diff --git a/gap.go b/gap.go index 898b60b..bd15c31 100644 --- a/gap.go +++ b/gap.go @@ -305,3 +305,17 @@ func (buf *rawAdvertisementPayload) addServiceUUID(uuid UUID) (ok bool) { return true } } + +// ConnectionParams are used when connecting to a peripherals. +type ConnectionParams struct { + // The timeout for the connection attempt. Not used during the rest of the + // connection. If no duration is specified, a default timeout will be used. + ConnectionTimeout Duration + + // Minimum and maximum connection interval. The shorter the interval, the + // faster data can travel between both devices but also the more power they + // will draw. If no intervals are specified, a default connection interval + // will be used. + MinInterval Duration + MaxInterval Duration +} diff --git a/gap_nrf528xx.go b/gap_nrf528xx.go index 31646e9..8a9c373 100644 --- a/gap_nrf528xx.go +++ b/gap_nrf528xx.go @@ -4,6 +4,7 @@ package bluetooth import ( "device/arm" + "errors" "runtime/volatile" "time" ) @@ -17,6 +18,8 @@ import ( */ import "C" +var errAlreadyConnecting = errors.New("bluetooth: already in a connection attempt") + // Memory buffers needed by sd_ble_gap_scan_start. var ( scanReportBuffer rawAdvertisementPayload @@ -83,6 +86,9 @@ func (a *Advertisement) Start() error { // Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern // is to cancel the scan when a particular device has been found. +// +// The callback is run on the same goroutine as the Scan function when using a +// SoftDevice. func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { if a.scanning { // There is a possible race condition here if Scan() is called from a @@ -140,5 +146,100 @@ func (a *Adapter) StopScan() error { return errNotScanning } a.scanning = false + + // TODO: stop immediately, not when the next scan result arrives. + return nil } + +// Device is a connection to a remote peripheral. +type Device struct { + connectionHandle uint16 +} + +// In-progress connection attempt. +var connectionAttempt struct { + state volatile.Register8 // 0 means unused, 1 means connecting, 2 means ready (connected or timeout) + connectionHandle uint16 +} + +// Connect starts a connection attempt to the given peripheral device address. +// +// Limitations on Nordic SoftDevices inclue that you cannot do more than one +// connection attempt at once and that the address parameter must have the +// IsRandom bit set correctly. This bit is set correctly for scan results, so +// you can reuse that address directly. +func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, error) { + // Construct an address object as used in the SoftDevice. + var addr C.ble_gap_addr_t + addr.addr = address.MAC + if address.IsRandom { + switch address.MAC[5] >> 6 { + case 0b11: + addr.set_bitfield_addr_type(C.BLE_GAP_ADDR_TYPE_RANDOM_STATIC) + case 0b01: + addr.set_bitfield_addr_type(C.BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE) + case 0b00: + addr.set_bitfield_addr_type(C.BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE) + } + } + + // Pick default values if some parameters aren't specified. + if params.ConnectionTimeout == 0 { + params.ConnectionTimeout = NewDuration(4 * time.Second) + } + if params.MinInterval == 0 && params.MaxInterval == 0 { + // Pick some semi-arbitrary range if these values haven't been + // configured. The values have been picked to be compliant with the + // guidelines from Apple (section 35.6 Connection Parameters): + // https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf + params.MinInterval = NewDuration(15 * time.Millisecond) + params.MaxInterval = NewDuration(150 * time.Millisecond) + } + + // Set scan params, presumably these parameters are used to re-scan for the + // device to connect to because only right after an advertisement has been + // received is the device connectable. + scanParams := C.ble_gap_scan_params_t{} + scanParams.set_bitfield_extended(0) + scanParams.set_bitfield_active(0) + scanParams.interval = uint16(NewDuration(40 * time.Millisecond)) + scanParams.window = uint16(NewDuration(30 * time.Millisecond)) + scanParams.timeout = uint16(params.ConnectionTimeout) + + connectionParams := C.ble_gap_conn_params_t{ + min_conn_interval: uint16(params.MinInterval) / 2, + max_conn_interval: uint16(params.MaxInterval) / 2, + slave_latency: 0, // mostly relevant to connected keyboards etc + conn_sup_timeout: 200, // 2 seconds (in 10ms units), the minimum recommended by Apple + } + + // Flag to the event handler that we are waiting for incoming connections. + // This should be safe as long as Connect is not called concurrently. And + // even then, it should catch most such race conditions. + if connectionAttempt.state.Get() != 0 { + return nil, errAlreadyConnecting + } + connectionAttempt.state.Set(1) + + // Start the connection attempt. We'll get a signal in the event handler. + errCode := C.sd_ble_gap_connect(&addr, &scanParams, &connectionParams, C.BLE_CONN_CFG_TAG_DEFAULT) + if errCode != 0 { + connectionAttempt.state.Set(0) + return nil, Error(errCode) + } + + // Wait until the connection is established. + // TODO: use some sort of condition variable once the scheduler supports + // them. + for connectionAttempt.state.Get() != 2 { + arm.Asm("wfe") + } + connectionHandle := connectionAttempt.connectionHandle + connectionAttempt.state.Set(0) + + // Connection has been established. + return &Device{ + connectionHandle: connectionHandle, + }, nil +} diff --git a/gattc_sd.go b/gattc_sd.go new file mode 100644 index 0000000..1388d83 --- /dev/null +++ b/gattc_sd.go @@ -0,0 +1,336 @@ +// +build softdevice,!s110v8 + +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" +) + +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 +} + +// DeviceService is a BLE service on a connected peripheral device. It is only +// valid as long as the device remains connected. +type DeviceService struct { + connectionHandle uint16 + startHandle uint16 + endHandle uint16 +} + +// 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 currently result in zero services being +// returned, but this may be changed in the future to 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 + } + + services := make([]DeviceService, len(uuids)) + for i, uuid := range uuids { + // Start discovery of this service. + shortUUID, errCode := uuid.shortUUID() + if errCode != 0 { + return nil, Error(errCode) + } + discoveringService.state.Set(1) + errCode = C.sd_ble_gattc_primary_services_discover(d.connectionHandle, 0, &shortUUID) + 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() + 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. + services[i] = DeviceService{ + connectionHandle: d.connectionHandle, + startHandle: startHandle, + endHandle: endHandle, + } + } + + 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 { + connectionHandle uint16 + valueHandle uint16 + cccdHandle uint16 + permissions CharacteristicPermissions +} + +// 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 currently result in zero characteristics +// being returned, but this may be changed in the future to return a complete +// list of characteristics. +func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) { + if len(uuids) == 0 { + // Nothing to do. This behavior might change in the future (if a nil + // uuids slice is passed). + return nil, nil + } + + if discoveringCharacteristic.handle_value.Get() != 0 { + return nil, errAlreadyDiscovering + } + + // Make a list of UUIDs in SoftDevice short form, for easier comparing. + shortUUIDs := make([]C.ble_uuid_t, len(uuids)) + 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 + characteristics := make([]DeviceCharacteristic, len(uuids)) + startHandle := s.startHandle + for numFound < len(uuids) && startHandle < s.endHandle { + // Discover the next characteristic in this service. + handles := C.ble_gattc_handle_range_t{ + start_handle: startHandle, + end_handle: s.endHandle, + } + 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) + + // Start the next request from the handle right after this one. + startHandle = foundCharacteristicHandle + 1 + + // Look whether we found a requested handle. + for i, shortUUID := range shortUUIDs { + if discoveringCharacteristic.uuid == shortUUID { + // 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 + } + characteristics[i].permissions = permissions + characteristics[i].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: s.endHandle, + }) + 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) + + characteristics[i].cccdHandle = foundDescriptorHandle + } + + numFound++ + break + } + } + } + + if 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) +} diff --git a/gatts_sd.go b/gatts_sd.go index abd17f4..796fb24 100644 --- a/gatts_sd.go +++ b/gatts_sd.go @@ -66,10 +66,13 @@ func (a *Adapter) AddService(service *Service) error { char.Handle.permissions = char.Flags } if char.Flags.Write() && char.WriteEvent != nil { - a.charWriteHandlers = append(a.charWriteHandlers, charWriteHandler{ + handlers := append(a.charWriteHandlers, charWriteHandler{ handle: handles.value_handle, callback: char.WriteEvent, }) + mask := DisableInterrupts() + a.charWriteHandlers = handlers + RestoreInterrupts(mask) } } return makeError(errCode)