├── LICENSE.txt ├── README.md ├── docs └── api │ ├── advertising-data-builder.md │ ├── att-errors.md │ ├── ble-manager.md │ ├── gatt-client.md │ ├── gatt-server.md │ ├── l2cap-coc.md │ └── security-manager.md ├── lib ├── advertising-data-builder.js ├── association-models.js ├── att-errors.js ├── ble-manager.js ├── hci-errors.js ├── index.js ├── internal │ ├── adapter.js │ ├── gatt.js │ ├── smp.js │ ├── storage.js │ └── utils.js ├── io-capabilities.js ├── l2cap-coc-errors.js └── smp-errors.js └── package.json /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017-2021 Emil Lenngren 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-ble-host 2 | 3 | A full-featured Bluetooth Low Energy host stack written in JavaScript. 4 | 5 | Use this library to programmatically setup or connect to BLE devices in Node.js on Raspberry Pi or other Linux devices. 6 | 7 | ## Feature support 8 | 9 | * Advertising 10 | * Scanning 11 | * The API allows for "multiplexing" multiple logical scanners with different filters and parameters onto one physical scanning 12 | * Initiating a connection 13 | * Possibility to cancel a pending connection 14 | * Peripheral 15 | * Central 16 | * Multiple concurrent GAP roles (scanner, initiator, advertiser, connection etc.) as long as the controller supports it 17 | * Multiple concurrent connections 18 | * GATT server role: 19 | * GATT database 20 | * Services, Characteristics, Descriptors, Included Services 21 | * Modify GATT db _after_ initialization is done 22 | * MTU Exchange with large MTU 23 | * Handle requests for Read, Write, Write Without Response, Reliable Write 24 | * Handle Long Reads, Long Writes 25 | * Notifications, Indications 26 | * Either program custom read/write handlers, or let the library handle these automatically 27 | * Configurable permissions (a specific security level or custom handler) 28 | * Indications are automatically enqueued and executed after each other, since GATT only allows one outstanding indication 29 | * GATT client role: 30 | * MTU Exchange with large MTU 31 | * Service Discovery 32 | * Service Discovery by Service UUID 33 | * Cache services on subsequent connections, when allowed by the spec 34 | * Re-discover services 35 | * Discover Characteristics, Descriptors, Included Services 36 | * Read, Write, Write Without Response, Reliable Write 37 | * Long Read / Long Write (automatically when needed by default or explicitly) 38 | * Notifications, Indications, Confirmations 39 | * Requests are automatically enqueued and executed after each other, since GATT only allows one outstanding request 40 | * L2CAP Connection-oriented channels (L2CAP CoC) 41 | * Server and client support 42 | * Flow control 43 | * Pairing / Bonding (Security Manager) 44 | * LE Security Mode 1 45 | * Encryption of the link 46 | * Persistent storage of LTK, IRK, Identity Address, GATT cache, cccd state etc. (JSON file-based for now) 47 | * Supports LE Legacy Pairing as well as LE Secure Connections Pairing (configurable whether SC should be supported) 48 | * Keypress notifications 49 | * Configurable Pairing process 50 | * Configurable I/O capabilities 51 | * Configurable key distribution 52 | 53 | ## Setup 54 | 55 | Installation: 56 | 57 | ``` 58 | npm install ble-host 59 | ``` 60 | 61 | If you have a Bluetooth LE compatible controller connected to Linux (like a USB dongle or built-in adapter), install the `hci-socket` package from https://github.com/Emill/node-hci-socket: 62 | 63 | ``` 64 | npm install hci-socket 65 | ``` 66 | 67 | In order to access the Bluetooth HCI layer from Node.js, either you will need to run `node` using `sudo` every time or execute ``sudo setcap cap_net_admin=ep $(eval readlink -f `which node`)`` first to give the `node` binary access to use Bluetooth HCI. 68 | 69 | Also it might be a good idea to turn off the bluetooth service `bluetoothd` so it doesn't interfere. 70 | 71 | ## API Documentation 72 | 73 | * [BleManager](docs/api/ble-manager.md) 74 | * [GattClient](docs/api/gatt-client.md) 75 | * [GattServer](docs/api/gatt-server.md) 76 | * [Attribute Errors](docs/api/att-errors.md) 77 | * [L2CAP CoC](docs/api/l2cap-coc.md) 78 | * [Security Manager](docs/api/security-manager.md) 79 | * [Advertising Data Builder](docs/api/advertising-data-builder.md) 80 | 81 | ## Design 82 | 83 | The library is asynchronous and is built mostly around callbacks. Events that can happen multiple times and events that do not happen as a result of an operation, use Node.js's Events mechanism. 84 | 85 | Most callbacks are passed an error code as the first argument, and the result value(s), if no error occurred, as the following argument(s). 86 | 87 | If an operation is to be executed within the context of a connection, for example a GATT Request, but the connection terminates before the response arrives, the corresponding callback will not be called. If logic is needed to handle this, that logic must be put in the `disconnect` event handler instead. Usually this results in cleaner code compared to having to handle the case where the connection dropped in every callback. 88 | 89 | Compared to many other libraries, this implementation has a GATT Client object per connection, not per device. This means every GATT operation executes within the context of a connection. After a re-connection, attempting to execute a GATT operation on the `gatt` object of a previous connection will result in that nothing is sent. This means there will be no accidental writes to "wrong" connections, which is important if there is a state the remote device must be in when an operation is executed. Compare this with TCP connections, where you send data in the context of a socket, not in the context of a remote host. 90 | 91 | All GATT values can be either strings or Buffers when writing, but will always be Buffers when read. A string value will be automatically converted to a Buffer using UTF-8 encoding. 92 | 93 | ## Full GATT Server Example (Peripheral) 94 | 95 | This example shows how to set up a peripheral with a GATT server with characteristics supporting read, write and notify. 96 | 97 | For a characteristic, the value can simply be stored within the characteristic as the `value` property. The library will then internally read or write to this value, when requested by the GATT client. Another way is to attach `onRead` and `onWrite` handlers, to handle the value in a custom way. 98 | 99 | The example also shows how to store a characteristic object in a variable (`notificationCharacteristic`) so that we can act upon it later. 100 | 101 | An `AdvertisingDataBuilder` is then used to construct the advertising data buffer for us, which we use to start advertising. 102 | 103 | After a connection is established as the result of advertising, advertising is automatically stopped by the controller. In order to accept new connections, we restart advertising when a connection disconnects. 104 | 105 | ```javascript 106 | const HciSocket = require('hci-socket'); 107 | const NodeBleHost = require('ble-host'); 108 | const BleManager = NodeBleHost.BleManager; 109 | const AdvertisingDataBuilder = NodeBleHost.AdvertisingDataBuilder; 110 | const HciErrors = NodeBleHost.HciErrors; 111 | const AttErrors = NodeBleHost.AttErrors; 112 | 113 | const deviceName = 'MyDevice'; 114 | 115 | var transport = new HciSocket(); // connects to the first hci device on the computer, for example hci0 116 | 117 | var options = { 118 | // optional properties go here 119 | }; 120 | 121 | BleManager.create(transport, options, function(err, manager) { 122 | // err is either null or an Error object 123 | // if err is null, manager contains a fully initialized BleManager object 124 | if (err) { 125 | console.error(err); 126 | return; 127 | } 128 | 129 | var notificationCharacteristic; 130 | 131 | manager.gattDb.setDeviceName(deviceName); 132 | manager.gattDb.addServices([ 133 | { 134 | uuid: '22222222-3333-4444-5555-666666666666', 135 | characteristics: [ 136 | { 137 | uuid: '22222222-3333-4444-5555-666666666667', 138 | properties: ['read', 'write'], 139 | value: 'some default value' // could be a Buffer for a binary value 140 | }, 141 | { 142 | uuid: '22222222-3333-4444-5555-666666666668', 143 | properties: ['read'], 144 | onRead: function(connection, callback) { 145 | callback(AttErrors.SUCCESS, new Date().toString()); 146 | } 147 | }, 148 | { 149 | uuid: '22222222-3333-4444-5555-666666666669', 150 | properties: ['write'], 151 | onWrite: function(connection, needsResponse, value, callback) { 152 | console.log('A new value was written:', value); 153 | callback(AttErrors.SUCCESS); // actually only needs to be called when needsResponse is true 154 | } 155 | }, 156 | notificationCharacteristic = { 157 | uuid: '22222222-3333-4444-5555-66666666666A', 158 | properties: ['notify'], 159 | onSubscriptionChange: function(connection, notification, indication, isWrite) { 160 | if (notification) { 161 | // Notifications are now enabled, so let's send something 162 | notificationCharacteristic.notify(connection, 'Sample notification'); 163 | } 164 | } 165 | } 166 | ] 167 | } 168 | ]); 169 | 170 | const advDataBuffer = new AdvertisingDataBuilder() 171 | .addFlags(['leGeneralDiscoverableMode', 'brEdrNotSupported']) 172 | .addLocalName(/*isComplete*/ true, deviceName) 173 | .add128BitServiceUUIDs(/*isComplete*/ true, ['22222222-3333-4444-5555-666666666666']) 174 | .build(); 175 | manager.setAdvertisingData(advDataBuffer); 176 | // call manager.setScanResponseData(...) if scan response data is desired too 177 | startAdv(); 178 | 179 | function startAdv() { 180 | manager.startAdvertising({/*options*/}, connectCallback); 181 | } 182 | 183 | function connectCallback(status, conn) { 184 | if (status != HciErrors.SUCCESS) { 185 | // Advertising could not be started for some controller-specific reason, try again after 10 seconds 186 | setTimeout(startAdv, 10000); 187 | return; 188 | } 189 | conn.on('disconnect', startAdv); // restart advertising after disconnect 190 | console.log('Connection established!', conn); 191 | } 192 | }); 193 | ``` 194 | 195 | ## Full GATT Client Example (Central) 196 | 197 | This example shows how to scan for devices advertising a particular service uuid, connect to that device, and how to use GATT read, write operations and how to listen to notifications. 198 | 199 | ```javascript 200 | const HciSocket = require('hci-socket'); 201 | const NodeBleHost = require('ble-host'); 202 | const BleManager = NodeBleHost.BleManager; 203 | const AdvertisingDataBuilder = NodeBleHost.AdvertisingDataBuilder; 204 | const HciErrors = NodeBleHost.HciErrors; 205 | const AttErrors = NodeBleHost.AttErrors; 206 | 207 | var transport = new HciSocket(); // connects to the first hci device on the computer, for example hci0 208 | 209 | var options = { 210 | // optional properties go here 211 | }; 212 | 213 | BleManager.create(transport, options, function(err, manager) { 214 | // err is either null or an Error object 215 | // if err is null, manager contains a fully initialized BleManager object 216 | if (err) { 217 | console.error(err); 218 | return; 219 | } 220 | 221 | var scanner = manager.startScan({scanFilters: [new BleManager.ServiceUUIDScanFilter('22222222-3333-4444-5555-666666666666')]}); 222 | scanner.on('report', function(eventData) { 223 | if (eventData.connectable) { 224 | console.log('Found device named ' + (eventData.parsedDataItems['localName'] || '(no name)') + ':', eventData); 225 | scanner.stopScan(); 226 | manager.connect(eventData.addressType, eventData.address, {/*options*/}, function(conn) { 227 | console.log('Connected to ' + conn.peerAddress); 228 | conn.gatt.exchangeMtu(function(err) { console.log('MTU: ' + conn.gatt.currentMtu); }); 229 | conn.gatt.discoverServicesByUuid('22222222-3333-4444-5555-666666666666', 1, function(services) { 230 | if (services.length == 0) { 231 | return; 232 | } 233 | var service = services[0]; 234 | service.discoverCharacteristics(function(characteristics) { 235 | for (var i = 0; i < characteristics.length; i++) { 236 | var c = characteristics[i]; 237 | console.log('Found ' + c.uuid); 238 | if (c.properties['read']) { 239 | c.read(function(err, value) { 240 | console.log('Read ' + value + ' from ' + c.uuid); 241 | }); 242 | } else if (c.properties['write']) { 243 | c.write(Buffer.from([65, 66, 67])); // Can add callback if we want the result status 244 | } 245 | if (c.properties['notify']) { 246 | // Write to the Client Characteristic Configuration Descriptor to enable notifications 247 | // Can add callback as the last parameter if we want the result status 248 | c.writeCCCD(/*enableNotifications*/ true, /*enableIndications*/ false); 249 | c.on('change', function(value) { 250 | console.log('New value:', value); 251 | }); 252 | } 253 | } 254 | }); 255 | }); 256 | conn.on('disconnect', function(reason) { 257 | console.log('Disconnected from ' + conn.peerAddress + ' due to ' + HciErrors.toString(reason)); 258 | }); 259 | }); 260 | } 261 | }); 262 | }); 263 | ``` 264 | 265 | ## L2CAP Connection-oriented channels 266 | 267 | L2CAP CoC is a great feature where you want to just send data packets to and from a device, when the GATT architecture doesn't make sense for your particular use case. For example, GATT has the issue that values can only be up to 512 bytes and the specification allows for notifications to be dropped (even if this implementation never drops notifications), and is generally not made for data transfer of large amounts of data. L2CAP CoC is similar to TCP, with the difference that the data is a sequence of packets, not a sequence of bytes, which usually makes the application code more clean. 268 | 269 | Both the server side and and client side L2CAP CoC are supported. 270 | 271 | A server can register a PSM on a BLE connection (if we are master or slave is irrelevant): 272 | 273 | ```javascript 274 | const L2CAPCoCErrors = NodeBleHost.L2CAPCoCErrors; 275 | 276 | // 0x0001 - 0x007f for fixed Bluetooth SIG-defined services, 0x0080 - 0x00ff for custom ones. 277 | const lePsm = 0x0080; 278 | 279 | conn.l2capCoCManager.registerLePsm(lePsm, function onRequestCallback(txMtu, callback) { 280 | // txMtu is the maximum packet (SDU) size the peer can receive, 281 | // which means we should never send a packet larger than this. 282 | 283 | // If we would reject, for example due to the reason we can only handle one l2cap connection at a time 284 | // callback(L2CAPCoCErrors.NO_RESOURCES_AVAILABLE); 285 | 286 | // Accept, enable receiving of packets immediately, with maximum receive size of 65535 bytes per packet 287 | var l2capCoC = callback(L2CAPCoCErrors.CONNECTION_SUCCESSFUL, /*initiallyPaused*/ false, /*rxMtu*/ 65535); 288 | 289 | // Now use the l2capCoC object 290 | ... 291 | }); 292 | ``` 293 | 294 | A client can connect using a specific PSM on a BLE connection (if we are master or slave is irrelevant): 295 | 296 | ```javascript 297 | const L2CAPCoCErrors = NodeBleHost.L2CAPCoCErrors; 298 | 299 | // 0x0001 - 0x007f for fixed Bluetooth SIG-defined services, 0x0080 - 0x00ff for custom ones. 300 | const lePsm = 0x0080; 301 | 302 | // Request a connection where we enable receiving of packets immediately, 303 | // with maximum receive size of 65535 bytes per packet 304 | conn.l2capCoCManager.connect(lePsm, /*initiallyPaused*/ false, /*rxMtu*/ 65535, function(result, l2capCoC) { 305 | if (result != L2CAPCoCErrors.CONNECTION_SUCCESSFUL) { 306 | console.log('L2CAP CoC connection failed: ' + L2CAPCoCErrors.toString(result)); 307 | return; 308 | } 309 | 310 | // Now use the l2capCoC object 311 | ... 312 | }); 313 | ``` 314 | 315 | Whenever a connection is established, it works the same regardless of which side initiated the connection. 316 | 317 | Simplest way to send and receive packets: 318 | 319 | ```javascript 320 | // Sending packets 321 | l2capCoC.send(Buffer.from([1, 2, 3])); 322 | 323 | // Receiving packets 324 | l2capCoC.on('data', function(buffer) { 325 | console.log(buffer); 326 | }); 327 | ``` 328 | 329 | The API is designed to work similar to Node.js's TCP or stream API, which means data can be appended to the output queue, even if the remote device is currently not accepting any new incoming packets. In general this results in a simple API that will work fine, except for the case when we attempt to send packets faster than the remote device can handle. To handle this, the `l2capCoC.txCredits` property returns the current balance. If positive, the remote device is ready to accept more packets. If zero, the device is not ready. If negative, we have buffered up some data internally that will be sent as soon as the remote device is ready. We can listen to the `credits` event to detect when the balance is updated. See the API documentation for further details. 330 | 331 | The flow control in the receive direction is even simpler and can be controlled by `l2capCoC.pause()` and `l2capCoC.resume()`. The call takes effect immediately, from the API user's point of view. The `initiallyPaused` parameter when creating the connection indicates whether receive flow is paused initially. The difference compared to just calling `l2capCoC.pause()` directly after the connection is created lies within whether we give initial credits to the peer or not. 332 | 333 | ## Bonding 334 | 335 | Bonding is supported and enabled by default. If not configured, "Just Works" pairing will be started automatically when requested by the remote device. 336 | 337 | Please read the API documentation for how to customizing the bonding process. As an example, to start pairing explicitly, call `conn.smp.sendPairingRequest(...)` when in the master role and `conn.smp.sendSecurityRequest(...)` when in slave role. 338 | 339 | ### Passkey example - master 340 | 341 | Pairing flow when the local device has a display that will show a passkey that is to be entered on the remote device by the user. 342 | 343 | ```javascript 344 | const IOCapabilities = NodeBleHost.IOCapabilities; 345 | const AssociationModels = NodeBleHost.AssociationModels; 346 | const SmpErrors = NodeBleHost.SmpErrors; 347 | 348 | conn.smp.sendPairingRequest({ioCap: IOCapabilities.DISPLAY_ONLY, bondingFlags: 1}); 349 | 350 | conn.smp.on('passkeyExchange', function(associationModel, userPasskey, callback) { 351 | // Note that Just works will be used if the remote device doesn't have a keyboard 352 | if (associationModel == AssociationModels.PASSKEY_ENTRY_RSP_INPUTS) { 353 | console.log('Please enter ' + userPasskey + ' on the remote device.'); 354 | } 355 | // callback would be used to tell the library the passkey the user entered, 356 | // if we had keyboard input support 357 | }); 358 | 359 | conn.smp.on('pairingComplete', function(resultObject) { 360 | console.log('The pairing process is now complete!'); 361 | console.log('MITM protection: ' + conn.smp.currentEncryptionLevel.mitm); 362 | console.log('LE Secure Connections used: ' + conn.smp.currentEncryptionLevel.sc); 363 | // Put logic here, e.g. read a protected characteristic 364 | }); 365 | 366 | conn.smp.on('pairingFailed', function(reason, isErrorFromRemote) { 367 | console.log('Pairing failed with reason ' + SmpErrors.toString(reason)); 368 | }); 369 | ``` 370 | 371 | To automatically start encryption after a reconnection, we must do that explicitly when the connection gets established: 372 | 373 | ```javascript 374 | if (conn.smp.hasLtk) { 375 | conn.smp.startEncryption(); 376 | } else { 377 | // start pairing here if we want to pair instead if a bond does not exist 378 | } 379 | 380 | conn.smp.on('encrypt', function(status, currentEncryptionLevel) { 381 | if (status != HciErrors.SUCCESS) { 382 | console.log('Could not start encryption due to ' + HciErrors.toString(status)); 383 | return; 384 | } 385 | console.log('The encryption process is now complete!'); 386 | console.log('MITM protection: ' + currentEncryptionLevel.mitm); 387 | console.log('LE Secure Connections used: ' + currentEncryptionLevel.sc); 388 | // Put logic here, e.g. read a protected characteristic 389 | }); 390 | ``` 391 | 392 | Note that the `encrypt` event will be emitted as well during the pairing process, in case we want a common handler for when the link gets encrypted. 393 | 394 | ### Passkey example - slave 395 | 396 | Pairing flow when the local device has a keyboard where the user can input six digits shown by the other device, or for the case when both devices have a keyboard and the user is requested to input a random passkey on both devices. 397 | 398 | ```javascript 399 | const IOCapabilities = NodeBleHost.IOCapabilities; 400 | const AssociationModels = NodeBleHost.AssociationModels; 401 | const SmpErrors = NodeBleHost.SmpErrors; 402 | 403 | // This only needs to be sent if the remote device doesn't send a pairing request on its own 404 | conn.smp.sendSecurityRequest(/*bond*/ true, /*mitm*/ true, /*sc*/ true, /*keypress*/ false); 405 | 406 | // Without this event handler the I/O capabilities will be no input, no output 407 | conn.smp.on('pairingRequest', function(req, callback) { 408 | callback({ioCap: IOCapabilities.KEYBOARD_ONLY, bondingFlags: 1, mitm: true}); 409 | }); 410 | 411 | conn.smp.on('passkeyExchange', function(associationModel, userPasskey, callback) { 412 | // Note that Just works will be used if the remote device has no I/O capabilities 413 | var doInput = false; 414 | if (associationModel == AssociationModels.PASSKEY_ENTRY_RSP_INPUTS) { 415 | console.log('Please enter the passkey shown on the other device:'); 416 | doInput = true; 417 | } else if (associationModel == AssociationModels.PASSKEY_ENTRY_BOTH_INPUTS) { 418 | console.log('Please enter the same random passkey on both devices:'); 419 | doInput = true; 420 | } 421 | if (doInput) { 422 | process.stdin.once('data', function(buffer) { 423 | callback(buffer.toString('utf8').replace(/[\r\n]/g, '')); 424 | }); 425 | } 426 | }); 427 | 428 | conn.smp.on('pairingComplete', function(resultObject) { 429 | console.log('The pairing process is now complete!'); 430 | console.log('MITM protection: ' + conn.smp.currentEncryptionLevel.mitm); 431 | console.log('LE Secure Connections used: ' + conn.smp.currentEncryptionLevel.sc); 432 | // Put logic here, e.g. read a protected characteristic 433 | }); 434 | 435 | conn.smp.on('pairingFailed', function(reason, isErrorFromRemote) { 436 | console.log('Pairing failed with reason ' + SmpErrors.toString(reason)); 437 | }); 438 | ``` 439 | 440 | The "security request" has two uses. The first is to ask the master to start the pairing procedure. The other is to ask the master to start encryption if there already exists a bond with the requested security level. 441 | 442 | To automatically start encryption after a reconnection (in case the master doesn't do that on its own), we send a security request with the same security level as the current key: 443 | 444 | ```javascript 445 | const IOCapabilities = NodeBleHost.IOCapabilities; 446 | const AssociationModels = NodeBleHost.AssociationModels; 447 | const SmpErrors = NodeBleHost.SmpErrors; 448 | 449 | if (conn.smp.hasLtk) { 450 | var level = conn.smp.availableLtkSecurityLevel; 451 | conn.smp.sendSecurityRequest(/*bond*/ true, level.mitm, /*sc*/ true, /*keypress*/ false); 452 | } 453 | 454 | conn.smp.on('encrypt', function(status, currentEncryptionLevel) { 455 | if (status != HciErrors.SUCCESS) { 456 | console.log('Master tried to initiate encryption with a key we do not have'); 457 | return; 458 | } 459 | console.log('The encryption process is now complete!'); 460 | console.log('MITM protection: ' + currentEncryptionLevel.mitm); 461 | console.log('LE Secure Connections used: ' + currentEncryptionLevel.sc); 462 | // Put logic here, e.g. read a protected characteristic 463 | }); 464 | ``` 465 | 466 | ### Example - pairing should be disabled 467 | 468 | In case we don't want to allow pairing to happen, we must explicitly handle this in the `pairingRequest` event (otherwise Just Works pairing will be used by default): 469 | 470 | ```javascript 471 | const SmpErrors = NodeBleHost.SmpErrors; 472 | 473 | conn.smp.on('pairingRequest', function(req, callback) { 474 | conn.smp.sendPairingFailed(SmpErrors.PAIRING_NOT_SUPPORTED); 475 | }); 476 | ``` 477 | 478 | ## License 479 | 480 | Licensed under the ISC license. [See full license text](LICENSE.txt). 481 | -------------------------------------------------------------------------------- /docs/api/advertising-data-builder.md: -------------------------------------------------------------------------------- 1 | 2 | # AdvertisingDataBuilder 3 | 4 | Utility class for constructing advertising packets into byte arrays. 5 | 6 | ## Class: AdvertisingDataBuilder 7 | 8 | Example: 9 | 10 | ```javascript 11 | const NodeBleHost = require('ble-host'); 12 | const AdvertisingDataBuilder = NodeBleHost.AdvertisingDataBuilder; 13 | 14 | const advDataBuffer = new AdvertisingDataBuilder() 15 | .addFlags(['leGeneralDiscoverableMode', 'brEdrNotSupported']) 16 | .addLocalName(true, 'MyDevice') 17 | .build(); 18 | ``` 19 | 20 | An advertising packet can be at most 31 bytes long. If an item that is being added would not fit, an `Error` will be thrown. 21 | 22 | All functions below, except `build()`, return `this` to allow chaining calls. 23 | 24 | ### advertisingDataBuilder.addFlags(flags) 25 | * `flags` {string[]} Array of flags to include 26 | 27 | Allowed flags: 28 | 29 | * 'leLimitedDiscoverableMode' 30 | * 'leGeneralDiscoverableMode' 31 | * 'brEdrNotSupported' 32 | * 'simultaneousLeAndBdEdrToSameDeviceCapableController' 33 | * 'simultaneousLeAndBrEdrToSameDeviceCapableHost' 34 | 35 | Generally 'leGeneralDiscoverableMode' and 'brEdrNotSupported' should be set. 36 | 37 | ### advertisingDataBuilder.add128BitServiceUUIDs(isComplete, uuids) 38 | * `isComplete` {boolean} Whether the provided list of UUIDs include all 128-bit services that exist in the device's GATT database. 39 | * `uuids` {string[]} An array of 128-bit UUIDs. 40 | 41 | ### advertisingDataBuilder.add16BitServiceUUIDs(isComplete, uuids) 42 | * `isComplete` {boolean} Whether the provided list of UUIDs include all 16-bit services that exist in the device's GATT database. 43 | * `uuids` {uuid[]} An array of 16-bit UUIDs. Each item can either be an integer, or a 128-bit UUID string using the base UUID. 44 | 45 | ### advertisingDataBuilder.addLocalName(isComplete, name) 46 | * `isComplete` {boolean} Whether the provided name is complete or truncated. 47 | * `name` {string} The name, or the truncated part of the name. 48 | 49 | ### advertisingDataBuilder.addManufacturerData(companyIdentifierCode, data) 50 | * `companyIdentifierCode` {integer} A 16-bit Bluetooth SIG assigned company identifier. 51 | * `data` {Buffer} Data. 52 | 53 | ### advertisingDataBuilder.addTxPowerLevel(txPowerLevel) 54 | * `txPowerLevel` {integer} The power level for this the transmitted advertising packet. Shall be between -127 and 127 dBm. 55 | 56 | ### advertisingDataBuilder.addSlaveConnectionIntervalRange(connIntervalMin, connIntervalMax) 57 | * `connIntervalMin` {integer} In units of 1.25 ms. 58 | * `connIntervalMax` {integer} In units of 1.25 ms. 59 | 60 | ### advertisingDataBuilder.add16BitServiceSolicitation(uuids) 61 | * `uuids` {uuid[]} An array of 16-bit UUIDs. Each item can either be an integer, or a 128-bit UUID string using the base UUID. 62 | 63 | ### advertisingDataBuilder.add128BitServiceSolicitation(uuids) 64 | * `uuids` {string[]} An array of 128-bit UUIDs. 65 | 66 | ### advertisingDataBuilder.add16BitServiceData(uuid, data) 67 | * `uuid` {uuid} An integer, or a 128-bit UUID string using the base UUID. 68 | * `data` {Buffer} Service data. 69 | 70 | ### advertisingDataBuilder.add128BitServiceData(uuid, data) 71 | * `uuid` {string} A 128-bit UUID. 72 | * `data` {Buffer} Service data. 73 | 74 | ### advertisingDataBuilder.addAppearance(appearanceNumber) 75 | * `appearanceNumber` {integer} A 16-bit number representing the appearance. 76 | 77 | ### advertisingDataBuilder.addPublicTargetAddresses(addresses) 78 | * `addresses` {string[]} An array of Public Bluetooth device addresses this advertisement targets. 79 | 80 | ### advertisingDataBuilder.addRandomTargetAddresses(addresses) 81 | * `addresses` {string[]} An array of Random Bluetooth device addresses this advertisement targets. 82 | 83 | ### advertisingDataBuilder.addAdvertisingInterval(interval) 84 | * `interval` {integer} In units of 0.625 ms. 85 | 86 | ### advertisingDataBuilder.addUri(uri) 87 | * `uri` {string} An URI. See the specification for the required format. 88 | 89 | ### advertisingDataBuilder.addLeSupportedFeatures(low, high) 90 | * `low` {integer} A 32-bit integer representing the least significant 32 bits. 91 | * `high` {integer} A 32-bit integer representing the most significant 32 bits. 92 | 93 | ### advertisingDataBuilder.build() 94 | 95 | Returns: A {Buffer} that contains a concatenation of all added items. 96 | -------------------------------------------------------------------------------- /docs/api/att-errors.md: -------------------------------------------------------------------------------- 1 | # Att Errors 2 | 3 | List of Attribute protocol errors. 4 | 5 | ```javascript 6 | const NodeBleHost = require('ble-host'); 7 | const AttErrors = NodeBleHost.AttErrors; 8 | ``` 9 | 10 | ## Integer constants 11 | 12 | The defined constants below are properties of `AttErrors`. 13 | 14 | Bluetooth SIG assigned constants: 15 | 16 | ```javascript 17 | INVALID_HANDLE: 0x01 18 | READ_NOT_PERMITTED: 0x02 19 | WRITE_NOT_PERMITTED: 0x03 20 | INVALID_PDU: 0x04 21 | INSUFFICIENT_AUTHENTICATION: 0x05 22 | REQUEST_NOT_SUPPORTED: 0x06 23 | INVALID_OFFSET: 0x07 24 | INSUFFICIENT_AUTHORIZATION: 0x08 25 | PREPARE_QUEUE_FULL: 0x09 26 | ATTRIBUTE_NOT_FOUND: 0x0a 27 | ATTRIBUTE_NOT_LONG: 0x0b 28 | INSUFFICIENT_ENCRYPTION_KEY_SIZE: 0x0c 29 | INVALID_ATTRIBUTE_VALUE_LENGTH: 0x0d 30 | UNLIKELY_ERROR: 0x0e 31 | INSUFFICIENT_ENCRYPTION: 0x0f 32 | UNSUPPORTED_GROUP_TYPE: 0x10 33 | INSUFFICIENT_RESOURCES: 0x11 34 | 35 | WRITE_REQUEST_REJECTED: 0xfc 36 | CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_IMPROPERLY_CONFIGURED: 0xfd 37 | PROCEDURE_ALREADY_IN_PROGRESS: 0xfe 38 | OUT_OF_RANGE: 0xff 39 | ``` 40 | 41 | Custom constants: 42 | 43 | ```javascript 44 | SUCCESS: 0 45 | RELIABLE_WRITE_RESPONSE_NOT_MATCHING: -1 46 | ``` 47 | 48 | The range `0x80` - `0x9F` is used for custom Application Errors. 49 | 50 | ## AttErrors.toString(code) 51 | * `code` {integer} Error code 52 | 53 | Returns the corresponding key (e.g. `OUT_OF_RANGE`) for a given code, `APPLICATION_ERROR_0x??` for an Application Error code, or `(unknown)` if not one of the above. 54 | -------------------------------------------------------------------------------- /docs/api/ble-manager.md: -------------------------------------------------------------------------------- 1 | # BLE Manager 2 | 3 | This is the high level class that is used to initiate scans, connect to devices etc. using a user-supplied transport object. 4 | 5 | ## Creating a BleManager object 6 | ```javascript 7 | const NodeBleHost = require('ble-host'); 8 | const BleManager = NodeBleHost.BleManager; 9 | var transport = ...; 10 | var options = { 11 | staticRandomAddress: 'CC:CC:CC:CC:CC:CC'; // optional property 12 | }; 13 | BleManager.create(transport, options, function(err, manager) { 14 | // err is either null or an Error object 15 | // if err is null, manager contains a fully initialized BleManager object 16 | ... 17 | }); 18 | ``` 19 | 20 | ## The transport object 21 | The transport object is the I/O object the BleManager uses to send and receive HCI packets. The only requirements are that it inherits from `EventEmitter`, has a `write` method (with a `Buffer` as argument which is used when one HCI packet is to be sent) and has a `data` event which shall be called with a `Buffer` as argument whenever a HCI packet is received. It may have a `close` event which should be emitted if the transport closes. The data format used is defined in Bluetooth Core specification v5, Volume 4 Part A - UART Transport Layer section 2. One single full packet per write/data event. 22 | 23 | For Linux, the use of `node-hci-socket` is recommended which implements these requirements: 24 | 25 | const NodeHciSocket = require('hci-socket'); 26 | var transport = new NodeHciSocket(0); // 0 is hci device id, as in hci0 as shown by hciconfig 27 | 28 | ## Class: BleManager 29 | 30 | ### manager.startScan(parameters) 31 | * `parameters` {Object} 32 | * `activeScan` {boolean} Whether the scan response data should be requested (default: true) 33 | * `scanWindow` {number} Integer multiple of 0.625 ms for the suggested scan window (default: 16) 34 | * `scanInterval {number} Integer multiple of 0.625 ms for the suggested scan interval (default: 16) 35 | * `filterDuplicates` {boolean} Whether duplicates should be removed (default: false) 36 | * `scanFilters` {ScanFilter[]} Array of scan filters (default: accept all devices) 37 | * Returns: {Scanner} The created and started scanner object 38 | 39 | This method starts a scan. Either none or both of `scanWindow` and `scanInterval` must be present. Note that these are just suggested values. If there are multiple concurrent scanners and/or pending connections the real scan window and scan interval may be set to compromised values. The default values (16 and 16) will only be used if there are no other concurrent scans and/or pending connections with other suggested values. 40 | 41 | Multiple scans can be started concurrently. (While the outer behaviour is that multiple scans are active at the same time, the stack only really issues one scan internally.) 42 | 43 | If `filterDuplicates` is active, there will be a cache with maximum size of 1024 devices. If this limit is reached old entries might not be filtered away the next time those devices advertise again. 44 | 45 | The `scanFilters` parameter, if present, should contain an array of scan filters (see below). An advertisement will only be reported if it matches any of the filters in the array. 46 | 47 | The returned scanner object can be used to stop the scan. Each advertisement will be reported by the `report` event to this object. 48 | 49 | ### manager.connect(bdAddrType, bdAddr, parameters, callback) 50 | * bdAddrType {string} `public` or `random` 51 | * bdAddr {string} Bluetooth Device Address of the device to connect to 52 | * parameters {Object} All sub-items are optional 53 | * connIntervalMin {number} Integer in the range 6-3200 in units of 1.25 ms (default: 20) 54 | * connIntervalMax {number} Integer in the range 6-3200 in units of 1.25 ms (default: 25) 55 | * connLatency {number} Integer in the range 0-499 defining Slave Latency (default: 0) 56 | * supervisionTimeout {number} Integer in the range 10-3200 in units of 10 ms (default: 500) 57 | * minimumCELength {number} Integer in the range 0-65535 in units of 0.625 ms (default: 0) 58 | * maximumCELength {number} Integer in the range 0-65535 in units of 0.625 ms (default: 0) 59 | * callback {Function} Callback called when the device connects 60 | * connection {Connection} The object used to interact with the remote device (GATT, SMP, L2CAPCoC etc.) 61 | * Returns: {PendingConnection} Object that can be used to cancel the connection attempt 62 | 63 | This method connects to the given device with the supplied parameters. All parameters are optional but there are a few rules. 64 | * Either none or both of connIntervalMin and connIntervalMax must be supplied, and min <= max. 65 | * Either none or both of minimumCELength and maximumCELength must be supplied, and min <= max. 66 | * If connection interval and supervision timeout is supplied, `connIntervalMax*1.25 * (connLatency + 1) * 2 < supervisionTimeout*10` per Bluetooth specification. This is to make sure the link won't be dropped if one packet gets missed. 67 | 68 | The minimumCELength/maximumCELength indicate the minimum and maximum Connection Event length. This defines how much radio time in each connection interval that can be used to send and receive packets. Setting CE length to be as long as the connection interval means that the devices will keep exchanging packets during the whole connection interval unless the event collides with another connection or scan. The default value is 0 which means it's up to the controller how many packets are sent in each connection event (this varies a lot between different manufacturers). 69 | 70 | Regardless of how many pending connections there are, the HCI protocol only allows one set of parameters for a pending connection to be active. Therefore all parameters are just suggested ones and the real values depend on the other pending connections' parameters. The default values will only be used if no pending connection have a suggested value. Also note that the Supervision Timeout default value is automatically adjusted if the formula above requires it to be higher. Similarly, the default Slave Latency value is also automatically decreased if the formula above requires it. 71 | 72 | When the connection completes, the callback will called with a `Connection` object as the only parameter. 73 | 74 | ### manager.gattDb 75 | 76 | {GattServerDb} 77 | 78 | See the GATT Server section 79 | 80 | ### manager.removeBond(identityAddressType, identityAddress) 81 | * identityAddressType {string} The identity address type, `public` or `random` 82 | * identityAddress {string} The identity address 83 | 84 | This method can be used to remove a bond between the currently used Bluetooth controller address and a peer device. There must be no active connection to the indicated device, or an error will be thrown. 85 | 86 | ### manager.setAdvertisingData(data[, callback]) 87 | * data {Buffer} A buffer of max 31 bytes containing Advertising Data 88 | * callback {Function} Callback 89 | 90 | This method sets the advertising data in the controller and calls `callback` with the HCI status code as the result parameter. 91 | 92 | ### manager.setScanResponseData(data[, callback]) 93 | * data {Buffer} A buffer of max 31 bytes containing Scan Response Data 94 | * callback {Function} Callback 95 | 96 | This method sets the scan response data in the controller and calls `callback` with the HCI status code as the result parameter. 97 | 98 | ### manager.startAdvertising(parameters, callback) 99 | * parameters {Object} All sub-items are optional 100 | * intervalMin {number} Advertising interval min, integer between 0x20 and 0x4000 in units of 0.625 ms (default: 62.5 ms) 101 | * intervalMax {number} Advertising interval max, integer between 0x20 and 0x4000 in units of 0.625 ms (default: 62.5 ms) 102 | * advertisingType {string} `ADV_IND`, `ADV_DIRECT_IND_HIGH_DUTY_CYCLE`, `ADV_SCAN_IND`, `ADV_NONCONN_IND` or `ADV_DIRECT_IND_LOW_DUTY_CYCLE` (default: `ADV_IND`) 103 | * directedAddress {Object} If a directed advertising type is selected, this parameter must be present 104 | * type {string} `public` or `random` 105 | * address {string} 106 | * channelMap {number[]} An array containing any combination of `37`, `38` and `39` (default: `[37, 38, 39]`) 107 | * callback {Function} Callback 108 | 109 | This method starts advertising with the given parameters. 110 | 111 | If the advertising could not be started, the callback is called with an HCI error code as parameter. 112 | 113 | When a master device connects, advertising is automatically stopped and the callback will be called with the following parameters: 114 | * status {number} Will be 0 115 | * conn {Connection} An object representing the connection 116 | 117 | ### manager.stopAdvertising([callback]) 118 | * callback {Function} Callback 119 | 120 | Stops an ongoing advertising. The callback will be called with an HCI status code as parameter. 121 | 122 | If the advertising is stopped and no master device connects, the callback of the `startAdvertising` method will not be called. 123 | 124 | Note that since the Bluetooth controller runs on a separate chip, it is possible that a master device connects after this method has been called, but before the Bluetooth controller has received the command. In this case both the callback indicating advertising has stopped as well as the callback in the `startAdvertising` method may be called. The Bluetooth specification is not clear in which order these events might happen. 125 | 126 | ## Class: Scanner 127 | 128 | ### scanner.stopScan() 129 | Stops the scan. No further reports will be emitted. 130 | 131 | ### Event: 'report' 132 | * eventData {Object} 133 | * connectable {boolean} If the device is connectable (i.e. it did not send `ADV_NONCONN_IND`) 134 | * addressType {string} `public` or `random` 135 | * address {string} 136 | * rssi {number} Signed integer in dBm (-127 to 20), 127 means not available 137 | * rawDataItems {Object[]} 138 | * type {number} 139 | * data {Buffer} 140 | * parsedDataItems {Object} Object with the advertising data items; only included fields will be present 141 | * flags {Object} 142 | * leLimitedDiscoverableMode {boolean} 143 | * leGeneralDiscoverableMode {boolean} 144 | * brEdrNotSupported {boolean} 145 | * simultaneousLeAndBdEdrToSameDeviceCapableController {boolean} 146 | * simultaneousLeAndBrEdrToSameDeviceCapableHost {boolean} 147 | * raw {Buffer} 148 | * serviceUuids {string[]} Array of UUIDs 149 | * localName {string} If only the shortened form is present, the string will end with `...` 150 | * txPowerLevel {number} Signed integer in dBm 151 | * slaveConnectionIntervalRange {Object} 152 | * min {number} Integer 153 | * max {number} Integer 154 | * serviceSolicitations {string[]} Array of UUIDs 155 | * serviceData {Object[]} 156 | * uuid {string} 157 | * data {Buffer} 158 | * appearance {number} 16-bit integer 159 | * publicTargetAddresses {string[]} 160 | * randomTargetAddresses {string[]} 161 | * advertisingInterval {number} 162 | * uri {string} 163 | * leSupportedFeatures {Object} 164 | * low {number} The 32 lower bits as an unsigned integer 165 | * high {number} The 32 higher bits as an unsigned integer 166 | * manufacturerSpecificData {Object[]} 167 | * companyIdentifierCode {number} 168 | * data {Buffer} 169 | 170 | ## Class: PendingConnection 171 | 172 | ### pendingConnection.cancel(callback) 173 | * callback {Function} Callback if the cancel succeeds 174 | 175 | This method is called to cancel a pending connection. Since the connection might complete at the exact moment this method is called (and it takes some time for the event to be sent from the controller to the host), it is possible that the cancel does not succeed. In that case the normal connect callback (and the `connect` event) will be called and the cancel callback will never be called. 176 | 177 | ### Event: 'connect' 178 | * connection {Connection} 179 | 180 | Emitted when the device connects. The `callback` parameter to the `connect` method of BleManager is internally registered as an event handler to this event. 181 | 182 | ## Class: Connection 183 | 184 | ### connection.ownAddressType 185 | 186 | {string} 187 | 188 | Contains the address type of the local address for this connection (`public` or `random`). 189 | 190 | ### connection.ownAddress 191 | 192 | {string} 193 | 194 | Contains the local address for this connection. 195 | 196 | ### connection.peerAddressType 197 | 198 | {string} 199 | 200 | Contains the address type of the peer address for this connection (`public` or `random`). 201 | 202 | ### connection.peerAddress 203 | 204 | {string} 205 | 206 | Contains the peer address for this connection. 207 | 208 | ### connection.peerIdentityAddressType 209 | 210 | {string} or {null} 211 | 212 | Contains the address type of the identity address of the peer device. This will be equal to `connection.peerAddressType` if a public or static random address is used. Otherwise it will be `null` unless the address could be resolved using an IRK in the bond storage. If the address could be resolved, this value will contain the address type of the identity address. The identity address, compared to a resolvable address, doesn't change and can hence be used as an identifier. 213 | 214 | This value will also be changed after pairing has completed. 215 | 216 | ### connection.peerIdentityAddress 217 | 218 | {string} or {null} 219 | 220 | Contains the identity address of the peer device. This will be equal to `connection.peerAddress` if a public or static random address is used. Otherwise it will be `null` unless the address could be resolved using an IRK in the bond storage. If the address could be resolved, this value will contain the identity address. The identity address, compared to a resolvable address, doesn't change and can hence be used as an identifier. 221 | 222 | This value will also be changed after pairing has completed. 223 | 224 | ### connection.disconnected 225 | 226 | {boolean} 227 | 228 | When the BLE link has finally disconnected, this property is set to `true`. The `disconnect` event is then emitted. 229 | 230 | 231 | ### connection.smp 232 | 233 | {SmpMasterConnection} or {SmpSlaveConnection} 234 | 235 | See the Security Manager section. 236 | 237 | ### connection.gatt 238 | 239 | {GattConnection} 240 | 241 | See the GATT Client section. 242 | 243 | ### connection.l2capCoCManager 244 | 245 | {L2CAPCoCManager} 246 | 247 | See the L2CAP CoC section. 248 | 249 | ### connection.setTimeout(callback, milliseconds) 250 | * callback {Function} A callback to invoke 251 | * milliseconds {number} After how long time the callback will be invoked 252 | * Returns: {Function} A function that when called cancels the timeout. 253 | 254 | After `milliseconds` ms has passed, the `callback` will be invoked with no parameters. If the connection becomes disconnected before the specified time has passed, the timeout is automatically cancelled. This method can be used for setting up timers that are only relevant if the connection stays alive. 255 | 256 | ### connection.disconnect([reason][, startIgnoreIncomingData]) 257 | * reason {number} An error code indicating reason for disconnecting (default: HciErrors.REMOTE_USER_TERMINATED_CONNECTION) 258 | * startIgnoreIncomingData {boolean} If no more incoming data should be processed until the connection actually disconnects (default: false) 259 | 260 | This method will initiate the disconnection procedure. Since the Bluetooth controller will wait for an acknowledgement from the peer before the link actually disconnects (or a timeout), the link will stay active until that happens. The `startIgnoreIncomingData` parameter can be used to avoid incoming data to be processed during this procedure, if desired. 261 | 262 | The `reason` can be any of: 263 | * HciErrors.AUTHENTICATION_FAILURE 264 | * HciErrors.REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES 265 | * HciErrors.REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_POWER_OFF 266 | * HciErrors.UNACCEPTABLE_CONNECTION_PARAMETERS 267 | * HciErrors.REMOTE_USER_TERMINATED_CONNECTION (default) 268 | 269 | When the link finally disconnects, the `disconnected` property will be set to true and the `disconnect` event will be emitted. 270 | 271 | ### connection.readRssi(callback) 272 | * callback {Function} Callback 273 | 274 | If the `disconnected` property is false, then an attempt to read the RSSI value is performed. The callback will then be called with the following parameters: 275 | * status {number} An HCI error code indicating the status 276 | * rssi {number} or {undefined} If status was 0, contains an indication of arriving signal strength at the antenna measured in dBm, where -127 means the signal strength indication is not available 277 | 278 | ### connection.updateConnParams(parameters[, callback]) 279 | * parameters {Object} See `manager.connect(bdAddrType, bdAddr, parameters, callback)` 280 | * callback {Function} Callback 281 | 282 | If the current role is master, a HCI command will be used to update the parameters. If the current role is slave, an L2CAP signalling packet is instead sent. 283 | 284 | When the procedure completes, the `callback` will be called with the following parameters: 285 | * status {number} For master role, contains the HCI status code of the result. For slave role, the following values are valid: 286 | * 0: The parameters were accepted 287 | * 1: The parameters were rejected 288 | * -1: No L2CAP response arrived from the peer within 30 seconds 289 | * -2: The request was never sent because new parameters were requested before this request could be sent 290 | 291 | ### Event: 'connectionUpdate' 292 | * interval {number} Connection interval in units of 1.25 ms 293 | * latency {number} Slave latency 294 | * timeout {number} Supervision timeout in units of 0.625 ms 295 | 296 | This event is emitted when the connection parameters have been updated. 297 | 298 | ### Event: 'updateConnParamsRequest' 299 | * parameters {Object} See `manager.connect(bdAddrType, bdAddr, parameters, callback)` 300 | * callback {Function} Callback 301 | 302 | If the current role is master, this event will be called when a slave sends a connection parameter update request over L2CAP. If this event has no listeners, the parameters are automatically accepted and applied. 303 | 304 | If this event is listened to, the callback must be called with a boolean parameter indicating if the parameters are accepted or not. The `connection.updateConnParams(parameters[, callback])` method must then be called to perform the update, normally passing in the same `parameters` object unless minor changes allowed by the standard are desired. 305 | 306 | ### Event: 'disconnect' 307 | * reason {number} A HCI error code indicating the reason for disconnecting 308 | 309 | This event indicates the link has finally been terminated. All objects having this connection as parent (or grandparent) are now considered dead, such as `gatt`, `smp`, `l2capCoCManager` and every GATT characteristic object etc. Any pending callback relating to those objects will not be called. 310 | 311 | ## Errors 312 | 313 | List of HCI Error codes. 314 | 315 | ```javascript 316 | const NodeBleHost = require('ble-host'); 317 | const HciErrors = NodeBleHost.HciErrors; 318 | ``` 319 | 320 | ## Integer constants 321 | 322 | The defined constants below are properties of `HciErrors`. It is a subset of error codes that should be usable within a BLE context. 323 | 324 | Bluetooth SIG assigned constants: 325 | 326 | ```javascript 327 | SUCCESS: 0x00 328 | UNKNOWN_CONNECTION_IDENTIFIER: 0x02 329 | HARDWARE_FAILURE: 0x03 330 | AUTHENTICATION_FAILURE: 0x05 331 | PIN_OR_KEY_MISSING: 0x06 332 | MEMORY_CAPACITY_EXCEEDED: 0x07 333 | CONNECTION_TIMEOUT: 0x08 334 | CONNECTION_LIMIT_EXCEEDED: 0x09 335 | CONNECTION_ALREADY_EXISTS: 0x0B 336 | COMMAND_DISALLOWED: 0x0C 337 | CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES: 0x0D 338 | CONNECTION_REJECTED_DUE_TO_SECURITY_REASONS: 0x0E 339 | UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE: 0x11 340 | INVALID_HCI_COMMAND_PARAMETERS: 0x12 341 | REMOTE_USER_TERMINATED_CONNECTION: 0x13 342 | REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES: 0x14 343 | REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_POWER_OFF: 0x15 344 | CONNECTION_TERMINATED_BY_LOCAL_HOST: 0x16 345 | REPEATED_ATTEMPTS: 0x17 346 | PAIRING_NOT_ALLOWED: 0x18 347 | UNSUPPORTED_REMOTE_FEATURE: 0x1A 348 | INVALID_LL_PARAMETERS: 0x1E 349 | UNSPECIFIED_ERROR: 0x1F 350 | UNSUPPORTED_LL_PARAMETER_VALUE: 0x20 351 | LL_RESPONSE_TIMEOUT: 0x22 352 | LL_PROCEDURE_COLLISION: 0x23 353 | INSTANT_PASSED: 0x28 354 | DIFFERENT_TRANSACTION_COLLISION: 0x2A 355 | PARAMETER_OUT_OF_MANDATORY_RANGE: 0x30 356 | CONTROLLER_BUSY: 0x3A 357 | UNACCEPTABLE_CONNECTION_PARAMETERS: 0x3B 358 | ADVERTISING_TIMEOUT: 0x3C 359 | CONNECTION_TERMINATED_DUE_TO_MIC_FAILURE: 0x3D 360 | CONNECTION_FAILED_TO_BE_ESTABLISHED: 0x3E 361 | LIMIT_REACHED: 0x43 362 | OPERATION_CANCELLED_BY_HOST: 0x44 363 | PACKET_TOO_LONG: 0x45 364 | ``` 365 | 366 | ## HciErrors.toString(code) 367 | * `code` {integer} Error code 368 | 369 | Returns the corresponding key (e.g. `SUCCESS`) for a given code, or `(unknown)` if not one of the above. 370 | -------------------------------------------------------------------------------- /docs/api/gatt-client.md: -------------------------------------------------------------------------------- 1 | # GATT client 2 | 3 | This implements support for a GATT client. 4 | 5 | ### Requests 6 | Since there can only be one outstanding ATT Request at a time, requests made using the API are internally enqueued if there is another pending request. There is no upper limit for the number of requests that can be enqueued. All request operations take a callback argument, to which either a Function or `undefined` should be passed. The first argument passed to the callback (if it is a Function) will be an `AttErrors` code, unless otherwise stated, which contains the response error, or 0 on success. 7 | 8 | ### MTU 9 | The default MTU for each new connection is 23. A change of this MTU can be initiated by either the server or client. This implementation always tries to negotiate the MTU of 517 bytes (upon request), since with that MTU the largest possible attribute value will always fit in each type of request/response. The current MTU can be retrieved through the `currentMtu` property of the GattConnection object. 10 | 11 | If you need to compare the length of some response with the current MTU value, make sure the MTU has been negotiated before the request is started to avoid cases when the MTU is changed in the middle of a procedure by the peer. 12 | 13 | ### UUIDs 14 | All UUIDs that this API outputs are always full 128-bit UUIDs written as uppercase strings in the format `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX`. All UUID inputs are required to be either strings in the same format (but lowercase are allowed), or a 16-bit unsigned integer number (in which case it is assumed to be an UUID in Bluetooth SIG's base range). 15 | 16 | ### Values 17 | All characteristic and descriptor values that this API outputs are always Buffer objects. All value inputs may be either Buffers or strings. If the value is a string, it is converted to a Buffer using the UTF-8 encoding. 18 | 19 | ## Class: GattConnection 20 | 21 | This class is used to perform GATT client operations. Each BLE connection object has an instance of a GattConnection, which can be retrieved through its `gatt` property. 22 | 23 | ### gatt.exchangeMtu([callback]) 24 | Performs an MTU exchange request. Since the specification only allows this request to be sent once per connection, an error will be thrown if this method is called multiple times. When the operation has completed, the MTU is available in the `currentMtu` property. 25 | 26 | See the MTU topic above for more information. 27 | 28 | ### gatt.discoverAllPrimaryServices(callback) 29 | Performs the Discover All Primary Service procedure, which internally issues multiple requests to discover all primary services. The services will be cached for the current connection. The cache will also be persisted between connections if bonded. If a cached version is available, the callback will be called immediately, bypassing the queue of requests. 30 | 31 | Callback should take a single argument `services` which is an array of all services found when the procedure completes. Each item will be of type `GattClientService`. 32 | 33 | ### gatt.discoverServicesByUuid(uuid, numToFind, callback) 34 | * `uuid` {string} or {number} UUID of the service to find 35 | * `numToFind` {number} or {undefined} If this is present, the search may stop earlier if at least this number of service instances have already been found 36 | * `callback` {Function} or {undefined} 37 | 38 | Performs the Discover Primary Service By Service UUID procedure, which internally issues multiple requests to discover primary services of the given UUID. 39 | 40 | Works just like `discoverAllPrimaryServices` with the exception that the `services` array will only contain services of the given UUID. 41 | 42 | ### gatt.invalidateServices(startHandle, endHandle[, callback]) 43 | * `startHandle` {number} Positive 16-bit unsigned integer where the invalidated range starts 44 | * `endHandle` {number} Positive 16-bit unsigned integer where the invalidated range ends (must not be less than the start handle) 45 | * `callback` {Function} or {undefined} Callback taking no arguments 46 | 47 | Invalidates services from the service cache. Notifications and Indications will no longer be emitted for characteristics in this range. The operation will be enqueued in the request queue, meaning all pending requests will be performed before the services are invalidated. The callback will be called with no arguments when all pending requests have been executed and the services have been invalidated. 48 | 49 | ### gatt.readUsingCharacteristicUuid(startHandle, endHandle, uuid, callback) 50 | * `startHandle` {number} Positive 16-bit unsigned integer where the search should start 51 | * `endHandle` {number} Positive 16-bit unsigned integer where the search should end (must not be less than the start handle) 52 | * `uuid` {string} or {number} Characteristic UUID 53 | * `callback` {Function} 54 | * `err` {number} An `AttErrors` code 55 | * `list` {Object[]} or {undefined} Non-empty array of results if no error 56 | * `attributeHandle` {number} Attribute handle 57 | * `attributeValue` {Buffer} Attribute value 58 | 59 | Performs a Read Using Characteristic UUID request to read characteristics of a given UUID in a specific handle range. Multiple values may be received in the same response if they have the same length and fits into one packet. Values are truncated to `min(253, ATT_MTU - 4)` bytes. 60 | 61 | ### gatt.beginReliableWrite() 62 | Tells the stack that a Reliable Write transaction is started. All characteristic writes except "Write Without Response" to characteristics following this method call will become one Reliable Write transaction, which means they will be queued up at the GATT server and executed atomically when the `commitReliableWrite` method is called. Long Writes to descriptors are not allowed while Reliable Write is active. 63 | 64 | ### gatt.cancelReliableWrite([callback]) 65 | Performs a request to cancel Reliable Writes, which means all pending writes (if any) at the server are discarded. 66 | 67 | ### gatt.commitReliableWrite([callback]) 68 | Performs a request to execute all pending writes at the server. From now on, all writes are "normal" again until `beginReliableWrite` is called again. 69 | 70 | ### gatt.currentMtu 71 | The current MTU. 72 | 73 | ### Event: 'timeout' 74 | Emitted when a GATT request times out (30 seconds after it was sent). When this happens, no more procedures may be executed on this BLE connection (so it's best to disconnect and reconnect). If there are no listeners for this event, the BLE connection will be disconnected. 75 | 76 | ## Class: GattClientService 77 | 78 | This class represents a service present on a remote GATT server. Instances of this class can only be obtained using discovery procedures. 79 | 80 | ### service.startHandle 81 | The start handle of the service. 82 | 83 | ### service.endHandle 84 | The end handle of the service. 85 | 86 | ### service.uuid 87 | The UUID of the service. 88 | 89 | ### service.findIncludedServices(callback) 90 | Performs the Find Included Services procedure, which internally issues multiple requests to find all included services for this service. Just like when discovering all primary services, the result may be cached. 91 | 92 | Callback should take a single argument `services` which is an array of all services found when the procedure completes. Each item will be of type `GattClientService`. 93 | 94 | ### service.discoverCharacteristics(callback) 95 | Performs the Discover All Characteristics of a Service procedure, which internally issues multiple requests to discover all characteristics of this service. Just like when discovering all primary services, the result may be cached. 96 | 97 | Callback should take a single argument `characteristics` which is an array of all characteristics found when the procedure completes. Each item will be of type `GattClientCharacteristic`. 98 | 99 | ## Class: GattClientCharacteristic 100 | 101 | This class represents a characteristic present on a remote GATT server. Instances of this class can be obtained by the method `discoverCharacteristics` of a service. 102 | 103 | ### characteristic.properties 104 | The declared properties for this characteristic. The property is an object containing the following keys. The corresponding value for each key is a boolean whether the property is declared or not. 105 | * broadcast 106 | * read 107 | * writeWithoutResponse 108 | * write 109 | * notify 110 | * indicate 111 | * authenticatedSignedWrites 112 | * extendedProperties 113 | 114 | To get the extended properties, the Extended Properties descriptor must be manually discovered, read and parsed. 115 | 116 | ### characteristic.declarationHandle 117 | The declaration handle of this characteristic 118 | 119 | ### characteristic.valueHandle 120 | The value handle of this characteristic 121 | 122 | ### characteristic.uuid 123 | The UUID of this characteristic 124 | 125 | ### characteristic.discoverDescriptors(callback) 126 | Performs the Discover All Characteristic Descriptors procedure, which internally issues multiple requests to discover all descriptors of this characteristic. Just like when discovering all primary services, the result may be cached. 127 | 128 | Callback should take a single argument `descriptors` which is an array of all descriptors found when the procedure completes. Each item will be of type `GattClientDescriptor`. 129 | 130 | ### characteristic.read(callback) 131 | * `callback` {Function} 132 | * `err` {number} An `AttErrors` code 133 | * `value` {Buffer} or {undefined} The read value if no error 134 | 135 | This performs the Read Long Characteristics Value procedure, which internally first issues a Read Request. If the value returned is as large as fits within one packet, the remainder is read using multiple Read Blob Requests. When completed, the complete value is forwarded to the callback. 136 | 137 | ### characteristic.readShort(callback) 138 | Same as the `read` method but only performs one Read Request, which means the value passed to the callback might be truncated. 139 | 140 | ### characteristic.readLong(offset, callback) 141 | Same as the `read` method but starts reading at a specific offset (integer between 0 and 512). The value passed to the callback will contain the characteristic value where the first `offset` bytes have been omitted. 142 | 143 | ### characteristic.write(value[, callback]) 144 | * `value` {Buffer} or {string} The value to write 145 | * `callback` {Function} or {undefined} Callback 146 | * `err` {number} An `AttErrors` code 147 | 148 | If Reliable Write is not active, performs either the Write Characteristic Value or the Write Long Characteristic Value procedure, depending on the value length and current MTU. 149 | 150 | If Reliable Write is active, Prepare Write Requests will be sent. The returned value will be compared to the sent value, per specification, and if the values don't match, the callback will be called with the error `AttErrors.RELIABLE_WRITE_RESPONSE_NOT_MATCHING`. If that happens, the Reliable Write state is also exited and all pending writes at the server are discarded. 151 | 152 | ### characteristic.writeLong(value, offset[, callback]) 153 | Same as `write` but starts writing to a particular offset (integer between 0 and 512). 154 | 155 | ### characteristic.writeWithoutResponse(value[, sentCallback][, completeCallback]) 156 | * `value` {Buffer} or {string} The value to write 157 | * `sentCallback` {Function} or {undefined} A callback when the packet has been sent to the controller 158 | * `completeCallback` {Function} or {undefined} A callback when the whole packet has been acknowledged by the peer's Link Layer or been flushed due to disconnection of the link 159 | 160 | Performs the Write Without Response procedure, which is not a request. Therefore the packet goes straight to the BLE connection's output buffer, bypassing the request queue. In case you want to write a large amount of packets, you should wait for the `sentCallback` before you write another packet, to make it possible for the stack to interleave other kinds of packets. This does not decrease the throughput, as opposed to waiting for the `completeCallback` between packets. 161 | 162 | The value will be truncated to `currentMtu - 3` bytes. 163 | 164 | ### characteristic.writeCCCD(enableNotifications, enableIndications[, callback]) 165 | * `enableNotifications` {boolean} If notifications should be enabled 166 | * `enableIndications` {boolean} If indications should be enabled 167 | * `callback` {Function} 168 | * `err` {number} An `AttErrors` code 169 | 170 | Utility function for first finding a Client Characteristic Configuration Descriptor, then writing the desired value to it. 171 | 172 | If no descriptor is found, the callback will be called with `AttErrors.ATTRIBUTE_NOT_FOUND` as error code. Otherwise, the code passed to the callback will be the result of the write. 173 | 174 | ### Event: 'change' 175 | * `value` {Buffer} The value notified / indicated 176 | * `isIndication` {boolean} If it is an indication (true) or notification (false) 177 | * `callback` {Function} Callback taking no arguments to be called if `isIndication` 178 | 179 | This event is emitted when a notification or indication is received for a characteristic. If it is an indication, a confirmation must be sent by calling the `callback` with no arguments within 30 seconds. 180 | 181 | Note that listening to this event does not mean that the Client Characteristic Configuration Descriptor is configured automatically. You need to first write to this descriptor to subscribe for notifications / indications. 182 | 183 | ## Class: GattClientDescriptor 184 | 185 | This class represents a descriptor present on a remote GATT server. Instances of this class can be obtained by the method `discoverDescriptors` of a characteristic. 186 | 187 | ### descriptor.handle 188 | The handle for this descriptor. 189 | 190 | ### descriptor.uuid 191 | The UUID for this descriptor. 192 | 193 | ### descriptor.read(callback) 194 | Same API as `read` for `GattClientCharacteristic`. 195 | 196 | ### descriptor.readShort(callback) 197 | Same API as `readShort` for `GattClientCharacteristic`. 198 | 199 | ### descriptor.readLong(offset, callback) 200 | Same API as `readLong` for `GattClientCharacteristic`. 201 | 202 | ### descriptor.write(value[, callback]) 203 | Same API as `write` for `GattClientCharacteristic` except that in case of Reliable Write is active, "Long" descriptor values are not allowed to be written (values larger than MTU - 3 bytes), and that "Short" descriptor values in this case are written using the normal Write Request. 204 | 205 | ### descriptor.writeLong(value, offset[, callback]) 206 | If offset is 0, same as `write`. Otherwise, same API as `writeLong` for `GattClientCharacteristic` except that in case of Reliable Write is active, this method is not allowed. 207 | -------------------------------------------------------------------------------- /docs/api/gatt-server.md: -------------------------------------------------------------------------------- 1 | # GATT Server 2 | 3 | Each device must have a GATT Server with a GATT DB. By default only two GATT Services are present in the database; the Generic Access Service and the GATT Service. More services can be added. 4 | 5 | ### UUIDs 6 | All UUID inputs are required to be either strings in the format `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX`, or a 16-bit unsigned integer number (in which case it is assumed to be an UUID in Bluetooth SIG's base range). 7 | 8 | ### Values 9 | All characteristic and descriptor values that this API outputs are always Buffer objects. All value inputs may be either Buffers or strings. If the value is a string, it is converted to a Buffer using the UTF-8 encoding. However, the `value` property of characteristics and descriptors is treated differently. See its documentation for more info. 10 | 11 | ### MTU 12 | The MTU value is shared with the GATT Client instance. See its documentation for more information. 13 | 14 | ## Class: GattServerDb 15 | 16 | This class is used to control the GATT DB. Each HCI Manager has an own instance of this class, which can be retrieved through its `gattDb` property. 17 | 18 | ### gattDb.setDeviceName(name) 19 | * `name` {Buffer} or {string} The new device name to store in the Device Name characteristic (max 248 bytes) 20 | 21 | Sets the Device Name characteristic. 22 | 23 | ### gattDb.setAppearance(appearance) 24 | * `appearance` {number} 16-bit unsigned integer 25 | 26 | Sets the Appearance characteristic. 27 | 28 | ### gettDb.getSvccCharacteristic() 29 | * Returns: {GattServerCharacteristic} The Service Changed Characteristic 30 | 31 | Returns the Service Changed Characteristic in the GATT service which is automatically created. Use this to send indications if the GATT DB is changed. The stack never sends indications on its own if the GATT DB is changed, so this must be done manually by the user. 32 | 33 | ### gattDb.addServices(services) 34 | * `services` {GattServerService[]} Array of services 35 | 36 | Adds one or more services to the GATT DB. 37 | 38 | ### gattDb.removeService(service) 39 | * `service` {GattServerService} A service to remove 40 | * Returns: {boolean} Whether the specified service was found and therefore could be removed 41 | 42 | Removes a service previously added. 43 | 44 | If services are removed, you should indicate to all current connections and all bonded devices that the services in the modified range have been changed. 45 | 46 | Note that if a service used as an included service is removed, the included service definition is not removed and will therefore be dangling. Therefore that "parent" service should also be removed, or a new service with the same UUID and size should be added back to the same position as the one being removed. 47 | 48 | ## Interface: GattServerService 49 | This interface describes the set of properties each object item in the `services` array of `gattDb.addServices(services)` must have. All properties are only read and inspected during the service is being added. 50 | 51 | ### service.uuid 52 | {string} or {number} 53 | 54 | UUID of the service. Mandatory property. 55 | 56 | ### service.isSecondaryService 57 | {boolean} 58 | 59 | Whether the service is secondary or primary. Secondary services cannot be discovered directly but are only meant to be included by other services. 60 | 61 | Optional property. Default: false. 62 | 63 | ### service.includedServices 64 | {GattServerService[]} 65 | 66 | Array of included services. Each item is a reference to either a previously added service or one of the services currently being added. 67 | 68 | Optional property. Default: empty array. 69 | 70 | ### service.startHandle 71 | {number} 72 | 73 | Optional property. Positive 16-bit unsigned integer of a proposed start handle. If the property exists and the service fits at this position, it will be used. Otherwise it is placed directly after the last current service. This algorithm is run for each service in the same order as declared in the `services` argument to `gattDb.addServices`. 74 | 75 | Once the service is added, this property will be set to the actual start handle by the stack. 76 | 77 | ### service.endHandle 78 | {number} 79 | 80 | This property is never read when the service is added, but is rather just assigned the actual end handle by the stack when the service has been added. 81 | 82 | This can be useful after a change of GATT services in the database, when we need to tell the client about the range of changed handles using the Service Changed Characteristic. 83 | 84 | ### service.characteristics 85 | {GattServiceCharacteristic[]} 86 | 87 | Array of characteristics. 88 | 89 | Optional property. Default: empty array. 90 | 91 | ## Interface: GattServerCharacteristic 92 | This interface describes the set of properties each object item in the array of `service.characteristics` must have. 93 | 94 | The `uuid`, `properties`, `maxLength`, `readPerm`, `writePerm` and `descriptors` properties are only read and inspected during the service is being added. 95 | 96 | ### characteristic.uuid 97 | {string} or {number} 98 | 99 | UUID of the characteristic. Mandatory property. 100 | 101 | ### characteristic.properties 102 | {string[]} 103 | 104 | Defines properties for this characteristic. This can be used by the client to detect the available features for this characteristic. The following property strings can be included in the array: 105 | * `broadcast` 106 | * `read` 107 | * `write-without-response` 108 | * `write` 109 | * `notify` 110 | * `indicate` 111 | * `authenticated-signed-writes` (not yet supported) 112 | * `reliable-write` 113 | * `writable-auxiliaries` 114 | 115 | Optional property. Default: empty array (which would be quite useless). 116 | 117 | ### characteristic.maxLength 118 | {number} 119 | 120 | An integer between 0 and 512 specifying the max length in bytes for this characteristic value. 121 | 122 | Optional property. Default: 512. 123 | 124 | ### characteristic.readPerm 125 | {string} 126 | 127 | Defines the permission needed to read the characteristic. Must be one of the following values: 128 | * `not-permitted` (Characteristic cannot be read) 129 | * `open` (Can always be read) 130 | * `encrypted` (Can only be read when the link is encrypted) 131 | * `encrypted-mitm` (Can only be read when the link is encrypted with a key that was generated with MITM protection) 132 | * `encrypted-mitm-sc` (Can only be read when the link is encrypted with a key that was generated with MITM protection and Secure Connections pairing) 133 | * `custom` (A user-provided method will called upon each read to determine if the read should be permitted) 134 | 135 | Optional property. Default: `open` if the characteristic has the `read` property, otherwise `not-permitted`. 136 | 137 | ### characteristic.writePerm 138 | {string} 139 | 140 | Defines the permission needed to write the characteristic. Must be one of the following values: 141 | * `not-permitted` (Characteristic cannot be written) 142 | * `open` (Can always be written) 143 | * `encrypted` (Can only be written when the link is encrypted) 144 | * `encrypted-mitm` (Can only be written when the link is encrypted with a key that was generated with MITM protection) 145 | * `encrypted-mitm-sc` (Can only be written when the link is encrypted with a key that was generated with MITM protection and Secure Connections pairing) 146 | * `custom` (A user-provided method will called upon each written to determine if the write should be permitted) 147 | 148 | Optional property. Default: `open` if the characteristic has any of the the `write`, `write-without-response`, `reliable-write` properties, otherwise `not-permitted`. 149 | 150 | ### characteristic.descriptors 151 | {GattServerDescriptor[]} 152 | 153 | Array or descriptors. 154 | 155 | Optional property. Default: empty array. 156 | 157 | ### characteristic.value 158 | {Buffer} or {string} 159 | 160 | Unless there are custom read and write handlers, the stack will read and write the value from/to this property. 161 | 162 | Upon a write, the type will be preserved (if it previously was a string, a string will be stored, otherwise a buffer will be stored). 163 | 164 | ### characteristic.onAuthorizeRead(connection, callback) 165 | * `connection` {Connection} The BLE connection that requests the read 166 | * `callback` {Function} Callback that should be called with the result 167 | * `err` {number} An `AttErrors` result code 168 | 169 | This method must be present if `readPerm` is set to `custom` (otherwise it is not used). Upon receiving any kind of request that reads the characteristic, this method will first be invoked to check if the read should be permitted or not. 170 | 171 | If the callback is called with the error code `AttErrors.SUCCESS`, the read is permitted and the read will be performed as usual (unless the connection disconnects before the callback is called). Otherwise the error code will be sent as response to the client. 172 | 173 | Allowed error codes: 174 | * `AttErrors.SUCCESS` 175 | * `AttErrors.READ_NOT_PERMITTED` 176 | * `AttErrors.INSUFFICIENT_ENCRYPTION` (only if bond exists, has LTK, but the link is currently not encrypted) 177 | * `AttErrors.INSUFFICIENT_ENCRYPTION_KEY_SIZE` (only if encrypted) 178 | * `AttErrors.INSUFFICIENT_AUTHENTICATION` 179 | * `AttErrors.INSUFFICIENT_AUTHORIZATION` 180 | * Application errors (0x80 - 0x9f) 181 | 182 | ### characteristic.onRead(connection, callback) 183 | * `connection` {Connection} The BLE connection that requests the read 184 | * `callback` {Function} Callback that should be called with the result 185 | * `err` {number} An `AttErrors` result code 186 | * `value` {Buffer}, {string} or {undefined} The value to send as response, if no error 187 | 188 | This optional method will be used to read the value of the characteristic when a request is received from a client. If it is not present, the stack will simply read the `value` property. 189 | 190 | The `value` should be the current full characteristic value. Depending on request type, it will automatically be sliced depending on request offset and MTU. 191 | 192 | Allowed error codes: 193 | * `AttErrors.SUCCESS` 194 | * `AttErrors.UNLIKELY_ERROR` 195 | * `AttErrors.INSUFFICIENT_RESOURCES` 196 | * `AttErrors.PROCEDURE_ALREADY_IN_PROGRESS` 197 | * Application errors (0x80 - 0x9f) 198 | 199 | ### characteristic.onPartialRead(connection, offset, callback) 200 | * `connection` {Connection} The BLE connection that requests the read 201 | * `offset` {number} The offset from where the client wants to read 202 | * `callback` {Function} Callback that should be called with the result 203 | * `err` {number} An `AttErrors` result code 204 | * `value` {Buffer}, {string} or {undefined} The value to send as response, if no error 205 | 206 | This optional method always overrides the `onRead` method and can be used in particular to handle Read Blob Requests in a more specialized way. The callback should be called with the value set to the current full characteristic value, but where the first `offset` bytes have been removed. 207 | 208 | Allowed error codes: 209 | * `AttErrors.SUCCESS` 210 | * `AttErrors.INVALID_OFFSET` 211 | * `AttErrors.ATTRIBUTE_NOT_LONG` (only when offset is not 0) 212 | * `AttErrors.UNLIKELY_ERROR` 213 | * `AttErrors.INSUFFICIENT_RESOURCES` 214 | * `AttErrors.PROCEDURE_ALREADY_IN_PROGRESS` 215 | * Application errors (0x80 - 0x9f) 216 | 217 | ### characteristic.onAuthorizeWrite(connection, callback) 218 | * `connection` {Connection} The BLE connection that requests the write 219 | * `callback` {Function} Callback that should be called with the result 220 | * `err` {number} An `AttErrors` result code 221 | 222 | This method must be present if `writePerm` is set to `custom` (otherwise it is not used). Upon receiving any kind of request or command that writes the characteristic, this method will first be invoked to check if the write should be permitted or not. 223 | 224 | If the callback is called with the error code `AttErrors.SUCCESS`, the write is permitted and the write will be performed as usual (unless the connection disconnects before the callback is called). Otherwise the error code will be sent as response to the client. 225 | 226 | For Write Requests and Write Without Responses, this method will be called just before the write attempt. For Long Writes and Reliable Writes, this method will be invoked for each received Prepare Write Request. When all Prepare Write Requests have been sent and the writes are later executed, the writes will be performed at once. 227 | 228 | * `AttErrors.SUCCESS` 229 | * `AttErrors.WRITE_NOT_PERMITTED` 230 | * `AttErrors.INSUFFICIENT_ENCRYPTION` (only if bond exists, has LTK, but the link is currently not encrypted) 231 | * `AttErrors.INSUFFICIENT_ENCRYPTION_KEY_SIZE` (only if encrypted) 232 | * `AttErrors.INSUFFICIENT_AUTHENTICATION` 233 | * `AttErrors.INSUFFICIENT_AUTHORIZATION` 234 | * Application errors (0x80 - 0x9f) 235 | 236 | ### characteristic.onWrite(connection, needsResponse, value, callback) 237 | * `connection` {Connection} The BLE connection that requests the write 238 | * `needsResponse` {boolean} Whether a response must be sent 239 | * `value` {Buffer} The value to write 240 | * `callback` {Function} Callback that should be called with the response, if needed 241 | * `err` {number} An `AttErrors` result code 242 | 243 | This optional method will be called when a write needs to be done. If this method is not present, the `value` property of the characteristic object is instead updated. 244 | 245 | In case for Prepared Writes, consecutive writes with offsets directly following the previous write to the same value are internally concatenated to the full value at the time the writes are committed. At that time this method will be called only once with the full value. 246 | 247 | The callback must be called when `needsResponse` is true. (Otherwise calling the callback is a NO-OP.) 248 | 249 | Allowed error codes: 250 | * `AttErrors.SUCCESS` 251 | * `AttErrors.INVALID_OFFSET` 252 | * `AttErrors.INVALID_ATTRIBUTE_VALUE_LENGTH` 253 | * `AttErrors.UNLIKELY_ERROR` 254 | * `AttErrors.INSUFFICIENT_RESOURCES` 255 | * `AttErrors.PROCEDURE_ALREADY_IN_PROGRESS` 256 | * `AttErrors.OUT_OF_RANGE` 257 | * `AttErrors.WRITE_REQUEST_REJECTED` 258 | * Application errors (0x80 - 0x9f) 259 | 260 | ### characteristic.onPartialWrite(connection, needsResponse, offset, value, callback) 261 | * `connection` {Connection} The BLE connection that requests the write 262 | * `needsResponse` {boolean} Whether a response must be sent 263 | * `offset` {number} Offset between 0 and 512 where to start the write 264 | * `value` {Buffer} The value to write 265 | * `callback` {Function} Callback that should be called with the response, if needed 266 | * `err` {number} An `AttErrors` result code 267 | 268 | This optional method always overrides `onWrite`. Same as `onWrite` but can be used to handle the cases where Partial Writes are used where the starting offset in the initial write is not 0. If this happens and only `onWrite` would be present, an `AttErrors.INVALID_OFFSET` error is sent in response by the stack without calling the `onWrite` method. 269 | 270 | ### characteristic.onSubscriptionChange(connection, notification, indication, isWrite) 271 | * `connection` {Connection} The BLE connection whose GATT client has changed subscription 272 | * `notification` {boolean} Whether the client has registered for notifications 273 | * `indication` {boolean} Whether the client has registered for indications 274 | * `isWrite` {boolean} Whether this was a real write to the CCCD or the change was due to a connection/disconnection 275 | 276 | Optional method which is invoked each time the client changes the subscription status. 277 | 278 | When the client writes to the Client Characteristic Configuration Descriptor of this characteristic, the `isWrite` argument is true. 279 | 280 | When a client disconnects and previously had either notifications or indications subscribed, this method will be called with the last three arguments set to false. 281 | 282 | When a bonded client connects, the previous CCCD value is read from the storage and if it was subscribed in the previous connection, this method will be called immediately after the connection gets established with the `isWrite` argument set to false. 283 | 284 | ### characteristic.notify(connection, value[, sentCallback][, completeCallback]) 285 | * `connection` {Connection} The BLE connection whose GATT client will be notified 286 | * `value` {Buffer} or {string} Value to notify 287 | * `sentCallback` {Function} or {undefined} A callback when the packet has been sent to the controller 288 | * `completeCallback` {Function} or {undefined} A callback when the whole packet has been acknowledged by the peer's Link Layer or been flushed due to disconnection of the link 289 | * Returns: {boolean} Whether the connection's GATT client was subscribed or not 290 | 291 | This method is attached by the stack to the characteristic object when the service is being added to the GATT db if it has the `notify` property. Calling it will notify the connection's GATT client with the new value. If the client wasn't subscribed, the method will do nothing and return false. 292 | 293 | If there is a pending Exchange MTU Request sent from this device, the notifications will be queued (per specification) and be sent when it completes. Otherwise the packet goes straight to the BLE connection's output buffer. In case you want to write a large amount of packets, you should wait for the `sentCallback` before you write another packet, to make it possible for the stack to interleave other kinds of packets. This does not decrease the throughput, as opposed to waiting for the `completeCallback` between packets. 294 | 295 | The value will be truncated to fit MTU - 3 bytes. 296 | 297 | ### characteristic.notifyAll(value) 298 | * `value` {Buffer} or {string} Value to notify 299 | 300 | This method is attached by the stack to the characteristic object when the service is being added to the GATT db if it has the `notify` property. Calling it will notify all subscribers with the new value. See the `notify` method for more information. 301 | 302 | ### characteristic.indicate(connection, value[, callback]) 303 | * `connection` {Connection} The BLE connection whose GATT client will be indicated 304 | * `value` {Buffer} or {string} Value to indicate 305 | * `sentCallback` {Function} or {undefined} A callback that will be called when the confirmation arrives 306 | * Returns: {boolean} Whether the connection's GATT client was subscribed or not 307 | 308 | This method is attached by the stack to the characteristic object when the service is being added to the GATT db if it has the `indicate` property. Calling it will indicate the connection's GATT client with the new value. If the client wasn't subscribed, the method will do nothing and return false. 309 | 310 | If there already is one or more pending indications or a pending Exchange MTU Request, the value will be enqueued and sent when the previous operations have completed. Otherwise the value is sent straight to the BLE connection's output buffer. 311 | 312 | The value will be truncated to fit MTU - 3 bytes. 313 | 314 | ### characteristic.indicateAll(value) 315 | * `value` {Buffer} or {string} Value to indicate 316 | 317 | This method is attached by the stack to the characteristic object when the service is being added to the GATT db if it has the `indicate` property. Calling it will indicate all subscribers with the new value. See the `indicate` method for more information. If you need the confirmation from the different connections, use the `indicate` method for each connection. 318 | 319 | ## Interface: GattServerDescriptor 320 | This interface describes the set of properties each object item in the array of `characteristics.descriptors` must have. 321 | 322 | The `uuid`, `maxLength`, `readPerm` and `writePerm` properties are only read and inspected during the service is being added. 323 | 324 | The Characteristic Extended Properties Descriptor is automatically added to a characteristic by the stack, if any declared properties needs it. This descriptor may not be added manually. 325 | 326 | The Client Characteristic Configuration Descriptor is automatically added to a characteristic by the stack, if the notify or indicate properties are declared. This will have open read and write permissions. If custom write permissions are needed, manually add a custom Client Characteristic Configuration Descriptor with the desired permissions. However, no other than the `uuid`, `writePerm` and `onAuthorizeWrite` properties will be used in this case. 327 | 328 | ### descriptor.uuid 329 | {string} or {number} 330 | 331 | UUID of the descriptor. Mandatory property. 332 | 333 | ### descriptor.maxLength 334 | Same API as `characteristic.maxLength`. 335 | 336 | ### descriptor.readPerm 337 | Same API as `characteristic.readPerm`. 338 | 339 | ### descriptor.writePerm 340 | Same API as `characteristic.writePerm`. 341 | 342 | ### descriptor.value 343 | Same API as `characteristic.value`. 344 | 345 | ### descriptor.onAuthorizeRead(connection, callback) 346 | Same API as `characteristic.onAuthorizeRead`. 347 | 348 | ### descriptor.onRead(connection, callback) 349 | Same API as `characteristic.onRead`. 350 | 351 | ### descriptor.onPartialRead(connection, offset, callback) 352 | Same API as `characteristic.onPartialRead`. 353 | 354 | ### descriptor.onAuthorizeWrite(connection, callback) 355 | Same API as `characteristic.onAuthorizeWrite`. 356 | 357 | ### descriptor.onWrite(connection, needsResponse, value, callback) 358 | Same API as `characteristic.onWrite`. 359 | 360 | ### descriptor.onPartialWrite(connection, needsResponse, offset, value, callback) 361 | Same API as `characteristic.onPartialWrite`. 362 | -------------------------------------------------------------------------------- /docs/api/l2cap-coc.md: -------------------------------------------------------------------------------- 1 | # L2CAP CoC 2 | 3 | This implements support for Connection-Oriented Channel in LE Credit Based Flow Control Mode. 4 | 5 | ### PSMs 6 | PSM is short for Protocol/Service Multiplexer and serves as an identifier of a protocol/service when establishing a CoC. The currently valid values for LE PSMs are 0x0001 - 0x007f for fixed Bluetooth SIG-defined services, and 0x0080 - 0x00ff for dynamic "custom" services. The establishment is server-client-based, where a server registers a PSM so that clients can later connect to this service. Usually, for custom services, the server selects a PSM value in the range 0x0080 to 0x00ff which should be placed in the GATT db (in an implementation-defined way) so that the client can obtain the PSM. For custom setups where interoperability is not required, the PSM can also be hardcoded by both the server and client. 7 | 8 | ## Class: L2CAPCoCManager 9 | 10 | This class is used to register PSMs and create CoC connections. Each BLE connection object has an instance of an L2CAPCoCManager, which can be retrieved through its `l2capCoCManager` property. 11 | 12 | ### l2capCoCManager.connect(lePsm, initiallyPaused, rxMtu, callback) 13 | * `lePsm` {number} Integer in the range 0x0001 - 0x00ff which identifies the protocol/service to connect to 14 | * `initiallyPaused` {boolean} Whether the RX flow is initially stopped (i.e. no initial credits given to peer) 15 | * `rxMtu` {number} 16-bit unsigned integer (at least 23) of how large each packet (SDU) the peer is allowed to send to us 16 | * `callback` {Function} Callback 17 | 18 | Creates a CoC between this device and the remote device, for a PSM that the remote device has registered. 19 | 20 | Callback should take two arguments `result` and `l2capCoC`, where `result` is an integer from `L2CAPCoCErrors` and `l2capCoC` is an `L2CAPCoC` object (`undefined` if result was not success). 21 | 22 | ### l2capCoCManager.registerLePsm(lePsm, onRequestCallback) 23 | * `lePsm` {number} Integer in the range 0x0001 - 0x00ff which identifies the protocol/service to register 24 | * `onRequestCallback` {Function} Callback 25 | 26 | Registers a PSM so that the remote peer can create a CoC using this PSM. If the PSM is already registered, the `onRequestCallback` will simply be replaced with the new one. 27 | 28 | `onRequestCallback` should take two arguments `txMtu` and `callback`. `txMtu` is the maximum packet (SDU) size the peer can receive. 29 | 30 | `callback` should be called in order to reject or accept the connection requests and takes the following arguments: 31 | * `result` {number} A result code from `L2CAPCoCErrors` 32 | * `initiallyPaused` {boolean} Whether the RX flow is initially stopped (i.e. no initial credits given to peer) 33 | * `rxMtu` {number} 16-bit unsigned integer (at least 23) of how large each packet (SDU) the peer is allowed to send to us 34 | * Returns: {L2CAPCoC} if result is success, otherwise `undefined` 35 | 36 | ### l2capCoCManager.unregisterLePsm(lePsm) 37 | * `lePsm` {number} A PSM previously registered 38 | 39 | Unregisters a PSM so that new connections are not allowed. Previously established CoCs are not affected. 40 | 41 | ## Class: L2CAPCoC 42 | 43 | This represents a Connection-Oriented Channel to the remote device with flow control. Flow control is possible due to each side has a credit balance which is decreased each time an LE-frame is sent. The receiving device will send new credits to the sending device when the sending device's credits are about to run out unless the receiving device has stopped the flow (because it needs time to process the data for example). 44 | 45 | This implementation has a `send` method which will always accept a packet (of size up to `txMtu`). If the credits have run out, it will instead be enqueued and the outgoing flow control will be managed internally. To add custom flow control, check the property `txCredits`. If it is positive, you may send a packet. If it is zero or negative, wait for the `credits` event and try again. 46 | 47 | For flow control in the receiving direction, there are the methods `pause` and `resume`. Between the calls to `pause` and `resume`, no `data` events will be emitted. 48 | 49 | ### l2capCoC.send(sdu[, sentCallback][, completeCallback]) 50 | * `sdu` {Buffer} A buffer of size up to `txMtu` to send 51 | * `sentCallback` {Function} or {undefined} A callback when the whole SDU has been sent to the controller 52 | * `completeCallback` {Function} or {undefined} A callback when the whole SDU has been acknowledged by the peer's Link Layer or been flushed due to disconnection of the link 53 | 54 | Sends a packet. The `txCredits` property will be decreased with as many LE-frames this SDU takes up. The number of LE-frames an SDU takes up can be calculated using `Math.ceil((2 + sdu.length) / l2capCoC.txMps)`. 55 | 56 | If the CoC has been disconnected, this method does nothing. 57 | 58 | ### l2capCoC.disconnect() 59 | Disconnects this CoC. No more `credits` events will be emitted from now on and all incoming data destinated to this CoC will be discarded. Enqueued outgoing SDUs will not be sent. To be sure the CoC is not disconnected before all outgoing data has been sent, wait for the `sentCallback` for the last outgoing SDU before disconnecting. 60 | 61 | The `disconnect` event will be emitted immediately. 62 | 63 | If the CoC has already been disconnected, this method does nothing. 64 | 65 | ### l2capCoC.pause() 66 | Pauses RX for this CoC. No `data` events will be emitted until `resume` has been called. No new credits will be given to the sender, but if packets are received (using credits that were left), those will be buffered internally and emitted when the `resume` method is called. 67 | 68 | ### l2capCoC.resume() 69 | Resumes RX for this CoC. `data` events will first be emitted immediately for possible SDUs that were received while the CoC was paused. From now on `data` events will be emitted directly when an SDU is received. In case the CoC or BLE link became disconnected while there were still buffered packets, those SDUs will still be emitted (immediately) to the `data` event. 70 | 71 | ### Event: 'data' 72 | * `sdu` {Buffer} An SDU received 73 | 74 | Emitted when an SDU is received. 75 | 76 | Note that **data will be lost** if there is no listener registered at the time `data` is emitted. 77 | 78 | ### Event: 'disconnect' 79 | Emitted when either the local or remote device disconnects the CoC, or when the remote device misbehaves (per specification). 80 | 81 | ### Event: 'credits' 82 | * `credits` {number} 16-bit unsigned integer with the number of credits to increase the current balance with 83 | 84 | Emitted when the remote device gives new credits. To get the new updated current TX credit balance, read the `txCredits` property. 85 | 86 | ### l2capCoC.txMps 87 | The TX MPS used. This is the maximum number of bytes of each TX LE-frame. 88 | 89 | ### l2capCoC.txMtu 90 | The TX MTU used. This is the maximum number of bytes of each TX SDU, i.e. the maximum size of the Buffer for the `send` method. 91 | 92 | ### l2capCoC.txCredits 93 | The current balance for TX packets. If this is positive, indicates how many LE-frames that can be sent directly without the need to be buffered. If this is negative, indicates how many LE-frames have been buffered, waiting for new credits to be received until they can be sent. 94 | 95 | ## Errors 96 | 97 | List of L2CAP CoC errors for accepting or rejecting a connection. 98 | 99 | ```javascript 100 | const NodeBleHost = require('ble-host'); 101 | const L2CAPCoCErrors = NodeBleHost.L2CAPCoCErrors; 102 | ``` 103 | 104 | ### Integer constants 105 | 106 | The defined constants below are properties of `L2CAPCoCErrors`. 107 | 108 | Bluetooth SIG assigned constants: 109 | 110 | ```javascript 111 | CONNECTION_SUCCESSFUL: 0 112 | LE_PSM_NOT_SUPPORTED: 2 113 | NO_RESOURCES_AVAILABLE: 4 114 | INSUFFICIENT_AUTHENTICATION: 5 115 | INSUFFICIENT_AUTHORIZATION: 6 116 | INSUFFICIENT_ENCRYPTION_KEY_SIZE: 7 117 | INSUFFICIENT_ENCRYPTION: 8 118 | INVALID_SOURCE_CID: 9 119 | SOURCE_CID_ALREADY_ALLOCATED: 10 120 | UNACCEPTABLE_PARAMETERS: 11 121 | ``` 122 | 123 | Custom constants: 124 | 125 | ```javascript 126 | TIMEOUT: -1 127 | ``` 128 | 129 | ### L2CAPCoCErrors.toString(code) 130 | * `code` {integer} Error code 131 | 132 | Returns the corresponding key (e.g. `CONNECTION_SUCCESSFUL`) for a given code, or `(unknown)` if not one of the above. 133 | -------------------------------------------------------------------------------- /docs/api/security-manager.md: -------------------------------------------------------------------------------- 1 | # Security Manager 2 | The Security Manager performs all the pairing, bonding and encryption setup logic. SMP is short for Security Manager Protocol. 3 | 4 | In SMP, the device in master role is called the initiator and the device in slave role is called the responder. 5 | 6 | ## Pairing features object 7 | Where the type PairingFeatures object is used, it should be an object having the following properties (all are optional and may therefore be undefined): 8 | * `ioCap` {number} A constant that describes the current device's IO capabilities (default: `IOCapabilities.NO_INPUT_NO_OUTPUT`), see `IOCapabilities` for allowed values 9 | * `bondingFlags` {number} 1 if a bond is requested, i.e. the resulting keys should be stored persistently, otherwise 0 (default: 1) 10 | * `mitm` {boolean} Whether MITM protection is requested (default: false) 11 | * `sc` {boolean} Whether Secure Connections is supported (default: true) 12 | * `keypress` {boolean} Whether Keypress Notifications are supported (default: false) 13 | * `maxKeySize` {number} Integer between 7 and 16 with max key size to be negotiated (default: 16) 14 | * `initKeyDistr` {Object} The keys which the initiator should distribute 15 | * `encKey` {boolean} Whether LTK should be distributed (default: true) 16 | * `idKey` {boolean} Whether IRK and Identity Address should be distributed (default: true) 17 | * `rspKeyDistr` {Object} The keys which the responder should distribute 18 | * `encKey` {boolean} Whether LTK should be distributed (default: true) 19 | * `idKey` {boolean} Whether IRK and Identity Address should be distributed (default: true) 20 | 21 | ## Class: SmpMasterConnection and SmpSlaveConnection 22 | Each BLE connection has one instance of either SmpMasterConnection or SmpSlaveConnection, depending on the role. The `smp` property of the BLE connection contains an SmpMasterConnection instance when the current role is master, otherwise it contains an SmpSlaveConnection instance. Their APIs are very similar. 23 | 24 | ### smp.setAvailableLtk(ltk, rand, ediv, mitm, sc) 25 | * `ltk` {Buffer} An LTK of length 7 to 16 26 | * `rand` {Buffer} The Random value of length 8 that identifies the LTK 27 | * `ediv` {number} The Encrypted Diversifier that identifies the LTK (16-bit unsigned integer) 28 | * `mitm` {boolean} Whether MITM protection was used when the pairing resulting in the LTK was performed 29 | * `sc` {boolean} Whether the Secure Connections pairing model was used to generate the LTK 30 | 31 | This method does normally not need to be called since an available LTK is always read from the bond storage. It can however be used for debugging purposes or if a custom LTK should be used. After this method has been called, this LTK will be used when encryption is later started. 32 | 33 | ### smp.startEncryption() 34 | * Returns: {boolean} If SMP was in the idle state, true. Otherwise false is returned and this method does nothing. 35 | 36 | Starts the encryption for a connection to a device that is bonded. Typically this method is called right after a connection has been established or before a request to a GATT characteristic that needs an encrypted link should be performed. 37 | 38 | The `'encrypt'` event will be emitted when the operation completes. 39 | 40 | Note: This method is only available for the master role (for the slave role, use `sendSecurityRequest` instead). An LTK must be available, otherwise an error will be thrown. Use the `hasLtk` property to see whether an LTK is available. Beware that unencrypted packets may arrive before the encryption is started. 41 | 42 | ### smp.sendPairingRequest(req) 43 | * `req` {PairingFeatures} The pairing features 44 | * Returns: {boolean} true if an ongoing pairing procedure is not outstanding, otherwise false and this method does nothing 45 | 46 | This method starts the pairing procedure. All properties of the `req` object are optional. For most use cases, the `ioCap` and the `mitm` properties are the only ones that need to be customised. 47 | 48 | Note: This method is only available for the master role. 49 | 50 | ### smp.sendSecurityRequest(bond, mitm, sc, keypress) 51 | * `bond` {boolean} Whether a bond is requested, i.e. if pairing is to be performed, the resulting keys should be stored persistently 52 | * `mitm` {boolean} Whether MITM protection is requested 53 | * `sc` {boolean} Whether the Secure Connections pairing model is supported 54 | * `keypress` {boolean} Whether Keypress Notifications are supported 55 | 56 | If the SMP is in the idle state, this method will send a Security Request to the master device. If the master has an LTK for this device, it should start encryption. If not (or if the link is already encrypted), it should start pairing. 57 | 58 | ### smp.sendPairingFailed(reason) 59 | * `reason` {number} Error code identifying why the pairing failed (see the Errors section below for a list of codes) 60 | 61 | If there is an ongoing pairing procedure, cancels this and the `'pairingFail'` event will be emitted when the pairing has successfully been cancelled. 62 | 63 | ### smp.sendKeypressNotification(notificationType) 64 | * `notificationType` {number} Notification Type 65 | 66 | If both devices have set the `keypress` flag in the pairing request/response, this method will send keypress notifications to the remote device. Available notification types: 67 | 68 | * 0: Passkey entry started 69 | * 1: Passkey digit entered 70 | * 2: Passkey digit erased 71 | * 3: Passkey cleared 72 | * 4: Passkey entry completed 73 | 74 | ### smp.isEncrypted 75 | {boolean} 76 | 77 | Whether the current link is encrypted or not. 78 | 79 | ### smp.currentEncryptionLevel 80 | {Object} 81 | * `mitm` {boolean} Whether MITM protection was used for the key in use 82 | * `sc` {boolean} Whether Secure Connections were used to generate the key in use 83 | * `keySize` {number} The key size of the key in use 84 | 85 | The current encryption level. This value will be `null` if the link is currently unencrypted. 86 | 87 | ### smp.isBonded 88 | {boolean} 89 | 90 | Whether a bond exists to the current device. 91 | 92 | ### smp.hasLtk 93 | {boolean} 94 | 95 | Whether a bond exists to the current device and an LTK is available that can be used to start the encryption. 96 | 97 | ### smp.availableLtkSecurityLevel 98 | {Object} 99 | * `mitm` {boolean} Whether MITM protection was used for the key available 100 | * `sc` {boolean} Whether Secure Connections were used to generate the key available 101 | * `keySize` {number} The key size of the key available 102 | 103 | The security properties for the available LTK, that can be used to start the encryption. This value will be `null` if no key exists. 104 | 105 | ### Event: 'pairingRequest' 106 | For a device in the slave role: 107 | * `req` {PairingFeatures} The pairing request 108 | * `callback` {Function} Callback 109 | * `rsp` {PairingFeatures} The pairing response 110 | 111 | This event will be emitted when the initiator sends a Pairing Request. The callback should be called with the responder's pairing features. The features that actually will be used are combined from the request and the response. It's also possible to call the `sendPairingFailed` method if the requested security is below the required level. 112 | 113 | If there is no listener for this event, the callback will automatically be called with a response where all values are the default. 114 | 115 | For a device in the master role: 116 | * `secReq` {Object} The security request 117 | * `callback` {Function} Callback 118 | * `req` {PairingFeatures} The pairing request 119 | 120 | This event will be emitted when the responder sends a Security Request and when the local or remote device does not possess an LTK with the requested security level (according to `secReq`). The `secReq` will contain the same kind of object as a `PairingFeatures`, but only `bondingFlags`, `mitm`, `sc` and `keypress` will be present. Either the callback should be called, or the `sendPairingRequest` should be called with the pairing features of the initiator. It's also possible to call the `sendPairingFailed` method if the master does not want to initiate a pairing procedure. 121 | 122 | If there is no listener for this event, the callback will automatically be called with a request where all values are the default. But if the device is bonded in this case, the pairing will fail if the `mitm` flag is false, the current link is not encrypted, or the used association model would be Just Works. 123 | 124 | ### Event: 'validatePairingFeatures' 125 | * `pairingFeatures` {PairingFeatures} The combined pairing features 126 | * `callback` {Function} Callback for accepting the pairing features 127 | 128 | This event will be emitted when the pairing features have been combined from the request and response. If the device accepts the pairing features, the callback should be called. Otherwise the `sendPairingFailed` method should be called. 129 | 130 | If there is no listener for this event, the pairing features will automatically be accepted, unless for some cases if the device is already bonded and the current role is master (see `pairingRequest` under no listener). 131 | 132 | ### Event: 'passkeyExchange' 133 | * `associationModel` {number} A constant defining which association model being used, see `AssociationModels` 134 | * `userPasskey` {string} or {null} A passkey to display to the user 135 | * `callback` {Function} Callback for passing an entered passkey 136 | * `passkeyResponse` {number}, {string} or {undefined} A 6-digit passkey the user has entered or `undefined` if the numeric comparison association model is used 137 | 138 | This event is emitted when the Passkey Exchange starts. For the relevant association models, the `userPasskey` should be displayed. If the user enters a passkey according to the association model, the callback should be called with the passkey the user enters. For the numeric comparison association model, the callback should be called with no parameters if the user confirms that both devices' values are equal, and otherwise call `sendPairingFailed` with the Numeric Comparison Failed error code. 139 | 140 | ### Event: 'keypress' 141 | * `notificationType` {number} Notification Type 142 | 143 | Can be emitted during the Passkey Exchange when the remote device sends passkey notifications (and both devices support these). See `sendKeypressNotification` for possible notification types. 144 | 145 | ### Event: 'pairingComplete' 146 | * `res` {Object} Result 147 | * `sc` {boolean} Whether Secure Connections were used 148 | * `mitm` {boolean} Whether MITM protection was used 149 | * `bond` {boolean} Whether bonding occurred 150 | * `rspEdiv` {number} or {null} 16-bit unsigned Encrypted Diversifier that identifies the responder's LTK 151 | * `rspRand` {Buffer} or {null} 8-byte Random Value that identifies the responder's LTK 152 | * `rspLtk` {Buffer} or {null} The responder's LTK 153 | * `initEdiv` {number} or {null} 16-bit unsigned Encrypted Diversifier that identifies the initiator's LTK 154 | * `initRand` {Buffer} or {null} 8-byte Random Value that identifies the initiator's LTK 155 | * `initLtk` {Buffer} or {null} The initiator's LTK 156 | * `rspIrk` {Buffer}, {null} or {undefined} The responder's IRK 157 | * `rspIdentityAddress` {Object}, {null} or {undefined} The responder's identity address 158 | * `addressType` {string} `public` or `random` 159 | * `address` {string} BD ADDR 160 | * `initIrk` {Buffer}, {null} or {undefined} The initiator's IRK 161 | * `initIdentityAddress` {Object}, {null} or {undefined} The initiator's identity address 162 | * `addressType` {string} `public` or `random` 163 | * `address` {string} BD ADDR 164 | 165 | This event is emitted when the pairing has completed successfully. The `rspIrk` and `rspIdentityAddress` will be `undefined` if the current device is the responder, and the `initIrk` and `initIdentityAddress` will be `undefined` if the current device is the initiator. The other keys and key identifiers will be present if those were distributed, otherwise `null`. 166 | 167 | ### Event: 'pairingFailed' 168 | * `reason` {number} Reason code (see the Errors section below for a list of codes) 169 | * `isErrorFromRemote` {boolean} If the remote device sent a pairing failed command, otherwise the local device sent the failed command 170 | 171 | The pairing has failed and the state for SMP is now idle. 172 | 173 | ### Event: 'encrypt' 174 | * `status` {number} HCI status code 175 | * `currentEncryptionLevel` {Object} or {undefined} Contains the current encryption level, if success 176 | * `mitm` {boolean} Whether MITM protection was used for the key in use 177 | * `sc` {boolean} Whether Secure Connections were used to generate the key in use 178 | * `keySize` {number} The key size of the key in use 179 | 180 | Emitted when the encryption has started, or encryption failed to start. A common error code for when encryption fails to start is the Pin or Key Missing error code. 181 | 182 | When in the slave role, the only possible non-success error code is `HciErrors.PIN_OR_KEY_MISSING` which will be emitted when the master tries to start encryption for a key we don't possess. 183 | 184 | ### Event: 'timeout' 185 | Emitted when the pairing protocol times out (30 seconds after the last packet). When this happens, no more SMP packets can be exchanged anymore on this link. If there are no listeners for this event, the BLE connection will be disconnected. 186 | 187 | ## Errors 188 | 189 | List of reason codes why pairing failed. 190 | 191 | ```javascript 192 | const NodeBleHost = require('ble-host'); 193 | const SmpErrors = NodeBleHost.SmpErrors; 194 | ``` 195 | 196 | ### Integer constants 197 | 198 | The defined constants below are properties of `SmpErrors`. 199 | 200 | Bluetooth SIG assigned constants: 201 | 202 | ```javascript 203 | PASSKEY_ENTRY_FAILED: 0x01 204 | OOB_NOT_AVAILABLE: 0x02 205 | AUTHENTICATION_REQUIREMENTS: 0x03 206 | CONFIRM_VALUE_FAILED: 0x04 207 | PAIRING_NOT_SUPPORTED: 0x05 208 | ENCRYPTION_KEY_SIZE: 0x06 209 | COMMAND_NOT_SUPPORTED: 0x07 210 | UNSPECIFIED_REASON: 0x08 211 | REPEATED_ATTEMPTS: 0x09 212 | INVALID_PARAMETERS: 0x0a 213 | DHKEY_CHECK_FAILED: 0x0b 214 | NUMERIC_COMPARISON_FAILED: 0x0c 215 | BR_EDR_PAIRING_IN_PROGRESS: 0x0d 216 | CROSS_TRANSPORT_KEY_DERIVATION_GENERATION_NOT_ALLOWED: 0x0e 217 | ``` 218 | 219 | ### SmpErrors.toString(code) 220 | * `code` {integer} Error code 221 | 222 | Returns the corresponding key (e.g. `PASSKEY_ENTRY_FAILED`) for a given code, or `(unknown)` if not one of the above. 223 | 224 | ## IOCapabilities 225 | 226 | Enumeration of I/O capabilities. 227 | 228 | ```javascript 229 | const NodeBleHost = require('ble-host'); 230 | const IOCapabilities = NodeBleHost.IOCapabilities; 231 | ``` 232 | 233 | ```javascript 234 | DISPLAY_ONLY: 0x00 235 | DISPLAY_YES_NO: 0x01 236 | KEYBOARD_ONLY: 0x02 237 | NO_INPUT_NO_OUTPUT: 0x03 238 | KEYBOARD_DISPLAY: 0x04 239 | ``` 240 | 241 | ### IOCapabilities.toString(value) 242 | * `value` {integer} Feature 243 | 244 | Returns the corresponding key (e.g. `DISPLAY_ONLY`) for a given code, or `(unknown)` if not one of the above. 245 | 246 | ## AssociationModels 247 | 248 | Enumeration of association models. 249 | 250 | ```javascript 251 | const NodeBleHost = require('ble-host'); 252 | const AssociationModels = NodeBleHost.AssociationModels; 253 | ``` 254 | 255 | ```javascript 256 | JUST_WORKS: 0 257 | PASSKEY_ENTRY_INIT_INPUTS: 1 258 | PASSKEY_ENTRY_RSP_INPUTS: 2 259 | PASSKEY_ENTRY_BOTH_INPUTS: 3 260 | NUMERIC_COMPARISON: 4 261 | ``` 262 | 263 | ### AssociationModels.toString(value) 264 | * `value` {integer} Association model 265 | 266 | Returns the corresponding key (e.g. `JUST_WORKS`) for a given code, or `(unknown)` if not one of the above. 267 | -------------------------------------------------------------------------------- /lib/advertising-data-builder.js: -------------------------------------------------------------------------------- 1 | const utils = require('./internal/utils'); 2 | 3 | const serializeUuid = utils.serializeUuid; 4 | const bdAddrToBuffer = utils.bdAddrToBuffer; 5 | const isValidBdAddr = utils.isValidBdAddr; 6 | 7 | function get16BitUuid(uuid) { 8 | var serialized = serializeUuid(uuid); 9 | if (serialized.length != 2) { 10 | throw new Error('Not a valid 16-bit uuid: ' + uuid); 11 | } 12 | return serialized; 13 | } 14 | 15 | function get128BitUuid(uuid) { 16 | uuid = uuid.replace(/-/g, ''); 17 | var serialized = Buffer.from(uuid, 'hex').reverse(); 18 | if (serialized.length != 16) { 19 | throw new Error('Not a valid 128-bit uuid: ' + uuid); 20 | } 21 | return serialized; 22 | } 23 | 24 | function AdvertisingDataBuilder() { 25 | var data = Buffer.alloc(0); 26 | var hasFlags = false; 27 | 28 | function addData(type, buf) { 29 | if (data.length + 2 + buf.length > 31) { 30 | throw new Error('This item does not fit, needs ' + (2 + buf.length) + ' bytes but have only ' + (31 - data.length) + ' left'); 31 | } 32 | data = Buffer.concat([data, Buffer.from([1 + buf.length, type]), buf]); 33 | } 34 | 35 | this.addFlags = function(flags) { 36 | var keys = ['leLimitedDiscoverableMode', 37 | 'leGeneralDiscoverableMode', 38 | 'brEdrNotSupported', 39 | 'simultaneousLeAndBdEdrToSameDeviceCapableController', 40 | 'simultaneousLeAndBrEdrToSameDeviceCapableHost']; 41 | 42 | var flagsByte = 0; 43 | 44 | for (var i = 0; i < keys.length; i++) { 45 | if (flags.includes(keys[i])) { 46 | flagsByte |= 1 << i; 47 | } 48 | } 49 | 50 | if (hasFlags) { 51 | throw new Error('Already has flags'); 52 | } 53 | 54 | addData(0x01, Buffer.from([flagsByte])); 55 | return this; 56 | }; 57 | 58 | this.add16BitServiceUUIDs = function(isComplete, uuids) { 59 | var buf = Buffer.concat(uuids.map(uuid => get16BitUuid(uuid))); 60 | addData(isComplete ? 0x03 : 0x02, buf); 61 | return this; 62 | }; 63 | 64 | this.add128BitServiceUUIDs = function(isComplete, uuids) { 65 | var buf = Buffer.concat(uuids.map(uuid => get128BitUuid(uuid))); 66 | addData(isComplete ? 0x07 : 0x06, buf); 67 | return this; 68 | }; 69 | 70 | this.addLocalName = function(isComplete, name) { 71 | addData(isComplete ? 0x09 : 0x08, Buffer.from(name)); 72 | return this; 73 | }; 74 | 75 | this.addManufacturerData = function(companyIdentifierCode, data) { 76 | addData(0xff, Buffer.concat([Buffer.from([companyIdentifierCode, companyIdentifierCode >> 8]), data])); 77 | return this; 78 | }; 79 | 80 | this.addTxPowerLevel = function(txPowerLevel) { 81 | addData(0x0a, Buffer.from([txPowerLevel])); 82 | return this; 83 | }; 84 | 85 | this.addSlaveConnectionIntervalRange = function(connIntervalMin, connIntervalMax) { 86 | var buf = Buffer.alloc(4); 87 | buf.writeUInt16LE(connIntervalMin, 0); 88 | buf.writeUInt16LE(connIntervalMax, 2); 89 | addData(0x12, buf); 90 | return this; 91 | }; 92 | 93 | this.add16BitServiceSolicitation = function(uuids) { 94 | var buf = Buffer.concat(uuids.map(uuid => get16BitUuid(uuid))); 95 | addData(0x14, buf); 96 | return this; 97 | }; 98 | 99 | this.add128BitServiceSolicitation = function(uuids) { 100 | var buf = Buffer.concat(uuids.map(uuid => get128BitUuid(uuid))); 101 | addData(0x15, buf); 102 | return this; 103 | }; 104 | 105 | this.add16BitServiceData = function(uuid, data) { 106 | addData(0x16, Buffer.concat([get16BitUuid(uuid), data])); 107 | return this; 108 | }; 109 | 110 | this.add128BitServiceData = function(uuid, data) { 111 | addData(0x21, Buffer.concat([get128BitUuid(uuid), data])); 112 | return this; 113 | }; 114 | 115 | this.addAppearance = function(appearanceNumber) { 116 | addData(0x19, Buffer.from([appearanceNumber, appearanceNumber >> 8])); 117 | return this; 118 | }; 119 | 120 | this.addPublicTargetAddresses = function(addresses) { 121 | addresses = Buffer.concat(addresses.map(address => { 122 | if (!isValidBdAddr(address)) { 123 | throw new Error('Invalid address: ' + address); 124 | } 125 | return bdAddrToBuffer(address); 126 | })); 127 | addData(0x17, addresses); 128 | return this; 129 | }; 130 | 131 | this.addRandomTargetAddresses = function(addresses) { 132 | addresses = Buffer.concat(addresses.map(address => { 133 | if (!isValidBdAddr(address)) { 134 | throw new Error('Invalid address: ' + address); 135 | } 136 | return bdAddrToBuffer(address); 137 | })); 138 | addData(0x18, addresses); 139 | return this; 140 | }; 141 | 142 | this.addAdvertisingInterval = function(interval) { 143 | addData(0x1a, Buffer.from([interval, interval >> 8])); 144 | return this; 145 | }; 146 | 147 | this.addUri = function(uri) { 148 | addData(0x24, Buffer.from(uri)); 149 | return this; 150 | }; 151 | 152 | this.addLeSupportedFeatures = function(low, high) { 153 | var buf = Buffer.alloc(8); 154 | buf.writeUInt32LE(low, 0); 155 | buf.writeUInt32LE(high, 4); 156 | var len; 157 | for (len = 8; len > 0; len--) { 158 | if (buf[len - 1] != 0) { 159 | break; 160 | } 161 | } 162 | addData(0x27, buf.slice(0, len)); 163 | return this; 164 | }; 165 | 166 | this.build = function() { 167 | return Buffer.from(data); 168 | }; 169 | } 170 | 171 | module.exports = AdvertisingDataBuilder; 172 | -------------------------------------------------------------------------------- /lib/association-models.js: -------------------------------------------------------------------------------- 1 | var obj = Object.freeze({ 2 | JUST_WORKS: 0, 3 | PASSKEY_ENTRY_INIT_INPUTS: 1, 4 | PASSKEY_ENTRY_RSP_INPUTS: 2, 5 | PASSKEY_ENTRY_BOTH_INPUTS: 3, 6 | NUMERIC_COMPARISON: 4, 7 | 8 | toString: function(v) { 9 | for (var key in obj) { 10 | if (obj.hasOwnProperty(key) && key != "toString" && obj[key] == v) { 11 | return key; 12 | } 13 | } 14 | return "(unknown)"; 15 | } 16 | }); 17 | 18 | module.exports = obj; 19 | -------------------------------------------------------------------------------- /lib/att-errors.js: -------------------------------------------------------------------------------- 1 | var obj = Object.freeze({ 2 | SUCCESS: 0x00, 3 | INVALID_HANDLE: 0x01, 4 | READ_NOT_PERMITTED: 0x02, 5 | WRITE_NOT_PERMITTED: 0x03, 6 | INVALID_PDU: 0x04, 7 | INSUFFICIENT_AUTHENTICATION: 0x05, 8 | REQUEST_NOT_SUPPORTED: 0x06, 9 | INVALID_OFFSET: 0x07, 10 | INSUFFICIENT_AUTHORIZATION: 0x08, 11 | PREPARE_QUEUE_FULL: 0x09, 12 | ATTRIBUTE_NOT_FOUND: 0x0a, 13 | ATTRIBUTE_NOT_LONG: 0x0b, 14 | INSUFFICIENT_ENCRYPTION_KEY_SIZE: 0x0c, 15 | INVALID_ATTRIBUTE_VALUE_LENGTH: 0x0d, 16 | UNLIKELY_ERROR: 0x0e, 17 | INSUFFICIENT_ENCRYPTION: 0x0f, 18 | UNSUPPORTED_GROUP_TYPE: 0x10, 19 | INSUFFICIENT_RESOURCES: 0x11, 20 | 21 | WRITE_REQUEST_REJECTED: 0xfc, 22 | CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_IMPROPERLY_CONFIGURED: 0xfd, 23 | PROCEDURE_ALREADY_IN_PROGRESS: 0xfe, 24 | OUT_OF_RANGE: 0xff, 25 | 26 | RELIABLE_WRITE_RESPONSE_NOT_MATCHING: -1, 27 | 28 | toString: function(v) { 29 | for (var key in obj) { 30 | if (obj.hasOwnProperty(key) && key != "toString" && obj[key] == v) { 31 | return key; 32 | } 33 | } 34 | if (v >= 0x80 && v <= 0x9f) { 35 | return "APPLICATION_ERROR_0x" + v.toString(16).toUpperCase(); 36 | } 37 | return "(unknown)"; 38 | } 39 | }); 40 | 41 | module.exports = obj; 42 | -------------------------------------------------------------------------------- /lib/hci-errors.js: -------------------------------------------------------------------------------- 1 | var obj = Object.freeze({ 2 | SUCCESS: 0x00, 3 | UNKNOWN_CONNECTION_IDENTIFIER: 0x02, 4 | HARDWARE_FAILURE: 0x03, 5 | AUTHENTICATION_FAILURE: 0x05, 6 | PIN_OR_KEY_MISSING: 0x06, 7 | MEMORY_CAPACITY_EXCEEDED: 0x07, 8 | CONNECTION_TIMEOUT: 0x08, 9 | CONNECTION_LIMIT_EXCEEDED: 0x09, 10 | CONNECTION_ALREADY_EXISTS: 0x0B, 11 | COMMAND_DISALLOWED: 0x0C, 12 | CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES: 0x0D, 13 | CONNECTION_REJECTED_DUE_TO_SECURITY_REASONS: 0x0E, 14 | UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE: 0x11, 15 | INVALID_HCI_COMMAND_PARAMETERS: 0x12, 16 | REMOTE_USER_TERMINATED_CONNECTION: 0x13, 17 | REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES: 0x14, 18 | REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_POWER_OFF: 0x15, 19 | CONNECTION_TERMINATED_BY_LOCAL_HOST: 0x16, 20 | REPEATED_ATTEMPTS: 0x17, 21 | PAIRING_NOT_ALLOWED: 0x18, 22 | UNSUPPORTED_REMOTE_FEATURE: 0x1A, 23 | INVALID_LL_PARAMETERS: 0x1E, 24 | UNSPECIFIED_ERROR: 0x1F, 25 | UNSUPPORTED_LL_PARAMETER_VALUE: 0x20, 26 | LL_RESPONSE_TIMEOUT: 0x22, 27 | LL_PROCEDURE_COLLISION: 0x23, 28 | INSTANT_PASSED: 0x28, 29 | DIFFERENT_TRANSACTION_COLLISION: 0x2A, 30 | PARAMETER_OUT_OF_MANDATORY_RANGE: 0x30, 31 | CONTROLLER_BUSY: 0x3A, 32 | UNACCEPTABLE_CONNECTION_PARAMETERS: 0x3B, 33 | ADVERTISING_TIMEOUT: 0x3C, 34 | CONNECTION_TERMINATED_DUE_TO_MIC_FAILURE: 0x3D, 35 | CONNECTION_FAILED_TO_BE_ESTABLISHED: 0x3E, 36 | LIMIT_REACHED: 0x43, 37 | OPERATION_CANCELLED_BY_HOST: 0x44, 38 | PACKET_TOO_LONG: 0x45, 39 | 40 | toString: function(v) { 41 | for (var key in obj) { 42 | if (obj.hasOwnProperty(key) && key != "toString" && obj[key] == v) { 43 | return key; 44 | } 45 | } 46 | return "(unknown)"; 47 | } 48 | }); 49 | 50 | module.exports = obj; 51 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | BleManager: require('./ble-manager'), 3 | HciErrors: require('./hci-errors'), 4 | AdvertisingDataBuilder: require('./advertising-data-builder'), 5 | AttErrors: require('./att-errors'), 6 | L2CAPCoCErrors: require('./l2cap-coc-errors'), 7 | SmpErrors: require('./smp-errors'), 8 | IOCapabilities: require('./io-capabilities'), 9 | AssociationModels: require('./association-models') 10 | }; 11 | -------------------------------------------------------------------------------- /lib/internal/adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Requirements for transport: 3 | * Needs write(Buffer) function 4 | * Needs 'data' event, where the only parameter of type Buffer is a complete HCI packet 5 | */ 6 | 7 | const EventEmitter = require('events'); 8 | const util = require('util'); 9 | 10 | const HciErrors = require('../hci-errors'); 11 | const Queue = require('./utils').Queue; 12 | 13 | const HCI_COMMAND_PKT = 0x01; 14 | const HCI_ACLDATA_PKT = 0x02; 15 | const HCI_EVENT_PKT = 0x04; 16 | 17 | const EVT_DISCONNECTION_COMPLETE = 0x05; 18 | const EVT_ENCRYPTION_CHANGE = 0x08; 19 | const EVT_READ_REMOTE_VERSION_INFORMATION_COMPLETE = 0x0c; 20 | const EVT_CMD_COMPLETE = 0x0e; 21 | const EVT_CMD_STATUS = 0x0f; 22 | const EVT_HARDWARE_ERROR = 0x10; 23 | const EVT_NUMBER_OF_COMPLETE_PACKETS = 0x13; 24 | const EVT_ENCRYPTION_KEY_REFRESH_COMPLETE = 0x30; 25 | const EVT_LE_META = 0x3e; 26 | 27 | const EVT_LE_CONNECTION_COMPLETE = 0x01; 28 | const EVT_LE_ADVERTISING_REPORT = 0x02; 29 | const EVT_LE_CONNECTION_UPDATE_COMPLETE = 0x03; 30 | const EVT_LE_READ_REMOTE_USED_FEATURES_COMPLETE = 0x04; 31 | const EVT_LE_LONG_TERM_KEY_REQUEST = 0x05; 32 | const EVT_LE_READ_LOCAL_P256_PUBLIC_KEY_COMPLETE = 0x08; 33 | const EVT_LE_GENERATE_DHKEY_COMPLETE = 0x09; 34 | const EVT_LE_ENHANCED_CONNECTION_COMPLETE = 0x0a; 35 | const EVT_LE_PHY_UPDATE_COMPLETE = 0x0c; 36 | const EVT_LE_EXTENDED_ADVERTISING_REPORT = 0x0d; 37 | 38 | const OGF_LINK_CTL = 0x01; 39 | const OGF_HOST_CTL = 0x03; 40 | const OGF_INFO_PARAM = 0x04; 41 | const OGF_STATUS_PARAM = 0x05; 42 | const OGF_LE_CTL = 0x08; 43 | 44 | const DISCONNECT_CMD = 0x0006 | (OGF_LINK_CTL << 10); 45 | const READ_REMOTE_VERSION_INFORMATION_CMD = 0x001d | (OGF_LINK_CTL << 10); 46 | 47 | const SET_EVENT_MASK_CMD = 0x0001 | (OGF_HOST_CTL << 10); 48 | const RESET_CMD = 0x0003 | (OGF_HOST_CTL << 10); 49 | 50 | const READ_LOCAL_VERSION_INFORMATION_CMD = 0x0001 | (OGF_INFO_PARAM << 10); 51 | const READ_BUFFER_SIZE_CMD = 0x0005 | (OGF_INFO_PARAM << 10); 52 | const READ_BD_ADDR_CMD = 0x0009 | (OGF_INFO_PARAM << 10); 53 | 54 | const READ_RSSI_CMD = 0x0005 | (OGF_STATUS_PARAM << 10); 55 | 56 | const LE_SET_EVENT_MASK_CMD = 0x0001 | (OGF_LE_CTL << 10); 57 | const LE_READ_BUFFER_SIZE_CMD = 0x0002 | (OGF_LE_CTL << 10); 58 | const LE_READ_LOCAL_SUPPORTED_FEATURES_CMD = 0x0003 | (OGF_LE_CTL << 10); 59 | const LE_SET_RANDOM_ADDRESS_CMD = 0x0005 | (OGF_LE_CTL << 10); 60 | const LE_SET_ADVERTISING_PARAMETERS_CMD = 0x0006 | (OGF_LE_CTL << 10); 61 | const LE_READ_ADVERTISING_CHANNEL_TX_POWER_CMD = 0x0007 | (OGF_LE_CTL << 10); 62 | const LE_SET_ADVERTISING_DATA_CMD = 0x0008 | (OGF_LE_CTL << 10); 63 | const LE_SET_SCAN_RESPONSE_DATA_CMD = 0x0009 | (OGF_LE_CTL << 10); 64 | const LE_SET_ADVERTISING_ENABLE_CMD = 0x000a | (OGF_LE_CTL << 10); 65 | const LE_SET_SCAN_PARAMETERS_CMD = 0x000b | (OGF_LE_CTL << 10); 66 | const LE_SET_SCAN_ENABLE_CMD = 0x000c | (OGF_LE_CTL << 10); 67 | const LE_CREATE_CONNECTION_CMD = 0x000d | (OGF_LE_CTL << 10); 68 | const LE_CREATE_CONNECTION_CANCEL_CMD = 0x000e | (OGF_LE_CTL << 10); 69 | const LE_READ_WHITE_LIST_SIZE_CMD = 0x000f | (OGF_LE_CTL << 10); 70 | const LE_CLEAR_WHITE_LIST_CMD = 0x0010 | (OGF_LE_CTL << 10); 71 | const LE_ADD_DEVICE_TO_WHITE_LIST_CMD = 0x0011 | (OGF_LE_CTL << 10); 72 | const LE_REMOVE_DEVICE_FROM_WHITE_LIST_CMD = 0x0012 | (OGF_LE_CTL << 10); 73 | const LE_CONNECTION_UPDATE_CMD = 0x0013 | (OGF_LE_CTL << 10); 74 | const LE_READ_REMOTE_USED_FEATURES_CMD = 0x0016 | (OGF_LE_CTL << 10); 75 | const LE_START_ENCRYPTION_CMD = 0x0019 | (OGF_LE_CTL << 10); 76 | const LE_LONG_TERM_KEY_REQUEST_REPLY_CMD = 0x001a | (OGF_LE_CTL << 10); 77 | const LE_LONG_TERM_KEY_REQUEST_NEGATIVE_REPLY_CMD = 0x001b | (OGF_LE_CTL << 10); 78 | const LE_READ_SUPPORTED_STATES_CMD = 0x001c | (OGF_LE_CTL << 10); 79 | const LE_SET_DATA_LENGTH_CMD = 0x0022 | (OGF_LE_CTL << 10); 80 | const LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_CMD = 0x0023 | (OGF_LE_CTL << 10); 81 | const LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_CMD = 0x0024 | (OGF_LE_CTL << 10); 82 | const LE_READ_LOCAL_P256_PUBLIC_KEY_CMD = 0x0025 | (OGF_LE_CTL << 10); 83 | const LE_GENERATE_DHKEY_CMD = 0x0026 | (OGF_LE_CTL << 10); 84 | const LE_READ_MAXIMUM_DATA_LENGTH_CMD = 0x002F | (OGF_LE_CTL << 10); 85 | const LE_SET_DEFAULT_PHY_CMD = 0x0031 | (OGF_LE_CTL << 10); 86 | const LE_SET_PHY_CMD = 0x0032 | (OGF_LE_CTL << 10); 87 | const LE_SET_EXTENDED_ADVERTISING_PARAMETERS_CMD = 0x0036 | (OGF_LE_CTL << 10); 88 | const LE_SET_EXTENDED_ADVERTISING_ENABLE_CMD = 0x0039 | (OGF_LE_CTL << 10); 89 | const LE_SET_EXTENDED_SCAN_PARAMETERS_CMD = 0x0041 | (OGF_LE_CTL << 10); 90 | const LE_SET_EXTENDED_SCAN_ENABLE_CMD = 0x0042 | (OGF_LE_CTL << 10); 91 | const LE_EXTENDED_CREATE_CONNECTION_CMD = 0x0043 | (OGF_LE_CTL << 10); 92 | 93 | const ROLE_MASTER = 0x00; 94 | const ROLE_SLAVE = 0x01; 95 | 96 | const EMPTY_BUFFER = Buffer.from([]); 97 | 98 | function isDisconnectErrorCode(c) { 99 | switch (c) { 100 | case HciErrors.CONNECTION_TIMEOUT: 101 | case HciErrors.REMOTE_USER_TERMINATED_CONNECTION: 102 | case HciErrors.REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES: 103 | case HciErrors.REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_POWER_OFF: 104 | case HciErrors.CONNECTION_TERMINATED_BY_LOCAL_HOST: 105 | case HciErrors.UNSUPPORTED_REMOTE_FEATURE: 106 | case HciErrors.LL_RESPONSE_TIMEOUT: 107 | case HciErrors.LL_PROCEDURE_COLLISION: 108 | case HciErrors.INSTANT_PASSED: 109 | case HciErrors.UNACCEPTABLE_CONNECTION_PARAMETERS: 110 | case HciErrors.CONNECTION_TERMINATED_DUE_TO_MIC_FAILURE: 111 | case HciErrors.CONNECTION_FAILED_TO_BE_ESTABLISHED: 112 | return true; 113 | } 114 | return false; 115 | } 116 | 117 | function PacketWriter() { 118 | var buf = []; 119 | this.u8 = function(v) { 120 | buf.push(v); 121 | return this; 122 | }; 123 | this.i8 = function(v) { 124 | buf.push(v); 125 | return this; 126 | }; 127 | this.u16 = function(v) { 128 | buf.push(v & 0xff); 129 | buf.push((v >>> 8) & 0xff); 130 | return this; 131 | }; 132 | this.u24 = function(v) { 133 | buf.push(v & 0xff); 134 | buf.push((v >>> 8) & 0xff); 135 | buf.push((v >>> 16) & 0xff); 136 | return this; 137 | }; 138 | this.u32 = function(v) { 139 | buf.push(v & 0xff); 140 | buf.push((v >>> 8) & 0xff); 141 | buf.push((v >>> 16) & 0xff); 142 | buf.push((v >>> 24) & 0xff); 143 | return this; 144 | }; 145 | this.bdAddr = function(v) { 146 | for (var i = 15; i >= 0; i -= 3) { 147 | buf.push(parseInt(v.substr(i, 2), 16)); 148 | } 149 | return this; 150 | }; 151 | this.buffer = function(v) { 152 | for (var i = 0; i < v.length; i++) { 153 | buf.push(v[i]); 154 | } 155 | return this; 156 | }; 157 | this.toBuffer = function() { 158 | return Buffer.from(buf); 159 | }; 160 | } 161 | 162 | function PacketReader(buffer, throwFn) { 163 | var pos = 0; 164 | this.u8 = function() { 165 | if (pos + 1 > buffer.length) { 166 | throwFn(); 167 | } 168 | return buffer[pos++]; 169 | }; 170 | this.i8 = function() { 171 | var v = this.u8(); 172 | return v >= 128 ? v - 256 : v; 173 | }; 174 | this.u16 = function() { 175 | if (pos + 2 > buffer.length) { 176 | throwFn(); 177 | } 178 | var v = buffer[pos] | (buffer[pos + 1] << 8); 179 | pos += 2; 180 | return v; 181 | }; 182 | this.u32 = function() { 183 | if (pos + 4 > buffer.length) { 184 | throwFn(); 185 | } 186 | var v = buffer[pos] | (buffer[pos + 1] << 8) | (buffer[pos + 2] << 16) | (buffer[pos + 3] << 24); 187 | pos += 4; 188 | return v; 189 | }; 190 | this.bdAddr = function() { 191 | if (pos + 6 > buffer.length) { 192 | throwFn(); 193 | } 194 | var str = ''; 195 | for (var i = 5; i >= 0; i--) { 196 | str += (0x100 + buffer[pos + i]).toString(16).substr(-2).toUpperCase(); 197 | if (i != 0) { 198 | str += ':'; 199 | } 200 | } 201 | pos += 6; 202 | return str; 203 | }; 204 | this.buffer = function(length) { 205 | if (pos + length > buffer.length) { 206 | throwFn(); 207 | } 208 | var v = buffer.slice(pos, pos + length); 209 | pos += length; 210 | return v; 211 | }; 212 | this.getRemainingBuffer = function() { 213 | return buffer.slice(pos); 214 | }; 215 | this.getRemainingSize = function() { 216 | return buffer.length - pos; 217 | }; 218 | } 219 | 220 | function HciAdapter(transport, hardwareErrorCallback) { 221 | var isStopped = false; 222 | var pendingCommand = null; // {opcode, callback, handle, ignoreResponse} 223 | var commandQueue = []; // {opcode, buffer, callback, handle} 224 | var activeConnections = Object.create(null); // {handle -> AclConnection} 225 | 226 | var hasSeparateLeAclBuffers = null; 227 | var aclMtu = 0; 228 | var numFreeBuffers = 0; 229 | 230 | var advCallback = null; 231 | var connCallback = null; 232 | var scanCallback = null; 233 | var leReadLocalP256PublicKeyCallback = null; 234 | var leGenerateDHKeyCallback = null; 235 | 236 | function AclConnection(handle, role) { 237 | EventEmitter.call(this); 238 | this.handle = handle; 239 | this.role = role; 240 | this.disconnecting = false; 241 | this.leConnectionUpdateCallback = null; 242 | this.leReadRemoteUsedFeaturesCallback = null; 243 | this.readRemoteVersionInformationCallback = null; 244 | this.encryptionChangeCallback = null; 245 | this.lePhyUpdateCallback = null; 246 | this.incomingL2CAPBuffer = []; // [Buffer] 247 | this.outgoingL2CAPBuffer = new Queue(); // [{isFirst, buffer, sentCallback, completeCallback}] 248 | this.outgoingL2CAPPacketsInController = []; 249 | } 250 | util.inherits(AclConnection, EventEmitter); 251 | 252 | function reallySendCommand(opcode, buffer, callback, handle) { 253 | if (isStopped) { 254 | return; 255 | } 256 | pendingCommand = {opcode: opcode, callback: callback, handle: handle, ignoreResponse: false}; 257 | var header = new PacketWriter().u8(HCI_COMMAND_PKT).u16(opcode).u8(buffer.length).toBuffer(); 258 | transport.write(Buffer.concat([header, buffer])); 259 | } 260 | 261 | function sendCommand(opcode, buffer, callback, handle) { 262 | if (isStopped) { 263 | return; 264 | } 265 | if (handle != 0 && !handle) { 266 | handle = null; 267 | } 268 | if (pendingCommand != null) { 269 | commandQueue.push({opcode: opcode, buffer: buffer, callback: callback, handle: handle}); 270 | } else { 271 | reallySendCommand(opcode, buffer, callback, handle); 272 | } 273 | } 274 | 275 | function triggerSendPackets(conn) { 276 | while (numFreeBuffers != 0) { 277 | if (isStopped) { 278 | return; 279 | } 280 | var handle; 281 | if (!conn) { 282 | var candidates = []; 283 | for (var handle in activeConnections) { 284 | if (!(handle in activeConnections)) { 285 | continue; 286 | } 287 | var c = activeConnections[handle]; 288 | if (c.outgoingL2CAPBuffer.getLength() != 0 && !c.disconnecting) { 289 | candidates.push(handle); 290 | } 291 | } 292 | if (candidates.length == 0) { 293 | break; 294 | } 295 | handle = candidates[Math.floor(Math.random() * candidates.length)]; 296 | selectedConn = activeConnections[handle]; 297 | } else { 298 | if (conn.disconnecting) { 299 | break; 300 | } 301 | handle = conn.handle; 302 | selectedConn = conn; 303 | } 304 | var item = selectedConn.outgoingL2CAPBuffer.shift(); 305 | if (!item) { 306 | break; 307 | } 308 | --numFreeBuffers; 309 | var isFirst = item.isFirst; 310 | var buffer = item.buffer; 311 | selectedConn.outgoingL2CAPPacketsInController.push(item.completeCallback); 312 | var header = new PacketWriter().u8(HCI_ACLDATA_PKT).u16((handle & 0xfff) | (isFirst ? 0 : 0x1000)).u16(buffer.length).toBuffer(); 313 | transport.write(Buffer.concat([header, buffer])); 314 | if (item.sentCallback) { 315 | item.sentCallback(); 316 | } 317 | } 318 | } 319 | 320 | this.sendData = function(handle, cid, data, sentCallback, completeCallback) { 321 | if (isStopped) { 322 | return; 323 | } 324 | data = Buffer.concat([new PacketWriter().u16(data.length).u16(cid).toBuffer(), data]); 325 | 326 | var conn = activeConnections[handle]; 327 | 328 | for (var i = 0; i < data.length; i += aclMtu) { 329 | var isFirst = i == 0; 330 | var isLast = i + aclMtu >= data.length; 331 | var slice = data.slice(i, isLast ? data.length : i + aclMtu); 332 | conn.outgoingL2CAPBuffer.push({isFirst: isFirst, buffer: slice, sentCallback: isLast ? sentCallback : null, completeCallback: isLast ? completeCallback : null}); 333 | } 334 | 335 | triggerSendPackets(conn); 336 | }; 337 | 338 | this.disconnect = function(handle, reason) { 339 | sendCommand(DISCONNECT_CMD, new PacketWriter().u16(handle).u8(reason).toBuffer(), function(status, r) { 340 | // Ignore 341 | }, handle); 342 | }; 343 | 344 | this.readRemoteVersionInformation = function(handle, callback) { 345 | sendCommand(READ_REMOTE_VERSION_INFORMATION_CMD, new PacketWriter().u16(handle).toBuffer(), function(status, r) { 346 | if (status != 0) { 347 | callback(status); 348 | } else { 349 | activeConnections[handle].readRemoteVersionInformationCallback = callback; 350 | } 351 | }, handle); 352 | }; 353 | 354 | this.setEventMask = function(low, high, callback) { 355 | sendCommand(SET_EVENT_MASK_CMD, new PacketWriter().u32(low).u32(high).toBuffer(), callback); 356 | }; 357 | 358 | this.reset = function(callback) { 359 | sendCommand(RESET_CMD, EMPTY_BUFFER, function(status, r) { 360 | if (status == 0) { 361 | activeConnections = Object.create(null); 362 | hasSeparateLeAclBuffers = null; 363 | aclMtu = 0; 364 | numFreeBuffers = 0; 365 | advCallback = null; 366 | connCallback = null; 367 | scanCallback = null; 368 | leReadLocalP256PublicKeyCallback = null; 369 | leGenerateDHKeyCallback = null; 370 | } 371 | callback(status); 372 | }); 373 | }; 374 | 375 | this.readLocalVersionInformation = function(callback) { 376 | sendCommand(READ_LOCAL_VERSION_INFORMATION_CMD, EMPTY_BUFFER, function(status, r) { 377 | if (status == 0) { 378 | var hciVersion = r.u8(); 379 | var hciRevision = r.u16(); 380 | var lmpPalVersion = r.u8(); 381 | var manufacturerName = r.u16(); 382 | var lmpPalSubversion = r.u16(); 383 | callback(status, hciVersion, hciRevision, lmpPalVersion, manufacturerName, lmpPalSubversion); 384 | } else { 385 | callback(status); 386 | } 387 | }); 388 | }; 389 | 390 | this.readBdAddr = function(callback) { 391 | sendCommand(READ_BD_ADDR_CMD, EMPTY_BUFFER, function(status, r) { 392 | if (status == 0) { 393 | var bdAddr = r.bdAddr(); 394 | callback(status, bdAddr); 395 | } else { 396 | callback(status); 397 | } 398 | }); 399 | }; 400 | 401 | this.readBufferSize = function(callback) { 402 | sendCommand(READ_BUFFER_SIZE_CMD, EMPTY_BUFFER, function(status, r) { 403 | if (status == 0) { 404 | var aclPacketLength = r.u16(); 405 | var syncPacketLength = r.u8(); 406 | var numAclPackets = r.u16(); 407 | var numSyncPackets = r.u16(); 408 | if (hasSeparateLeAclBuffers === false && aclMtu == 0) { 409 | aclMtu = Math.min(aclPacketLength, 1023); // Linux can't handle more than 1023 bytes 410 | numFreeBuffers = numAclPackets; 411 | } 412 | callback(status, aclPacketLength, syncPacketLength, numAclPackets, numSyncPackets); 413 | } else { 414 | callback(status); 415 | } 416 | }); 417 | }; 418 | 419 | this.readRssi = function(handle, callback) { 420 | sendCommand(READ_RSSI_CMD, new PacketWriter().u16(handle).toBuffer(), function(status, r) { 421 | if (status == 0) { 422 | r.u16(); // handle 423 | var rssi = r.i8(); 424 | callback(status, rssi); 425 | } else { 426 | callback(status); 427 | } 428 | }, handle); 429 | }; 430 | 431 | this.leSetEventMask = function(low, high, callback) { 432 | sendCommand(LE_SET_EVENT_MASK_CMD, new PacketWriter().u32(low).u32(high).toBuffer(), callback); 433 | }; 434 | 435 | this.leReadBufferSize = function(callback) { 436 | sendCommand(LE_READ_BUFFER_SIZE_CMD, EMPTY_BUFFER, function(status, r) { 437 | if (status == 0) { 438 | var packetLength = r.u16(); 439 | var numPackets = r.u8(); 440 | if (hasSeparateLeAclBuffers == null) { 441 | aclMtu = Math.min(packetLength, 1023); // Linux can't handle more than 1023 bytes 442 | numFreeBuffers = numPackets; 443 | } 444 | hasSeparateLeAclBuffers = packetLength != 0; 445 | callback(status, packetLength, numPackets); 446 | } else { 447 | callback(status); 448 | } 449 | }); 450 | }; 451 | 452 | this.leReadLocalSupportedFeatures = function(callback) { 453 | sendCommand(LE_READ_LOCAL_SUPPORTED_FEATURES_CMD, EMPTY_BUFFER, function(status, r) { 454 | if (status == 0) { 455 | var low = r.u32(); 456 | var high = r.u32(); 457 | callback(status, low, high); 458 | } else { 459 | callback(status); 460 | } 461 | }); 462 | }; 463 | 464 | this.leSetRandomAddress = function(randomAddress, callback) { 465 | sendCommand(LE_SET_RANDOM_ADDRESS_CMD, new PacketWriter().bdAddr(randomAddress).toBuffer(), callback); 466 | }; 467 | 468 | this.leSetAdvertisingParameters = function(advertisingIntervalMin, advertisingIntervalMax, advertisingType, ownAddressType, peerAddressType, peerAddress, advertisingChannelMap, advertisingFilterPolicy, callback) { 469 | var pkt = new PacketWriter().u16(advertisingIntervalMin).u16(advertisingIntervalMax).u8(advertisingType).u8(ownAddressType).u8(peerAddressType).bdAddr(peerAddress).u8(advertisingChannelMap).u8(advertisingFilterPolicy).toBuffer(); 470 | sendCommand(LE_SET_ADVERTISING_PARAMETERS_CMD, pkt, callback); 471 | }; 472 | 473 | this.leReadAdvertisingChannelTxPower = function(callback) { 474 | sendCommand(LE_READ_ADVERTISING_CHANNEL_TX_POWER_CMD, EMPTY_BUFFER, function(status, r) { 475 | if (status == 0) { 476 | var transmitPowerLevel = r.i8(); 477 | callback(status, transmitPowerLevel); 478 | } else { 479 | callback(status); 480 | } 481 | }); 482 | }; 483 | 484 | this.leSetAdvertisingData = function(advertisingData, callback) { 485 | var pkt = Buffer.alloc(32); 486 | pkt[0] = advertisingData.length; 487 | advertisingData.copy(pkt, 1); 488 | sendCommand(LE_SET_ADVERTISING_DATA_CMD, pkt, callback); 489 | }; 490 | 491 | this.leSetScanResponseData = function(scanResponseData, callback) { 492 | var pkt = Buffer.alloc(32); 493 | pkt[0] = scanResponseData.length; 494 | scanResponseData.copy(pkt, 1); 495 | sendCommand(LE_SET_SCAN_RESPONSE_DATA_CMD, pkt, callback); 496 | }; 497 | 498 | var that = this; 499 | this.leSetAdvertisingEnable = function(advertisingEnable, callback, advConnCallback) { 500 | sendCommand(LE_SET_ADVERTISING_ENABLE_CMD, new PacketWriter().u8(advertisingEnable ? 1 : 0).toBuffer(), function(status, r) { 501 | //console.log("leSetAdvertisingEnable done " + advertisingEnable + " " + status); 502 | if (advertisingEnable && status == 0) { 503 | //console.log("setting advCallback to " + advConnCallback); 504 | advCallback = advConnCallback; 505 | } 506 | callback(status); 507 | }); 508 | }; 509 | 510 | this.leSetScanParameters = function(leScanType, leScanInterval, leScanWindow, ownAddressType, scanningFilterPolicy, callback) { 511 | var pkt = new PacketWriter().u8(leScanType).u16(leScanInterval).u16(leScanWindow).u8(ownAddressType).u8(scanningFilterPolicy).toBuffer(); 512 | sendCommand(LE_SET_SCAN_PARAMETERS_CMD, pkt, callback); 513 | }; 514 | 515 | this.leSetScanEnable = function(leScanEnable, filterDuplicates, reportCallback, callback) { 516 | var pkt = new PacketWriter().u8(leScanEnable ? 1 : 0).u8(filterDuplicates ? 1 : 0).toBuffer(); 517 | sendCommand(LE_SET_SCAN_ENABLE_CMD, pkt, function(status, r) { 518 | if (status == 0) { 519 | scanCallback = leScanEnable ? reportCallback : null; 520 | } 521 | callback(status); 522 | }); 523 | }; 524 | 525 | this.leCreateConnection = function(leScanInterval, leScanWindow, initiatorFilterPolicy, peerAddressType, peerAddress, ownAddressType, connIntervalMin, connIntervalMax, connLatency, supervisionTimeout, minCELen, maxCELen, callback, completeCallback) { 526 | var pkt = new PacketWriter().u16(leScanInterval).u16(leScanWindow).u8(initiatorFilterPolicy).u8(peerAddressType).bdAddr(peerAddress).u8(ownAddressType).u16(connIntervalMin).u16(connIntervalMax).u16(connLatency).u16(supervisionTimeout).u16(minCELen).u16(maxCELen).toBuffer(); 527 | sendCommand(LE_CREATE_CONNECTION_CMD, pkt, function(status, r) { 528 | if (status == 0) { 529 | connCallback = completeCallback; 530 | } 531 | callback(status); 532 | }); 533 | }; 534 | 535 | this.leCreateConnectionCancel = function(callback) { 536 | sendCommand(LE_CREATE_CONNECTION_CANCEL_CMD, EMPTY_BUFFER, callback); 537 | }; 538 | 539 | this.leReadWhiteListSize = function(callback) { 540 | sendCommand(LE_READ_WHITE_LIST_SIZE_CMD, EMPTY_BUFFER, function(status, r) { 541 | if (status == 0) { 542 | var whiteListSize = r.u8(); 543 | callback(status, whiteListSize); 544 | } else { 545 | callback(status); 546 | } 547 | }); 548 | }; 549 | 550 | this.leClearWhiteList = function(callback) { 551 | sendCommand(LE_CLEAR_WHITE_LIST_CMD, EMPTY_BUFFER, callback); 552 | }; 553 | 554 | this.leAddDeviceToWhiteList = function(addressType, address, callback) { 555 | sendCommand(LE_ADD_DEVICE_TO_WHITE_LIST_CMD, new PacketWriter().u8(addressType).bdAddr(address).toBuffer(), callback); 556 | }; 557 | 558 | this.leRemoveDeviceFromWhiteList = function(addressType, address, callback) { 559 | sendCommand(LE_REMOVE_DEVICE_FROM_WHITE_LIST_CMD, new PacketWriter().u8(addressType).bdAddr(address).toBuffer(), callback); 560 | }; 561 | 562 | this.leConnectionUpdate = function(handle, intervalMin, intervalMax, latency, timeout, minCELen, maxCELen, callback) { 563 | var pkt = new PacketWriter().u16(handle).u16(intervalMin).u16(intervalMax).u16(latency).u16(timeout).u16(minCELen).u16(maxCELen).toBuffer(); 564 | sendCommand(LE_CONNECTION_UPDATE_CMD, pkt, function(status, r) { 565 | if (status != 0) { 566 | callback(status); 567 | } else { 568 | activeConnections[handle].leConnectionUpdateCallback = callback; 569 | } 570 | }, handle); 571 | }; 572 | 573 | this.leReadRemoteUsedFeatures = function(handle, callback) { 574 | sendCommand(LE_READ_REMOTE_USED_FEATURES_CMD, new PacketWriter().u16(handle).toBuffer(), function(status, r) { 575 | if (status != 0) { 576 | callback(status); 577 | } else { 578 | activeConnections[handle].leReadRemoteUsedFeaturesCallback = callback; 579 | } 580 | }, handle); 581 | }; 582 | 583 | this.leStartEncryption = function(handle, randomNumber, ediv, ltk, statusCallback, completeCallback) { 584 | var pkt = new PacketWriter().u16(handle).buffer(randomNumber).u16(ediv).buffer(ltk).toBuffer(); 585 | sendCommand(LE_START_ENCRYPTION_CMD, pkt, function(status, r) { 586 | if (status == 0) { 587 | activeConnections[handle].encryptionChangeCallback = completeCallback; 588 | } 589 | statusCallback(status); 590 | }, handle); 591 | }; 592 | 593 | this.leLongTermKeyRequestReply = function(handle, ltk, callback) { 594 | sendCommand(LE_LONG_TERM_KEY_REQUEST_REPLY_CMD, new PacketWriter().u16(handle).buffer(ltk).toBuffer(), function(status, r) { 595 | // NOTE: Connection_Handle is also sent, but should be redundant 596 | if (status != 0) { 597 | callback(status); 598 | } else { 599 | activeConnections[handle].encryptionChangeCallback = callback; 600 | } 601 | }, handle); 602 | }; 603 | 604 | this.leLongTermKeyNequestNegativeReply = function(handle, callback) { 605 | sendCommand(LE_LONG_TERM_KEY_REQUEST_NEGATIVE_REPLY_CMD, new PacketWriter().u16(handle).toBuffer(), function(status, r) { 606 | // NOTE: Connection_Handle is also sent, but should be redundant 607 | callback(status); 608 | }, handle); 609 | }; 610 | 611 | this.leReadSupportedStates = function(callback) { 612 | sendCommand(LE_READ_SUPPORTED_STATES_CMD, EMPTY_BUFFER, function(status, r) { 613 | if (status == 0) { 614 | var low = r.u32(); 615 | var high = r.u32(); 616 | callback(status, low, high); 617 | } else { 618 | callback(status); 619 | } 620 | }); 621 | }; 622 | 623 | this.leSetDataLength = function(handle, txOctets, txTime, callback) { 624 | sendCommand(LE_SET_DATA_LENGTH_CMD, new PacketWriter().u16(handle).u16(txOctets).u16(txTime).toBuffer(), function(status, r) { 625 | callback(status, handle); 626 | }); 627 | }; 628 | 629 | this.leReadSuggestedDefaultDataLength = function(callback) { 630 | sendCommand(LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_CMD, EMPTY_BUFFER, function(status, r) { 631 | if (status == 0) { 632 | var suggestedMaxTxOctets = r.u16(); 633 | var suggestedMaxTxTime = r.u16(); 634 | callback(status, suggestedMaxTxOctets, suggestedMaxTxTime); 635 | } else { 636 | callback(status); 637 | } 638 | }); 639 | }; 640 | 641 | this.leWriteSuggestedDefaultDataLength = function(suggestedMaxTxOctets, suggestedMaxTxTime, callback) { 642 | sendCommand(LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_CMD, new PacketWriter().u16(suggestedMaxTxOctets).u16(suggestedMaxTxTime).toBuffer(), callback); 643 | }; 644 | 645 | this.leReadLocalP256PublicKey = function(callback) { 646 | sendCommand(LE_READ_LOCAL_P256_PUBLIC_KEY_CMD, EMPTY_BUFFER, function(status, r) { 647 | if (status == 0) { 648 | leReadLocalP256PublicKeyCallback = callback; 649 | } else { 650 | callback(status); 651 | } 652 | }); 653 | }; 654 | 655 | this.leGenerateDHKey = function(remoteP256PublicKey, callback) { 656 | sendCommand(LE_GENERATE_DHKEY_CMD, new PacketWriter().buffer(remoteP256PublicKey).toBuffer(), function(status, r) { 657 | if (status == 0) { 658 | leGenerateDHKeyCallback = callback; 659 | } else { 660 | callback(status); 661 | } 662 | }); 663 | }; 664 | 665 | this.leReadMaximumDataLength = function(callback) { 666 | sendCommand(LE_READ_MAXIMUM_DATA_LENGTH_CMD, EMPTY_BUFFER, function(status, r) { 667 | if (status == 0) { 668 | var supportedMaxTxOctets = r.u16(); 669 | var supportedMaxTxTime = r.u16(); 670 | var supportedMaxRxOctets = r.u16(); 671 | var supportedMaxRxTime = r.u16(); 672 | callback(status, supportedMaxRxOctets, supportedMaxTxTime, supportedMaxRxOctets, supportedMaxRxTime); 673 | } else { 674 | callback(status); 675 | } 676 | }); 677 | }; 678 | 679 | this.leSetDefaultPhy = function(allPhys, txPhys, rxPhys, callback) { 680 | sendCommand(LE_SET_DEFAULT_PHY_CMD, new PacketWriter().u8(allPhys).u8(txPhys).u8(rxPhys).toBuffer(), callback); 681 | }; 682 | 683 | this.leSetPhy = function(handle, allPhys, txPhys, rxPhys, phyOptions, callback) { 684 | sendCommand(LE_SET_PHY_CMD, new PacketWriter().u16(handle).u8(allPhys).u8(txPhys).u8(rxPhys).u16(phyOptions).toBuffer(), function(status, r) { 685 | if (status != 0) { 686 | callback(status); 687 | } else { 688 | activeConnections[handle].lePhyUpdateCallback = callback; 689 | } 690 | }); 691 | }; 692 | 693 | this.leSetExtendedScanParameters = function(ownAddressType, scanningFilterPolicy, scanningPhys, phyArr, callback) { 694 | var writer = new PacketWriter().u8(ownAddressType).u8(scanningFilterPolicy).u8(scanningPhys); 695 | var arrPos = 0; 696 | if (scanningPhys & 1) { 697 | // 1M 698 | writer.u8(phyArr[arrPos].scanType).u16(phyArr[arrPos].scanInterval).u16(phyArr[arrPos].scanWindow); 699 | ++arrPos; 700 | } 701 | if (scanningPhys & 4) { 702 | // Coded PHY 703 | writer.u8(phyArr[arrPos].scanType).u16(phyArr[arrPos].scanInterval).u16(phyArr[arrPos].scanWindow); 704 | ++arrPos; 705 | } 706 | sendCommand(LE_SET_EXTENDED_SCAN_PARAMETERS_CMD, writer.toBuffer(), callback); 707 | }; 708 | 709 | this.leSetExtendedScanEnable = function(leScanEnable, filterDuplicates, duration, period, reportCallback, callback) { 710 | var pkt = new PacketWriter().u8(leScanEnable ? 1 : 0).u8(filterDuplicates).u16(duration).u16(period).toBuffer(); 711 | sendCommand(LE_SET_EXTENDED_SCAN_ENABLE_CMD, pkt, function(status, r) { 712 | if (status == 0) { 713 | scanCallback = leScanEnable ? reportCallback : null; 714 | } 715 | callback(status); 716 | }); 717 | }; 718 | 719 | this.leExtendedCreateConnection = function(initiatorFilterPolicy, ownAddressType, peerAddressType, peerAddress, initiatingPhys, phyArr, callback, completeCallback) { 720 | var writer = new PacketWriter().u8(initiatorFilterPolicy).u8(ownAddressType).u8(peerAddressType).bdAddr(peerAddress).u8(initiatingPhys); 721 | var arrPos = 0; 722 | for (var i = 0; i < 3; i++) { 723 | if (initiatingPhys & (1 << i)) { 724 | writer.u16(phyArr[arrPos].scanInterval).u16(phyArr[arrPos].scanWindow).u16(phyArr[arrPos].connIntervalMin).u16(phyArr[arrPos].connIntervalMax).u16(phyArr[arrPos].connLatency).u16(phyArr[arrPos].supervisionTimeout).u16(phyArr[arrPos].minCELen).u16(phyArr[arrPos].maxCELen); 725 | ++arrPos; 726 | } 727 | } 728 | sendCommand(LE_EXTENDED_CREATE_CONNECTION_CMD, writer.toBuffer(), function(status, r) { 729 | if (status == 0) { 730 | connCallback = completeCallback; 731 | } 732 | callback(status); 733 | }); 734 | }; 735 | 736 | this.leSetExtendedAdvertisingParameters = function(advertisingHandle, advertisingEventProperties, primaryAdvertisingIntervalMin, primaryAdvertisingIntervalMax, primaryAdvertisingChannelMap, ownAddressType, peerAddressType, peerAddress, advertisingFilterPolicy, advertisingTxPower, primaryAdvertisingPhy, secondaryAdvertisingMaxSkip, secondaryAdvertisingPhy, advertisingSid, scanRequestNotificationEnable, callback) { 737 | var pkt = new PacketWriter() 738 | .u8(advertisingHandle) 739 | .u16(advertisingEventProperties) 740 | .u24(primaryAdvertisingIntervalMin) 741 | .u24(primaryAdvertisingIntervalMax) 742 | .u8(primaryAdvertisingChannelMap) 743 | .u8(ownAddressType) 744 | .u8(peerAddressType) 745 | .bdAddr(peerAddress) 746 | .u8(advertisingFilterPolicy) 747 | .i8(advertisingTxPower) 748 | .u8(primaryAdvertisingPhy) 749 | .u8(secondaryAdvertisingMaxSkip) 750 | .u8(secondaryAdvertisingPhy) 751 | .u8(advertisingSid) 752 | .u8(scanRequestNotificationEnable) 753 | .toBuffer(); 754 | sendCommand(LE_SET_EXTENDED_ADVERTISING_PARAMETERS_CMD, pkt, function(status, r) { 755 | if (status == 0) { 756 | var selectedTxPower = r.i8(); 757 | callback(status, selectedTxPower); 758 | } else { 759 | callback(status); 760 | } 761 | }); 762 | 763 | }; 764 | 765 | this.leSetExtendedAdvertisingEnable = function(enable, advertisingSets, callback) { 766 | var writer = new PacketWriter().u8(enable).u8(advertisingSets.length); 767 | for (var i = 0; i < advertisingSets.length; i++) { 768 | var set = advertisingSets[i]; 769 | writer.u8(set.advertisingHandle).u16(set.duration).u8(set.maxExtendedAdvertisingEvents); 770 | } 771 | sendCommand(LE_SET_EXTENDED_ADVERTISING_ENABLE_CMD, writer.toBuffer(), function(status) { 772 | if (status == 0 && enable) { 773 | // TODO: If multiple sets, multiple callbacks needed 774 | advCallback = callback; 775 | } else { 776 | callback(status); 777 | } 778 | }); 779 | }; 780 | 781 | function handleDisconnectionComplete(r) { 782 | var status = r.u8(); 783 | if (status != 0) { 784 | return; 785 | } 786 | var handle = r.u16(); 787 | var reason = r.u8(); 788 | var conn = activeConnections[handle]; 789 | if (!conn) { 790 | return; 791 | } 792 | delete activeConnections[handle]; 793 | commandQueue = commandQueue.filter(cmd => cmd.handle != handle); 794 | if (pendingCommand != null && pendingCommand.handle == handle) { 795 | pendingCommand.ignoreResponse = true; 796 | } 797 | numFreeBuffers += conn.outgoingL2CAPPacketsInController.length; 798 | conn.emit('disconnect', reason); 799 | triggerSendPackets(); 800 | } 801 | function handleEncryptionChange(r) { 802 | var status = r.u8(); 803 | var handle = r.u16(); 804 | var conn = activeConnections[handle]; 805 | if (!conn) { 806 | return; 807 | } 808 | var callback = conn.encryptionChangeCallback; 809 | if (callback) { 810 | conn.encryptionChangeCallback = null; 811 | if (status != 0) { 812 | callback(status); 813 | return; 814 | } 815 | var encryptionEnabled = r.u8(); 816 | callback(status, encryptionEnabled); 817 | } 818 | } 819 | function handleReadRemoteVersionInformationComplete(r) { 820 | var status = r.u8(); 821 | var handle = r.u16(); 822 | var conn = activeConnections[handle]; 823 | if (!conn) { 824 | return; 825 | } 826 | var callback = conn.readRemoteVersionInformationCallback; 827 | if (callback) { 828 | conn.readRemoteVersionInformationCallback = null; 829 | if (status != 0) { 830 | callback(status); 831 | return; 832 | } 833 | var version = r.u8(); 834 | var manufacturer = r.u16(); 835 | var subversion = r.u16(); 836 | callback(status, version, manufacturer, subversion); 837 | } 838 | } 839 | function handleHardwareError(r) { 840 | var hardwareCode = r.u8(); 841 | pendingCommand = null; 842 | commandQueue = []; 843 | // Rest will be reset when Reset Command is sent 844 | hardwareErrorCallback(hardwareCode); 845 | } 846 | function handleNumberOfCompletePackets(r) { 847 | var numHandles = r.u8(); 848 | var callbacks = []; 849 | for (var i = 0; i < numHandles; i++) { 850 | var handle = r.u16(); 851 | var numCompleted = r.u16(); 852 | var conn = activeConnections[handle]; 853 | if (!conn) { 854 | // TODO: Print warning about buggy controller 855 | continue; 856 | } 857 | if (numCompleted > conn.outgoingL2CAPPacketsInController.length) { 858 | // TODO: Print warning about buggy controller 859 | numCompleted = conn.outgoingL2CAPPacketsInController.length; 860 | } 861 | numFreeBuffers += numCompleted; 862 | callbacks.push(conn.outgoingL2CAPPacketsInController.splice(0, numCompleted)); 863 | } 864 | for (var i = 0; i < callbacks.length; i++) { 865 | for (var j = 0; j < callbacks[i].length; j++) { 866 | if (callbacks[i][j]) { 867 | callbacks[i][j](); 868 | } 869 | } 870 | } 871 | triggerSendPackets(); 872 | } 873 | function handleEncryptionKeyRefreshComplete(r) { 874 | var status = r.u8(); 875 | var handle = r.u16(); 876 | var conn = activeConnections[handle]; 877 | if (!conn) { 878 | return; 879 | } 880 | var callback = conn.encryptionChangeCallback; 881 | if (callback) { 882 | conn.encryptionChangeCallback = null; 883 | if (status == 0) { 884 | callback(status, 0x01); 885 | } else { 886 | callback(status); 887 | } 888 | } 889 | } 890 | function handleLeConnectionComplete(r) { 891 | var status = r.u8(); 892 | if (status == HciErrors.ADVERTISING_TIMEOUT) { 893 | var ac = advCallback; 894 | advCallback = null; 895 | if (ac) { 896 | ac(status); 897 | } 898 | } else if (status != 0) { 899 | var cc = connCallback; 900 | connCallback = null; 901 | if (cc) { 902 | cc(status); 903 | } 904 | } else { 905 | var handle = r.u16(); 906 | var role = r.u8(); 907 | var peerAddressType = r.u8(); 908 | var peerAddress = r.bdAddr(); 909 | var connInterval = r.u16(); 910 | var connLatency = r.u16(); 911 | var supervisionTimeout = r.u16(); 912 | var masterClockAccuracy = r.u8(); 913 | 914 | if (handle in activeConnections) { 915 | // TODO: what to do here? 916 | throw new Error('Handle ' + handle + ' already connected'); 917 | } 918 | 919 | var aclConn = new AclConnection(handle, role); 920 | activeConnections[handle] = aclConn; 921 | 922 | var callback; 923 | if (role == ROLE_MASTER) { 924 | callback = connCallback; 925 | connCallback = null; 926 | } else { 927 | //console.log("slave conn complete " + advCallback); 928 | callback = advCallback; 929 | advCallback = null; 930 | if (!callback) { 931 | // Unexpected, kill this connection 932 | var reason = 0x13; 933 | sendCommand(DISCONNECT_CMD, new PacketWriter().u16(handle).u8(reason).toBuffer(), function(status, r) { 934 | // Ignore 935 | }, handle); 936 | return; 937 | } 938 | } 939 | callback(status, aclConn, role, peerAddressType, peerAddress, connInterval, connLatency, supervisionTimeout, masterClockAccuracy); 940 | } 941 | } 942 | function handleLeAdvertisingReport(r) { 943 | if (scanCallback) { 944 | var numReports = r.u8(); 945 | // At least BCM20702A0 can send numReports > 1 but then actually have only one report in the packet, 946 | // so gracefully abort if the buffer ends early. 947 | for (var i = 0; i < numReports && r.getRemainingSize() > 0; i++) { 948 | var eventType = r.u8(); 949 | var addressType = r.u8(); 950 | var address = r.bdAddr(); 951 | var lengthData = r.u8(); 952 | var data = r.buffer(lengthData); 953 | var rssi = r.i8(); 954 | 955 | scanCallback(eventType, addressType, address, data, rssi); 956 | } 957 | } 958 | } 959 | function handleLeConnectionUpdateComplete(r) { 960 | var status = r.u8(); 961 | var handle = r.u16(); 962 | var conn = activeConnections[handle]; 963 | if (!conn) { 964 | return; 965 | } 966 | var interval, latency, timeout; 967 | var callback = conn.leConnectionUpdateCallback; 968 | if (status == 0) { 969 | interval = r.u16(); 970 | latency = r.u16(); 971 | timeout = r.u16(); 972 | } 973 | if (callback) { 974 | conn.leConnectionUpdateCallback = null; 975 | if (status != 0) { 976 | callback(status); 977 | return; 978 | } 979 | callback(status, interval, latency, timeout); 980 | } 981 | if (status == 0) { 982 | conn.emit('connectionUpdate', interval, latency, timeout); 983 | } 984 | } 985 | function handleLeReadRemoteUsedFeaturesComplete(r) { 986 | var status = r.u8(); 987 | var handle = r.u16(); 988 | var conn = activeConnections[handle]; 989 | if (!conn) { 990 | return; 991 | } 992 | var callback = conn.leReadRemoteUsedFeaturesCallback; 993 | if (callback) { 994 | conn.leReadRemoteUsedFeaturesCallback = null; 995 | if (status != 0) { 996 | callback(status); 997 | return; 998 | } 999 | var low = r.u32(); 1000 | var high = r.u32(); 1001 | callback(status, low, high); 1002 | } 1003 | } 1004 | function handleLeLongTermKeyRequest(r) { 1005 | var handle = r.u16(); 1006 | var conn = activeConnections[handle]; 1007 | if (!conn || conn.role != ROLE_SLAVE) { 1008 | return; 1009 | } 1010 | var randomNumber = r.buffer(8); 1011 | var ediv = r.u16(); 1012 | conn.emit('ltkRequest', randomNumber, ediv); 1013 | } 1014 | function handleLeReadLocalP256PublicKeyComplete(r) { 1015 | var status = r.u8(); 1016 | var callback = leReadLocalP256PublicKeyCallback; 1017 | if (callback) { 1018 | leReadLocalP256PublicKeyCallback = null; 1019 | if (status != 0) { 1020 | callback(status); 1021 | return; 1022 | } 1023 | var localP256PublicKey = r.buffer(64); 1024 | callback(status, localP256PublicKey); 1025 | } 1026 | } 1027 | function handleLeGenerateDHKeyComplete(r) { 1028 | var status = r.u8(); 1029 | var callback = leGenerateDHKeyCallback; 1030 | if (callback) { 1031 | leGenerateDHKeyCallback = null; 1032 | if (status != 0) { 1033 | callback(status); 1034 | return; 1035 | } 1036 | var dhKey = r.buffer(32); 1037 | callback(status, dhKey); 1038 | } 1039 | } 1040 | function handleLeEnhancedConnectionComplete(r) { 1041 | var status = r.u8(); 1042 | if (status == HciErrors.ADVERTISING_TIMEOUT) { 1043 | var ac = advCallback; 1044 | advCallback = null; 1045 | if (ac) { 1046 | ac(status); 1047 | } 1048 | } else if (status != 0) { 1049 | var cc = connCallback; 1050 | connCallback = null; 1051 | if (cc) { 1052 | cc(status); 1053 | } 1054 | } else { 1055 | var handle = r.u16(); 1056 | var role = r.u8(); 1057 | var peerAddressType = r.u8(); 1058 | var peerAddress = r.bdAddr(); 1059 | var localResolvablePrivateAddress = r.bdAddr(); 1060 | var peerResolvablePrivateAddress = r.bdAddr(); 1061 | var connInterval = r.u16(); 1062 | var connLatency = r.u16(); 1063 | var supervisionTimeout = r.u16(); 1064 | var masterClockAccuracy = r.u8(); 1065 | 1066 | if (handle in activeConnections) { 1067 | // TODO: what to do here? 1068 | throw new Error('Handle ' + handle + ' already connected'); 1069 | } 1070 | 1071 | var aclConn = new AclConnection(handle, role); 1072 | activeConnections[handle] = aclConn; 1073 | 1074 | var callback; 1075 | if (role == ROLE_MASTER) { 1076 | callback = connCallback; 1077 | connCallback = null; 1078 | } else { 1079 | //console.log("slave conn complete " + advCallback); 1080 | callback = advCallback; 1081 | advCallback = null; 1082 | } 1083 | callback(status, aclConn, role, peerAddressType, peerAddress, localResolvablePrivateAddress, peerResolvablePrivateAddress, connInterval, connLatency, supervisionTimeout, masterClockAccuracy); 1084 | } 1085 | } 1086 | function handleLePhyUpdateComplete(r) { 1087 | var status = r.u8(); 1088 | var handle = r.u16(); 1089 | var conn = activeConnections[handle]; 1090 | if (!conn) { 1091 | return; 1092 | } 1093 | var txPhy, rxPhy; 1094 | var callback = conn.lePhyUpdateCallback; 1095 | if (status == 0) { 1096 | txPhy = r.u8(); 1097 | rxPhy = r.u8(); 1098 | } 1099 | if (callback) { 1100 | conn.lePhyUpdateCallback = null; 1101 | if (status != 0) { 1102 | callback(status); 1103 | return; 1104 | } 1105 | callback(status, txPhy, rxPhy); 1106 | } 1107 | if (status == 0) { 1108 | conn.emit('connectionUpdate', txPhy, rxPhy); 1109 | } 1110 | } 1111 | function handleLeExtendedAdvertisingReport(r) { 1112 | if (scanCallback) { 1113 | var numReports = r.u8(); 1114 | for (var i = 0; i < numReports; i++) { 1115 | var eventType = r.u16(); 1116 | var addressType = r.u8(); 1117 | var address = r.bdAddr(); 1118 | var primaryPhy = r.u8(); 1119 | var secondaryPhy = r.u8(); 1120 | var advertisingSid = r.u8(); 1121 | var txPower = r.i8(); 1122 | var rssi = r.i8(); 1123 | var periodicAdvertisingInterval = r.u16(); 1124 | var directAddressType = r.u8(); 1125 | var directAddress = r.bdAddr(); 1126 | var lengthData = r.u8(); 1127 | var data = r.buffer(lengthData); 1128 | 1129 | scanCallback(eventType, addressType, address, primaryPhy, secondaryPhy, advertisingSid, txPower, rssi, periodicAdvertisingInterval, directAddressType, directAddress, data); 1130 | } 1131 | } 1132 | } 1133 | 1134 | function onData(data) { 1135 | function throwInvalidLength() { 1136 | throw new Error('invalid packet length for ' + data.toString('hex') + ', ' + data); 1137 | } 1138 | if (data.length == 0) { 1139 | throwInvalidLength(); 1140 | } 1141 | var r = new PacketReader(data, throwInvalidLength); 1142 | var packetType = r.u8(); 1143 | if (packetType == HCI_EVENT_PKT) { 1144 | if (data.length < 3) { 1145 | throwInvalidLength(); 1146 | } 1147 | var eventCode = r.u8(); 1148 | var paramLen = r.u8(); 1149 | if (paramLen + 3 != data.length) { 1150 | throwInvalidLength(); 1151 | } 1152 | if (eventCode == EVT_CMD_COMPLETE || eventCode == EVT_CMD_STATUS) { 1153 | var status; 1154 | if (eventCode == EVT_CMD_STATUS) { 1155 | status = r.u8(); 1156 | } 1157 | var numPkts = r.u8(); 1158 | var opcode = r.u16(); 1159 | 1160 | if (pendingCommand == null || pendingCommand.opcode != opcode) { 1161 | // TODO: ignore? probably command sent by other process 1162 | } else { 1163 | if (eventCode == EVT_CMD_COMPLETE) { 1164 | status = r.u8(); // All packets we can handle have status as first parameter 1165 | } 1166 | 1167 | var pc = pendingCommand; 1168 | pendingCommand = null; 1169 | if (commandQueue.length != 0) { 1170 | var cmd = commandQueue.shift(); 1171 | reallySendCommand(cmd.opcode, cmd.buffer, cmd.callback, cmd.handle); 1172 | } 1173 | if (pc.callback && !pc.ignoreResponse) { 1174 | pc.callback(status, r); 1175 | } 1176 | } 1177 | } else { 1178 | switch (eventCode) { 1179 | case EVT_DISCONNECTION_COMPLETE: handleDisconnectionComplete(r); break; 1180 | case EVT_ENCRYPTION_CHANGE: handleEncryptionChange(r); break; 1181 | case EVT_READ_REMOTE_VERSION_INFORMATION_COMPLETE: handleReadRemoteVersionInformationComplete(r); break; 1182 | case EVT_HARDWARE_ERROR: handleHardwareError(r); break; 1183 | case EVT_NUMBER_OF_COMPLETE_PACKETS: handleNumberOfCompletePackets(r); break; 1184 | case EVT_ENCRYPTION_KEY_REFRESH_COMPLETE: handleEncryptionKeyRefreshComplete(r); break; 1185 | case EVT_LE_META: switch(r.u8()) { 1186 | case EVT_LE_CONNECTION_COMPLETE: handleLeConnectionComplete(r); break; 1187 | case EVT_LE_ADVERTISING_REPORT: handleLeAdvertisingReport(r); break; 1188 | case EVT_LE_CONNECTION_UPDATE_COMPLETE: handleLeConnectionUpdateComplete(r); break; 1189 | case EVT_LE_READ_REMOTE_USED_FEATURES_COMPLETE: handleLeReadRemoteUsedFeaturesComplete(r); break; 1190 | case EVT_LE_LONG_TERM_KEY_REQUEST: handleLeLongTermKeyRequest(r); break; 1191 | case EVT_LE_READ_LOCAL_P256_PUBLIC_KEY_COMPLETE: handleLeReadLocalP256PublicKeyComplete(r); break; 1192 | case EVT_LE_GENERATE_DHKEY_COMPLETE: handleLeGenerateDHKeyComplete(r); break; 1193 | case EVT_LE_ENHANCED_CONNECTION_COMPLETE: handleLeEnhancedConnectionComplete(r); break; 1194 | case EVT_LE_PHY_UPDATE_COMPLETE: handleLePhyUpdateComplete(r); break; 1195 | case EVT_LE_EXTENDED_ADVERTISING_REPORT: handleLeExtendedAdvertisingReport(r); break; 1196 | } 1197 | } 1198 | } 1199 | } else if (packetType == HCI_ACLDATA_PKT) { 1200 | if (data.length < 5) { 1201 | throwInvalidLength(); 1202 | } 1203 | var conhdl = r.u16(); 1204 | var pb = (conhdl >> 12) & 0x3; 1205 | var bc = (conhdl >> 14) & 0x3; 1206 | conhdl &= 0xfff; 1207 | var len = r.u16(); 1208 | var aclConn = activeConnections[conhdl]; 1209 | if (aclConn) { 1210 | var ib = aclConn.incomingL2CAPBuffer; 1211 | if (pb == 2) { 1212 | // First packet 1213 | if (ib.length != 0) { 1214 | // Warning: incomplete incoming packet, dropping 1215 | ib.length = 0; 1216 | } 1217 | ib.totalLength = 0; 1218 | //console.log('first packet'); 1219 | if (len < 4) { 1220 | // Possibly invalid on the LL layer, but allow this 1221 | ib.push(r.getRemainingBuffer()); 1222 | ib.totalLength += ib[ib.length - 1].length; 1223 | } else { 1224 | var l2capLength = (data[5] | (data[6] << 8)); 1225 | //console.log('l2capLength: ' + l2capLength + ', len: ' + len); 1226 | if (4 + l2capLength == len) { 1227 | // Full complete packet 1228 | r.u16(); // Length 1229 | var cid = r.u16(); 1230 | //console.log('full packet with cid ' + cid); 1231 | aclConn.emit('data', cid, r.getRemainingBuffer()); 1232 | } else if (4 + l2capLength < len) { 1233 | // Invalid, dropping 1234 | } else if (4 + l2capLength > len) { 1235 | ib.push(r.getRemainingBuffer()); 1236 | ib.totalLength += ib[ib.length - 1].length; 1237 | } 1238 | } 1239 | } else if (pb == 1) { 1240 | // Continuation 1241 | var buf = r.getRemainingBuffer(); 1242 | if (ib.length == 0) { 1243 | // Not a continuation, dropping 1244 | } else { 1245 | if (ib[ib.length - 1].length < 4) { 1246 | ib[ib.length - 1] = Buffer.concat([ib[ib.length - 1], buf]); 1247 | } else { 1248 | ib.push(buf); 1249 | } 1250 | ib.totalLength += buf.length; 1251 | if (ib.totalLength >= 4) { 1252 | var l2capLength = (ib[0][0] | (ib[0][1] << 8)); 1253 | if (4 + l2capLength == ib.totalLength) { 1254 | var completePacket = new PacketReader(Buffer.concat(ib, ib.totalLength)); 1255 | completePacket.u16(); // Length 1256 | var cid = completePacket.u16(); 1257 | ib.length = 0; 1258 | ib.totalLength = 0; 1259 | aclConn.emit('data', cid, completePacket.getRemainingBuffer()); 1260 | } 1261 | } 1262 | } 1263 | } else { 1264 | // Invalid pb 1265 | } 1266 | } 1267 | } else { 1268 | // Ignore unknown packet type 1269 | } 1270 | } 1271 | 1272 | transport.on('data', onData); 1273 | 1274 | this.stop = function() { 1275 | if (isStopped) { 1276 | return; 1277 | } 1278 | isStopped = true; 1279 | transport.removeListener('data', onData); 1280 | transport = {write: function(data) {}}; 1281 | }; 1282 | 1283 | this.getAdvCallback = function() { 1284 | return advCallback; 1285 | }; 1286 | } 1287 | 1288 | module.exports = function(transport) { 1289 | return new HciAdapter(transport); 1290 | }; 1291 | -------------------------------------------------------------------------------- /lib/internal/storage.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const crypto = require('crypto'); 4 | const utils = require('./utils'); 5 | const DuplicateCache = utils.DuplicateCache; 6 | 7 | const emptyBuffer = Buffer.alloc(0); 8 | 9 | const basePath = "NodeBleLib-storage-dir"; 10 | 11 | var cache = Object.create(null); 12 | 13 | function bufferToHex(b) { 14 | return !b ? null : b.toString('hex'); 15 | } 16 | 17 | function fixAddressToPath(a) { 18 | return a.replace(/:/g, '-'); 19 | } 20 | 21 | function fixAddressFromPath(a) { 22 | return a.replace(/-/g, ':'); 23 | } 24 | 25 | function timingSafeEqual(a, b) { 26 | return crypto.timingSafeEqual ? crypto.timingSafeEqual(a, b) : a.equals(b); 27 | } 28 | 29 | function mkdirRecursive(pathItems) { 30 | for (var i = 1; i <= pathItems.length; i++) { 31 | try { 32 | var p = path.join.apply(null, pathItems.slice(0, i)); 33 | fs.mkdirSync(p); 34 | } catch (e) { 35 | if (e.code == 'EEXIST') { 36 | continue; 37 | } 38 | //console.log('mkdirRecursive', pathItems, e); 39 | return false; 40 | } 41 | } 42 | return true; 43 | } 44 | 45 | function writeFile(pathItems, data) { 46 | if (!mkdirRecursive(pathItems.slice(0, -1))) { 47 | return false; 48 | } 49 | try { 50 | fs.writeFileSync(path.join.apply(null, pathItems), data); 51 | } catch (e) { 52 | //console.log('writeFileSync', pathItems, data, e); 53 | return false; 54 | } 55 | return true; 56 | } 57 | 58 | function constructAddress(type, address) { 59 | return (type == 'public' ? '00:' : '01:') + address; 60 | } 61 | 62 | function storeKeys(ownAddress, peerAddress, mitm, sc, irk, localLtk, localRand, localEdiv, peerLtk, peerRand, peerEdiv) { 63 | ownAddress = fixAddressToPath(ownAddress); 64 | peerAddress = fixAddressToPath(peerAddress); 65 | 66 | if (!(ownAddress in cache)) { 67 | init(ownAddress); 68 | } 69 | 70 | var cacheEntry = cache[ownAddress]; 71 | 72 | if (irk) { 73 | var irkRev = Buffer.from(irk); 74 | irkRev.reverse(); 75 | cacheEntry.irks[peerAddress] = {aes: crypto.createCipheriv('AES-128-ECB', irkRev, emptyBuffer), irk: irk}; 76 | } 77 | 78 | cacheEntry.ltks[peerAddress] = { 79 | mitm: mitm, 80 | sc: sc, 81 | localLtk: !localLtk ? null : { 82 | rand: localRand, 83 | ediv: localEdiv, 84 | ltk: localLtk 85 | }, 86 | peerLtk: !peerLtk ? null : { 87 | rand: peerRand, 88 | ediv: peerEdiv, 89 | ltk: peerLtk 90 | } 91 | }; 92 | 93 | // keys.json: {"mitm":(boolean),"sc":(boolean),"irk":(hex),"localLtk":{"ediv":(integer),"rand":(hex),"ltk":(hex)},"peerLtk":{"ediv":(integer),"rand":(hex),"ltk":(hex)}} 94 | var json = JSON.stringify({ 95 | mitm: mitm, 96 | sc: sc, 97 | irk: bufferToHex(irk), 98 | localLtk: !localLtk ? null : { 99 | rand: localRand.toString('hex'), 100 | ediv: localEdiv, 101 | ltk: localLtk.toString('hex') 102 | }, 103 | peerLtk: !peerLtk ? null : { 104 | rand: peerRand.toString('hex'), 105 | ediv: peerEdiv, 106 | ltk: peerLtk.toString('hex'), 107 | } 108 | }); 109 | 110 | if (!writeFile([basePath, ownAddress, 'bonds', peerAddress, 'keys.json'], json)) { 111 | // TODO 112 | } 113 | } 114 | 115 | function resolveAddress(ownAddress, peerRandomAddress) { 116 | // input format is tt:aa:aa:aa:bb:bb:bb, where tt is 00 for public and 01 for random, rest is MSB -> LSB 117 | // returns identity address (or address used during pairing if BD_ADDR field was zero in Identity Address Informamtion) in same format or null 118 | 119 | ownAddress = fixAddressToPath(ownAddress); 120 | peerRandomAddress = peerRandomAddress.replace(/:/g, ''); 121 | 122 | var prand = Buffer.alloc(16); 123 | Buffer.from(peerRandomAddress.substr(2, 6), 'hex').copy(prand, 13); 124 | var hash = Buffer.from(peerRandomAddress.substr(8), 'hex'); 125 | 126 | //console.log('Resolving address', ownAddress, peerRandomAddress, prand, hash); 127 | 128 | if (!(ownAddress in cache)) { 129 | init(ownAddress); 130 | } 131 | var irks = cache[ownAddress].irks; 132 | for (var candidatePeerAddress in irks) { 133 | //console.log('Testing ', candidatePeerAddress); 134 | if (timingSafeEqual(irks[candidatePeerAddress].aes.update(prand).slice(13), hash)) { 135 | //console.log('yes!'); 136 | return fixAddressFromPath(candidatePeerAddress); 137 | } 138 | } 139 | return null; 140 | } 141 | 142 | function getKeys(ownAddress, peerAddress) { 143 | ownAddress = fixAddressToPath(ownAddress); 144 | peerAddress = fixAddressToPath(peerAddress); 145 | 146 | if (!(ownAddress in cache)) { 147 | init(ownAddress); 148 | } 149 | 150 | var keys = cache[ownAddress].ltks[peerAddress]; 151 | return keys; 152 | } 153 | 154 | function storeCccd(ownAddress, peerAddress, handle, value) { 155 | ownAddress = fixAddressToPath(ownAddress); 156 | peerAddress = fixAddressToPath(peerAddress); 157 | 158 | if (!(ownAddress in cache)) { 159 | init(ownAddress); 160 | } 161 | 162 | var cacheEntry = cache[ownAddress]; 163 | if (!cacheEntry.cccdValues[peerAddress]) { 164 | cacheEntry.cccdValues[peerAddress] = Object.create(null); 165 | } 166 | if (cacheEntry.cccdValues[peerAddress][handle] != value) { 167 | cacheEntry.cccdValues[peerAddress][handle] = value; 168 | writeFile([basePath, ownAddress, 'bonds', peerAddress, 'gatt_server_cccds', ("000" + handle.toString(16)).substr(-4) + '.json'], JSON.stringify(value)); 169 | } 170 | } 171 | 172 | function getCccd(ownAddress, peerAddress, handle) { 173 | ownAddress = fixAddressToPath(ownAddress); 174 | peerAddress = fixAddressToPath(peerAddress); 175 | 176 | if (!(ownAddress in cache)) { 177 | init(ownAddress); 178 | } 179 | 180 | var cacheEntry = cache[ownAddress]; 181 | 182 | if (cacheEntry.cccdValues[peerAddress]) { 183 | return cacheEntry.cccdValues[peerAddress][handle]; 184 | } 185 | 186 | return 0; 187 | } 188 | 189 | function storeGattCache(ownAddress, peerAddress, isBonded, obj) { 190 | ownAddress = fixAddressToPath(ownAddress); 191 | peerAddress = fixAddressToPath(peerAddress); 192 | 193 | if (!(ownAddress in cache)) { 194 | init(ownAddress); 195 | } 196 | 197 | obj.timestamp = Date.now(); 198 | 199 | var cacheEntry = cache[ownAddress]; 200 | if (isBonded) { 201 | cacheEntry.bondedPeerGattDbs[peerAddress] = obj; 202 | } else { 203 | cacheEntry.unbondedPeerGattDbs.add(peerAddress, obj); 204 | } 205 | 206 | writeFile([basePath, ownAddress, isBonded ? 'bonds' : 'unbonded', peerAddress, 'gatt_client_cache.json'], JSON.stringify(obj)); 207 | } 208 | 209 | function getGattCache(ownAddress, peerAddress, isBonded) { 210 | ownAddress = fixAddressToPath(ownAddress); 211 | peerAddress = fixAddressToPath(peerAddress); 212 | 213 | if (!(ownAddress in cache)) { 214 | init(ownAddress); 215 | } 216 | 217 | var cacheEntry = cache[ownAddress]; 218 | 219 | if (isBonded) { 220 | return cacheEntry.bondedPeerGattDbs[peerAddress] || null; 221 | } else { 222 | return cacheEntry.unbondedPeerGattDbs.get(peerAddress); 223 | } 224 | } 225 | 226 | function removeBond(ownAddress, peerAddress) { 227 | ownAddress = fixAddressToPath(ownAddress); 228 | peerAddress = fixAddressToPath(peerAddress); 229 | 230 | if (!(ownAddress in cache)) { 231 | init(ownAddress); 232 | } 233 | 234 | var cacheEntry = cache[ownAddress]; 235 | 236 | var remove = false; 237 | 238 | if (peerAddress in cacheEntry.irks) { 239 | remove = true; 240 | delete cacheEntry.irks[peerAddress]; 241 | } 242 | 243 | if (peerAddress in cacheEntry.ltks) { 244 | remove = true; 245 | delete cacheEntry.ltks[peerAddress]; 246 | } 247 | 248 | if (peerAddress in cacheEntry.cccdValues) { 249 | remove = true; 250 | delete cacheEntry.cccdValues[peerAddress]; 251 | } 252 | 253 | if (remove) { 254 | var bondPath = path.join(basePath, ownAddress, 'bonds', peerAddress); 255 | function recurseRemove(dirPath) { 256 | fs.readdirSync(dirPath).forEach(p => { 257 | entryPath = path.join(dirPath, p); 258 | if (fs.lstatSync(entryPath).isDirectory()) { 259 | recurseRemove(entryPath); 260 | } else { 261 | fs.unlinkSync(entryPath); 262 | } 263 | }); 264 | fs.rmdirSync(dirPath); 265 | } 266 | try { 267 | recurseRemove(bondPath); 268 | } catch (e) { 269 | } 270 | } 271 | } 272 | 273 | function init(ownAddress) { 274 | ownAddress = fixAddressToPath(ownAddress); 275 | 276 | if (!(ownAddress in cache)) { 277 | var cacheEntry = { 278 | irks: Object.create(null), 279 | ltks: Object.create(null), 280 | cccdValues: Object.create(null), 281 | bondedPeerGattDbs: Object.create(null), 282 | unbondedPeerGattDbs: new DuplicateCache(50) 283 | }; 284 | cache[ownAddress] = cacheEntry; 285 | 286 | try { 287 | var dir = path.join(basePath, ownAddress, 'bonds'); 288 | fs.readdirSync(dir).forEach(peerAddress => { 289 | try { 290 | // TODO: validate that all buffers are of correct size, ediv is a 16-bit integer etc. 291 | 292 | var keys = JSON.parse(fs.readFileSync(path.join(dir, peerAddress, 'keys.json'))); 293 | if (keys.irk) { 294 | var irkBuffer = Buffer.from(keys.irk, 'hex'); 295 | var irkBufferRev = irkBuffer; 296 | irkBufferRev.reverse(); 297 | var aes = crypto.createCipheriv('AES-128-ECB', irkBufferRev, emptyBuffer); 298 | cacheEntry.irks[peerAddress] = {aes: aes, irk: irkBuffer}; 299 | } 300 | if (keys.localLtk || keys.peerLtk) { 301 | var obj = {mitm: keys.mitm, sc: keys.sc, localLtk: null, peerLtk: null}; 302 | if (keys.localLtk) { 303 | obj.localLtk = {rand: Buffer.from(keys.localLtk.rand, 'hex'), ediv: keys.localLtk.ediv, ltk: Buffer.from(keys.localLtk.ltk, 'hex')}; 304 | } 305 | if (keys.peerLtk) { 306 | obj.peerLtk = {rand: Buffer.from(keys.peerLtk.rand, 'hex'), ediv: keys.peerLtk.ediv, ltk: Buffer.from(keys.peerLtk.ltk, 'hex')}; 307 | } 308 | cacheEntry.ltks[peerAddress] = obj; 309 | } 310 | } catch(e) { 311 | //console.log('readFileSync', e); 312 | } 313 | 314 | try { 315 | var obj = JSON.parse(fs.readFileSync(path.join(dir, peerAddress, 'gatt_client_cache.json'))); 316 | cacheEntry.bondedPeerGattDbs[peerAddress] = obj; 317 | } catch(e) { 318 | //console.log('readFileSync 2', e); 319 | } 320 | 321 | try { 322 | fs.readdirSync(path.join(dir, peerAddress, 'gatt_server_cccds')).forEach(handleFileName => { 323 | if (/^[a-zA-Z0-9]{4}\.json$/.test(handleFileName)) { 324 | try { 325 | var v = JSON.parse(fs.readFileSync(path.join(dir, peerAddress, 'gatt_server_cccds', handleFileName))); 326 | if (v === 0 || v === 1 || v === 2 || v === 3) { 327 | if (!cacheEntry.cccdValues[peerAddress]) { 328 | cacheEntry.cccdValues[peerAddress] = Object.create(null); 329 | } 330 | cacheEntry.cccdValues[peerAddress][parseInt(handleFileName, 16)] = v; 331 | } 332 | } catch(e) { 333 | //console.log('readFileSync', e); 334 | } 335 | } 336 | }); 337 | } catch(e) { 338 | //console.log('readdir', e); 339 | } 340 | }); 341 | 342 | } catch(e) { 343 | //console.log('readdir', e); 344 | } 345 | 346 | cacheEntry.unbondedPeerGattDbs.on('remove', peerAddress => { 347 | try { 348 | fs.unlinkSync(path.join(basePath, ownAddress, 'unbonded', peerAddress, 'gatt_client_cache.json')); 349 | } catch(e) { 350 | } 351 | }); 352 | 353 | try { 354 | var unbondedGattCaches = []; 355 | var dir = path.join(basePath, ownAddress, 'unbonded'); 356 | fs.readdirSync(dir).forEach(peerAddress => { 357 | try { 358 | var obj = JSON.parse(fs.readFileSync(path.join(dir, peerAddress, 'gatt_client_cache.json'))); 359 | unbondedGattCaches.push({peerAddress: peerAddress, obj: obj}); 360 | } catch(e) { 361 | } 362 | }); 363 | unbondedGattCaches.sort((a, b) => a.obj.timestamp - b.obj.timestamp); 364 | unbondedGattCaches.forEach(item => { 365 | cacheEntry.unbondedPeerGattDbs.add(item.peerAddress, item.obj); 366 | }); 367 | } catch(e) { 368 | } 369 | 370 | //console.log(cacheEntry); 371 | } 372 | } 373 | 374 | module.exports = Object.freeze({ 375 | constructAddress: constructAddress, 376 | storeKeys: storeKeys, 377 | getKeys: getKeys, 378 | resolveAddress: resolveAddress, 379 | storeCccd: storeCccd, 380 | getCccd: getCccd, 381 | storeGattCache: storeGattCache, 382 | getGattCache: getGattCache, 383 | removeBond: removeBond 384 | }); 385 | -------------------------------------------------------------------------------- /lib/internal/utils.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const util = require('util'); 3 | 4 | const BASE_UUID_SECOND_PART = '-0000-1000-8000-00805F9B34FB'; 5 | 6 | function Queue() { 7 | var q = []; 8 | var pos = 0; 9 | 10 | this.getLength = function() { 11 | return q.length - pos; 12 | }; 13 | 14 | this.push = function(v) { 15 | q.push(v); 16 | }; 17 | 18 | this.shift = function() { 19 | if (pos == q.length) { 20 | return undefined; 21 | } 22 | var elem = q[pos++]; 23 | if (pos * 2 >= q.length) { 24 | q.splice(0, pos); 25 | pos = 0; 26 | } 27 | return elem; 28 | }; 29 | 30 | this.peek = function() { 31 | if (pos == q.length) { 32 | return undefined; 33 | } 34 | return q[pos]; 35 | }; 36 | 37 | this.getAt = function(i) { 38 | if (pos + i >= q.length) { 39 | return undefined; 40 | } 41 | return q[pos + i]; 42 | }; 43 | } 44 | 45 | function IdGenerator() { 46 | var last = ""; 47 | this.next = function() { 48 | for (var pos = last.length - 1; pos >= 0; --pos) { 49 | if (last[pos] != 'z') { 50 | return last = last.substr(0, pos) + String.fromCharCode(last.charCodeAt(pos) + 1) + 'a'.repeat(last.length - pos - 1); 51 | } 52 | } 53 | return last = 'a'.repeat(last.length + 1); 54 | }; 55 | } 56 | 57 | function DuplicateCache(capacity) { 58 | if (capacity <= 0) { 59 | throw new Error("Invalid capacity"); 60 | } 61 | EventEmitter.call(this); 62 | 63 | var first = null; 64 | var last = null; 65 | var nodeMap = Object.create(null); 66 | 67 | var dc = this; 68 | 69 | this.isDuplicate = function(key) { 70 | return key in nodeMap; 71 | }; 72 | 73 | this.get = function(key) { 74 | if (key in nodeMap) { 75 | return nodeMap[key].value; 76 | } else { 77 | return null; 78 | } 79 | }; 80 | 81 | this.remove = function(key) { 82 | if (key in nodeMap) { 83 | var node = nodeMap[key]; 84 | delete nodeMap[key]; 85 | if (node.next != null) { 86 | node.next.prev = node.prev; 87 | } 88 | if (first == node) { 89 | first = node.next; 90 | } 91 | if (last == node) { 92 | last = node.prev; 93 | } 94 | ++capacity; 95 | return true; 96 | } 97 | return false; 98 | }; 99 | 100 | // Adds or updates the value. Returns false if the key already was in the cache (but updates regardless). 101 | this.add = function(key, value) { 102 | var exists = dc.remove(key); 103 | var firstKey, removedFirstKey = false; 104 | if (capacity == 0) { 105 | firstKey = first.key; 106 | removedFirstKey = true; 107 | delete nodeMap[first.key]; 108 | first = first.next; 109 | if (first) { 110 | first.prev = null; 111 | } else { 112 | last = null; 113 | } 114 | ++capacity; 115 | } 116 | var node = {prev: last, next: null, key: key, value: value}; 117 | nodeMap[key] = node; 118 | last = node; 119 | if (first == null) { 120 | first = node; 121 | } 122 | --capacity; 123 | if (removedFirstKey && firstKey != key) { 124 | dc.emit('remove', firstKey); 125 | } 126 | return !exists; 127 | }; 128 | } 129 | util.inherits(DuplicateCache, EventEmitter); 130 | 131 | function serializeUuid(v) { 132 | if (typeof v === 'string') { 133 | if (v.substr(8) == BASE_UUID_SECOND_PART && v.substr(0, 4) == '0000') { 134 | return Buffer.from(v.substr(4, 4), 'hex').reverse(); 135 | } 136 | var ret = Buffer.from(v.replace(/-/g, ''), 'hex').reverse(); 137 | if (ret.length == 16) { 138 | return ret; 139 | } 140 | } else if (Number.isInteger(v) && v >= 0 && v <= 0xffff) { 141 | return Buffer.from([v, v >> 8]); 142 | } 143 | throw new Error('Invalid uuid: ' + v); 144 | } 145 | 146 | function isValidBdAddr(bdAddr) { 147 | return typeof bdAddr === 'string' && /^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/.test(bdAddr); 148 | } 149 | 150 | function bdAddrToBuffer(address) { 151 | var buf = []; 152 | for (var i = 15; i >= 0; i -= 3) { 153 | buf.push(parseInt(address.substr(i, 2), 16)); 154 | } 155 | return Buffer.from(buf); 156 | } 157 | 158 | module.exports = Object.freeze({ 159 | Queue: Queue, 160 | IdGenerator: IdGenerator, 161 | DuplicateCache: DuplicateCache, 162 | serializeUuid: serializeUuid, 163 | isValidBdAddr: isValidBdAddr, 164 | bdAddrToBuffer: bdAddrToBuffer 165 | }); 166 | -------------------------------------------------------------------------------- /lib/io-capabilities.js: -------------------------------------------------------------------------------- 1 | var obj = Object.freeze({ 2 | DISPLAY_ONLY: 0x00, 3 | DISPLAY_YES_NO: 0x01, 4 | KEYBOARD_ONLY: 0x02, 5 | NO_INPUT_NO_OUTPUT: 0x03, 6 | KEYBOARD_DISPLAY: 0x04, 7 | 8 | toString: function(v) { 9 | for (var key in obj) { 10 | if (obj.hasOwnProperty(key) && key != "toString" && obj[key] == v) { 11 | return key; 12 | } 13 | } 14 | return "(unknown)"; 15 | } 16 | }); 17 | 18 | module.exports = obj; 19 | -------------------------------------------------------------------------------- /lib/l2cap-coc-errors.js: -------------------------------------------------------------------------------- 1 | var obj = Object.freeze({ 2 | TIMEOUT: -1, 3 | CONNECTION_SUCCESSFUL: 0, 4 | LE_PSM_NOT_SUPPORTED: 2, 5 | NO_RESOURCES_AVAILABLE: 4, 6 | INSUFFICIENT_AUTHENTICATION: 5, 7 | INSUFFICIENT_AUTHORIZATION: 6, 8 | INSUFFICIENT_ENCRYPTION_KEY_SIZE: 7, 9 | INSUFFICIENT_ENCRYPTION: 8, 10 | INVALID_SOURCE_CID: 9, 11 | SOURCE_CID_ALREADY_ALLOCATED: 10, 12 | UNACCEPTABLE_PARAMETERS: 11, 13 | 14 | toString: function(v) { 15 | for (var key in obj) { 16 | if (obj.hasOwnProperty(key) && key != "toString" && obj[key] == v) { 17 | return key; 18 | } 19 | } 20 | return "(unknown)"; 21 | } 22 | }); 23 | 24 | module.exports = obj; 25 | -------------------------------------------------------------------------------- /lib/smp-errors.js: -------------------------------------------------------------------------------- 1 | var obj = Object.freeze({ 2 | PASSKEY_ENTRY_FAILED: 0x01, 3 | OOB_NOT_AVAILABLE: 0x02, 4 | AUTHENTICATION_REQUIREMENTS: 0x03, 5 | CONFIRM_VALUE_FAILED: 0x04, 6 | PAIRING_NOT_SUPPORTED: 0x05, 7 | ENCRYPTION_KEY_SIZE: 0x06, 8 | COMMAND_NOT_SUPPORTED: 0x07, 9 | UNSPECIFIED_REASON: 0x08, 10 | REPEATED_ATTEMPTS: 0x09, 11 | INVALID_PARAMETERS: 0x0a, 12 | DHKEY_CHECK_FAILED: 0x0b, 13 | NUMERIC_COMPARISON_FAILED: 0x0c, 14 | BR_EDR_PAIRING_IN_PROGRESS: 0x0d, 15 | CROSS_TRANSPORT_KEY_DERIVATION_GENERATION_NOT_ALLOWED: 0x0e, 16 | 17 | toString: function(v) { 18 | for (var key in obj) { 19 | if (obj.hasOwnProperty(key) && key != "toString" && obj[key] == v) { 20 | return key; 21 | } 22 | } 23 | return "(unknown)"; 24 | } 25 | }); 26 | 27 | module.exports = obj; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ble-host", 3 | "version": "1.0.3", 4 | "author": "Emil Lenngren", 5 | "optionalDependencies": { 6 | "hci-socket": "^1.0.0" 7 | }, 8 | "main": "./lib/index.js", 9 | "repository": "github:Emill/node-ble-host", 10 | "homepage": "https://github.com/Emill/node-ble-host", 11 | "bugs": "https://github.com/Emill/node-ble-host/issues", 12 | "license": "ISC", 13 | "keywords": [ 14 | "bluetooth", 15 | "hci", 16 | "noble", 17 | "bleno", 18 | "ble", 19 | "bluetooth-le", 20 | "bluetooth-low-energy", 21 | "bluetooth low energy", 22 | "BleManager", 23 | "node-ble-host", 24 | "NodeBleHost", 25 | "ble-host", 26 | "gatt", 27 | "l2cap", 28 | "central", 29 | "peripheral" 30 | ], 31 | "description": "A full-featured Bluetooth Low Energy (BLE) host stack written in JavaScript that lets you set up and program peripherals and centrals." 32 | } 33 | --------------------------------------------------------------------------------