Skip to content

Commit

Permalink
nrf: add GATT client
Browse files Browse the repository at this point in the history
This is not entirely complete (some errors are not handled properly) but
it's a start.
  • Loading branch information
aykevl committed Jun 27, 2020
1 parent 8129f7e commit 15b3e8e
Show file tree
Hide file tree
Showing 12 changed files with 742 additions and 11 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
106 changes: 104 additions & 2 deletions adapter_nrf528xx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions adapter_s110.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
12 changes: 12 additions & 0 deletions adapter_s132.c
Original file line number Diff line number Diff line change
Expand Up @@ -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};
12 changes: 12 additions & 0 deletions adapter_s140.c
Original file line number Diff line number Diff line change
Expand Up @@ -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};
16 changes: 16 additions & 0 deletions adapter_sd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
129 changes: 129 additions & 0 deletions examples/nusclient/main.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
}
}
14 changes: 14 additions & 0 deletions gap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading

0 comments on commit 15b3e8e

Please sign in to comment.