@stoprocent/noble 1.16.1 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -279,7 +279,6 @@ Gap.prototype.parseServices = function (
279
279
 
280
280
  if (leMetaEventType !== LE_META_EVENT_TYPE_SCAN_RESPONSE) {
281
281
  // reset service data every non-scan response event
282
- advertisement.serviceData = [];
283
282
  advertisement.serviceUuids = [];
284
283
  advertisement.serviceSolicitationUuids = [];
285
284
  }
@@ -371,40 +370,40 @@ Gap.prototype.parseServices = function (
371
370
  }
372
371
  break;
373
372
 
374
- case 0x16: // 16-bit Service Data, there can be multiple occurences
375
- advertisement.serviceData.push({
376
- uuid: bytes
377
- .slice(0, 2)
378
- .toString('hex')
379
- .match(/.{1,2}/g)
380
- .reverse()
381
- .join(''),
382
- data: bytes.slice(2, bytes.length)
383
- });
384
- break;
385
-
386
- case 0x20: // 32-bit Service Data, there can be multiple occurences
387
- advertisement.serviceData.push({
388
- uuid: bytes
389
- .slice(0, 4)
390
- .toString('hex')
391
- .match(/.{1,2}/g)
392
- .reverse()
393
- .join(''),
394
- data: bytes.slice(4, bytes.length)
395
- });
396
- break;
397
-
398
- case 0x21: // 128-bit Service Data, there can be multiple occurences
399
- advertisement.serviceData.push({
400
- uuid: bytes
401
- .slice(0, 16)
373
+ case 0x16: // 16-bit Service Data
374
+ case 0x20: // 32-bit Service Data
375
+ case 0x21: // 128-bit Service Data
376
+ {
377
+ let uuidLength;
378
+ if (eirType === 0x16) {
379
+ uuidLength = 2;
380
+ } else if (eirType === 0x20) {
381
+ uuidLength = 4;
382
+ } else {
383
+ uuidLength = 16;
384
+ }
385
+
386
+ const serviceUuid = bytes
387
+ .slice(0, uuidLength)
402
388
  .toString('hex')
403
389
  .match(/.{1,2}/g)
404
390
  .reverse()
405
- .join(''),
406
- data: bytes.slice(16, bytes.length)
407
- });
391
+ .join('');
392
+
393
+ // Find existing service data index
394
+ const existingIndex = advertisement.serviceData.findIndex(s => s.uuid === serviceUuid);
395
+ const serviceData = {
396
+ uuid: serviceUuid,
397
+ data: bytes.slice(uuidLength, bytes.length)
398
+ };
399
+
400
+ // Update or append service data
401
+ if (existingIndex >= 0) {
402
+ advertisement.serviceData[existingIndex] = serviceData;
403
+ } else {
404
+ advertisement.serviceData.push(serviceData);
405
+ }
406
+ }
408
407
  break;
409
408
 
410
409
  case 0x1f: // List of 32 bit solicitation UUIDs
@@ -8,7 +8,7 @@ using namespace winrt::Windows::Storage::Streams;
8
8
  using winrt::Windows::Devices::Bluetooth::BluetoothCacheMode;
9
9
  using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristicsResult;
10
10
  using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattDescriptorsResult;
11
- using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceServicesResult;
11
+ using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceServicesResult;
12
12
  using winrt::Windows::Foundation::AsyncStatus;
13
13
  using winrt::Windows::Foundation::IAsyncOperation;
14
14
 
@@ -31,6 +31,62 @@ PeripheralWinrt::~PeripheralWinrt()
31
31
  }
32
32
  }
33
33
 
34
+ void PeripheralWinrt::ProcessServiceData(const BluetoothLEAdvertisementDataSection& ds, size_t uuidSize)
35
+ {
36
+ auto d = ds.Data();
37
+ auto dr = DataReader::FromBuffer(d);
38
+ dr.ByteOrder(ByteOrder::LittleEndian);
39
+
40
+ std::vector<uint8_t> data;
41
+ std::string uuidStr;
42
+
43
+ if (uuidSize == 16) { // 128-bit UUID
44
+ uint64_t low = dr.ReadUInt64();
45
+ uint64_t high = dr.ReadUInt64();
46
+
47
+ char uuid[37];
48
+ snprintf(uuid, sizeof(uuid),
49
+ "%08x-%04x-%04x-%04x-%012llx",
50
+ (uint32_t)((high >> 32) & 0xFFFFFFFF),
51
+ (uint16_t)((high >> 16) & 0xFFFF),
52
+ (uint16_t)(high & 0xFFFF),
53
+ (uint16_t)((low >> 48) & 0xFFFF),
54
+ (unsigned long long)(low & 0xFFFFFFFFFFFF));
55
+ uuidStr = uuid;
56
+ }
57
+ else { // 16-bit or 32-bit UUID
58
+ char uuid[9]; // Max 8 chars for 32-bit UUID + null terminator
59
+ if (uuidSize == 2) {
60
+ uint16_t serviceUuid = dr.ReadUInt16();
61
+ snprintf(uuid, sizeof(uuid), "%04x", serviceUuid);
62
+ } else { // 4 bytes
63
+ uint32_t serviceUuid = dr.ReadUInt32();
64
+ snprintf(uuid, sizeof(uuid), "%08x", serviceUuid);
65
+ }
66
+ uuidStr = uuid;
67
+ }
68
+
69
+ // Read remaining data
70
+ data.resize(d.Length() - uuidSize);
71
+ dr.ReadBytes(data);
72
+
73
+ // Find and update existing entry or add new one
74
+ bool found = false;
75
+ for (auto& pair : serviceData) {
76
+ if (pair.first == uuidStr) {
77
+ pair.second = data;
78
+ found = true;
79
+ break;
80
+ }
81
+ }
82
+
83
+ if (!found) {
84
+ serviceData.push_back(std::make_pair(uuidStr, data));
85
+ }
86
+
87
+ dr.Close();
88
+ }
89
+
34
90
  void PeripheralWinrt::Update(const int rssiValue, const BluetoothLEAdvertisement& advertisment,
35
91
  const BluetoothLEAdvertisementType& advertismentType)
