Skip to content

Commit 0087e05

Browse files
aykevldeadprogram
authored andcommitted
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.
1 parent d82232b commit 0087e05

File tree

6 files changed

+109
-46
lines changed

6 files changed

+109
-46
lines changed

adapter_darwin.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,19 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
141141
serviceUUIDs = append(serviceUUIDs, parsedUUID)
142142
}
143143

144-
manufacturerData := make(map[uint16][]byte)
144+
var manufacturerData []ManufacturerDataElement
145145
if len(advFields.ManufacturerData) > 2 {
146+
// Note: CoreBluetooth seems to assume there can be only one
147+
// manufacturer data fields in an advertisement packet, while the
148+
// specification allows multiple such fields. See the Bluetooth Core
149+
// Specification Supplement, table 1.1:
150+
// https://www.bluetooth.com/specifications/css-11/
146151
manufacturerID := uint16(advFields.ManufacturerData[0])
147152
manufacturerID += uint16(advFields.ManufacturerData[1]) << 8
148-
manufacturerData[manufacturerID] = advFields.ManufacturerData[2:]
153+
manufacturerData = append(manufacturerData, ManufacturerDataElement{
154+
CompanyID: manufacturerID,
155+
Data: advFields.ManufacturerData[2:],
156+
})
149157
}
150158

151159
// Peripheral UUID is randomized on macOS, which means to

examples/advertisement/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ func main() {
1313
adv := adapter.DefaultAdvertisement()
1414
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
1515
LocalName: "Go Bluetooth",
16+
ManufacturerData: []bluetooth.ManufacturerDataElement{
17+
{CompanyID: 0xffff, Data: []byte{0x01, 0x02}},
18+
},
1619
}))
1720
must("start adv", adv.Start())
1821

gap.go

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,21 @@ type AdvertisementOptions struct {
5656

5757
// ManufacturerData stores Advertising Data.
5858
// Keys are the Manufacturer ID to associate with the data.
59-
ManufacturerData map[uint16]interface{}
59+
ManufacturerData []ManufacturerDataElement
60+
}
61+
62+
// Manufacturer data that's part of an advertisement packet.
63+
type ManufacturerDataElement struct {
64+
// The company ID, which must be one of the assigned company IDs.
65+
// The full list is in here:
66+
// https://www.bluetooth.com/specifications/assigned-numbers/
67+
// The list can also be viewed here:
68+
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
69+
// The value 0xffff can also be used for testing.
70+
CompanyID uint16
71+
72+
// The value, which can be any value but can't be very large.
73+
Data []byte
6074
}
6175

6276
// 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 {
112126

113127
// ManufacturerData returns a map with all the manufacturer data present in the
114128
//advertising. IT may be empty.
115-
ManufacturerData() map[uint16][]byte
129+
ManufacturerData() []ManufacturerDataElement
116130
}
117131

118132
// AdvertisementFields contains advertisement fields in structured form.
@@ -127,7 +141,7 @@ type AdvertisementFields struct {
127141
ServiceUUIDs []UUID
128142

129143
// ManufacturerData is the manufacturer data of the advertisement.
130-
ManufacturerData map[uint16][]byte
144+
ManufacturerData []ManufacturerDataElement
131145
}
132146

133147
// advertisementFields wraps AdvertisementFields to implement the
@@ -161,7 +175,7 @@ func (p *advertisementFields) Bytes() []byte {
161175
}
162176

