Skip to content

Commit

Permalink
all: change ManufacturerData from a map to a slice
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aykevl authored and deadprogram committed Feb 21, 2024
1 parent d82232b commit 0087e05
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 46 deletions.
12 changes: 10 additions & 2 deletions adapter_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions examples/advertisement/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
89 changes: 50 additions & 39 deletions gap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
return false
}

fieldLength := len(data) + 3
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
}

// Build manufacturer ID parts
manufacturerDataBit := byte(0xFF)
manufacturerIDPart1 := byte(manufacturerID & 0xFF)
manufacturerIDPart2 := byte((manufacturerID >> 8) & 0xFF)
// 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)

payloadData = append(payloadData, byte(fieldLength), manufacturerDataBit, manufacturerIDPart1, manufacturerIDPart2)
payloadData = append(payloadData, data...)
}
buf.len = uint8(len(payloadData))
copy(buf.data[:], payloadData)
return true
}

Expand Down
17 changes: 14 additions & 3 deletions gap_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
})
}
}

Expand Down
27 changes: 27 additions & 0 deletions gap_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package bluetooth

import (
"reflect"
"testing"
"time"
)
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}
}
7 changes: 5 additions & 2 deletions gap_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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),
})
}
}

Expand Down

0 comments on commit 0087e05

Please sign in to comment.