diff --git a/adapter_darwin.go b/adapter_darwin.go index 292d168..eb43c15 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -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 diff --git a/examples/advertisement/main.go b/examples/advertisement/main.go index 56a5e8c..41a01fc 100644 --- a/examples/advertisement/main.go +++ b/examples/advertisement/main.go @@ -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()) diff --git a/gap.go b/gap.go index f6a418c..e09ff62 100644 --- a/gap.go +++ b/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. - return false - } - - fieldLength := len(data) + 3 - - // 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...) +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 } - buf.len = uint8(len(payloadData)) - copy(buf.data[:], payloadData) + + // 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) + return true } diff --git a/gap_linux.go b/gap_linux.go index 80ecf1a..5f97bad 100644 --- a/gap_linux.go +++ b/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), + }) } } diff --git a/gap_test.go b/gap_test.go index c504325..4b69260 100644 --- a/gap_test.go +++ b/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) + } } } diff --git a/gap_windows.go b/gap_windows.go index c800c25..146f2aa 100644 --- a/gap_windows.go +++ b/gap_windows.go @@ -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), + }) } }