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.
This commit is contained in:
Ayke van Laethem 2020-06-01 14:07:07 +02:00
parent 0474d7b750
commit 086c797e0f
No known key found for this signature in database
GPG key ID: E97FF5335DFDFDED
8 changed files with 82 additions and 93 deletions

View file

@ -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.

View file

@ -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...")

View file

@ -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

View file

@ -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

57
gap.go
View file

@ -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
}

View file

@ -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

View file

@ -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)
}

View file

@ -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{