all: change ManufacturerData from a map to a slice

This is a breaking change, but I believe it is necessary for
correctness. Because maps have an undefined iteration order, the actual
advertised packet could change each time which I think is a bad thing.
In addition to that, using a slice should be much more lightweight than
using a map.

I've also added some tests (that should have been there in the first
place) and added some manufacturer data to the advertisement example.

Furthermore, I've optimized the code that constructs manufacturer data
for raw advertisement payloads, it should now be entirely free of heap
allocations.
This commit is contained in:
Ayke van Laethem 2024-02-20 21:16:29 +01:00 committed by Ron Evans
parent d82232b16d
commit 0087e0549b
6 changed files with 110 additions and 47 deletions

View file

@ -141,11 +141,19 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
serviceUUIDs = append(serviceUUIDs, parsedUUID)
}
manufacturerData := make(map[uint16][]byte)
var manufacturerData []ManufacturerDataElement
if len(advFields.ManufacturerData) > 2 {
// Note: CoreBluetooth seems to assume there can be only one
// manufacturer data fields in an advertisement packet, while the
// specification allows multiple such fields. See the Bluetooth Core
// Specification Supplement, table 1.1:
// https://www.bluetooth.com/specifications/css-11/
manufacturerID := uint16(advFields.ManufacturerData[0])
manufacturerID += uint16(advFields.ManufacturerData[1]) << 8
manufacturerData[manufacturerID] = advFields.ManufacturerData[2:]
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: manufacturerID,
Data: advFields.ManufacturerData[2:],
})
}
// Peripheral UUID is randomized on macOS, which means to

View file

@ -13,6 +13,9 @@ func main() {
adv := adapter.DefaultAdvertisement()
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
LocalName: "Go Bluetooth",
ManufacturerData: []bluetooth.ManufacturerDataElement{
{CompanyID: 0xffff, Data: []byte{0x01, 0x02}},
},
}))
must("start adv", adv.Start())

85
gap.go
View file

