all: add support for sending notifications

This is done by enabling the Notify permission and writing to the
characteristics: writes will automatically notify connected centrals.
This commit is contained in:
Ayke van Laethem 2020-06-03 17:33:32 +02:00
parent f69df9d879
commit b568c93250
No known key found for this signature in database
GPG Key ID: E97FF5335DFDFDED
14 changed files with 285 additions and 21 deletions

View File

@ -11,16 +11,21 @@ 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/nusserver
@md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/scanner
@md5sum test.hex
# Test some more boards that are not tested above.
$(TINYGO) build -o test.hex -size=short -target=pca10056-s140v7 ./examples/advertisement
@md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=microbit-s110v8 ./examples/advertisement
$(TINYGO) build -o test.hex -size=short -target=microbit-s110v8 ./examples/nusserver
@md5sum test.hex
smoketest-linux:
# Test on Linux.
GOOS=linux go build -o /tmp/go-build-discard ./examples/advertisement
GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate
GOOS=linux go build -o /tmp/go-build-discard ./examples/nusserver
GOOS=linux go build -o /tmp/go-build-discard ./examples/scanner
smoketest-windows:

View File

@ -12,6 +12,7 @@ This package attempts to build a cross-platform Bluetooth Low Energy module for
| 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
@ -53,6 +54,7 @@ Flashing will then need to be done a bit differently, using the CMSIS-DAP interf
Some things that will probably change:
* Add options to the `Scan` method, for example to filter on UUID.
* Extra options to the `Enable` function, to request particular features (such as the number of peripheral connections supported).
## License

View File