36
92
  {
@@ -56,7 +112,7 @@ void PeripheralWinrt::Update(const int rssiValue, const BluetoothLEAdvertisement
56
112
  txPowerLevel -= 256;
57
113
  dr.Close();
58
114
  }
59
- if (ds.DataType() == BluetoothLEAdvertisementDataTypes::ManufacturerSpecificData())
115
+ else if (ds.DataType() == BluetoothLEAdvertisementDataTypes::ManufacturerSpecificData())
60
116
  {
61
117
  auto d = ds.Data();
62
118
  auto dr = DataReader::FromBuffer(d);
@@ -64,6 +120,18 @@ void PeripheralWinrt::Update(const int rssiValue, const BluetoothLEAdvertisement
64
120
  dr.ReadBytes(manufacturerData);
65
121
  dr.Close();
66
122
  }
123
+ else if (ds.DataType() == BluetoothLEAdvertisementDataTypes::ServiceData16BitUuids())
124
+ {
125
+ ProcessServiceData(ds, 2); // 2 bytes for 16-bit UUID
126
+ }
127
+ else if (ds.DataType() == BluetoothLEAdvertisementDataTypes::ServiceData32BitUuids())
128
+ {
129
+ ProcessServiceData(ds, 4); // 4 bytes for 32-bit UUID
130
+ }
131
+ else if (ds.DataType() == BluetoothLEAdvertisementDataTypes::ServiceData128BitUuids())
132
+ {
133
+ ProcessServiceData(ds, 16); // 16 bytes for 128-bit UUID
134
+ }
67
135
  }
68
136
 
69
137
  serviceUuids.clear();
@@ -258,7 +326,8 @@ void PeripheralWinrt::GetDescriptor(winrt::guid serviceUuid, winrt::guid charact
258
326
  [=](std::optional<GattCharacteristic> characteristic) {
259
327
  if (characteristic)
260
328
  {
261
- GetDescriptorFromCharacteristic(*characteristic, descriptorUuid, callback);
329
+ GetDescriptorFromCharacteristic(*characteristic, descriptorUuid,
330
+ callback);
262
331
  }
263
332
  else
264
333
  {
@@ -79,4 +79,5 @@ private:
79
79
  GetDescriptorFromCharacteristic(GattCharacteristic characteristic, winrt::guid descriptorUuid,
80
80
  std::function<void(std::optional<GattDescriptor>)> callback);
81
81
  std::unordered_map<winrt::guid, CachedService> cachedServices;
82
+ void ProcessServiceData(const BluetoothLEAdvertisementDataSection& ds, size_t uuidSize);
82
83
  };
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "license": "MIT",
7
7
  "name": "@stoprocent/noble",
8
8
  "description": "A Node.js BLE (Bluetooth Low Energy) central library.",
9
- "version": "1.16.1",
9
+ "version": "1.17.0",
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "https://github.com/stoprocent/noble.git"
@@ -1085,6 +1085,151 @@ describe('hci-socket gap', () => {
1085
1085
 
1086
1086
  assert.notCalled(discoverCallback);
1087
1087
  });
1088
+
1089
+ it('should accumulate different service UUIDs and data across multiple reports', () => {
1090
+ const hci = {
1091
+ on: sinon.spy()
1092
+ };
1093
+
1094
+ const status = 'status';
1095
+ const type = 0x01;
1096
+ const address = 'a:d:d:r:e:s:s';
1097
+ const addressType = 'addressType';
1098
+ const rssi = 'rssi';
1099
+
1100
+ // First report
1101
+ const eirType1 = 0x16;
1102
+ const serviceUuid1 = Buffer.from([0x01, 0x02]);
1103
+ const serviceData1 = Buffer.from([0x03, 0x04]);
1104
+ const eirLength1 = serviceUuid1.length + serviceData1.length + 1;
1105
+ const eirLengthAndType1 = Buffer.from([eirLength1, eirType1]);
1106
+ const eir1 = Buffer.concat([eirLengthAndType1, serviceUuid1, serviceData1]);
1107
+
1108
+ const gap = new Gap(hci);
1109
+ gap.onHciLeAdvertisingReport(status, type, address, addressType, eir1, rssi);
1110
+
1111
+ // Second report with different UUID and data
1112
+ const eirType2 = 0x16;
1113
+ const serviceUuid2 = Buffer.from([0x05, 0x06]);
1114
+ const serviceData2 = Buffer.from([0x07, 0x08]);
1115
+ const eirLength2 = serviceUuid2.length + serviceData2.length + 1;
1116
+ const eirLengthAndType2 = Buffer.from([eirLength2, eirType2]);
1117
+ const eir2 = Buffer.concat([eirLengthAndType2, serviceUuid2, serviceData2]);
1118
+
1119
+ gap.onHciLeAdvertisingReport(status, type, address, addressType, eir2, rssi);
1120
+
1121
+ const expectedDiscovery = {
1122
+ address,
1123
+ addressType,
1124
+ connectable: true,
1125
+ advertisement: {
1126
+ localName: undefined,
1127
+ txPowerLevel: undefined,
1128
+ manufacturerData: undefined,
1129
+ serviceData: [
1130
+ {
1131
+ uuid: '0201',
1132
+ data: serviceData1
1133
+ },
1134
+ {
1135
+ uuid: '0605',
1136
+ data: serviceData2
1137
+ }
1138
+ ],
1139
+ serviceUuids: [],
1140
+ solicitationServiceUuids: [],
1141
+ serviceSolicitationUuids: []
1142
+ },
1143
+ rssi,
1144
+ count: 2,
1145
+ hasScanResponse: false
1146
+ };
1147
+
1148
+ should(gap._discoveries[address]).deepEqual(expectedDiscovery);
1149
+ });
1150
+
1151
+ it('should overwrite service data when receiving same UUID with different data', () => {
1152
+ const hci = {
1153
+ on: sinon.spy()
1154
+ };
1155
+
1156
+ const status = 'status';
1157
+ const type = 0x01;
1158
+ const address = 'a:d:d:r:e:s:s';
1159
+ const addressType = 'addressType';
1160
+ const rssi = 'rssi';
1161
+
1162
+ // First report
1163
+ const eirType = 0x16;
1164
+ const serviceUuid = Buffer.from([0x01, 0x02]);
1165
+ const serviceData1 = Buffer.from([0x03, 0x04]);
1166
+ const eirLength1 = serviceUuid.length + serviceData1.length + 1;
1167
+ const eirLengthAndType1 = Buffer.from([eirLength1, eirType]);
1168
+ const eir1 = Buffer.concat([eirLengthAndType1, serviceUuid, serviceData1]);
1169
+
1170
+ const gap = new Gap(hci);
1171
+ gap.onHciLeAdvertisingReport(status, type, address, addressType, eir1, rssi);
1172
+
1173
+ // Verify first state
1174
+ const expectedDiscovery1 = {
1175
+ address,
1176
+ addressType,
1177
+ connectable: true,
1178
+ advertisement: {
1179
+ localName: undefined,
1180
+ txPowerLevel: undefined,
1181
+ manufacturerData: undefined,
1182
+ serviceData: [
1183
+ {
1184
+ uuid: '0201',
1185
+ data: serviceData1
1186
+ }
1187
+ ],
1188
+ serviceUuids: [],
1189
+ solicitationServiceUuids: [],
1190
+ serviceSolicitationUuids: []
1191
+ },
1192
+ rssi,
1193
+ count: 1,
1194
+ hasScanResponse: false
1195
+ };
1196
+
1197
+ should(gap._discoveries[address]).deepEqual(expectedDiscovery1);
1198
+
1199
+ // Second report with same UUID but different data
1200
+ const serviceData2 = Buffer.from([0x05, 0x06]);
1201
+ const eirLength2 = serviceUuid.length + serviceData2.length + 1;
1202
+ const eirLengthAndType2 = Buffer.from([eirLength2, eirType]);
1203
+ const eir2 = Buffer.concat([eirLengthAndType2, serviceUuid, serviceData2]);
1204
+
1205
+ gap.onHciLeAdvertisingReport(status, type, address, addressType, eir2, rssi);
1206
+
1207
+ // Verify data was overwritten
1208
+ const expectedDiscovery2 = {
1209
+ address,
1210
+ addressType,
1211
+ connectable: true,
1212
+ advertisement: {
1213
+ localName: undefined,
1214
+ txPowerLevel: undefined,
1215
+ manufacturerData: undefined,
1216
+ serviceData: [
1217
+ {
1218
+ uuid: '0201',
1219
+ data: serviceData2
1220
+ }
1221
+ ],
1222
+ serviceUuids: [],
1223
+ solicitationServiceUuids: [],
1224
+ serviceSolicitationUuids: []
1225
+ },
1226
+ rssi,
1227
+ count: 2,
1228
+ hasScanResponse: false
1229
+ };
1230
+
1231
+ should(gap._discoveries[address]).deepEqual(expectedDiscovery2);
1232
+ });
1088
1233
  });
1089
1234
 
1090
1235
  it('should reset the service data on non scan responses', () => {