@ -56,7 +56,21 @@ type AdvertisementOptions struct {
// ManufacturerData stores Advertising Data.
// Keys are the Manufacturer ID to associate with the data.
ManufacturerData map[uint16]interface{}
ManufacturerData []ManufacturerDataElement
}
// Manufacturer data that's part of an advertisement packet.
type ManufacturerDataElement struct {
// The company ID, which must be one of the assigned company IDs.
// The full list is in here:
// https://www.bluetooth.com/specifications/assigned-numbers/
// The list can also be viewed here:
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
// The value 0xffff can also be used for testing.
CompanyID uint16
// The value, which can be any value but can't be very large.
Data []byte
}
// Duration is the unit of time used in BLE, in 0.625µs units. This unit of time
@ -112,7 +126,7 @@ type AdvertisementPayload interface {
// ManufacturerData returns a map with all the manufacturer data present in the
//advertising. IT may be empty.
ManufacturerData() map[uint16][]byte
ManufacturerData() []ManufacturerDataElement
}
// AdvertisementFields contains advertisement fields in structured form.
@ -127,7 +141,7 @@ type AdvertisementFields struct {
ServiceUUIDs []UUID
// ManufacturerData is the manufacturer data of the advertisement.
ManufacturerData map[uint16][]byte
ManufacturerData []ManufacturerDataElement
}
// advertisementFields wraps AdvertisementFields to implement the
@ -161,7 +175,7 @@ func (p *advertisementFields) Bytes() []byte {
}
// ManufacturerData returns the underlying ManufacturerData field.
func (p *advertisementFields) ManufacturerData() map[uint16][]byte {
func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement {
return p.AdvertisementFields.ManufacturerData
}
@ -254,22 +268,24 @@ func (buf *rawAdvertisementPayload) HasServiceUUID(uuid UUID) bool {
}
// ManufacturerData returns the manufacturer data in the advertisement payload.
func (buf *rawAdvertisementPayload) ManufacturerData() map[uint16][]byte {
mData := make(map[uint16][]byte)
data := buf.Bytes()
for len(data) >= 2 {
fieldLength := data[0]
if int(fieldLength)+1 > len(data) {
// Invalid field length.
return nil
func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement {
var manufacturerData []ManufacturerDataElement
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
fieldLength := int(buf.data[index+0])
if fieldLength < 3 {
continue
}
// If this is the manufacturer data
if byte(0xFF) == data[1] {
mData[uint16(data[2])+(uint16(data[3])<<8)] = data[4 : fieldLength+1]
fieldType := buf.data[index+1]
if fieldType != 0xff {
continue
}
data = data[fieldLength+1:]
key := uint16(buf.data[index+2]) | uint16(buf.data[index+3])<<8
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: key,
Data: buf.data[index+4 : index+fieldLength+1],
})
}
return mData
return manufacturerData
}
// reset restores this buffer to the original state.
@ -300,36 +316,31 @@ func (buf *rawAdvertisementPayload) addFromOptions(options AdvertisementOptions)
}
}
if len(options.ManufacturerData) > 0 {
buf.addManufacturerData(options.ManufacturerData)
for _, element := range options.ManufacturerData {
if !buf.addManufacturerData(element.CompanyID, element.Data) {
return false
}
}
return true
}
// addManufacturerData adds manufacturer data ([]byte) entries to the advertisement payload.
func (buf *rawAdvertisementPayload) addManufacturerData(manufacturerData map[uint16]interface{}) (ok bool) {
payloadData := buf.Bytes()
for manufacturerID, rawData := range manufacturerData {
data := rawData.([]byte)
// Check if the manufacturer ID is within the range of 16 bits (0-65535).
if manufacturerID > 0xFFFF {
// Invalid manufacturer ID.
func (buf *rawAdvertisementPayload) addManufacturerData(key uint16, value []byte) (ok bool) {
// Check whether the field can fit this manufacturer data.
fieldLength := len(value) + 4
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
fieldLength := len(data) + 3
// Add the data.
buf.data[buf.len+0] = uint8(fieldLength - 1)
buf.data[buf.len+1] = 0xff
buf.data[buf.len+2] = uint8(key)
buf.data[buf.len+3] = uint8(key >> 8)
copy(buf.data[buf.len+4:], value)
buf.len += uint8(fieldLength)
// Build manufacturer ID parts
manufacturerDataBit := byte(0xFF)
manufacturerIDPart1 := byte(manufacturerID & 0xFF)
manufacturerIDPart2 := byte((manufacturerID >> 8) & 0xFF)
payloadData = append(payloadData, byte(fieldLength), manufacturerDataBit, manufacturerIDPart1, manufacturerIDPart2)
payloadData = append(payloadData, data...)
}
buf.len = uint8(len(payloadData))
copy(buf.data[:], payloadData)
return true
}

View file

@ -54,14 +54,22 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
serviceUUIDs = append(serviceUUIDs, uuid.String())
}
// Convert map[uint16][]byte to map[uint16]any because that's what BlueZ needs.
manufacturerData := map[uint16]any{}
for _, element := range options.ManufacturerData {
manufacturerData[element.CompanyID] = element.Data
}
// Build an org.bluez.LEAdvertisement1 object, to be exported over DBus.
// See:
// https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/org.bluez.LEAdvertisement.rst
id := atomic.AddUint64(&advertisementID, 1)
a.path = dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/advertisement%d", id))
propsSpec := map[string]map[string]*prop.Prop{
"org.bluez.LEAdvertisement1": {
"Type": {Value: "broadcast"},
"ServiceUUIDs": {Value: serviceUUIDs},
"ManufacturerData": {Value: options.ManufacturerData},
"ManufacturerData": {Value: manufacturerData},
"LocalName": {Value: options.LocalName},
// The documentation states:
// > Timeout of the advertisement in seconds. This defines the
@ -266,10 +274,13 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
a := Address{MACAddress{MAC: addr}}
a.SetRandom(props["AddressType"].Value().(string) == "random")
manufacturerData := make(map[uint16][]byte)
var manufacturerData []ManufacturerDataElement
if mdata, ok := props["ManufacturerData"].Value().(map[uint16]dbus.Variant); ok {
for k, v := range mdata {
manufacturerData[k] = v.Value().([]byte)
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: k,
Data: v.Value().([]byte),
})
}
}

View file

@ -1,6 +1,7 @@
package bluetooth
import (
"reflect"
"testing"
"time"
)
@ -55,6 +56,28 @@ func TestCreateAdvertisementPayload(t *testing.T) {
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\a\xff\x34\x12asdf", // manufacturer data
parsed: AdvertisementOptions{
ManufacturerData: []ManufacturerDataElement{
{0x1234, []byte("asdf")},
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x04\xff\x34\x12\x05" + // manufacturer data 1
"\x05\xff\xff\xff\x03\x07" + // manufacturer data 2
"\x03\xff\x11\x00", // manufacturer data 3
parsed: AdvertisementOptions{
ManufacturerData: []ManufacturerDataElement{
{0x1234, []byte{5}},
{0xffff, []byte{3, 7}},
{0x0011, []byte{}},
},
},
},
}
for _, tc := range tests {
var expectedRaw rawAdvertisementPayload
@ -66,5 +89,9 @@ func TestCreateAdvertisementPayload(t *testing.T) {
if raw != expectedRaw {
t.Errorf("error when serializing options: %#v\nexpected: %#v\nactual: %#v\n", tc.parsed, tc.raw, string(raw.data[:raw.len]))
}
mdata := raw.ManufacturerData()
if !reflect.DeepEqual(mdata, tc.parsed.ManufacturerData) {
t.Errorf("ManufacturerData was not parsed as expected:\nexpected: %#v\nactual: %#v", tc.parsed.ManufacturerData, mdata)
}
}
}

View file

@ -114,7 +114,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
Address: adr,
}
var manufacturerData map[uint16][]byte = make(map[uint16][]byte)
var manufacturerData []ManufacturerDataElement
if winAdv, err := args.GetAdvertisement(); err == nil && winAdv != nil {
vector, _ := winAdv.GetManufacturerData()
size, _ := vector.GetSize()
@ -123,7 +123,10 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
manData := (*advertisement.BluetoothLEManufacturerData)(element)
companyID, _ := manData.GetCompanyId()
buffer, _ := manData.GetData()
manufacturerData[companyID] = bufferToSlice(buffer)
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: companyID,
Data: bufferToSlice(buffer),
})
}
}