@ -46,8 +46,7 @@ func handleEvent() {
gapEvent := eventBuf.evt.unionfield_gap_evt()
switch id {
case C.BLE_GAP_EVT_CONNECTED:
// This event is ignored for now. It might be useful for the API
// user, but until there is a good use case it's best left out.
currentConnection.Reg = gapEvent.conn_handle
case C.BLE_GAP_EVT_DISCONNECTED:
if defaultAdvertisement.isAdvertising.Get() != 0 {
// The advertisement was running but was automatically stopped
@ -58,7 +57,7 @@ func handleEvent() {
// necessary.
defaultAdvertisement.start()
}
// Ignore this event otherwise.
currentConnection.Reg = C.BLE_CONN_HANDLE_INVALID
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
// Respond with the default PPCP connection parameters by passing
// nil:

View File

@ -49,8 +49,7 @@ func handleEvent() {
gapEvent := eventBuf.evt.unionfield_gap_evt()
switch id {
case C.BLE_GAP_EVT_CONNECTED:
// This event is ignored for now. It might be useful for the API
// user, but until there is a good use case it's best left out.
currentConnection.Reg = gapEvent.conn_handle
case C.BLE_GAP_EVT_DISCONNECTED:
if defaultAdvertisement.isAdvertising.Get() != 0 {
// The advertisement was running but was automatically stopped
@ -61,7 +60,7 @@ func handleEvent() {
// necessary.
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
}
// Ignore this event otherwise.
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 {

View File

@ -6,6 +6,7 @@ import (
"device/nrf"
"errors"
"runtime/interrupt"
"runtime/volatile"
"unsafe"
)
@ -18,6 +19,9 @@ var (
defaultDeviceName = [6]byte{'T', 'i', 'n', 'y', 'G', 'o'}
)
// There can only be one connection at a time in the default configuration.
var currentConnection = volatile.Register16{C.BLE_CONN_HANDLE_INVALID}
// Globally allocated buffer for incoming SoftDevice events.
var eventBuf struct {
C.ble_evt_t

View File

@ -0,0 +1,33 @@
// +build linux,!baremetal
package main
import (
"os"
"golang.org/x/crypto/ssh/terminal"
)
var (
stdout = os.Stdout
terminalState *terminal.State
)
func getchar() byte {
var b [1]byte
os.Stdin.Read(b[:])
return b[0]
}
func putchar(ch byte) {
b := [1]byte{ch}
os.Stdout.Write(b[:])
}
func initTerminal() {
terminalState, _ = terminal.MakeRaw(0)
}
func restoreTerminal() {
terminal.Restore(0, terminalState)
}

102
examples/nusserver/main.go Normal file
View File

@ -0,0 +1,102 @@
package main
// This example implements a NUS (Nordic UART Service) peripheral.
// I can't find much official documentation on the protocol, but this can be
// helpful:
// https://learn.adafruit.com/introducing-adafruit-ble-bluetooth-low-energy-friend/uart-service
//
// Code to interact with a raw terminal is in separate files with build tags.
import (
"github.com/tinygo-org/bluetooth"
)
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})
)
func main() {
println("starting")
adapter := bluetooth.DefaultAdapter
must("enable BLE stack", adapter.Enable())
adv := adapter.DefaultAdvertisement()
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
LocalName: "NUS", // Nordic UART Service
ServiceUUIDs: []bluetooth.UUID{serviceUUID},
Interval: bluetooth.NewAdvertisementInterval(100),
}))
must("start adv", adv.Start())
var rxChar bluetooth.Characteristic
var txChar bluetooth.Characteristic
must("add service", adapter.AddService(&bluetooth.Service{
UUID: serviceUUID,
Characteristics: []bluetooth.CharacteristicConfig{
{
Handle: &rxChar,
UUID: rxUUID,
Flags: bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission,
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
for _, c := range value {
// TODO: echo these characters back.
putchar(c)
if c == '\r' {
putchar('\n')
}
}
},
},
{
Handle: &txChar,
UUID: txUUID,
Flags: bluetooth.CharacteristicNotifyPermission | bluetooth.CharacteristicReadPermission,
},
},
}))
initTerminal()
defer restoreTerminal()
print("NUS console enabled, use Ctrl-X to exit\r\n")
var line []byte
for {
ch := getchar()
putchar(ch)
line = append(line, ch)
// Send the current line to the central.
if ch == '\x18' {
// The user pressed Ctrl-X, exit the terminal.
break
} else if ch == '\r' {
// Send another newline (consoles only seem to receive CR chars).
putchar('\n')
line = append(line, '\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 also sends a notification.
_, err := txChar.Write(part)
must("send notification", err)
}
}
}
}
func must(action string, err error) {
if err != nil {
panic("failed to " + action + ": " + err.Error())
}
}

38
examples/nusserver/nrf.go Normal file
View File

@ -0,0 +1,38 @@
// +build nrf
package main
import (
"machine"
"time"
)
var (
serial = machine.UART0
stdout = machine.UART0
)
func getchar() byte {
for {
// TODO: let ReadByte block instead of polling here.
time.Sleep(1 * time.Millisecond)
if stdout.Buffered() <= 0 {
continue
}
ch, _ := stdout.ReadByte()
if ch == 0 {
continue
}
return ch
}
}
func putchar(ch byte) {
stdout.WriteByte(ch)
}
func initTerminal() {
}
func restoreTerminal() {
}

View File

@ -7,13 +7,6 @@ type Service struct {
Characteristics []CharacteristicConfig
}
// Characteristic is a single characteristic in a service. It has an UUID and a
// value.
type Characteristic struct {
handle uint16
permissions CharacteristicPermissions
}
// CharacteristicConfig contains some parameters for the configuration of a
// single characteristic.
//
@ -27,12 +20,6 @@ type CharacteristicConfig struct {
WriteEvent func(client Connection, offset int, value []byte)
}
// Handle returns the numeric handle for this characteristic. This is used
// internally in the Bluetooth stack to identify this characteristic.
func (c *Characteristic) Handle() uint16 {
return c.handle
}
// CharacteristicPermissions lists a number of basic permissions/capabilities
// that clients have regarding this characteristic. For example, if you want to
// allow clients to read the value of this characteristic (a common scenario),

View File

@ -7,6 +7,13 @@ import (
"github.com/muka/go-bluetooth/bluez/profile/gatt"
)
// Characteristic is a single characteristic in a service. It has an UUID and a
// value.
type Characteristic struct {
handle *service.Char
permissions CharacteristicPermissions
}
// AddService creates a new service with the characteristics listed in the
// Service struct.
func (a *Adapter) AddService(s *Service) error {
@ -50,6 +57,11 @@ func (a *Adapter) AddService(s *Service) error {
}
bluezChar.Properties.Value = char.Value
if char.Handle != nil {
char.Handle.handle = bluezChar
char.Handle.permissions = char.Flags
}
// Do a callback when the value changes.
if char.WriteEvent != nil {
callback := char.WriteEvent
@ -73,3 +85,16 @@ func (a *Adapter) AddService(s *Service) error {
return app.Run()
}
// Write replaces the characteristic value with a new value.
func (c *Characteristic) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil // nothing to do
}
gattError := c.handle.WriteValue(p, nil)
if gattError != nil {
return 0, gattError
}
return len(p), nil
}

9
gatts_other.go Normal file
View File

@ -0,0 +1,9 @@
// +build !linux
package bluetooth
// Characteristic is a single characteristic in a service. It has an UUID and a
// value.
type Characteristic struct {
permissions CharacteristicPermissions
}

View File

@ -11,6 +11,13 @@ package bluetooth
*/
import "C"
// Characteristic is a single characteristic in a service. It has an UUID and a
// value.
type Characteristic struct {
handle uint16
permissions CharacteristicPermissions
}
// AddService creates a new service with the characteristics listed in the
// Service struct.
func (a *Adapter) AddService(service *Service) error {
@ -43,12 +50,13 @@ func (a *Adapter) AddService(service *Service) error {
},
init_len: uint16(len(char.Value)),
init_offs: 0,
max_len: uint16(len(char.Value)),
max_len: 20, // This is a conservative maximum length.
}
if len(char.Value) != 0 {
value.p_value = &char.Value[0]
}
value.p_attr_md.set_bitfield_vloc(C.BLE_GATTS_VLOC_STACK)
value.p_attr_md.set_bitfield_vlen(1)
errCode = C.sd_ble_gatts_characteristic_add(service.handle, &metadata, &value, &handles)
if errCode != 0 {
return Error(errCode)
@ -90,3 +98,48 @@ func (a *Adapter) getCharWriteHandler(handle uint16) *charWriteHandler {
}
return nil // not found
}
// Write replaces the characteristic value with a new value.
func (c *Characteristic) Write(p []byte) (n int, err error) {
if len(p) == 0 {
// Nothing to write.
return 0, nil
}
connHandle := currentConnection.Get()
if connHandle != C.BLE_CONN_HANDLE_INVALID {
// There is a connected central.
p_len := uint16(len(p))
errCode := C.sd_ble_gatts_hvx(connHandle, &C.ble_gatts_hvx_params_t{
handle: c.handle,
_type: C.BLE_GATT_HVX_NOTIFICATION,
p_len: &p_len,
p_data: &p[0],
})
// Check for some expected errors. Don't report them as errors, but
// instead fall through and do a normal characteristic value update.
// Only return (and possibly report an error) in other cases.
//
// TODO: improve CGo so that the C constant can be used.
if errCode == 0x0008 { // C.NRF_ERROR_INVALID_STATE
// May happen when the central has unsubscribed from the
// characteristic.
} else if errCode == 0x3401 { // C.BLE_ERROR_GATTS_SYS_ATTR_MISSING
// May happen when the central is not subscribed to this
// characteristic.
} else {
return int(p_len), makeError(errCode)
}
}
errCode := C.sd_ble_gatts_value_set(C.BLE_CONN_HANDLE_INVALID, c.handle, &C.ble_gatts_value_t{
len: uint16(len(p)),
p_value: &p[0],
})
if errCode != 0 {
return 0, Error(errCode)
}
return len(p), nil
}

1
go.mod
View File

@ -5,4 +5,5 @@ go 1.14
require (
github.com/go-ole/go-ole v1.2.4
github.com/muka/go-bluetooth v0.0.0-20200601103727-d7408229e514
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed
)

7
go.sum
View File

@ -23,9 +23,16 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/suapapa/go_eddystone v0.0.0-20190827074641-8d8c1bb79363/go.mod h1:O/oFfbntg0b1z5NM/IGoTMKYPO3lkzPSA53E+J99lDU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed h1:g4KENRiCMEx58Q7/ecwfT0N2o8z35Fnbsjig/Alf2T4=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=