@stoprocent/noble 1.16.0 → 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.
- package/examples/peripheral-explorer-async.js +1 -1
- package/index.d.ts +1 -0
- package/lib/hci-socket/gap.js +31 -32
- package/lib/hci-socket/hci.js +4 -2
- package/lib/noble.js +20 -0
- package/lib/win/src/peripheral_winrt.cc +72 -3
- package/lib/win/src/peripheral_winrt.h +1 -0
- package/package.json +1 -1
- package/prebuilds/darwin-x64+arm64/@stoprocent+noble.node +0 -0
- package/prebuilds/win32-ia32/@stoprocent+noble.node +0 -0
- package/prebuilds/win32-x64/@stoprocent+noble.node +0 -0
- package/test/lib/hci-socket/gap.test.js +145 -0
- package/test/lib/hci-socket/hci.test.js +1 -1
|
@@ -9,7 +9,7 @@ noble.on('stateChange', async (state) => {
|
|
|
9
9
|
if (state === 'poweredOn') {
|
|
10
10
|
if (directConnect === '1') {
|
|
11
11
|
await noble.stopScanningAsync();
|
|
12
|
-
await noble.connectAsync(peripheralIdOrAddress.replace(/:/g, ''))
|
|
12
|
+
await noble.connectAsync(peripheralIdOrAddress.replace(/:/g, ''));
|
|
13
13
|
} else {
|
|
14
14
|
await noble.startScanningAsync();
|
|
15
15
|
}
|
package/index.d.ts
CHANGED
package/lib/hci-socket/gap.js
CHANGED
|
@@ -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
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
package/lib/hci-socket/hci.js
CHANGED
|
@@ -928,8 +928,10 @@ Hci.prototype.processCmdCompleteEvent = function (cmd, status, result) {
|
|
|
928
928
|
this.readBdAddr();
|
|
929
929
|
} else if (cmd === LE_READ_LOCAL_SUPPORTED_FEATURES) {
|
|
930
930
|
if (status === 0) {
|
|
931
|
-
|
|
932
|
-
this._isExtended = (
|
|
931
|
+
// Set _isExtended based on leExtendedAdvertising bit (12)
|
|
932
|
+
this._isExtended = !!(result.readUInt32LE(0) & (1 << 12));
|
|
933
|
+
|
|
934
|
+
this.emit('leFeatures', result);
|
|
933
935
|
|
|
934
936
|
if (this._isExtended) {
|
|
935
937
|
this.setCodedPhySupport();
|
package/lib/noble.js
CHANGED
|
@@ -118,6 +118,26 @@ Noble.prototype.setAddress = function (address) {
|
|
|
118
118
|
}
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
+
Noble.prototype.waitForPoweredOn = function (timeout = 10000) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const timeoutId = setTimeout(() => {
|
|
124
|
+
reject(new Error('Timeout waiting for Noble to be powered on'));
|
|
125
|
+
}, timeout);
|
|
126
|
+
|
|
127
|
+
let listener;
|
|
128
|
+
listener = (state) => {
|
|
129
|
+
clearTimeout(timeoutId);
|
|
130
|
+
if (state === 'poweredOn') {
|
|
131
|
+
resolve();
|
|
132
|
+
} else {
|
|
133
|
+
this.once('stateChange', listener);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
this.once('stateChange', listener);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
121
141
|
const startScanning = function (serviceUuids, allowDuplicates, callback) {
|
|
122
142
|
if (typeof serviceUuids === 'function') {
|
|
123
143
|
this.emit('warning', 'calling startScanning(callback) is deprecated');
|
|
@@ -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,
|
|
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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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', () => {
|
|
@@ -744,7 +744,7 @@ describe('hci-socket hci', () => {
|
|
|
744
744
|
it('should process with extended features', () => {
|
|
745
745
|
const cmd = 8195;
|
|
746
746
|
const status = 0;
|
|
747
|
-
const result = Buffer.from(
|
|
747
|
+
const result = Buffer.from("bd5f660000000000", "hex");
|
|
748
748
|
|
|
749
749
|
hci.processCmdCompleteEvent(cmd, status, result);
|
|
750
750
|
|