Skip to content

Commit 4c9167c

Browse files
dnlwgndradhus
authored andcommitted
add ServiceData advertising element (#243)
* gap: fix comment * gap: expose ServiceData() in AdvertisementFields * macos: include ServiceData in AdvertisementFields * gap/linux: include ServiceData in AdvertisementFields * gap: add unimplemented ServiceData() to raw advertisement * added ServiceData advertising element also to the sending pieces * more explicitly use the ad element type ids * added a test case for ServiceData * linux: added ServiceData advertising element * sd: fix: handle no servicedata present * linux: bluez uses string uuids for service data * linux: fix: correct datatype for advertise with ServiceData * uuid: add 32-Bit functions * ServiceData now also uses a slice instead of a map as in #244 * Revert unnessesary changes * formatting * remove extra check --------- Co-authored-by: William Johansson <[email protected]>
1 parent bc01890 commit 4c9167c

File tree

5 files changed

+211
-3
lines changed

5 files changed

+211
-3
lines changed

adapter_darwin.go

+14
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,19 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
156156
})
157157
}
158158

159+
var serviceData []ServiceDataElement
160+
for _, svcData := range advFields.ServiceData {
161+
cbgoUUID := svcData.UUID
162+
uuid, err := ParseUUID(cbgoUUID.String())
163+
if err != nil {
164+
continue
165+
}
166+
serviceData = append(serviceData, ServiceDataElement{
167+
UUID: uuid,
168+
Data: svcData.Data,
169+
})
170+
}
171+
159172
// Peripheral UUID is randomized on macOS, which means to
160173
// different centrals it will appear to have a different UUID.
161174
return ScanResult{
@@ -168,6 +181,7 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
168181
LocalName: advFields.LocalName,
169182
ServiceUUIDs: serviceUUIDs,
170183
ManufacturerData: manufacturerData,
184+
ServiceData: serviceData,
171185
},
172186
},
173187
}

gap.go

+119-3
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ type AdvertisementOptions struct {
5555
Interval Duration
5656

5757
// ManufacturerData stores Advertising Data.
58-
// Keys are the Manufacturer ID to associate with the data.
5958
ManufacturerData []ManufacturerDataElement
59+
60+
// ServiceData stores Advertising Data.
61+
ServiceData []ServiceDataElement
6062
}
6163

6264
// Manufacturer data that's part of an advertisement packet.
@@ -73,6 +75,17 @@ type ManufacturerDataElement struct {
7375
Data []byte
7476
}
7577

78+
// ServiceDataElement strores a uuid/byte-array pair used as ServiceData advertisment elements
79+
type ServiceDataElement struct {
80+
// service uuid or company uuid
81+
// The list can also be viewed here:
82+
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
83+
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/service_uuids.yaml
84+
UUID UUID
85+
// the data byte array
86+
Data []byte
87+
}
88+
7689
// Duration is the unit of time used in BLE, in 0.625µs units. This unit of time
7790
// is used throughout the BLE stack.
7891
type Duration uint16
@@ -124,9 +137,13 @@ type AdvertisementPayload interface {
124137
// if this data is not available.
125138
Bytes() []byte
126139

127-
// ManufacturerData returns a map with all the manufacturer data present in the
128-
//advertising. IT may be empty.
140+
// ManufacturerData returns a slice with all the manufacturer data present in the
141+
// advertising. It may be empty.
129142
ManufacturerData() []ManufacturerDataElement
143+
144+
// ServiceData returns a slice with all the service data present in the
145+
// advertising. It may be empty.
146+
ServiceData() []ServiceDataElement
130147
}
131148

132149
// AdvertisementFields contains advertisement fields in structured form.
@@ -142,6 +159,9 @@ type AdvertisementFields struct {
142159

143160
// ManufacturerData is the manufacturer data of the advertisement.
144161
ManufacturerData []ManufacturerDataElement
162+
163+
// ServiceData is the service data of the advertisement.
164+
ServiceData []ServiceDataElement
145165
}
146166

147167
// advertisementFields wraps AdvertisementFields to implement the
@@ -179,6 +199,11 @@ func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement {
179199
return p.AdvertisementFields.ManufacturerData
180200
}
181201

202+
// ServiceData returns the underlying ServiceData field.
203+
func (p *advertisementFields) ServiceData() []ServiceDataElement {
204+
return p.AdvertisementFields.ServiceData
205+
}
206+
182207
// rawAdvertisementPayload encapsulates a raw advertisement packet. Methods to
183208
// get the data (such as LocalName()) will parse just the needed field. Scanning
184209
// the data should be fast as most advertisement packets only have a very small
@@ -288,6 +313,40 @@ func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement
288313
return manufacturerData
289314
}
290315

