@stoprocent/noble 1.15.2 → 1.16.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/examples/peripheral-explorer-async.js +16 -4
- package/index.d.ts +1 -0
- package/lib/hci-socket/bindings.js +3 -2
- package/lib/hci-socket/hci.js +38 -11
- package/lib/noble.js +20 -0
- package/lib/win/binding.gyp +1 -1
- 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/bindings.test.js +13 -2
- package/test/lib/hci-socket/hci.test.js +78 -15
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
const noble = require('../');
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const directConnect = process.argv[2].toLowerCase();
|
|
4
|
+
const peripheralIdOrAddress = process.argv[3].toLowerCase();
|
|
5
|
+
|
|
6
|
+
const starTime = Date.now();
|
|
4
7
|
|
|
5
8
|
noble.on('stateChange', async (state) => {
|
|
6
9
|
if (state === 'poweredOn') {
|
|
7
|
-
|
|
10
|
+
if (directConnect === '1') {
|
|
11
|
+
await noble.stopScanningAsync();
|
|
12
|
+
await noble.connectAsync(peripheralIdOrAddress.replace(/:/g, ''));
|
|
13
|
+
} else {
|
|
14
|
+
await noble.startScanningAsync();
|
|
15
|
+
}
|
|
8
16
|
}
|
|
9
17
|
});
|
|
10
18
|
|
|
11
19
|
noble.on('discover', async (peripheral) => {
|
|
12
20
|
if ([peripheral.id, peripheral.address].includes(peripheralIdOrAddress)) {
|
|
13
21
|
await noble.stopScanningAsync();
|
|
14
|
-
|
|
22
|
+
|
|
15
23
|
console.log(`Peripheral with ID ${peripheral.id} found`);
|
|
16
24
|
const advertisement = peripheral.advertisement;
|
|
17
25
|
|
|
@@ -59,7 +67,9 @@ const explore = async (peripheral) => {
|
|
|
59
67
|
process.exit(0);
|
|
60
68
|
});
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
if (peripheral.state !== 'connected') {
|
|
71
|
+
await peripheral.connectAsync();
|
|
72
|
+
}
|
|
63
73
|
|
|
64
74
|
const services = await peripheral.discoverServicesAsync([]);
|
|
65
75
|
|
|
@@ -114,7 +124,9 @@ const explore = async (peripheral) => {
|
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
|
|
127
|
+
console.log(`Time taken: ${Date.now() - starTime}ms`);
|
|
117
128
|
await peripheral.disconnectAsync();
|
|
129
|
+
|
|
118
130
|
};
|
|
119
131
|
|
|
120
132
|
process.on('SIGINT', function () {
|
package/index.d.ts
CHANGED
|
@@ -53,11 +53,12 @@ NobleBindings.prototype.stopScanning = function () {
|
|
|
53
53
|
|
|
54
54
|
NobleBindings.prototype.connect = function (peripheralUuid, parameters) {
|
|
55
55
|
let address = this._addresses[peripheralUuid];
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
let addressType = this._addresseTypes[peripheralUuid] || 'random'; // Default to 'random' if type is not defined
|
|
57
|
+
|
|
58
58
|
// If address is not available, generate it from the UUID using the transformation logic inline
|
|
59
59
|
if (!address) {
|
|
60
60
|
address = peripheralUuid.match(/.{1,2}/g).join(':'); // Converts UUID back to MAC address format
|
|
61
|
+
addressType = (parseInt(address.slice(0, 2).slice(0,2), 16) & 0x02) === 0 ? 'public' : 'random';
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
// Manage connection attempts
|
package/lib/hci-socket/hci.js
CHANGED
|
@@ -49,6 +49,7 @@ const OCF_READ_RSSI = 0x0005;
|
|
|
49
49
|
const OGF_LE_CTL = 0x08;
|
|
50
50
|
const OCF_LE_SET_EVENT_MASK = 0x0001;
|
|
51
51
|
const OCF_LE_READ_BUFFER_SIZE = 0x0002;
|
|
52
|
+
const OCF_LE_READ_LOCAL_SUPPORTED_FEATURES = 0x0003;
|
|
52
53
|
const OCF_LE_SET_EXTENDED_SCAN_PARAMETERS = 0x0041;
|
|
53
54
|
const OCF_LE_SET_EXTENDED_SCAN_ENABLE = 0x0042;
|
|
54
55
|
const OCF_LE_SET_SCAN_PARAMETERS = 0x000b;
|
|
@@ -77,6 +78,7 @@ const READ_RSSI_CMD = OCF_READ_RSSI | (OGF_STATUS_PARAM << 10);
|
|
|
77
78
|
|
|
78
79
|
const LE_SET_EVENT_MASK_CMD = OCF_LE_SET_EVENT_MASK | (OGF_LE_CTL << 10);
|
|
79
80
|
const LE_READ_BUFFER_SIZE_CMD = OCF_LE_READ_BUFFER_SIZE | (OGF_LE_CTL << 10);
|
|
81
|
+
const LE_READ_LOCAL_SUPPORTED_FEATURES = OCF_LE_READ_LOCAL_SUPPORTED_FEATURES | (OGF_LE_CTL << 10);
|
|
80
82
|
const LE_SET_EXTENDED_SCAN_PARAMETERS_CMD =
|
|
81
83
|
OCF_LE_SET_EXTENDED_SCAN_PARAMETERS | (OGF_LE_CTL << 10);
|
|
82
84
|
const LE_SET_EXTENDED_SCAN_ENABLE_CMD =
|
|
@@ -164,7 +166,8 @@ Hci.prototype.init = function (options) {
|
|
|
164
166
|
this._bound = true;
|
|
165
167
|
}
|
|
166
168
|
this._socket.start();
|
|
167
|
-
|
|
169
|
+
|
|
170
|
+
this.reset();
|
|
168
171
|
this.pollIsDevUp();
|
|
169
172
|
}
|
|
170
173
|
};
|
|
@@ -181,17 +184,9 @@ Hci.prototype.pollIsDevUp = function () {
|
|
|
181
184
|
return;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
if (this._isExtended) {
|
|
185
|
-
this.setCodedPhySupport();
|
|
186
|
-
}
|
|
187
187
|
this.setSocketFilter();
|
|
188
|
-
this.
|
|
189
|
-
|
|
190
|
-
this.readLocalVersion();
|
|
191
|
-
this.writeLeHostSupported();
|
|
192
|
-
this.readLeHostSupported();
|
|
193
|
-
this.readLeBufferSize();
|
|
194
|
-
this.readBdAddr();
|
|
188
|
+
this.readLeSupportedFeatures();
|
|
189
|
+
// Subsequent calls moved to processCmdCompleteEvent for LE_READ_LOCAL_SUPPORTED_FEATURES
|
|
195
190
|
} else {
|
|
196
191
|
this.emit('stateChange', 'poweredOff');
|
|
197
192
|
}
|
|
@@ -369,6 +364,20 @@ Hci.prototype.setLeEventMask = function () {
|
|
|
369
364
|
this._socket.write(cmd);
|
|
370
365
|
};
|
|
371
366
|
|
|
367
|
+
Hci.prototype.readLeSupportedFeatures = function () {
|
|
368
|
+
const cmd = Buffer.alloc(4);
|
|
369
|
+
|
|
370
|
+
// header
|
|
371
|
+
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
|
|
372
|
+
cmd.writeUInt16LE(LE_READ_LOCAL_SUPPORTED_FEATURES, 1);
|
|
373
|
+
|
|
374
|
+
// length
|
|
375
|
+
cmd.writeUInt8(0x0, 3);
|
|
376
|
+
|
|
377
|
+
debug(`le read supported feature - writing: ${cmd.toString('hex')}`);
|
|
378
|
+
this._socket.write(cmd);
|
|
379
|
+
};
|
|
380
|
+
|
|
372
381
|
Hci.prototype.readLeBufferSize = function () {
|
|
373
382
|
const cmd = Buffer.alloc(4);
|
|
374
383
|
|
|
@@ -917,6 +926,24 @@ Hci.prototype.processCmdCompleteEvent = function (cmd, status, result) {
|
|
|
917
926
|
this.setLeEventMask();
|
|
918
927
|
this.readLocalVersion();
|
|
919
928
|
this.readBdAddr();
|
|
929
|
+
} else if (cmd === LE_READ_LOCAL_SUPPORTED_FEATURES) {
|
|
930
|
+
if (status === 0) {
|
|
931
|
+
// Set _isExtended based on leExtendedAdvertising bit (12)
|
|
932
|
+
this._isExtended = !!(result.readUInt32LE(0) & (1 << 12));
|
|
933
|
+
|
|
934
|
+
this.emit('leFeatures', result);
|
|
935
|
+
|
|
936
|
+
if (this._isExtended) {
|
|
937
|
+
this.setCodedPhySupport();
|
|
938
|
+
}
|
|
939
|
+
this.setEventMask();
|
|
940
|
+
this.setLeEventMask();
|
|
941
|
+
this.readLocalVersion();
|
|
942
|
+
this.writeLeHostSupported();
|
|
943
|
+
this.readLeHostSupported();
|
|
944
|
+
this.readLeBufferSize();
|
|
945
|
+
this.readBdAddr();
|
|
946
|
+
}
|
|
920
947
|
} else if (cmd === READ_LE_HOST_SUPPORTED_CMD) {
|
|
921
948
|
if (status === 0) {
|
|
922
949
|
const le = result.readUInt8(0);
|
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');
|
package/lib/win/binding.gyp
CHANGED
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.16.1",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "https://github.com/stoprocent/noble.git"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"mocha": "^10.7.0",
|
|
53
53
|
"nyc": "^17.0.0",
|
|
54
54
|
"prebuildify": "^6.0.1",
|
|
55
|
-
"prebuildify-cross": "
|
|
55
|
+
"prebuildify-cross": "^5.1.1",
|
|
56
56
|
"semantic-release": "^24.1.1",
|
|
57
57
|
"jshint": "^2.13.6",
|
|
58
58
|
"prettier": "^3.3.3",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -132,7 +132,7 @@ describe('hci-socket bindings', () => {
|
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
describe('connect', () => {
|
|
135
|
-
it('missing peripheral, no queue', () => {
|
|
135
|
+
it('missing peripheral, no queue, public address', () => {
|
|
136
136
|
bindings._hci.createLeConn = fake.resolves(null);
|
|
137
137
|
|
|
138
138
|
bindings.connect('112233445566', 'parameters');
|
|
@@ -140,7 +140,18 @@ describe('hci-socket bindings', () => {
|
|
|
140
140
|
should(bindings._pendingConnectionUuid).eql('112233445566');
|
|
141
141
|
|
|
142
142
|
assert.calledOnce(bindings._hci.createLeConn);
|
|
143
|
-
assert.calledWith(bindings._hci.createLeConn, '11:22:33:44:55:66', '
|
|
143
|
+
assert.calledWith(bindings._hci.createLeConn, '11:22:33:44:55:66', 'public', 'parameters');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('missing peripheral, no queue, random address', () => {
|
|
147
|
+
bindings._hci.createLeConn = fake.resolves(null);
|
|
148
|
+
|
|
149
|
+
bindings.connect('f32233445566', 'parameters');
|
|
150
|
+
|
|
151
|
+
should(bindings._pendingConnectionUuid).eql('f32233445566');
|
|
152
|
+
|
|
153
|
+
assert.calledOnce(bindings._hci.createLeConn);
|
|
154
|
+
assert.calledWith(bindings._hci.createLeConn, 'f3:22:33:44:55:66', 'random', 'parameters');
|
|
144
155
|
});
|
|
145
156
|
|
|
146
157
|
it('existing peripheral, no queue', () => {
|
|
@@ -109,6 +109,7 @@ describe('hci-socket hci', () => {
|
|
|
109
109
|
hci.readLeBufferSize = sinon.spy();
|
|
110
110
|
hci.readBdAddr = sinon.spy();
|
|
111
111
|
hci.init = sinon.spy();
|
|
112
|
+
hci.readLeSupportedFeatures = sinon.spy();
|
|
112
113
|
hci.setCodedPhySupport = sinon.spy();
|
|
113
114
|
|
|
114
115
|
hci.on('stateChange', callback);
|
|
@@ -170,13 +171,7 @@ describe('hci-socket hci', () => {
|
|
|
170
171
|
hci.pollIsDevUp();
|
|
171
172
|
|
|
172
173
|
assert.calledOnceWithExactly(hci.setSocketFilter);
|
|
173
|
-
assert.calledOnceWithExactly(hci.
|
|
174
|
-
assert.calledOnceWithExactly(hci.setLeEventMask);
|
|
175
|
-
assert.calledOnceWithExactly(hci.readLocalVersion);
|
|
176
|
-
assert.calledOnceWithExactly(hci.writeLeHostSupported);
|
|
177
|
-
assert.calledOnceWithExactly(hci.readLeHostSupported);
|
|
178
|
-
assert.calledOnceWithExactly(hci.readLeBufferSize);
|
|
179
|
-
assert.calledOnceWithExactly(hci.readBdAddr);
|
|
174
|
+
assert.calledOnceWithExactly(hci.readLeSupportedFeatures);
|
|
180
175
|
|
|
181
176
|
assert.notCalled(hci.setCodedPhySupport);
|
|
182
177
|
assert.notCalled(hci._socket.removeAllListeners);
|
|
@@ -193,14 +188,7 @@ describe('hci-socket hci', () => {
|
|
|
193
188
|
hci.pollIsDevUp();
|
|
194
189
|
|
|
195
190
|
assert.calledOnceWithExactly(hci.setSocketFilter);
|
|
196
|
-
assert.calledOnceWithExactly(hci.
|
|
197
|
-
assert.calledOnceWithExactly(hci.setLeEventMask);
|
|
198
|
-
assert.calledOnceWithExactly(hci.readLocalVersion);
|
|
199
|
-
assert.calledOnceWithExactly(hci.writeLeHostSupported);
|
|
200
|
-
assert.calledOnceWithExactly(hci.readLeHostSupported);
|
|
201
|
-
assert.calledOnceWithExactly(hci.readLeBufferSize);
|
|
202
|
-
assert.calledOnceWithExactly(hci.readBdAddr);
|
|
203
|
-
assert.calledOnceWithExactly(hci.setCodedPhySupport);
|
|
191
|
+
assert.calledOnceWithExactly(hci.readLeSupportedFeatures);
|
|
204
192
|
|
|
205
193
|
assert.notCalled(hci._socket.removeAllListeners);
|
|
206
194
|
assert.notCalled(hci.init);
|
|
@@ -698,6 +686,81 @@ describe('hci-socket hci', () => {
|
|
|
698
686
|
should(hci._aclConnections).have.keys(4660, 4661);
|
|
699
687
|
should(hci._aclConnections.get(4660)).deepEqual({ pending: 3 });
|
|
700
688
|
should(hci._aclConnections.get(4661)).deepEqual({ pending: 2 });
|
|
689
|
+
|
|
690
|
+
describe('LE_READ_LOCAL_SUPPORTED_FEATURES', () => {
|
|
691
|
+
beforeEach(() => {
|
|
692
|
+
hci.setCodedPhySupport = sinon.spy();
|
|
693
|
+
hci.setEventMask = sinon.spy();
|
|
694
|
+
hci.setLeEventMask = sinon.spy();
|
|
695
|
+
hci.readLocalVersion = sinon.spy();
|
|
696
|
+
hci.writeLeHostSupported = sinon.spy();
|
|
697
|
+
hci.readLeHostSupported = sinon.spy();
|
|
698
|
+
hci.readLeBufferSize = sinon.spy();
|
|
699
|
+
hci.readBdAddr = sinon.spy();
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('should not process on error status', () => {
|
|
703
|
+
const cmd = 8195;
|
|
704
|
+
const status = 1;
|
|
705
|
+
const result = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
|
706
|
+
|
|
707
|
+
hci.processCmdCompleteEvent(cmd, status, result);
|
|
708
|
+
|
|
709
|
+
// Verify no methods were called
|
|
710
|
+
assert.notCalled(hci.setCodedPhySupport);
|
|
711
|
+
assert.notCalled(hci.setEventMask);
|
|
712
|
+
assert.notCalled(hci.setLeEventMask);
|
|
713
|
+
assert.notCalled(hci.readLocalVersion);
|
|
714
|
+
assert.notCalled(hci.writeLeHostSupported);
|
|
715
|
+
assert.notCalled(hci.readLeHostSupported);
|
|
716
|
+
assert.notCalled(hci.readLeBufferSize);
|
|
717
|
+
assert.notCalled(hci.readBdAddr);
|
|
718
|
+
|
|
719
|
+
should(hci._isExtended).equal(false);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('should process without extended features', () => {
|
|
723
|
+
const cmd = 8195;
|
|
724
|
+
const status = 0;
|
|
725
|
+
const result = Buffer.from([0x00, 0x00, 0x00, 0x00]); // No bits set
|
|
726
|
+
|
|
727
|
+
hci.processCmdCompleteEvent(cmd, status, result);
|
|
728
|
+
|
|
729
|
+
// Verify extended-specific method not called
|
|
730
|
+
assert.notCalled(hci.setCodedPhySupport);
|
|
731
|
+
|
|
732
|
+
// Verify other methods were called
|
|
733
|
+
assert.calledOnce(hci.setEventMask);
|
|
734
|
+
assert.calledOnce(hci.setLeEventMask);
|
|
735
|
+
assert.calledOnce(hci.readLocalVersion);
|
|
736
|
+
assert.calledOnce(hci.writeLeHostSupported);
|
|
737
|
+
assert.calledOnce(hci.readLeHostSupported);
|
|
738
|
+
assert.calledOnce(hci.readLeBufferSize);
|
|
739
|
+
assert.calledOnce(hci.readBdAddr);
|
|
740
|
+
|
|
741
|
+
should(hci._isExtended).equal(false);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('should process with extended features', () => {
|
|
745
|
+
const cmd = 8195;
|
|
746
|
+
const status = 0;
|
|
747
|
+
const result = Buffer.from("bd5f660000000000", "hex");
|
|
748
|
+
|
|
749
|
+
hci.processCmdCompleteEvent(cmd, status, result);
|
|
750
|
+
|
|
751
|
+
// Verify all methods were called including extended-specific
|
|
752
|
+
assert.calledOnce(hci.setCodedPhySupport);
|
|
753
|
+
assert.calledOnce(hci.setEventMask);
|
|
754
|
+
assert.calledOnce(hci.setLeEventMask);
|
|
755
|
+
assert.calledOnce(hci.readLocalVersion);
|
|
756
|
+
assert.calledOnce(hci.writeLeHostSupported);
|
|
757
|
+
assert.calledOnce(hci.readLeHostSupported);
|
|
758
|
+
assert.calledOnce(hci.readLeBufferSize);
|
|
759
|
+
assert.calledOnce(hci.readBdAddr);
|
|
760
|
+
|
|
761
|
+
should(hci._isExtended).equal(true);
|
|
762
|
+
});
|
|
763
|
+
});
|
|
701
764
|
});
|
|
702
765
|
|
|
703
766
|
it('should only processCmdStatusEvent - HCI_EVENT_PKT / EVT_CMD_STATUS', () => {
|