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:
parent
f69df9d879
commit
b568c93250
7
Makefile
7
Makefile
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
13
gatts.go
13
gatts.go
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
55
gatts_sd.go
55
gatts_sd.go
|
@ -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
1
go.mod
|
@ -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
7
go.sum
|
@ -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=
|
||||
|
|
Loading…
Reference in New Issue