316+
// ServiceData returns the service data in the advertisment payload
317+
func (buf *rawAdvertisementPayload) ServiceData() []ServiceDataElement {
318+
var serviceData []ServiceDataElement
319+
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
320+
fieldLength := int(buf.data[index+0])
321+
if fieldLength < 3 { // field has only length and type and no data
322+
continue
323+
}
324+
fieldType := buf.data[index+1]
325+
switch fieldType {
326+
case 0x16: // 16-bit uuid
327+
serviceData = append(serviceData, ServiceDataElement{
328+
UUID: New16BitUUID(uint16(buf.data[index+2]) + (uint16(buf.data[index+3]) << 8)),
329+
Data: buf.data[index+4 : index+fieldLength+1],
330+
})
331+
case 0x20: // 32-bit uuid
332+
serviceData = append(serviceData, ServiceDataElement{
333+
UUID: New32BitUUID(uint32(buf.data[index+2]) + (uint32(buf.data[index+3]) << 8) + (uint32(buf.data[index+4]) << 16) + (uint32(buf.data[index+5]) << 24)),
334+
Data: buf.data[index+6 : index+fieldLength+1],
335+
})
336+
case 0x21: // 128-bit uuid
337+
var uuidArray [16]byte
338+
copy(uuidArray[:], buf.data[index+2:index+18])
339+
serviceData = append(serviceData, ServiceDataElement{
340+
UUID: NewUUID(uuidArray),
341+
Data: buf.data[index+18 : index+fieldLength+1],
342+
})
343+
default:
344+
continue
345+
}
346+
}
347+
return serviceData
348+
}
349+
291350
// reset restores this buffer to the original state.
292351
func (buf *rawAdvertisementPayload) reset() {
293352
// The data is not reset (only the length), because with a zero length the
@@ -322,6 +381,12 @@ func (buf *rawAdvertisementPayload) addFromOptions(options AdvertisementOptions)
322381
}
323382
}
324383

384+
for _, element := range options.ServiceData {
385+
if !buf.addServiceData(element.UUID, element.Data) {
386+
return false
387+
}
388+
}
389+
325390
return true
326391
}
327392

@@ -344,6 +409,57 @@ func (buf *rawAdvertisementPayload) addManufacturerData(key uint16, value []byte
344409
return true
345410
}
346411

412+
// addServiceData adds service data ([]byte) entries to the advertisement payload.
413+
func (buf *rawAdvertisementPayload) addServiceData(uuid UUID, data []byte) (ok bool) {
414+
switch {
415+
case uuid.Is16Bit():
416+
// check if it fits
417+
fieldLength := 1 + 1 + 2 + len(data) // 1 byte length, 1 byte ad type, 2 bytes uuid, actual service data
418+
if int(buf.len)+fieldLength > len(buf.data) {
419+
return false
420+
}
421+
// Add the data.
422+
buf.data[buf.len+0] = byte(fieldLength - 1)
423+
buf.data[buf.len+1] = 0x16
424+
buf.data[buf.len+2] = byte(uuid.Get16Bit())
425+
buf.data[buf.len+3] = byte(uuid.Get16Bit() >> 8)
426+
copy(buf.data[buf.len+4:], data)
427+
buf.len += uint8(fieldLength)
428+
429+
case uuid.Is32Bit():
430+
// check if it fits
431+
fieldLength := 1 + 1 + 4 + len(data) // 1 byte length, 1 byte ad type, 4 bytes uuid, actual service data
432+
if int(buf.len)+fieldLength > len(buf.data) {
433+
return false
434+
}
435+
// Add the data.
436+
buf.data[buf.len+0] = byte(fieldLength - 1)
437+
buf.data[buf.len+1] = 0x20
438+
buf.data[buf.len+2] = byte(uuid.Get32Bit())
439+
buf.data[buf.len+3] = byte(uuid.Get32Bit() >> 8)
440+
buf.data[buf.len+4] = byte(uuid.Get32Bit() >> 16)
441+
buf.data[buf.len+5] = byte(uuid.Get32Bit() >> 24)
442+
copy(buf.data[buf.len+6:], data)
443+
buf.len += uint8(fieldLength)
444+
445+
default: // must be 128-bit uuid
446+
// check if it fits
447+
fieldLength := 1 + 1 + 16 + len(data) // 1 byte length, 1 byte ad type, 16 bytes uuid, actual service data
448+
if int(buf.len)+fieldLength > len(buf.data) {
449+
return false
450+
}
451+
// Add the data.
452+
buf.data[buf.len+0] = byte(fieldLength - 1)
453+
buf.data[buf.len+1] = 0x21
454+
uuid_bytes := uuid.Bytes()
455+
copy(buf.data[buf.len+2:], uuid_bytes[:])
456+
copy(buf.data[buf.len+2+16:], data)
457+
buf.len += uint8(fieldLength)
458+
459+
}
460+
return true
461+
}
462+
347463
// addFlags adds a flags field to the advertisement buffer. It returns true on
348464
// success (the flags can be added) and false on failure.
349465
func (buf *rawAdvertisementPayload) addFlags(flags byte) (ok bool) {

gap_linux.go

+20
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
5353
for _, uuid := range options.ServiceUUIDs {
5454
serviceUUIDs = append(serviceUUIDs, uuid.String())
5555
}
56+
var serviceData = make(map[string]interface{})
57+
for _, element := range options.ServiceData {
58+
serviceData[element.UUID.String()] = element.Data
59+
}
5660

5761
// Convert map[uint16][]byte to map[uint16]any because that's what BlueZ needs.
5862
manufacturerData := map[uint16]any{}
@@ -71,6 +75,7 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
7175
"ServiceUUIDs": {Value: serviceUUIDs},
7276
"ManufacturerData": {Value: manufacturerData},
7377
"LocalName": {Value: options.LocalName},
78+
"ServiceData": {Value: serviceData},
7479
// The documentation states:
7580
// > Timeout of the advertisement in seconds. This defines the
7681
// > lifetime of the advertisement.
@@ -288,6 +293,20 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
288293
localName, _ := props["Name"].Value().(string)
289294
rssi, _ := props["RSSI"].Value().(int16)
290295

296+
var serviceData []ServiceDataElement
297+
if sdata, ok := props["ServiceData"].Value().(map[string]dbus.Variant); ok {
298+
for k, v := range sdata {
299+
uuid, err := ParseUUID(k)
300+
if err != nil {
301+
continue
302+
}
303+
serviceData = append(serviceData, ServiceDataElement{
304+
UUID: uuid,
305+
Data: v.Value().([]byte),
306+
})
307+
}
308+
}
309+
291310
return ScanResult{
292311
RSSI: rssi,
293312
Address: a,
@@ -296,6 +315,7 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
296315
LocalName: localName,
297316
ServiceUUIDs: serviceUUIDs,
298317
ManufacturerData: manufacturerData,
318+
ServiceData: serviceData,
299319
},
300320
},
301321
}

