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:
parent
d82232b16d
commit
0087e0549b
6 changed files with 110 additions and 47 deletions
|
@ -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
|
||||
|
|
|
@ -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
85
gap.go
|
@ -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
|
||||
}
|
||||
|
||||
|
|
17
gap_linux.go
17
gap_linux.go
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
27
gap_test.go
27
gap_test.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue