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:
parent
0474d7b750
commit
086c797e0f
8 changed files with 82 additions and 93 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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...")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
57
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
|
||||
}
|
||||
|
|
40
gap_linux.go
40
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
|
||||
|
|
25
gap_nrf51.go
25
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)
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in a new issue