@stoprocent/bleno 0.7.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.
Files changed (77) hide show
  1. package/.jshintrc +5 -0
  2. package/LICENSE +20 -0
  3. package/README.md +409 -0
  4. package/binding.gyp +17 -0
  5. package/examples/battery-service/README.md +14 -0
  6. package/examples/battery-service/battery-level-characteristic.js +45 -0
  7. package/examples/battery-service/battery-service.js +16 -0
  8. package/examples/battery-service/main.js +28 -0
  9. package/examples/battery-service/package-lock.json +1314 -0
  10. package/examples/battery-service/package.json +20 -0
  11. package/examples/blink1/README.md +44 -0
  12. package/examples/blink1/blink1-fade-rgb-characteristic.js +42 -0
  13. package/examples/blink1/blink1-rgb-characteristic.js +38 -0
  14. package/examples/blink1/blink1-service.js +19 -0
  15. package/examples/blink1/device-information-service.js +19 -0
  16. package/examples/blink1/hardware-revision-characteristic.js +32 -0
  17. package/examples/blink1/main.js +32 -0
  18. package/examples/blink1/serial-number-characteristic.js +21 -0
  19. package/examples/echo/characteristic.js +45 -0
  20. package/examples/echo/main.js +33 -0
  21. package/examples/pizza/README.md +16 -0
  22. package/examples/pizza/peripheral.js +57 -0
  23. package/examples/pizza/pizza-bake-characteristic.js +40 -0
  24. package/examples/pizza/pizza-crust-characteristic.js +52 -0
  25. package/examples/pizza/pizza-service.js +20 -0
  26. package/examples/pizza/pizza-toppings-characteristic.js +41 -0
  27. package/examples/pizza/pizza.js +58 -0
  28. package/examples/uart/main.js +23 -0
  29. package/index.d.ts +153 -0
  30. package/index.js +1 -0
  31. package/lib/bleno.js +231 -0
  32. package/lib/characteristic.js +91 -0
  33. package/lib/descriptor.js +17 -0
  34. package/lib/hci-socket/acl-stream.js +37 -0
  35. package/lib/hci-socket/bindings.js +219 -0
  36. package/lib/hci-socket/crypto.js +74 -0
  37. package/lib/hci-socket/gap.js +212 -0
  38. package/lib/hci-socket/gatt.js +1028 -0
  39. package/lib/hci-socket/hci-status.json +67 -0
  40. package/lib/hci-socket/hci.js +796 -0
  41. package/lib/hci-socket/mgmt.js +89 -0
  42. package/lib/hci-socket/smp.js +160 -0
  43. package/lib/hci-socket/vs.js +156 -0
  44. package/lib/mac/binding.gyp +39 -0
  45. package/lib/mac/bindings.js +12 -0
  46. package/lib/mac/src/ble_peripheral_manager.h +32 -0
  47. package/lib/mac/src/ble_peripheral_manager.mm +241 -0
  48. package/lib/mac/src/bleno_mac.h +26 -0
  49. package/lib/mac/src/bleno_mac.mm +167 -0
  50. package/lib/mac/src/callbacks.h +76 -0
  51. package/lib/mac/src/callbacks.mm +124 -0
  52. package/lib/mac/src/napi_objc.h +30 -0
  53. package/lib/mac/src/napi_objc.mm +286 -0
  54. package/lib/mac/src/noble_mac.h +34 -0
  55. package/lib/mac/src/noble_mac.mm +260 -0
  56. package/lib/mac/src/objc_cpp.h +27 -0
  57. package/lib/mac/src/objc_cpp.mm +144 -0
  58. package/lib/mac/src/peripheral.h +23 -0
  59. package/lib/primary-service.js +19 -0
  60. package/lib/resolve-bindings.js +19 -0
  61. package/lib/uuid-util.js +7 -0
  62. package/package.json +77 -0
  63. package/prebuilds/android-arm/node.napi.armv7.node +0 -0
  64. package/prebuilds/android-arm64/node.napi.armv8.node +0 -0
  65. package/prebuilds/darwin-x64+arm64/node.napi.node +0 -0
  66. package/prebuilds/linux-arm/node.napi.armv6.node +0 -0
  67. package/prebuilds/linux-arm/node.napi.armv7.node +0 -0
  68. package/prebuilds/linux-arm64/node.napi.armv8.node +0 -0
  69. package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
  70. package/prebuilds/linux-x64/node.napi.musl.node +0 -0
  71. package/prebuilds/win32-ia32/node.napi.node +0 -0
  72. package/prebuilds/win32-x64/node.napi.node +0 -0
  73. package/test/characteristic.test.js +174 -0
  74. package/test/descriptor.test.js +46 -0
  75. package/test/mocha.setup.js +0 -0
  76. package/with-bindings.js +5 -0
  77. package/with-custom-binding.js +6 -0
