@stoprocent/noble 1.16.1 → 1.17.1
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/README.md +15 -7
- package/index.d.ts +4 -0
- package/lib/hci-socket/gap.js +31 -32
- package/lib/win/src/peripheral_winrt.cc +72 -3
- package/lib/win/src/peripheral_winrt.h +1 -0
- package/package.json +2 -2
- 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/README.md
CHANGED
|
@@ -16,9 +16,12 @@ This fork of `noble` was created to introduce several key improvements and new f
|
|
|
16
16
|
|
|
17
17
|
2. **macOS Native Bindings Fix**: I have fixed the native bindings for macOS, ensuring better compatibility and performance on Apple devices.
|
|
18
18
|
|
|
19
|
-
3. **
|
|
20
|
-
|
|
19
|
+
3. **Windows Native Bindings Fix**: I have fixed the native bindings for Windows, adding support for `Service Data` from advertisements.
|
|
20
|
+
|
|
21
|
+
4. **New Features**:
|
|
22
|
+
- A `setAddress(...)` function has been added, allowing users to set the MAC address of the central device.
|
|
21
23
|
- A `connect(...)/connectAsync(...)` function has been added, allowing users to connect directly to specific device by address/identifier without a need to prior scan.
|
|
24
|
+
- A `waitForPoweredOn(...)` function to wait for the adapter to be powered on in await/async functions.
|
|
22
25
|
- Additionally, I plan to add raw L2CAP channel support, enhancing low-level Bluetooth communication capabilities.
|
|
23
26
|
|
|
24
27
|
If you appreciate these enhancements and the continued development of this project, please consider supporting my work.
|
|
@@ -49,14 +52,16 @@ const noble = require('@stoprocent/noble');
|
|
|
49
52
|
|
|
50
53
|
```javascript
|
|
51
54
|
// Read the battery level of the first found peripheral exposing the Battery Level characteristic
|
|
55
|
+
const noble = require('../');
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
noble.
|
|
56
|
-
if (state === 'poweredOn') {
|
|
57
|
+
async function run() {
|
|
58
|
+
try {
|
|
59
|
+
await noble.waitForPoweredOn();
|
|
57
60
|
await noble.startScanningAsync(['180f'], false);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(error);
|
|
58
63
|
}
|
|
59
|
-
}
|
|
64
|
+
}
|
|
60
65
|
|
|
61
66
|
noble.on('discover', async (peripheral) => {
|
|
62
67
|
await noble.stopScanningAsync();
|
|
@@ -69,6 +74,9 @@ noble.on('discover', async (peripheral) => {
|
|
|
69
74
|
await peripheral.disconnectAsync();
|
|
70
75
|
process.exit(0);
|
|
71
76
|
});
|
|
77
|
+
|
|
78
|
+
run();
|
|
79
|
+
|
|
72
80
|
```
|
|
73
81
|
## Use Noble With BLE5 Extended Features With HCI
|
|
74
82
|
|
package/index.d.ts
CHANGED
|
@@ -54,6 +54,10 @@ export declare function removeListener(event: string, listener: Function): event
|
|
|
54
54
|
|
|
55
55
|
export declare function removeAllListeners(event?: string): events.EventEmitter;
|
|
56
56
|
|
|
57
|
+
export var state: "unknown" | "resetting" | "unsupported" | "unauthorized" | "poweredOff" | "poweredOn";
|
|
58
|
+
/**
|
|
59
|
+
* @deprecated Use `state` instead.
|
|
60
|
+
*/
|
|
57
61
|
export var _state: "unknown" | "resetting" | "unsupported" | "unauthorized" | "poweredOff" | "poweredOn";
|
|
58
62
|
|
|
59
63
|
export var _bindings: any;
|
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
|
|
@@ -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
|
@@ -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.
|
|
9
|
+
"version": "1.17.1",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "https://github.com/stoprocent/noble.git"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"node-gyp-build": "^4.8.1"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@stoprocent/bluetooth-hci-socket": "^1.4.
|
|
37
|
+
"@stoprocent/bluetooth-hci-socket": "^1.4.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@commitlint/cli": "^19.3.0",
|
|
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', () => {
|