Skip to content

Commit

Permalink
ServiceData now also uses a slice instead of a map as in #244
Browse files Browse the repository at this point in the history
  • Loading branch information
dnlwgnd committed Feb 23, 2024
1 parent 13570ad commit e357a2a
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 55 deletions.
153 changes: 109 additions & 44 deletions gap.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,10 @@ type AdvertisementOptions struct {
Interval Duration

// ManufacturerData stores Advertising Data.
// Keys are the Manufacturer ID to associate with the data.
ManufacturerData []ManufacturerDataElement

// ServiceData stores Advertising Data.
// Keys are the Manufacturer/Service ID to associate with the data.
ServiceData map[uint16]interface{}
ServiceData []AdvertismentDataElement
}

// Manufacturer data that's part of an advertisement packet.
Expand All @@ -76,6 +75,17 @@ type ManufacturerDataElement struct {
Data []byte
}

// AdvertismentDataElement strores a uuid/byte-array pair used as ServiceData or ManufacturerData advertisment elements
type AdvertismentDataElement struct {
// service uuid or company uuid
// The list can also be viewed here:
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/service_uuids.yaml
UUID UUID
// the data byte array
Data []byte
}

// Duration is the unit of time used in BLE, in 0.625µs units. This unit of time
// is used throughout the BLE stack.
type Duration uint16
Expand Down Expand Up @@ -127,13 +137,13 @@ type AdvertisementPayload interface {
// if this data is not available.
Bytes() []byte

// ManufacturerData returns a map with all the manufacturer data present in the
// ManufacturerData returns a slice with all the manufacturer data present in the
// advertising. It may be empty.
ManufacturerData() []ManufacturerDataElement

// ServiceData returns a map with all the service data present in the
// ServiceData returns a slice with all the service data present in the
// advertising. It may be empty.
ServiceData() map[uint16][]byte
ServiceData() []AdvertismentDataElement
}

// AdvertisementFields contains advertisement fields in structured form.
Expand All @@ -151,7 +161,7 @@ type AdvertisementFields struct {
ManufacturerData []ManufacturerDataElement

// ServiceData is the service data of the advertisement.
ServiceData map[uint16][]byte
ServiceData []AdvertismentDataElement
}

// advertisementFields wraps AdvertisementFields to implement the
Expand Down Expand Up @@ -190,7 +200,7 @@ func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement {
}

