From 086c797e0f22f6f8f420fd62bf7ea9c5073079c5 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 1 Jun 2020 14:07:07 +0200 Subject: [PATCH] all: simplify advertisement configuration This changes the previous raw advertisement packets to structured advertisement configuration. That means you can set the local name not with a raw byte array but with a normal string. While this departs a bit from the original low-level interface as is often used on microcontroller BLE stacks, it is certainly easier to use and better matches higher level APIs that are commonly provided by general-purpose operating systems. If there is a need for raw BLE packets (for baremetal systems only), this can easily be added in the future. --- README.md | 1 - examples/advertisement/main.go | 11 +++---- examples/heartrate/main.go | 11 +++---- examples/ledcolor/main.go | 11 +++---- gap.go | 57 +++++++++++++++++++++++++++------- gap_linux.go | 40 ++++-------------------- gap_nrf51.go | 25 ++++++--------- gap_nrf528xx.go | 19 ++++++------ 8 files changed, 82 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 8895e73..10bd538 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ 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. - * Change advertisement configuration to accept structured BLE packets instead of raw packets. * Connect/disconnect events. * The behavior around advertisement. Nordic SoftDevices stop advertising when a device connects, which is somewhat unintuitive. diff --git a/examples/advertisement/main.go b/examples/advertisement/main.go index 408176d..7a1f45f 100644 --- a/examples/advertisement/main.go +++ b/examples/advertisement/main.go @@ -6,18 +6,15 @@ import ( "github.com/tinygo-org/bluetooth" ) -// flags + local name -var advPayload = []byte("\x02\x01\x06" + "\x07\x09TinyGo") - var adapter = bluetooth.DefaultAdapter func main() { must("enable BLE stack", adapter.Enable()) adv := adapter.NewAdvertisement() - options := &bluetooth.AdvertiseOptions{ - Interval: bluetooth.NewAdvertiseInterval(100), - } - must("config adv", adv.Configure(advPayload, nil, options)) + must("config adv", adv.Configure(bluetooth.AdvertisementOptions{ + LocalName: "Go Bluetooth", + Interval: bluetooth.NewAdvertisementInterval(100), + })) must("start adv", adv.Start()) println("advertising...") diff --git a/examples/heartrate/main.go b/examples/heartrate/main.go index 642657e..2a90629 100644 --- a/examples/heartrate/main.go +++ b/examples/heartrate/main.go @@ -6,9 +6,6 @@ import ( "github.com/tinygo-org/bluetooth" ) -// flags + local name -var advPayload = []byte("\x02\x01\x06" + "\x07\x09TinyGo") - var adapter = bluetooth.DefaultAdapter // TODO: use atomics to access this value. @@ -19,10 +16,10 @@ func main() { adapter.SetEventHandler(handleBluetoothEvents) must("enable BLE stack", adapter.Enable()) adv := adapter.NewAdvertisement() - options := &bluetooth.AdvertiseOptions{ - Interval: bluetooth.NewAdvertiseInterval(100), - } - must("config adv", adv.Configure(advPayload, nil, options)) + must("config adv", adv.Configure(bluetooth.AdvertisementOptions{ + LocalName: "Go HRS", + Interval: bluetooth.NewAdvertisementInterval(100), + })) must("start adv", adv.Start()) var heartRateMeasurement bluetooth.Characteristic diff --git a/examples/ledcolor/main.go b/examples/ledcolor/main.go index ac22f76..5fa6e5c 100644 --- a/examples/ledcolor/main.go +++ b/examples/ledcolor/main.go @@ -7,9 +7,6 @@ import ( "github.com/tinygo-org/bluetooth" ) -// flags + local name -var advPayload = []byte("\x02\x01\x06" + "\x0b\x09LED colors") - var adapter = bluetooth.DefaultAdapter // TODO: use atomics to access this value. @@ -27,10 +24,10 @@ func main() { adapter.SetEventHandler(handleBluetoothEvents) must("enable BLE stack", adapter.Enable()) adv := adapter.NewAdvertisement() - options := &bluetooth.AdvertiseOptions{ - Interval: bluetooth.NewAdvertiseInterval(100), - } - must("config adv", adv.Configure(advPayload, nil, options)) + must("config adv", adv.Configure(bluetooth.AdvertisementOptions{ + LocalName: "LED colors", + Interval: bluetooth.NewAdvertisementInterval(100), + })) must("start adv", adv.Start()) var ledColorCharacteristic bluetooth.Characteristic diff --git a/gap.go b/gap.go index f361624..1dbbe44 100644 --- a/gap.go +++ b/gap.go @@ -3,24 +3,31 @@ package bluetooth import "errors" var ( - errScanning = errors.New("bluetooth: a scan is already in progress") - errNotScanning = errors.New("bluetooth: there is no scan in progress") - errMalformedAdvertisementPayload = errors.New("bluetooth: malformed advertisement packet") + errScanning = errors.New("bluetooth: a scan is already in progress") + errNotScanning = errors.New("bluetooth: there is no scan in progress") + errAdvertisementPacketTooBig = errors.New("bluetooth: advertisement packet overflows") ) -// AdvertiseOptions configures everything related to BLE advertisements. -type AdvertiseOptions struct { - Interval AdvertiseInterval +// AdvertisementOptions configures an advertisement instance. More options may +// be added over time. +type AdvertisementOptions struct { + // The (complete) local name that will be advertised. Optional, omitted if + // this is a zero-length string. + LocalName string + + // Interval in BLE-specific units. Create an interval by using + // NewAdvertiseInterval. + Interval AdvertisementInterval } -// AdvertiseInterval is the advertisement interval in 0.625µs units. -type AdvertiseInterval uint32 +// AdvertisementInterval is the advertisement interval in 0.625µs units. +type AdvertisementInterval uint32 -// NewAdvertiseInterval returns a new advertisement interval, based on an +// NewAdvertisementInterval returns a new advertisement interval, based on an // interval in milliseconds. -func NewAdvertiseInterval(intervalMillis uint32) AdvertiseInterval { +func NewAdvertisementInterval(intervalMillis uint32) AdvertisementInterval { // Convert an interval to units of - return AdvertiseInterval(intervalMillis * 8 / 5) + return AdvertisementInterval(intervalMillis * 8 / 5) } // Connection is a numeric identifier that indicates a connection handle. @@ -146,3 +153,31 @@ func (buf *rawAdvertisementPayload) LocalName() string { } return "" } + +// addFlags adds a flags field to the advertisement buffer. It returns true on +// success (the flags can be added) and false on failure. +func (buf *rawAdvertisementPayload) addFlags(flags byte) (ok bool) { + if int(buf.len)+3 > len(buf.data) { + return false // flags don't fit + } + + buf.data[buf.len] = 2 // length of field (including type) + buf.data[buf.len+1] = 0x01 // type, 0x01 means Flags + buf.data[buf.len+2] = flags // the flags + buf.len += 3 + return true +} + +// addFlags adds the Complete Local Name field to the advertisement buffer. It +// returns true on success (the name fits) and false on failure. +func (buf *rawAdvertisementPayload) addCompleteLocalName(name string) (ok bool) { + if int(buf.len)+len(name)+2 > len(buf.data) { + return false // name doesn't fit + } + + buf.data[buf.len] = byte(len(name) + 1) // length of field (including type) + buf.data[buf.len+1] = 9 // type, 0x09 means Complete Local name + copy(buf.data[buf.len+2:], name) // copy the name into the buffer + buf.len += byte(len(name) + 2) + return true +} diff --git a/gap_linux.go b/gap_linux.go index 6fd8bfe..d8d3ffd 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -3,18 +3,12 @@ package bluetooth import ( - "errors" - "github.com/muka/go-bluetooth/api" "github.com/muka/go-bluetooth/bluez/profile/adapter" "github.com/muka/go-bluetooth/bluez/profile/advertising" "github.com/muka/go-bluetooth/bluez/profile/device" ) -var ( - ErrMalformedAdvertisement = errors.New("bluetooth: malformed advertisement packet") -) - // Advertisement encapsulates a single advertisement instance. type Advertisement struct { adapter *Adapter @@ -31,39 +25,17 @@ func (a *Adapter) NewAdvertisement() *Advertisement { } // Configure this advertisement. -func (a *Advertisement) Configure(broadcastData, scanResponseData []byte, options *AdvertiseOptions) error { +// +// On Linux with BlueZ, it is not possible to set the advertisement interval. +func (a *Advertisement) Configure(options AdvertisementOptions) error { if a.advertisement != nil { panic("todo: configure advertisement a second time") } - if scanResponseData != nil { - panic("todo: scan response data") - } - // Quick-and-dirty advertisement packet parser. a.properties = &advertising.LEAdvertisement1Properties{ - Type: advertising.AdvertisementTypeBroadcast, - Timeout: 1<<16 - 1, - } - for len(broadcastData) != 0 { - if len(broadcastData) < 2 { - return ErrMalformedAdvertisement - } - fieldLength := broadcastData[0] - fieldType := broadcastData[1] - fieldValue := broadcastData[2 : fieldLength+1] - if int(fieldLength) > len(broadcastData) { - return ErrMalformedAdvertisement - } - switch fieldType { - case 1: - // BLE advertisement flags. Ignore. - case 9: - // Complete Local Name. - a.properties.LocalName = string(fieldValue) - default: - return ErrMalformedAdvertisement - } - broadcastData = broadcastData[fieldLength+1:] + Type: advertising.AdvertisementTypeBroadcast, + Timeout: 1<<16 - 1, + LocalName: options.LocalName, } return nil diff --git a/gap_nrf51.go b/gap_nrf51.go index 2312881..1e17d66 100644 --- a/gap_nrf51.go +++ b/gap_nrf51.go @@ -13,7 +13,7 @@ import "C" // Advertisement encapsulates a single advertisement instance. type Advertisement struct { - interval AdvertiseInterval + interval AdvertisementInterval } var globalAdvertisement Advertisement @@ -28,22 +28,15 @@ func (a *Adapter) NewAdvertisement() *Advertisement { } // Configure this advertisement. Must be called after SoftDevice initialization. -func (a *Advertisement) Configure(broadcastData, scanResponseData []byte, options *AdvertiseOptions) error { - var ( - p_data *byte - dlen byte - p_sr_data *byte - srdlen byte - ) - if broadcastData != nil { - p_data = &broadcastData[0] - dlen = uint8(len(broadcastData)) +func (a *Advertisement) Configure(options AdvertisementOptions) error { + var payload rawAdvertisementPayload + payload.addFlags(0x06) + if options.LocalName != "" { + if !payload.addCompleteLocalName(options.LocalName) { + return errAdvertisementPacketTooBig + } } - if scanResponseData != nil { - p_sr_data = &scanResponseData[0] - srdlen = uint8(len(scanResponseData)) - } - errCode := C.sd_ble_gap_adv_data_set(p_data, dlen, p_sr_data, srdlen) + errCode := C.sd_ble_gap_adv_data_set(&payload.data[0], payload.len, nil, 0) a.interval = options.Interval return makeError(errCode) } diff --git a/gap_nrf528xx.go b/gap_nrf528xx.go index 16415ec..8321cb7 100644 --- a/gap_nrf528xx.go +++ b/gap_nrf528xx.go @@ -37,19 +37,18 @@ func (a *Adapter) NewAdvertisement() *Advertisement { } // Configure this advertisement. Must be called after SoftDevice initialization. -func (a *Advertisement) Configure(broadcastData, scanResponseData []byte, options *AdvertiseOptions) error { +func (a *Advertisement) Configure(options AdvertisementOptions) error { data := C.ble_gap_adv_data_t{} - if broadcastData != nil { - data.adv_data = C.ble_data_t{ - p_data: &broadcastData[0], - len: uint16(len(broadcastData)), + var payload rawAdvertisementPayload + payload.addFlags(0x06) + if options.LocalName != "" { + if !payload.addCompleteLocalName(options.LocalName) { + return errAdvertisementPacketTooBig } } - if scanResponseData != nil { - data.scan_rsp_data = C.ble_data_t{ - p_data: &scanResponseData[0], - len: uint16(len(scanResponseData)), - } + data.adv_data = C.ble_data_t{ + p_data: &payload.data[0], + len: uint16(payload.len), } params := C.ble_gap_adv_params_t{ properties: C.ble_gap_adv_properties_t{