gap_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,42 @@ func TestCreateAdvertisementPayload(t *testing.T) {
7878
},
7979
},
8080
},
81+
{
82+
raw: "\x02\x01\x06" + // flags
83+
"\x05\x16\xD2\xFC\x40\x02" + // service data 16-Bit UUID
84+
"\x06\x20\xD2\xFC\x40\x02\xC4", // service data 32-Bit UUID
85+
parsed: AdvertisementOptions{
86+
ServiceData: []ServiceDataElement{
87+
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40, 0x02}},
88+
{UUID: New32BitUUID(0x0240FCD2), Data: []byte{0xC4}},
89+
},
90+
},
91+
},
92+
{
93+
raw: "\x02\x01\x06" + // flags
94+
"\x05\x16\xD2\xFC\x40\x02" + // service data 16-Bit UUID
95+
"\x05\x16\xD3\xFC\x40\x02", // service data 16-Bit UUID
96+
parsed: AdvertisementOptions{
97+
ServiceData: []ServiceDataElement{
98+
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40, 0x02}},
99+
{UUID: New16BitUUID(0xFCD3), Data: []byte{0x40, 0x02}},
100+
},
101+
},
102+
},
103+
{
104+
raw: "\x02\x01\x06" + // flags
105+
"\x04\x16\xD2\xFC\x40" + // service data 16-Bit UUID
106+
"\x12\x21\xB8\x6C\x75\x05\xE9\x25\xBD\x93\xA8\x42\x32\xC3\x00\x01\xAF\xAD\x09", // service data 128-Bit UUID
107+
parsed: AdvertisementOptions{
108+
ServiceData: []ServiceDataElement{
109+
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40}},
110+
{
111+
UUID: NewUUID([16]byte{0xad, 0xaf, 0x01, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8}),
112+
Data: []byte{0x09},
113+
},
114+
},
115+
},
116+
},
81117
}
82118
for _, tc := range tests {
83119
var expectedRaw rawAdvertisementPayload

uuid.go

+22
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ func New16BitUUID(shortUUID uint16) UUID {
3838
return uuid
3939
}
4040

41+
// New32BitUUID returns a new 128-bit UUID based on a 32-bit UUID.
42+
//
43+
// Note: only use registered UUIDs. See
44+
// https://www.bluetooth.com/specifications/gatt/services/ for a list.
45+
func New32BitUUID(shortUUID uint32) UUID {
46+
// https://stackoverflow.com/questions/36212020/how-can-i-convert-a-bluetooth-16-bit-service-uuid-into-a-128-bit-uuid
47+
var uuid UUID
48+
uuid[0] = 0x5F9B34FB
49+
uuid[1] = 0x80000080
50+
uuid[2] = 0x00001000
51+
uuid[3] = shortUUID
52+
return uuid
53+
}
54+
4155
// Replace16BitComponent returns a new UUID where bits 16..32 have been replaced
4256
// with the bits given in the argument. These bits are the same bits that vary
4357
// in the 16-bit compressed UUID form.
@@ -68,6 +82,14 @@ func (uuid UUID) Get16Bit() uint16 {
6882
return uint16(uuid[3])
6983
}
7084

85+
// Get32Bit returns the 32-bit version of this UUID. This is only valid if it
86+
// actually is a 32-bit UUID, see Is32Bit.
87+
func (uuid UUID) Get32Bit() uint32 {
88+
// Note: using a Get* function as a getter because method names can't start
89+
// with a number.
90+
return uuid[3]
91+
}
92+
7193
// Bytes returns a 16-byte array containing the raw UUID.
7294
func (uuid UUID) Bytes() [16]byte {
7395
buf := [16]byte{}

0 commit comments

Comments
 (0)