// ServiceData returns the underlying ServiceData field.
func (p *advertisementFields) ServiceData() map[uint16][]byte {
func (p *advertisementFields) ServiceData() []AdvertismentDataElement {
return p.AdvertisementFields.ServiceData
}

Expand Down Expand Up @@ -228,6 +238,28 @@ func (buf *rawAdvertisementPayload) findField(fieldType byte) []byte {
return nil
}

// findAllFields returns the a slice of all data of a specific field in the advertisement packet.
//
// See this list of field types:
// https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/
func (buf *rawAdvertisementPayload) findAllFields(fieldType byte) [][]byte {
var all_fields [][]byte
data := buf.Bytes()
for len(data) >= 2 {
fieldLength := data[0]
if int(fieldLength)+1 > len(data) {
// Invalid field length.
return nil
}
if fieldType == data[1] {
all_fields = append(all_fields, data[2 : fieldLength+1])
}
data = data[fieldLength+1:]
}
return all_fields
}


// LocalName returns the local name (complete or shortened) in the advertisement
// payload.
func (buf *rawAdvertisementPayload) LocalName() string {
Expand Down Expand Up @@ -304,15 +336,29 @@ func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement
}

// ServiceData returns the service data in the advertisment payload
func (buf *rawAdvertisementPayload) ServiceData() map[uint16][]byte {
sData := make(map[uint16][]byte)
data := buf.findField(0x16)
if data == nil {
return sData
func (buf *rawAdvertisementPayload) ServiceData() []AdvertismentDataElement {
var serviceData []AdvertismentDataElement
for _, data := range buf.findAllFields(0x16) { //16-Bit uuid
serviceData = append(serviceData, AdvertismentDataElement{
UUID: New16BitUUID(uint16(data[0])+(uint16(data[1])<<8)),
Data: data[2:],
})
}
for _, data := range buf.findAllFields(0x20) { //32-Bit uuid
serviceData = append(serviceData, AdvertismentDataElement{
UUID: New32BitUUID(uint32(data[0])+(uint32(data[1])<<8)+(uint32(data[2])<<16)+(uint32(data[3])<<24)),
Data: data[4:],
})
}
for _, data := range buf.findAllFields(0x21) { //128-Bit uuid
var uuidArray [16]byte
copy(uuidArray[:], data[:16])
serviceData = append(serviceData, AdvertismentDataElement{
UUID: NewUUID(uuidArray),
Data: data[16:],
})
}
sData[uint16(data[0])+(uint16(data[1])<<8)]= data[2:]

return sData
return serviceData
}

// reset restores this buffer to the original state.
Expand Down Expand Up @@ -349,8 +395,8 @@ func (buf *rawAdvertisementPayload) addFromOptions(options AdvertisementOptions)
}
}

if len(options.ServiceData) > 0 {
if err := buf.addServiceData(options.ServiceData); err != nil {
for _, element := range options.ServiceData {
if err := buf.addServiceData(element.UUID, element.Data); err != nil {
return err
}
}
Expand Down Expand Up @@ -378,35 +424,54 @@ func (buf *rawAdvertisementPayload) addManufacturerData(key uint16, value []byte
}

// addServiceData adds service data ([]byte) entries to the advertisement payload.
func (buf *rawAdvertisementPayload) addServiceData(serviceData map[uint16]interface{}) (err error) {
//check if it fits
var size int
for _, sData := range serviceData {
size +=1 // ad type byte
size +=2 // 16-bit uuid
size +=len(sData.([]byte)) // actual service data
func (buf *rawAdvertisementPayload) addServiceData(uuid UUID, data []byte) (err error) {
switch {
case uuid.Is16Bit(): {
// check if it fits
fieldLength := 1 + 1 + 2 + len(data) // 1 byte length, 1 byte ad type, 2 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return errAdvertisementPacketTooBig
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x16
buf.data[buf.len+2] = byte(uuid.Get16Bit())
buf.data[buf.len+3] = byte(uuid.Get16Bit()>>8)
copy(buf.data[buf.len+4:], data)
buf.len += uint8(fieldLength)

}
if int(buf.len)+size > len(buf.data) {
return errAdvertisementPacketTooBig // service data doesn't fit
case uuid.Is32Bit(): {
// check if it fits
fieldLength := 1 + 1 + 4 + len(data) // 1 byte length, 1 byte ad type, 4 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return errAdvertisementPacketTooBig
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x20
buf.data[buf.len+2] = byte(uuid.Get32Bit())
buf.data[buf.len+3] = byte(uuid.Get32Bit()>>8)
buf.data[buf.len+4] = byte(uuid.Get32Bit()>>16)
buf.data[buf.len+5] = byte(uuid.Get32Bit()>>24)
copy(buf.data[buf.len+6:], data)
buf.len += uint8(fieldLength)
}
default: { // must be 128-bit uuid
// check if it fits
fieldLength := 1 + 1 + 16 + len(data) // 1 byte length, 1 byte ad type, 16 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return errAdvertisementPacketTooBig
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x21
uuid_bytes := uuid.Bytes()
copy(buf.data[buf.len+2:], uuid_bytes[:])
copy(buf.data[buf.len+2+16:], data)
buf.len += uint8(fieldLength)
}

payloadData := buf.Bytes()
for uuid, rawData := range serviceData {
data := rawData.([]byte)
// Check if the manufacturer ID is within the range of 16 bits (0-65535).
// if uuid > 0xFFFF {
// // Invalid uuid.
// return false
// }

fieldLength := byte(len(data) + 3)

// append service data element to existing payload
payloadData = append(payloadData, fieldLength, byte(0x16), byte(uuid & 0xFF), byte((uuid >> 8) & 0xFF))
payloadData = append(payloadData, data...)
}
buf.len = uint8(len(payloadData))
copy(buf.data[:], payloadData)
return nil
}

Expand Down
15 changes: 9 additions & 6 deletions gap_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
serviceUUIDs = append(serviceUUIDs, uuid.String())
}
var serviceData = make(map[string]interface{})
for uuid, data := range options.ServiceData {
serviceData[New16BitUUID(uuid).String()] = data.([]byte)
for _, element := range options.ServiceData {
serviceData[element.UUID.String()] = element.Data
}

// Convert map[uint16][]byte to map[uint16]any because that's what BlueZ needs.
Expand Down Expand Up @@ -293,14 +293,17 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
localName, _ := props["Name"].Value().(string)
rssi, _ := props["RSSI"].Value().(int16)

serviceData := make(map[uint16][]byte)
var serviceData []AdvertismentDataElement
if sdata, ok := props["ServiceData"].Value().(map[string]dbus.Variant); ok {
for k, v := range sdata {
// looks like bluez delivers the long-128bit-uuid even for 16-bit uuid ad-element and it needs to be converted
uuid, err := ParseUUID(k)
if err == nil && uuid.Is16Bit(){
serviceData[uuid.Get16Bit()] = v.Value().([]byte)
if err != nil {
continue
}
serviceData = append(serviceData, AdvertismentDataElement{
UUID: uuid,
Data: v.Value().([]byte),
})
}
}

Expand Down
35 changes: 30 additions & 5 deletions gap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,37 @@ func TestCreateAdvertisementPayload(t *testing.T) {
},
{
raw: "\x02\x01\x06" + // flags
"\x0B\x09\x44\x49\x59\x2D\x73\x65\x6E\x73\x6F\x72" + // local name
"\x0A\x16\xD2\xFC\x40\x02\xC4\x09\x03\xBF\x13", // service UUID
"\x05\x16\xD2\xFC\x40\x02" + // service data 16-Bit UUID
"\x06\x20\xD2\xFC\x40\x02\xC4", // service data 32-Bit UUID
parsed: AdvertisementOptions{
LocalName: "DIY-sensor",
ServiceData: map[uint16]interface{}{
0xFCD2: []byte{0x40, 0x02, 0xC4, 0x09, 0x03, 0xBF, 0x13},
ServiceData: []AdvertismentDataElement{
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40, 0x02}, },
{UUID: New32BitUUID(0x0240FCD2), Data: []byte{0xC4}, },
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x05\x16\xD2\xFC\x40\x02" + // service data 16-Bit UUID
"\x05\x16\xD3\xFC\x40\x02" , // service data 16-Bit UUID
parsed: AdvertisementOptions{
ServiceData: []AdvertismentDataElement{
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40, 0x02}, },
{UUID: New16BitUUID(0xFCD3), Data: []byte{0x40, 0x02}, },
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x04\x16\xD2\xFC\x40" + // service data 16-Bit UUID
"\x12\x21\xB8\x6C\x75\x05\xE9\x25\xBD\x93\xA8\x42\x32\xC3\x00\x01\xAF\xAD\x09", // service data 128-Bit UUID
parsed: AdvertisementOptions{
ServiceData: []AdvertismentDataElement{
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40}, },
{
UUID: NewUUID([16]byte{0xad, 0xaf, 0x01, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8}),
Data: []byte{0x09,},
},
},
},
},
Expand Down

0 comments on commit e357a2a

Please sign in to comment.