@@ -0,0 +1,219 @@
1
+ const debug = require('debug')('bindings');
2
+
3
+ const { EventEmitter } = require('events');
4
+ const os = require('os');
5
+
6
+ const AclStream = require('./acl-stream');
7
+ const Hci = require('./hci');
8
+ const Gap = require('./gap');
9
+ const Gatt = require('./gatt');
10
+
11
+ class BlenoBindings extends EventEmitter {
12
+ constructor (options) {
13
+ super();
14
+
15
+ this._state = null;
16
+
17
+ this._advertising = false;
18
+
19
+ this._hci = new Hci(options);
20
+ this._gap = new Gap(this._hci);
21
+ this._gatt = new Gatt(this._hci);
22
+
23
+ this._address = null;
24
+ this._handle = null;
25
+ this._aclStream = null;
26
+ }
27
+
28
+ setAddress (address) {
29
+ this._hci.setAddress(address);
30
+ }
31
+
32
+ startAdvertising (name, serviceUuids) {
33
+ this._advertising = true;
34
+
35
+ this._gap.startAdvertising(name, serviceUuids);
36
+ }
37
+
38
+ startAdvertisingIBeacon (data) {
39
+ this._advertising = true;
40
+
41
+ this._gap.startAdvertisingIBeacon(data);
42
+ }
43
+
44
+ startAdvertisingWithEIRData (advertisementData, scanData) {
45
+ this._advertising = true;
46
+
47
+ this._gap.startAdvertisingWithEIRData(advertisementData, scanData);
48
+ }
49
+
50
+ stopAdvertising () {
51
+ this._advertising = false;
52
+
53
+ this._gap.stopAdvertising();
54
+ }
55
+
56
+ setServices (services) {
57
+ this._gatt.setServices(services);
58
+
59
+ this.emit('servicesSet');
60
+ }
61
+
62
+ disconnect () {
63
+ if (this._handle) {
64
+ debug('disconnect by server');
65
+
66
+ this._hci.disconnect(this._handle);
67
+ }
68
+ }
69
+
70
+ updateRssi () {
71
+ if (this._handle) {
72
+ this._hci.readRssi(this._handle);
73
+ }
74
+ }
75
+
76
+ init () {
77
+ this.onSigIntBinded = this.onSigInt.bind(this);
78
+
79
+ process.on('SIGINT', this.onSigIntBinded);
80
+ process.on('exit', this.onExit.bind(this));
81
+
82
+ this._gap.on('advertisingStart', this.onAdvertisingStart.bind(this));
83
+ this._gap.on('advertisingStop', this.onAdvertisingStop.bind(this));
84
+
85
+ this._gatt.on('mtuChange', this.onMtuChange.bind(this));
86
+
87
+ this._hci.on('stateChange', this.onStateChange.bind(this));
88
+ this._hci.on('addressChange', this.onAddressChange.bind(this));
89
+ this._hci.on('readLocalVersion', this.onReadLocalVersion.bind(this));
90
+
91
+ this._hci.on('leConnComplete', this.onLeConnComplete.bind(this));
92
+ this._hci.on('leConnUpdateComplete', this.onLeConnUpdateComplete.bind(this));
93
+ this._hci.on('rssiRead', this.onRssiRead.bind(this));
94
+ this._hci.on('disconnComplete', this.onDisconnComplete.bind(this));
95
+ this._hci.on('encryptChange', this.onEncryptChange.bind(this));
96
+ this._hci.on('leLtkNegReply', this.onLeLtkNegReply.bind(this));
97
+ this._hci.on('aclDataPkt', this.onAclDataPkt.bind(this));
98
+
99
+ this.emit('platform', os.platform());
100
+
101
+ this._hci.init();
102
+ }
103
+
104
+ onStateChange (state) {
105
+ if (this._state === state) {
106
+ return;
107
+ }
108
+ this._state = state;
109
+
110
+ if (state === 'unauthorized') {
111
+ console.log('Bleno warning: adapter state unauthorized, please run as root or with sudo');
112
+ console.log(' or see README for information on running without root/sudo:');
113
+ console.log(' https://github.com/abandonware/bleno#running-on-linux');
114
+ } else if (state === 'unsupported') {
115
+ console.log('Bleno warning: adapter does not support Bluetooth Low Energy (BLE, Bluetooth Smart).');
116
+ console.log(' Try to run with environment variable:');
117
+ console.log(' [sudo] BLENO_HCI_DEVICE_ID=x node ...');
118
+ }
119
+
120
+ this.emit('stateChange', state);
121
+ }
122
+
123
+ onAddressChange (address) {
124
+ this.emit('addressChange', address);
125
+ }
126
+
127
+ onReadLocalVersion (hciVer, hciRev, lmpVer, manufacturer, lmpSubVer) {
128
+ }
129
+
130
+ onAdvertisingStart (error) {
131
+ this.emit('advertisingStart', error);
132
+ }
133
+
134
+ onAdvertisingStop () {
135
+ this.emit('advertisingStop');
136
+ }
137
+
138
+ onLeConnComplete (status, handle, role, addressType, address, interval, latency, supervisionTimeout, masterClockAccuracy) {
139
+ if (role !== 1) {
140
+ // not slave, ignore
141
+ return;
142
+ }
143
+
144
+ this._address = address;
145
+ this._handle = handle;
146
+ this._aclStream = new AclStream(this._hci, handle, this._hci.addressType, this._hci.address, addressType, address);
147
+ this._gatt.setAclStream(this._aclStream);
148
+
149
+ this.emit('accept', address);
150
+ }
151
+
152
+ onLeConnUpdateComplete (handle, interval, latency, supervisionTimeout) {
153
+ // no-op
154
+ }
155
+
156
+ onDisconnComplete (handle, reason) {
157
+ if (this._aclStream) {
158
+ this._aclStream.push(null, null);
159
+ }
160
+
161
+ const address = this._address;
162
+
163
+ this._address = null;
164
+ this._handle = null;
165
+ this._aclStream = null;
166
+
167
+ if (address) {
168
+ this.emit('disconnect', address); // TODO: use reason
169
+ }
170
+
171
+ if (this._advertising) {
172
+ this._gap.restartAdvertising();
173
+ }
174
+ }
175
+
176
+ onEncryptChange (handle, encrypt) {
177
+ if (this._handle === handle && this._aclStream) {
178
+ this._aclStream.pushEncrypt(encrypt);
179
+ }
180
+ }
181
+
182
+ onLeLtkNegReply (handle) {
183
+ if (this._handle === handle && this._aclStream) {
184
+ this._aclStream.pushLtkNegReply();
185
+ }
186
+ }
187
+
188
+ onMtuChange (mtu) {
189
+ this.emit('mtuChange', mtu);
190
+ }
191
+
192
+ onRssiRead (handle, rssi) {
193
+ this.emit('rssiUpdate', rssi);
194
+ }
195
+
196
+ onAclDataPkt (handle, cid, data) {
197
+ if (this._handle === handle && this._aclStream) {
198
+ this._aclStream.push(cid, data);
199
+ }
200
+ }
201
+
202
+ onSigInt () {
203
+ const sigIntListeners = process.listeners('SIGINT');
204
+
205
+ if (sigIntListeners[sigIntListeners.length - 1] === this.onSigIntBinded) {
206
+ // we are the last listener, so exit
207
+ // this will trigger onExit, and clean up
208
+ process.exit(1);
209
+ }
210
+ }
211
+
212
+ onExit () {
213
+ this._gap.stopAdvertising();
214
+
215
+ this.disconnect();
216
+ }
217
+ }
218
+
219
+ module.exports = BlenoBindings;
@@ -0,0 +1,74 @@
1
+ const crypto = require('crypto');
2
+
3
+ function r () {
4
+ return crypto.randomBytes(16);
5
+ }
6
+
7
+ function c1 (k, r, pres, preq, iat, ia, rat, ra) {
8
+ const p1 = Buffer.concat([
9
+ iat,
10
+ rat,
11
+ preq,
12
+ pres
13
+ ]);
14
+
15
+ const p2 = Buffer.concat([
16
+ ra,
17
+ ia,
18
+ Buffer.from('00000000', 'hex')
19
+ ]);
20
+
21
+ let res = xor(r, p1);
22
+ res = e(k, res);
23
+ res = xor(res, p2);
24
+ res = e(k, res);
25
+
26
+ return res;
27
+ }
28
+
29
+ function s1 (k, r1, r2) {
30
+ return e(k, Buffer.concat([
31
+ r2.slice(0, 8),
32
+ r1.slice(0, 8)
33
+ ]));
34
+ }
35
+
36
+ function e (key, data) {
37
+ key = swap(key);
38
+ data = swap(data);
39
+
40
+ const cipher = crypto.createCipheriv('aes-128-ecb', key, '');
41
+ cipher.setAutoPadding(false);
42
+
43
+ return swap(Buffer.concat([
44
+ cipher.update(data),
45
+ cipher.final()
46
+ ]));
47
+ }
48
+
49
+ function xor (b1, b2) {
50
+ const result = Buffer.alloc(b1.length);
51
+
52
+ for (let i = 0; i < b1.length; i++) {
53
+ result[i] = b1[i] ^ b2[i];
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ function swap (input) {
60
+ const output = Buffer.alloc(input.length);
61
+
62
+ for (let i = 0; i < output.length; i++) {
63
+ output[i] = input[input.length - i - 1];
64
+ }
65
+
66
+ return output;
67
+ }
68
+
69
+ module.exports = {
70
+ r,
71
+ c1,
72
+ s1,
73
+ e
74
+ };
@@ -0,0 +1,212 @@
1
+ const debug = require('debug')('gap');
2
+
3
+ const { EventEmitter } = require('events');
4
+ const os = require('os');
5
+
6
+ const Hci = require('./hci');
7
+
8
+ const isLinux = (os.platform() === 'linux');
9
+ const isIntelEdison = isLinux && (os.release().includes('edison'));
10
+ const isYocto = isLinux && (os.release().includes('yocto'));
11
+
12
+ class Gap extends EventEmitter {
13
+ constructor (hci) {
14
+ super();
15
+
16
+ this._hci = hci;
17
+
18
+ this._advertiseState = null;
19
+
20
+ this._hci.on('error', this.onHciError.bind(this));
21
+
22
+ this._hci.on('leAdvertisingParametersSet', this.onHciLeAdvertisingParametersSet.bind(this));
23
+ this._hci.on('leAdvertisingDataSet', this.onHciLeAdvertisingDataSet.bind(this));
24
+ this._hci.on('leScanResponseDataSet', this.onHciLeScanResponseDataSet.bind(this));
25
+ this._hci.on('leAdvertiseEnableSet', this.onHciLeAdvertiseEnableSet.bind(this));
26
+ }
27
+
28
+ startAdvertising (name, serviceUuids) {
29
+ debug('startAdvertising: name = ' + name + ', serviceUuids = ' + JSON.stringify(serviceUuids, null, 2));
30
+
31
+ let advertisementDataLength = 3;
32
+ let scanDataLength = 0;
33
+
34
+ const serviceUuids16bit = [];
35
+ const serviceUuids128bit = [];
36
+
37
+ if (name && name.length) {
38
+ scanDataLength += 2 + name.length;
39
+ }
40
+
41
+ if (serviceUuids && serviceUuids.length) {
42
+ for (let i = 0; i < serviceUuids.length; i++) {
43
+ const serviceUuid = Buffer.from(serviceUuids[i].match(/.{1,2}/g).reverse().join(''), 'hex');
44
+
45
+ if (serviceUuid.length === 2) {
46
+ serviceUuids16bit.push(serviceUuid);
47
+ } else if (serviceUuid.length === 16) {
48
+ serviceUuids128bit.push(serviceUuid);
49
+ }
50
+ }
51
+ }
52
+
53
+ if (serviceUuids16bit.length) {
54
+ advertisementDataLength += 2 + 2 * serviceUuids16bit.length;
55
+ }
56
+
57
+ if (serviceUuids128bit.length) {
58
+ advertisementDataLength += 2 + 16 * serviceUuids128bit.length;
59
+ }
60
+
61
+ const advertisementData = Buffer.alloc(advertisementDataLength);
62
+ const scanData = Buffer.alloc(scanDataLength);
63
+
64
+ // flags
65
+ advertisementData.writeUInt8(2, 0);
66
+ advertisementData.writeUInt8(0x01, 1);
67
+ advertisementData.writeUInt8(0x06, 2);
68
+
69
+ let advertisementDataOffset = 3;
70
+
71
+ if (serviceUuids16bit.length) {
72
+ advertisementData.writeUInt8(1 + 2 * serviceUuids16bit.length, advertisementDataOffset);
73
+ advertisementDataOffset++;
74
+
75
+ advertisementData.writeUInt8(0x03, advertisementDataOffset);
76
+ advertisementDataOffset++;
77
+
78
+ for (let i = 0; i < serviceUuids16bit.length; i++) {
79
+ serviceUuids16bit[i].copy(advertisementData, advertisementDataOffset);
80
+ advertisementDataOffset += serviceUuids16bit[i].length;
81
+ }
82
+ }
83
+
84
+ if (serviceUuids128bit.length) {
85
+ advertisementData.writeUInt8(1 + 16 * serviceUuids128bit.length, advertisementDataOffset);
86
+ advertisementDataOffset++;
87
+
88
+ advertisementData.writeUInt8(0x06, advertisementDataOffset);
89
+ advertisementDataOffset++;
90
+
91
+ for (let i = 0; i < serviceUuids128bit.length; i++) {
92
+ serviceUuids128bit[i].copy(advertisementData, advertisementDataOffset);
93
+ advertisementDataOffset += serviceUuids128bit[i].length;
94
+ }
95
+ }
96
+
97
+ // name
98
+ if (name && name.length) {
99
+ const nameBuffer = Buffer.from(name);
100
+
101
+ scanData.writeUInt8(1 + nameBuffer.length, 0);
102
+ scanData.writeUInt8(0x08, 1);
103
+ nameBuffer.copy(scanData, 2);
104
+ }
105
+
106
+ this.startAdvertisingWithEIRData(advertisementData, scanData);
107
+ }
108
+
109
+ startAdvertisingIBeacon (data) {
110
+ debug('startAdvertisingIBeacon: data = ' + data.toString('hex'));
111
+
112
+ const dataLength = data.length;
113
+ const manufacturerDataLength = 4 + dataLength;
114
+ const advertisementDataLength = 5 + manufacturerDataLength;
115
+ // const scanDataLength = 0;
116
+
117
+ const advertisementData = Buffer.alloc(advertisementDataLength);
118
+ const scanData = Buffer.alloc(0);
119
+
120
+ // flags
121
+ advertisementData.writeUInt8(2, 0);
122
+ advertisementData.writeUInt8(0x01, 1);
123
+ advertisementData.writeUInt8(0x06, 2);
124
+
125
+ advertisementData.writeUInt8(manufacturerDataLength + 1, 3);
126
+ advertisementData.writeUInt8(0xff, 4);
127
+ advertisementData.writeUInt16LE(0x004c, 5); // Apple Company Identifier LE (16 bit)
128
+ advertisementData.writeUInt8(0x02, 7); // type, 2 => iBeacon
129
+ advertisementData.writeUInt8(dataLength, 8);
130
+
131
+ data.copy(advertisementData, 9);
132
+
133
+ this.startAdvertisingWithEIRData(advertisementData, scanData);
134
+ }
135
+
136
+ startAdvertisingWithEIRData (advertisementData, scanData) {
137
+ advertisementData = advertisementData || Buffer.alloc(0);
138
+ scanData = scanData || Buffer.alloc(0);
139
+
140
+ debug('startAdvertisingWithEIRData: advertisement data = ' + advertisementData.toString('hex') + ', scan data = ' + scanData.toString('hex'));
141
+
142
+ let error = null;
143
+
144
+ if (advertisementData.length > 31) {
145
+ error = new Error('Advertisement data is over maximum limit of 31 bytes');
146
+ } else if (scanData.length > 31) {
147
+ error = new Error('Scan data is over maximum limit of 31 bytes');
148
+ }
149
+
150
+ if (error) {
151
+ this.emit('advertisingStart', error);
152
+ } else {
153
+ this._advertiseState = 'starting';
154
+
155
+ if (isIntelEdison || isYocto) {
156
+ // work around for Intel Edison
157
+ debug('skipping first set of scan response and advertisement data');
158
+ } else {
159
+ this._hci.setScanResponseData(scanData);
160
+ this._hci.setAdvertisingData(advertisementData);
161
+ }
162
+ this._hci.setAdvertiseEnable(true);
163
+ this._hci.setScanResponseData(scanData);
164
+ this._hci.setAdvertisingData(advertisementData);
165
+ }
166
+ }
167
+
168
+ restartAdvertising () {
169
+ this._advertiseState = 'restarting';
170
+
171
+ this._hci.setAdvertiseEnable(true);
172
+ }
173
+
174
+ stopAdvertising () {
175
+ this._advertiseState = 'stopping';
176
+
177
+ this._hci.setAdvertiseEnable(false);
178
+ }
179
+
180
+ onHciError (error) {
181
+ this.emit('error', error);
182
+ }
183
+
184
+ onHciLeAdvertisingParametersSet (status) {
185
+ }
186
+
187
+ onHciLeAdvertisingDataSet (status) {
188
+ }
189
+
190
+ onHciLeScanResponseDataSet (status) {
191
+ }
192
+
193
+ onHciLeAdvertiseEnableSet (status) {
194
+ if (this._advertiseState === 'starting') {
195
+ this._advertiseState = 'started';
196
+
197
+ let error = null;
198
+
199
+ if (status) {
200
+ error = new Error(Hci.STATUS_MAPPER[status] || ('Unknown (' + status + ')'));
201
+ }
202
+
203
+ this.emit('advertisingStart', error);
204
+ } else if (this._advertiseState === 'stopping') {
205
+ this._advertiseState = 'stopped';
206
+
207
+ this.emit('advertisingStop');
208
+ }
209
+ }
210
+ }
211
+
212
+ module.exports = Gap;