163177
// ManufacturerData returns the underlying ManufacturerData field.
164-
func (p *advertisementFields) ManufacturerData() map[uint16][]byte {
178+
func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement {
165179
return p.AdvertisementFields.ManufacturerData
166180
}
167181

@@ -254,22 +268,24 @@ func (buf *rawAdvertisementPayload) HasServiceUUID(uuid UUID) bool {
254268
}
255269

256270
// ManufacturerData returns the manufacturer data in the advertisement payload.
257-
func (buf *rawAdvertisementPayload) ManufacturerData() map[uint16][]byte {
258-
mData := make(map[uint16][]byte)
259-
data := buf.Bytes()
260-
for len(data) >= 2 {
261-
fieldLength := data[0]
262-
if int(fieldLength)+1 > len(data) {
263-
// Invalid field length.
264-
return nil
271+
func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement {
272+
var manufacturerData []ManufacturerDataElement
273+
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
274+
fieldLength := int(buf.data[index+0])
275+
if fieldLength < 3 {
276+
continue
265277
}
266-
// If this is the manufacturer data
267-
if byte(0xFF) == data[1] {
268-
mData[uint16(data[2])+(uint16(data[3])<<8)] = data[4 : fieldLength+1]
278+
fieldType := buf.data[index+1]
279+
if fieldType != 0xff {
280+
continue
269281
}
270-
data = data[fieldLength+1:]
282+
key := uint16(buf.data[index+2]) | uint16(buf.data[index+3])<<8
283+
manufacturerData = append(manufacturerData, ManufacturerDataElement{
284+
CompanyID: key,
285+
Data: buf.data[index+4 : index+fieldLength+1],
286+
})
271287
}
272-
return mData
288+
return manufacturerData
273289
}
274290

275291
// reset restores this buffer to the original state.
@@ -300,36 +316,31 @@ func (buf *rawAdvertisementPayload) addFromOptions(options AdvertisementOptions)
300316
}
301317
}
302318

303-
if len(options.ManufacturerData) > 0 {
304-
buf.addManufacturerData(options.ManufacturerData)
319+
for _, element := range options.ManufacturerData {
320+
if !buf.addManufacturerData(element.CompanyID, element.Data) {
321+
return false
322+
}
305323
}
306324

307325
return true
308326
}
309327

310328
// addManufacturerData adds manufacturer data ([]byte) entries to the advertisement payload.
311-
func (buf *rawAdvertisementPayload) addManufacturerData(manufacturerData map[uint16]interface{}) (ok bool) {
312-
payloadData := buf.Bytes()
313-
for manufacturerID, rawData := range manufacturerData {
314-
data := rawData.([]byte)
315-
// Check if the manufacturer ID is within the range of 16 bits (0-65535).
316-
if manufacturerID > 0xFFFF {
317-
// Invalid manufacturer ID.
318-
return false
319-
}
320-
321-
fieldLength := len(data) + 3
329+
func (buf *rawAdvertisementPayload) addManufacturerData(key uint16, value []byte) (ok bool) {
330+
// Check whether the field can fit this manufacturer data.
331+
fieldLength := len(value) + 4
332+
if int(buf.len)+fieldLength > len(buf.data) {
333+
return false
334+
}
322335

323-
// Build manufacturer ID parts
324-
manufacturerDataBit := byte(0xFF)
325-
manufacturerIDPart1 := byte(manufacturerID & 0xFF)
326-
manufacturerIDPart2 := byte((manufacturerID >> 8) & 0xFF)
336+
// Add the data.
337+
buf.data[buf.len+0] = uint8(fieldLength - 1)
338+
buf.data[buf.len+1] = 0xff
339+
buf.data[buf.len+2] = uint8(key)
340+
buf.data[buf.len+3] = uint8(key >> 8)
341+
copy(buf.data[buf.len+4:], value)
342+
buf.len += uint8(fieldLength)
327343

328-
payloadData = append(payloadData, byte(fieldLength), manufacturerDataBit, manufacturerIDPart1, manufacturerIDPart2)
329-
payloadData = append(payloadData, data...)
330-
}
331-
buf.len = uint8(len(payloadData))
332-
copy(buf.data[:], payloadData)
333344
return true
334345
}
335346

gap_linux.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,22 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
5454
serviceUUIDs = append(serviceUUIDs, uuid.String())
5555
}
5656

57+
// Convert map[uint16][]byte to map[uint16]any because that's what BlueZ needs.
58+
manufacturerData := map[uint16]any{}
59+
for _, element := range options.ManufacturerData {
60+
manufacturerData[element.CompanyID] = element.Data
61+
}
62+
5763
// Build an org.bluez.LEAdvertisement1 object, to be exported over DBus.
64+
// See:
65+
// https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/org.bluez.LEAdvertisement.rst
5866
id := atomic.AddUint64(&advertisementID, 1)
5967
a.path = dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/advertisement%d", id))
6068
propsSpec := map[string]map[string]*prop.Prop{
6169
"org.bluez.LEAdvertisement1": {
6270
"Type": {Value: "broadcast"},
6371
"ServiceUUIDs": {Value: serviceUUIDs},
64-
"ManufacturerData": {Value: options.ManufacturerData},
72+
"ManufacturerData": {Value: manufacturerData},
6573
"LocalName": {Value: options.LocalName},
6674
// The documentation states:
6775
// > Timeout of the advertisement in seconds. This defines the
@@ -266,10 +274,13 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
266274
a := Address{MACAddress{MAC: addr}}
267275
a.SetRandom(props["AddressType"].Value().(string) == "random")
268276

269-
manufacturerData := make(map[uint16][]byte)
277+
var manufacturerData []ManufacturerDataElement
270278
if mdata, ok := props["ManufacturerData"].Value().(map[uint16]dbus.Variant); ok {
271279
for k, v := range mdata {
272-
manufacturerData[k] = v.Value().([]byte)
280+
manufacturerData = append(manufacturerData, ManufacturerDataElement{
281+
CompanyID: k,
282+
Data: v.Value().([]byte),
283+
})
273284
}
274285
}
275286

gap_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package bluetooth
22

33
import (
4+
"reflect"
45
"testing"
56
"time"
67
)
@@ -55,6 +56,28 @@ func TestCreateAdvertisementPayload(t *testing.T) {
5556
},
5657
},
5758
},
59+
{
60+
raw: "\x02\x01\x06" + // flags
61+
"\a\xff\x34\x12asdf", // manufacturer data
62+
parsed: AdvertisementOptions{
63+
ManufacturerData: []ManufacturerDataElement{
64+
{0x1234, []byte("asdf")},
65+
},
66+
},
67+
},
68+
{
69+
raw: "\x02\x01\x06" + // flags
70+
"\x04\xff\x34\x12\x05" + // manufacturer data 1
71+
"\x05\xff\xff\xff\x03\x07" + // manufacturer data 2
72+
"\x03\xff\x11\x00", // manufacturer data 3
73+
parsed: AdvertisementOptions{
74+
ManufacturerData: []ManufacturerDataElement{
75+
{0x1234, []byte{5}},
76+
{0xffff, []byte{3, 7}},
77+
{0x0011, []byte{}},
78+
},
79+
},
80+
},
5881
}
5982
for _, tc := range tests {
6083
var expectedRaw rawAdvertisementPayload
@@ -66,5 +89,9 @@ func TestCreateAdvertisementPayload(t *testing.T) {
6689
if raw != expectedRaw {
6790
t.Errorf("error when serializing options: %#v\nexpected: %#v\nactual: %#v\n", tc.parsed, tc.raw, string(raw.data[:raw.len]))
6891
}
92+
mdata := raw.ManufacturerData()
93+
if !reflect.DeepEqual(mdata, tc.parsed.ManufacturerData) {
94+
t.Errorf("ManufacturerData was not parsed as expected:\nexpected: %#v\nactual: %#v", tc.parsed.ManufacturerData, mdata)
95+
}
6996
}
7097
}

gap_windows.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
114114
Address: adr,
115115
}
116116

117-
var manufacturerData map[uint16][]byte = make(map[uint16][]byte)
117+
var manufacturerData []ManufacturerDataElement
118118
if winAdv, err := args.GetAdvertisement(); err == nil && winAdv != nil {
119119
vector, _ := winAdv.GetManufacturerData()
120120
size, _ := vector.GetSize()
@@ -123,7 +123,10 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
123123
manData := (*advertisement.BluetoothLEManufacturerData)(element)
124124
companyID, _ := manData.GetCompanyId()
125125
buffer, _ := manData.GetData()
126-
manufacturerData[companyID] = bufferToSlice(buffer)
126+
manufacturerData = append(manufacturerData, ManufacturerDataElement{
127+
CompanyID: companyID,
128+
Data: bufferToSlice(buffer),
129+
})
127130
}
128131
}
129132

0 commit comments

Comments
 (0)