├── .gitignore ├── LICENSE ├── README.md ├── ancs-notification.js ├── ancs-service.js ├── examples ├── test-gpio.js ├── test-webpage.js ├── test.js └── views │ └── index.jade ├── generic-characteristic.js ├── index.js ├── lib ├── able.js ├── characteristics.json ├── descriptors.json ├── hci-socket │ ├── acl-stream.js │ ├── bindings.js │ ├── crypto.js │ ├── gap.js │ ├── gatt.js │ ├── hci-status.json │ ├── hci.js │ ├── local-gatt.js │ ├── mgmt.js │ └── smp.js ├── local-characteristic.js ├── local-descriptor.js ├── peripheral.js ├── primary-service.js ├── remote-characteristic.js ├── remote-descriptor.js ├── service.js ├── services.json └── uuid-util.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Sandeep Mistry 4 | Copyright (c) 2015 Luke Berndt 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BLE ANCS 2 | =========== 3 | 4 | This is a NodeJS library that provides Linux systems with notifications from iOS devices via Bluetooth LE, using the [Apple Notification Center Service (ANCS)](https://developer.apple.com/library/ios/documentation/CoreBluetooth/Reference/AppleNotificationCenterServiceSpecification/Introduction/Introduction.html). 5 | 6 | This library is mostly a combination of 3 great libraries from [Sandeep Mistry](https://github.com/sandeepmistry): 7 | * [noble](https://github.com/sandeepmistry/noble) - A Node.js BLE (Bluetooth Low Energy) central module 8 | * [bleno](https://github.com/sandeepmistry/bleno) - A Node.js module for implementing BLE (Bluetooth Low Energy) peripherals 9 | * [node-ancs](https://github.com/sandeepmistry/node-ancs) - A node.js lib to access the Apple Notification Center Service (ANCS) 10 | 11 | I have combined noble and bleno together so it is possible to easily pivot from being a peripheral to a central role. This allows for a Pairing to be established between the iOS device and the Linux system without any requiring any iOS Apps. You can simply go into the Bluetooth setting and connect with the Linux system to establish an ANCS pairing. 12 | 13 | ## Prerequisites 14 | 15 | ### Linux (Ubuntu) 16 | 17 | * Kernel version 3.6 or above 18 | * ```libbluetooth-dev``` 19 | 20 | #### Ubuntu/Debian/Raspbian 21 | 22 | ```sh 23 | sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev 24 | ``` 25 | 26 | #### Fedora / Other-RPM based 27 | 28 | ```sh 29 | sudo yum install bluez bluez-libs bluez-libs-devel 30 | ``` 31 | 32 | #### Running without root/sudo 33 | 34 | Run the following command: 35 | 36 | ```sh 37 | sudo setcap cap_net_raw+eip $(eval readlink -f `which node`) 38 | ``` 39 | 40 | This grants the ```node``` binary ```cap_net_raw``` privileges, so it can start/stop BLE advertising. 41 | 42 | __Note:__ The above command requires ```setcap``` to be installed, it can be installed using the following: 43 | 44 | * apt: ```sudo apt-get install libcap2-bin``` 45 | * yum: ```su -c \'yum install libcap2-bin\'``` 46 | 47 | ### IMPORTANT! 48 | You need to have the Bluetooth Service / `bluetoothd` initially running while the bluetooth LE dongle is attached. After they have initialized the dongle, stop the bluetooth service from running. Watch out for it respawning. Do a quick check with `ps -A | grep 'blue'`. 49 | 50 | ```sh 51 | sudo stop bluetooth 52 | ``` 53 | 54 | or 55 | 56 | ```sh 57 | sudo /etc/init.d/bluetooth stop 58 | ``` 59 | 60 | 61 | 62 | 63 | 64 | Usage 65 | ----- 66 | 67 | var BleAncs = require('ble-ancs'); 68 | 69 | var ancs = new BleAncs(); 70 | 71 | __Notification Events__ 72 | 73 | ancs.on('notification', function(notification) { 74 | ... 75 | }); 76 | 77 | * notification has the following properties 78 | * event (one of): 79 | * added 80 | * modified 81 | * removed 82 | * flags (array): 83 | * silent 84 | * important 85 | * category (one of): 86 | * other 87 | * incomingCall 88 | * missedCall 89 | * voicemail 90 | * schedule 91 | * email 92 | * other 93 | * news 94 | * healthAndFitness 95 | * businessAndFinance 96 | * location 97 | * entertianment 98 | * categoryCount 99 | * uid 100 | 101 | __Operations for 'added' or 'modified' notifications (event property)__ 102 | 103 | Read App Identifier 104 | 105 | notification.readAppIdentifier(function(appIdentifier) { 106 | ... 107 | }); 108 | 109 | Read Title 110 | 111 | notification.readTitle(function(title) { 112 | ... 113 | }); 114 | 115 | Read Subtitle 116 | 117 | notification.readSubtitle(function(subtitle) { 118 | ... 119 | }); 120 | 121 | Read Message 122 | 123 | notification.readMessage(function(message) { 124 | ... 125 | }); 126 | 127 | Read Date 128 | 129 | notification.readDate(function(date) { 130 | ... 131 | }); 132 | 133 | Read All Attributes 134 | 135 | notification.readAttributes(function(attributes) { 136 | ... 137 | }); 138 | 139 | * attributes has the following properties 140 | * appIdentifier 141 | * title 142 | * subtitle 143 | * message 144 | * date 145 | 146 | ## Useful Links 147 | 148 | * [Bluetooth Development Portal](http://developer.bluetooth.org) 149 | * [GATT Specifications](http://developer.bluetooth.org/gatt/Pages/default.aspx) 150 | * [Bluetooth: ATT and GATT](http://epx.com.br/artigos/bluetooth_gatt.php) 151 | 152 | ## License 153 | 154 | Copyright (C) 2015 Sandeep Mistry 155 | Copyright (C) 2015 Luke Berndt 156 | 157 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 158 | 159 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 160 | 161 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 162 | -------------------------------------------------------------------------------- /ancs-notification.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | 4 | var EVENT_ID = [ 5 | 'added', 6 | 'modified', 7 | 'removed' 8 | ]; 9 | 10 | var CATEGORY_ID = [ 11 | 'other', 12 | 'incomingCall', 13 | 'missedCall', 14 | 'voicemail', 15 | 'social', 16 | 'schedule', 17 | 'email', 18 | 'news', 19 | 'healthAndFitness', 20 | 'businessAndFinance', 21 | 'location', 22 | 'entertianment' 23 | ]; 24 | 25 | var APP_IDENTIFIER = 0; 26 | var TITLE = 1; 27 | var SUBTITLE = 2; 28 | var MESSAGE = 3; 29 | var MESSAGE_SIZE = 4; 30 | var DATE = 5; 31 | var POSITIVE_LABEL = 6; 32 | var NEGATIVE_LABEL = 7; 33 | 34 | var ATTRIBUTE_ID = [ 35 | 'appIdentifier', 36 | 'title', 37 | 'subtitle', 38 | 'message', 39 | 'messageSize', 40 | 'date', 41 | 'positiveLabel', 42 | 'negativeLabel' 43 | ]; 44 | 45 | var Notification = function(ancs, data) { 46 | var eventId = data.readUInt8(0); 47 | var eventFlags = data.readUInt8(1); 48 | var categoryId = data.readUInt8(2); 49 | var categoryCount = data.readUInt8(3); 50 | var uid = data.readUInt32LE(4); 51 | 52 | 53 | this._ancs = ancs; 54 | this._buffer = ''; 55 | 56 | this.event = EVENT_ID[eventId]; 57 | this.flags = []; 58 | this.versions = []; 59 | 60 | if (eventFlags & 1) { 61 | this.flags.push('silent'); 62 | } 63 | 64 | if (eventFlags & 1) { 65 | this.flags.push('important'); 66 | } 67 | 68 | this.category = CATEGORY_ID[categoryId]; 69 | this.categoryCount = categoryCount; 70 | 71 | this.uid = uid; 72 | this.title = ""; 73 | this.subtitle = ""; 74 | this.date = ""; 75 | this.message = ""; 76 | this.messageSize = 0; 77 | this.on('data', this.onData.bind(this)); 78 | }; 79 | 80 | util.inherits(Notification, events.EventEmitter); 81 | 82 | Notification.prototype.toString = function() { 83 | return JSON.stringify({ 84 | event: this.event, 85 | flags: this.flags, 86 | category: this.category, 87 | categoryCount: this.categoryCount, 88 | uid: this.uid, 89 | title: this.title, 90 | subtitle: this.subtitle, 91 | message: this.message, 92 | messageSize: this.messageSize, 93 | date: this.date, 94 | positiveLabel: this.positiveLabel, 95 | negativeLabel: this.negativeLabel 96 | }); 97 | }; 98 | 99 | Notification.prototype.onData = function(data) { 100 | // console.log('notification data = ' + data.toString('hex')); 101 | 102 | this._buffer += data.toString('hex'); 103 | data = new Buffer(this._buffer, 'hex'); 104 | 105 | var attributeId = data.readUInt8(0); 106 | var attributeLength = data.readUInt16LE(1); 107 | var attributeData = data.slice(3); 108 | 109 | if (attributeLength === attributeData.length) { 110 | if (attributeId === APP_IDENTIFIER) { 111 | var appIdentifier = this.appIdentifier = attributeData.toString(); 112 | 113 | this.emit('appIdentifier', appIdentifier); 114 | } else if (attributeId === TITLE) { 115 | var title = this.title = attributeData.toString(); 116 | 117 | this.emit('title', title); 118 | } else if (attributeId === SUBTITLE) { 119 | var subtitle = this.subtitle = attributeData.toString(); 120 | 121 | this.emit('subtitle', subtitle); 122 | } else if (attributeId === MESSAGE) { 123 | var message = this.message = attributeData.toString(); 124 | 125 | this.emit('message', message); 126 | } else if (attributeId === MESSAGE_SIZE) { 127 | var messageSize = this.messageSize = parseInt(attributeData.toString(), 10); 128 | 129 | this.emit('messageSize', messageSize); 130 | } else if (attributeId === DATE) { 131 | var dateString = attributeData.toString(); 132 | 133 | var year = parseInt(dateString.substring(0, 4), 10); 134 | var month = parseInt(dateString.substring(4, 6), 10); 135 | var day = parseInt(dateString.substring(6, 8), 10); 136 | 137 | var hours = parseInt(dateString.substring(9, 11), 10); 138 | var minutes = parseInt(dateString.substring(11, 13), 10); 139 | var seconds = parseInt(dateString.substring(13, 15), 10); 140 | 141 | var date = this.date = new Date(year, month, day, hours, minutes, seconds); 142 | 143 | this.emit('date', date); 144 | } else if (attributeId === POSITIVE_LABEL) { 145 | var positiveLabel = this.positiveLabel = attributeData.toString(); 146 | 147 | this.emit('positiveLabel', positiveLabel); 148 | } else if (attributeId === NEGATIVE_LABEL) { 149 | var negativeLabel = this.negativeLabel = attributeData.toString(); 150 | 151 | this.emit('negativeLabel', negativeLabel); 152 | } 153 | 154 | this._buffer = ''; 155 | } 156 | }; 157 | 158 | Notification.prototype.readAppIdentifier = function(callback) { 159 | this.once('appIdentifier', function(appIdentifier) { 160 | callback(appIdentifier); 161 | }); 162 | 163 | this._ancs.queueAttributeRequest(this.uid, APP_IDENTIFIER); 164 | 165 | //this._ancs.requestNotificationAttribute(this.uid, APP_IDENTIFIER); 166 | }; 167 | 168 | Notification.prototype.readTitle = function(callback) { 169 | this.once('title', function(title) { 170 | callback(title); 171 | }); 172 | 173 | this._ancs.queueAttributeRequest(this.uid, TITLE); 174 | 175 | //this._ancs.requestNotificationAttribute(this.uid, TITLE, 255); 176 | }; 177 | 178 | Notification.prototype.readSubtitle = function(callback) { 179 | this.once('subtitle', function(subtitle) { 180 | callback(subtitle); 181 | }); 182 | 183 | this._ancs.queueAttributeRequest(this.uid, SUBTITLE); 184 | 185 | //this._ancs.requestNotificationAttribute(this.uid, SUBTITLE, 255); 186 | }; 187 | 188 | Notification.prototype.readMessage = function(callback) { 189 | this.readMessageSize(function(messageSize) { 190 | this.once('message', function(message) { 191 | callback(message); 192 | }); 193 | 194 | this._ancs.queueAttributeRequest(this.uid, MESSAGE); 195 | 196 | //this._ancs.requestNotificationAttribute(this.uid, MESSAGE, messageSize); 197 | }.bind(this)); 198 | }; 199 | 200 | Notification.prototype.readMessageSize = function(callback) { 201 | this.once('messageSize', function(messageSize) { 202 | callback(messageSize); 203 | }); 204 | 205 | this._ancs.queueAttributeRequest(this.uid, MESSAGE_SIZE); 206 | 207 | //this._ancs.requestNotificationAttribute(this.uid, MESSAGE_SIZE); 208 | }; 209 | 210 | Notification.prototype.readDate = function(callback) { 211 | this.once('date', function(date) { 212 | callback(date); 213 | }); 214 | 215 | this._ancs.queueAttributeRequest(this.uid, DATE); 216 | 217 | this._ancs.requestNotificationAttribute(this.uid, DATE); 218 | }; 219 | 220 | Notification.prototype.readPositiveLabel = function(callback) { 221 | this.once('positiveLabel', function(positiveLabel) { 222 | callback(positiveLabel); 223 | }); 224 | 225 | this._ancs.queueAttributeRequest(this.uid, POSITIVE_LABEL); 226 | 227 | //this._ancs.requestNotificationAttribute(this.uid, POSITIVE_LABEL, 255); 228 | }; 229 | 230 | Notification.prototype.readNegativeLabel = function(callback) { 231 | this.once('negativeLabel', function(negativeLabel) { 232 | callback(negativeLabel); 233 | }); 234 | 235 | this._ancs.queueAttributeRequest(this.uid, NEGATIVE_LABEL); 236 | 237 | //this._ancs.requestNotificationAttribute(this.uid, NEGATIVE_LABEL, 255); 238 | }; 239 | 240 | 241 | Notification.prototype.readAttributes = function(callback) { 242 | this.readAppIdentifier(function(appIdentifier) { 243 | this.readTitle(function(title) { 244 | this.readSubtitle(function(subtitle) { 245 | this.readMessage(function(message) { 246 | this.readDate(function(date) { 247 | callback({ 248 | appIdentifier: appIdentifier, 249 | title: title, 250 | subtitle: subtitle, 251 | message: message, 252 | date: date 253 | }); 254 | }.bind(this)); 255 | }.bind(this)); 256 | }.bind(this)); 257 | }.bind(this)); 258 | }.bind(this)); 259 | }; 260 | 261 | module.exports = Notification; 262 | -------------------------------------------------------------------------------- /ancs-service.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var able = require('./index'); 4 | var AblePrimaryService = able.PrimaryService; 5 | 6 | //var Blink1RGBCharacteristic = require('./blink1-rgb-characteristic'); 7 | //var Blink1FadeRGBCharacteristic = require('./blink1-fade-rgb-characteristic'); 8 | 9 | function AncsService() { 10 | AncsService.super_.call(this, { 11 | uuid: '7905f431b5ce4e99a40f4b1e122d00d0', 12 | characteristics: [] 13 | }); 14 | } 15 | 16 | util.inherits(AncsService, AblePrimaryService); 17 | 18 | module.exports = AncsService; 19 | -------------------------------------------------------------------------------- /examples/test-gpio.js: -------------------------------------------------------------------------------- 1 | var BleAncs = require('../index'); 2 | var Gpio = require('onoff').Gpio, 3 | led = new Gpio(4, 'out'); 4 | 5 | 6 | var ancs = new BleAncs(); 7 | 8 | ancs.on('notification', function(notification) { 9 | 10 | //will blink an led on GPIO pin 4 everytime there is a new notification 11 | 12 | notification.readTitle( function(title) { 13 | notification.readMessage( function(message) { 14 | console.log("Notification: " + notification); 15 | 16 | }); 17 | }); 18 | led.write(1, function() { // Set pin 16 high (1) 19 | setTimeout(function() { 20 | led.writeSync(0); // Close pin 16 21 | },1000); 22 | }); 23 | 24 | }); -------------------------------------------------------------------------------- /examples/test-webpage.js: -------------------------------------------------------------------------------- 1 | var BleAncs = require('../index'); 2 | 3 | var ancs = new BleAncs(); 4 | 5 | 6 | var express = require('express'); 7 | var app = express(); 8 | 9 | 10 | app.set('view engine', 'jade'); 11 | 12 | app.get('/', function (req, res) { 13 | res.render('index', { notifications: ancs._notifications}); 14 | }); 15 | 16 | var server = app.listen(3000, function () { 17 | var host = server.address().address; 18 | var port = server.address().port; 19 | 20 | console.log('Example app listening at http://%s:%s', host, port); 21 | }); 22 | 23 | 24 | ancs.on('notification', function(notification) { 25 | notification.readTitle( function(title) {}); 26 | notification.readSubtitle( function(title) {}); 27 | notification.readDate( function(title) {}); 28 | notification.readMessage( function(message) { 29 | console.log("Notification: " + notification); 30 | }); 31 | }); -------------------------------------------------------------------------------- /examples/test.js: -------------------------------------------------------------------------------- 1 | var BleAncs = require('../index'); 2 | 3 | var ancs = new BleAncs(); 4 | 5 | 6 | ancs.on('notification', function(notification) { 7 | notification.readTitle( function(title) { 8 | notification.readMessage( function(message) { 9 | console.log("Notification: " + notification); 10 | }); 11 | }); 12 | }); -------------------------------------------------------------------------------- /examples/views/index.jade: -------------------------------------------------------------------------------- 1 | html(lang="en") 2 | head 3 | 4 | body 5 | 6 | div 7 | table 8 | thead 9 | tr: th Notifications 10 | tr 11 | td Event ID 12 | td Event 13 | td Category 14 | td Count 15 | td Date 16 | td Title 17 | td Subtitle 18 | td Message 19 | tbody 20 | each notification, i in notifications 21 | tr 22 | td #{notification.uid} 23 | td #{notification.event} 24 | td #{notification.category} 25 | td #{notification.categoryCount} 26 | td #{notification.date} 27 | td #{notification.title} 28 | td #{notification.subtitle} 29 | td #{notification.message} 30 | if notification.versions 31 | each ver, j in notification.versions 32 | tr 33 | td #{ver.uid} - #{j} 34 | td #{ver.event} 35 | td #{ver.category} 36 | td #{ver.categoryCount} 37 | td #{ver.date} 38 | td #{ver.title} 39 | td #{ver.subtitle} 40 | td #{ver.message} -------------------------------------------------------------------------------- /generic-characteristic.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | Able = require('./index'), 3 | Descriptor = require('./lib/local-characteristic'), 4 | Characteristic = require('./lib/local-characteristic');//Able.Characteristic; 5 | 6 | var GenericCharacteristic = function() { 7 | GenericCharacteristic.super_.call(this, { 8 | uuid: '2803', 9 | properties: ['read'], 10 | secure: ['read'], 11 | descriptors: [ 12 | new Descriptor({ 13 | uuid: '2901', 14 | value: 'Generic!' 15 | }), 16 | new Descriptor({ 17 | uuid: '2904', 18 | value: new Buffer([0x04, 0x01, 0x27, 0xAD, 0x01, 0x00, 0x00 ]) // maybe 12 0xC unsigned 8 bit 19 | }) 20 | ] 21 | }); 22 | }; 23 | 24 | util.inherits(GenericCharacteristic, Characteristic); 25 | 26 | GenericCharacteristic.prototype.onReadRequest = function(offset, callback) { 27 | 28 | 29 | // return hardcoded value 30 | callback(this.RESULT_SUCCESS, new Buffer([98])); 31 | 32 | }; 33 | 34 | module.exports = GenericCharacteristic; 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Able = require('./lib/able'); 2 | 3 | var util = require('util'); 4 | var debug = require('debug')('BleAncs'); 5 | var events = require('events'); 6 | 7 | var AblePrimaryService = require('./lib/primary-service.js'); 8 | var GenericCharacteristic = require('./generic-characteristic'); 9 | var Notification = require('./ancs-notification'); 10 | 11 | 12 | var SERVICE_UUID = '7905f431b5ce4e99a40f4b1e122d00d0'; 13 | var NOTIFICATION_SOURCE_UUID = '9fbf120d630142d98c5825e699a21dbd'; 14 | var CONTROL_POINT_UUID = '69d1d8f345e149a898219bbdfdaad9d9'; 15 | var DATA_SOURCE_UUID = '22eac6e924d64bb5be44b36ace7c7bfb'; 16 | 17 | function AttributeRequest(uid, attributeId) { 18 | this.uid = uid; 19 | this.attributeId = attributeId; 20 | }; 21 | 22 | function BleAncs() { 23 | this._able = new Able(); 24 | 25 | this._characteristics = {}; 26 | this._notifications = {}; 27 | this._lastUid = null; 28 | this._requestQueue = []; 29 | this._requestTimeout = null; 30 | this._pendingRequest = false; 31 | 32 | this._able.on('stateChange', this.onStateChange.bind(this)); 33 | this._able.on('accept', this.onAccept.bind(this)); 34 | this._able.on('mtuChange', this.onMtuChange.bind(this)); 35 | this._able.on('advertisingStart', this.onAdvertisingStart.bind(this)); 36 | this._able.on('encryptChange', this.onEncryptChange.bind(this)); 37 | this._able.on('encryptFail', this.onEncryptFail.bind(this)); 38 | this._able.on('connect', this.onConnect.bind(this)); 39 | this._able.on('disconnect', this.onDisconnect.bind(this)); 40 | }; 41 | 42 | util.inherits(BleAncs, events.EventEmitter); 43 | 44 | BleAncs.prototype.discoverServicesAndCharacteristics = function(callback) { 45 | this._peripheral.findServiceAndCharacteristics(SERVICE_UUID, [], function(error, services, characteristics) { 46 | for (var i in characteristics) { 47 | /* console.log("CHARECTERISTIC: "+characteristics[i]); 48 | if (characteristics[i].uuid == NOTIFICATION_SOURCE_UUID) { 49 | console.log("NOTIFICATION_SOURCE_UUID"); 50 | } 51 | if (characteristics[i].uuid == DATA_SOURCE_UUID) { 52 | console.log("DATA_SOURCE_UUID"); 53 | }*/ 54 | this._characteristics[characteristics[i].uuid] = characteristics[i]; 55 | } 56 | 57 | this._characteristics[NOTIFICATION_SOURCE_UUID].on('read', this.onNotification.bind(this)); 58 | this._characteristics[DATA_SOURCE_UUID].on('read', this.onData.bind(this)); 59 | 60 | this._characteristics[NOTIFICATION_SOURCE_UUID].notify(true); 61 | this._characteristics[DATA_SOURCE_UUID].notify(true); 62 | 63 | callback(); 64 | }.bind(this)); 65 | }; 66 | 67 | BleAncs.prototype.onNotification = function(data) { 68 | var notification = new Notification(this, data); 69 | 70 | if (notification.event == 'removed') { 71 | debug('Notification Removed: ' + notification); 72 | } else if (notification.event == 'added') { 73 | debug('Notification Added: ' + notification); 74 | } else if (notification.event == 'added') { 75 | debug('Notification Modified: ' + notification); 76 | } 77 | 78 | if (notification.uid in this._notifications) { 79 | 80 | if (notification.event != 'added') { 81 | var old_notification = this._notifications[notification.uid]; 82 | 83 | notification.versions = old_notification.versions; 84 | old_notification.versions = undefined; 85 | notification.versions.push(old_notification); 86 | this._notifications[notification.uid] = notification; 87 | } 88 | } else { 89 | this._notifications[notification.uid] = notification; 90 | } 91 | 92 | 93 | this.emit('notification', notification); 94 | 95 | }; 96 | 97 | BleAncs.prototype.onData = function(data) { 98 | var commandId = data.readUInt8(0); 99 | 100 | if (commandId === 0x00) { 101 | var uid = data.readUInt32LE(1); 102 | var notificationData = data.slice(5); 103 | 104 | this._lastUid = uid; 105 | 106 | this._notifications[uid].emit('data', notificationData); 107 | } else { 108 | if (this._lastUid) { 109 | this._notifications[this._lastUid].emit('data',data); 110 | } 111 | } 112 | clearTimeout(this._requestTimeout); 113 | this._pendingRequest = false; 114 | this.unqueueAttributeRequest(); 115 | }; 116 | 117 | BleAncs.prototype.requestNotificationAttribute = function(uid, attributeId, maxLength) { 118 | var buffer = new Buffer(maxLength ? 8 : 6); 119 | 120 | buffer.writeUInt8(0x00, 0); 121 | buffer.writeUInt32LE(uid, 1); 122 | buffer.writeUInt8(attributeId, 5); 123 | if (maxLength) { 124 | buffer.writeUInt16LE(maxLength, 6); 125 | } 126 | 127 | this._characteristics[CONTROL_POINT_UUID].write(buffer, false); 128 | }; 129 | 130 | BleAncs.prototype.unqueueAttributeRequest = function() { 131 | if (this._requestQueue.length) { 132 | var request = this._requestQueue.shift(); 133 | console.log("Unqueing req, length: " + this._requestQueue.length); 134 | 135 | if (request) { 136 | this._pendingRequest = true; 137 | setTimeout(function(){ 138 | if ((request.attributeId == 0) || (request.attributeId == 4) || (request.attributeId == 5)) { 139 | this.requestNotificationAttribute(request.uid, request.attributeId); 140 | } else { 141 | this.requestNotificationAttribute(request.uid, request.attributeId, 255); 142 | } 143 | this._requestTimeout = setTimeout(this.unqueueAttributeRequest.bind(this),1000000); 144 | }.bind(this), 1000); 145 | } 146 | } 147 | }; 148 | 149 | BleAncs.prototype.queueAttributeRequest = function(uid,attributeId) { 150 | 151 | 152 | 153 | console.log("Adding to the queue: " + uid + " attributeId: " + attributeId + " queue: " + this._requestQueue.length); 154 | var request = new AttributeRequest(uid, attributeId); 155 | this._requestQueue.push(request); 156 | this._requestTimeout = setTimeout(this.unqueueAttributeRequest.bind(this),1000000); 157 | if (this._pendingRequest == false ) { 158 | this.unqueueAttributeRequest(); 159 | } 160 | 161 | }; 162 | 163 | BleAncs.prototype.onStateChange = function(state) { 164 | console.log('on -> stateChange: ' + state); 165 | 166 | if (state === 'poweredOn') { 167 | if (this._able.startAdvertisingWithEIRData) { 168 | /*var ad = new Buffer([ 169 | // flags 170 | 0x02, 0x01, 0x02, 171 | 172 | // ANCS solicitation 173 | 0x11, 0x15, 0xd0, 0x00, 0x2D, 0x12, 0x1E, 0x4B, 0x0F, 174 | 0xA4, 0x99, 0x4E, 0xCE, 0xB5, 0x31, 0xF4, 0x05, 0x79 175 | ]);*/ 176 | 177 | var ad = new Buffer([ 178 | // flags 179 | 0x02, 0x01, 0x05, 180 | 181 | //device name 182 | 0x0a, 0x09, 0x41, 0x4e, 0x42, 0x52, 0x21, 0x54, 0x75, 0x73, 0x6b, 183 | 184 | // Appearence 185 | 0x03, 0x19, 0x40, 0x02 186 | 187 | ]); 188 | 189 | //var scan = new Buffer([0x05, 0x08, 0x74, 0x65, 0x73, 0x74]); // name 190 | var scan = new Buffer([0x11, 0x15, 0xd0, 0x00, 0x2D, 0x12, 0x1E, 0x4B, 0x0F, 191 | 0xA4, 0x99, 0x4E, 0xCE, 0xB5, 0x31, 0xF4, 0x05, 0x79]); 192 | this._able.startAdvertisingWithEIRData(ad, scan); 193 | } else { 194 | this._able.startAdvertising('ancs-test', ['7905f431b5ce4e99a40f4b1e122d00d0']); 195 | } 196 | 197 | } else { 198 | this._able.stopAdvertising(); 199 | } 200 | }; 201 | 202 | 203 | 204 | BleAncs.prototype.onAccept = function(peripheral) { 205 | 206 | console.log('on -> accept: ' ); 207 | this._peripheral = peripheral; 208 | 209 | 210 | this.uuid = peripheral.uuid; 211 | 212 | this._peripheral.on('disconnect', this.onDisconnect.bind(this)); 213 | }; 214 | 215 | BleAncs.prototype.onAdvertisingStart = function(error) { 216 | 217 | console.log('on -> advertisingStart: ' + (error ? 'error ' + error : 'success')); 218 | 219 | this._able.setServices( [ new AblePrimaryService({ 220 | uuid: '13333333333333333333333333333337', //'7905f431b5ce4e99a40f4b1e122d00d0', 221 | characteristics: [new GenericCharacteristic()] 222 | }) 223 | ]); 224 | }; 225 | 226 | BleAncs.prototype.onMtuChange = function() { 227 | 228 | }; 229 | 230 | 231 | BleAncs.prototype.onEncryptChange = function() { 232 | console.log("able encryptChange!!!"); 233 | this.discoverServicesAndCharacteristics(function() { 234 | }); 235 | }; 236 | 237 | 238 | BleAncs.prototype.onEncryptFail = function() { 239 | console.log("able -> encryptFail"); 240 | }; 241 | 242 | BleAncs.prototype.onConnect = function() { 243 | console.log('able -> connect'); 244 | }; 245 | 246 | BleAncs.prototype.onDisconnect = function() { 247 | console.log('Got a disconnect'); 248 | 249 | this._lastUid = null; 250 | this._requestQueue = []; 251 | this._notifications = {}; 252 | if (this._requestTimeout){ 253 | clearTimeout(this._requestTimeout); 254 | this._requestTimeout = null; 255 | } 256 | 257 | this._pendingRequest = false; 258 | 259 | this.emit('disconnect'); 260 | }; 261 | 262 | 263 | 264 | 265 | module.exports = BleAncs; 266 | -------------------------------------------------------------------------------- /lib/able.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('Able'); 2 | 3 | var events = require('events'); 4 | var os = require('os'); 5 | var util = require('util'); 6 | 7 | var Peripheral = require('./peripheral'); 8 | var PrimaryService = require('./primary-service'); 9 | var Service = require('./service'); 10 | var RemoteCharacteristic = require('./remote-characteristic'); 11 | var RemoteDescriptor = require('./remote-descriptor'); 12 | var Characteristic = require('./local-characteristic'); 13 | var LocalDescriptor = require('./local-descriptor'); 14 | 15 | var bindings = null; 16 | 17 | var platform = os.platform(); 18 | 19 | if (platform === 'linux' || platform === 'win32') { 20 | bindings = require('./hci-socket/bindings'); 21 | } else { 22 | throw new Error('Unsupported platform'); 23 | } 24 | 25 | function Able() { 26 | this.state = 'unknown'; 27 | this.address = 'unknown'; 28 | 29 | this._bindings = bindings; 30 | this._peripherals = {}; 31 | this._services = {}; 32 | this._characteristics = {}; 33 | this._descriptors = {}; 34 | this._discoveredPeripheralUUids = []; 35 | this._allowDuplicates = true; 36 | this._bindings._scanServiceUuids = []; 37 | 38 | this._bindings.on('stateChange', this.onStateChange.bind(this)); 39 | this._bindings.on('addressChange', this.onAddressChange.bind(this)); 40 | this._bindings.on('advertisingStart', this.onAdvertisingStart.bind(this)); 41 | this._bindings.on('advertisingStop', this.onAdvertisingStop.bind(this)); 42 | this._bindings.on('servicesSet', this.onServicesSet.bind(this)); 43 | this._bindings.on('accept', this.onAccept.bind(this)); 44 | this._bindings.on('mtuChange', this.onMtuChange.bind(this)); 45 | this._bindings.on('disconnect', this.onDisconnect.bind(this)); 46 | this._bindings.on('scanStart', this.onScanStart.bind(this)); 47 | this._bindings.on('scanStop', this.onScanStop.bind(this)); 48 | this._bindings.on('discover', this.onDiscover.bind(this)); 49 | this._bindings.on('connect', this.onConnect.bind(this)); 50 | this._bindings.on('rssiUpdate', this.onRssiUpdate.bind(this)); 51 | this._bindings.on('servicesDiscover', this.onServicesDiscover.bind(this)); 52 | this._bindings.on('includedServicesDiscover', this.onIncludedServicesDiscover.bind(this)); 53 | this._bindings.on('characteristicsDiscover', this.onCharacteristicsDiscover.bind(this)); 54 | this._bindings.on('read', this.onRead.bind(this)); 55 | this._bindings.on('write', this.onWrite.bind(this)); 56 | this._bindings.on('broadcast', this.onBroadcast.bind(this)); 57 | this._bindings.on('notify', this.onNotify.bind(this)); 58 | this._bindings.on('encryptChange', this.onEncryptChange.bind(this)); 59 | this._bindings.on('encryptFail', this.onEncryptFail.bind(this)); 60 | this._bindings.on('descriptorsDiscover', this.onDescriptorsDiscover.bind(this)); 61 | this._bindings.on('valueRead', this.onValueRead.bind(this)); 62 | this._bindings.on('valueWrite', this.onValueWrite.bind(this)); 63 | this._bindings.on('handleRead', this.onHandleRead.bind(this)); 64 | this._bindings.on('handleWrite', this.onHandleWrite.bind(this)); 65 | this._bindings.on('handleNotify', this.onHandleNotify.bind(this)); 66 | 67 | this.on('warning', function (message) { 68 | if (this.listeners('warning').length === 1) { 69 | console.warn('Able: ' + message); 70 | } 71 | }.bind(this)); 72 | } 73 | 74 | Able.prototype.PrimaryService = PrimaryService; 75 | Able.prototype.Characteristic = Characteristic; 76 | Able.prototype.Descriptor = LocalDescriptor; 77 | util.inherits(Able, events.EventEmitter); 78 | 79 | Able.prototype.onStateChange = function (state) { 80 | debug('stateChange ' + state); 81 | 82 | this.state = state; 83 | 84 | this.emit('stateChange', state); 85 | }; 86 | 87 | Able.prototype.onEncryptChange = function () { 88 | debug('encryptChange'); 89 | 90 | this.emit('encryptChange'); 91 | }; 92 | 93 | Able.prototype.onEncryptFail = function () { 94 | debug('encryptFail '); 95 | 96 | this.emit('encryptFail'); 97 | }; 98 | 99 | Able.prototype.onAddressChange = function (address) { 100 | debug('addressChange ' + address); 101 | 102 | this.address = address; 103 | }; 104 | 105 | Able.prototype.onAccept = function (uuid, address, addressType) { 106 | debug('accept ' + address); 107 | 108 | var peripheral = this._peripherals[uuid]; 109 | var connectable = true; 110 | var advertisement = { 111 | localName: undefined, 112 | txPowerLevel: undefined, 113 | manufacturerData: undefined, 114 | serviceData: [], 115 | serviceUuids: [] 116 | 117 | }; 118 | var rssi = 127; 119 | 120 | if (!peripheral) { 121 | peripheral = new Peripheral(this, uuid, address, addressType, connectable, advertisement, rssi); 122 | peripheral.state = 'connected'; 123 | this._peripherals[uuid] = peripheral; 124 | this._services[uuid] = {}; 125 | this._characteristics[uuid] = {}; 126 | this._descriptors[uuid] = {}; 127 | } else { 128 | // "or" the advertisment data with existing 129 | /* 130 | for (var i in advertisement) { 131 | if (advertisement[i] !== undefined) { 132 | peripheral.advertisement[i] = advertisement[i]; 133 | } 134 | }*/ 135 | 136 | peripheral.rssi = rssi; 137 | } 138 | 139 | var previouslyDiscoverd = (this._discoveredPeripheralUUids.indexOf(uuid) !== -1); 140 | 141 | if (!previouslyDiscoverd) { 142 | this._discoveredPeripheralUUids.push(uuid); 143 | } 144 | 145 | this.emit('accept', peripheral); 146 | }; 147 | 148 | Able.prototype.onMtuChange = function (mtu) { 149 | debug('mtu ' + mtu); 150 | 151 | this.mtu = mtu; 152 | 153 | this.emit('mtuChange', mtu); 154 | }; 155 | 156 | 157 | Able.prototype.startScanning = function (serviceUuids, allowDuplicates, callback) { 158 | debug("Starting SCcanning"); 159 | if (this.state !== 'poweredOn') { 160 | var error = new Error('Could not start scanning, state is ' + this.state + ' (not poweredOn)'); 161 | 162 | if (typeof callback === 'function') { 163 | callback(error); 164 | } else { 165 | throw error; 166 | } 167 | } else { 168 | if (callback) { 169 | this.once('scanStart', callback); 170 | } 171 | 172 | this._discoveredPeripheralUUids = []; 173 | this._allowDuplicates = allowDuplicates; 174 | 175 | this._bindings.startScanning(serviceUuids, allowDuplicates); 176 | } 177 | }; 178 | 179 | Able.prototype.onScanStart = function () { 180 | debug('scanStart'); 181 | this.emit('scanStart'); 182 | }; 183 | 184 | Able.prototype.stopScanning = function (callback) { 185 | if (callback) { 186 | this.once('scanStop', callback); 187 | } 188 | this._bindings.stopScanning(); 189 | }; 190 | 191 | Able.prototype.onScanStop = function () { 192 | debug('scanStop'); 193 | this.emit('scanStop'); 194 | }; 195 | 196 | Able.prototype.onDiscover = function (uuid, address, addressType, connectable, advertisement, rssi) { 197 | var peripheral = this._peripherals[uuid]; 198 | 199 | if (!peripheral) { 200 | peripheral = new Peripheral(this, uuid, address, addressType, connectable, advertisement, rssi); 201 | 202 | this._peripherals[uuid] = peripheral; 203 | this._services[uuid] = {}; 204 | this._characteristics[uuid] = {}; 205 | this._descriptors[uuid] = {}; 206 | } else { 207 | // "or" the advertisment data with existing 208 | for (var i in advertisement) { 209 | if (advertisement[i] !== undefined) { 210 | peripheral.advertisement[i] = advertisement[i]; 211 | } 212 | } 213 | 214 | peripheral.rssi = rssi; 215 | } 216 | 217 | var previouslyDiscoverd = (this._discoveredPeripheralUUids.indexOf(uuid) !== -1); 218 | 219 | if (!previouslyDiscoverd) { 220 | this._discoveredPeripheralUUids.push(uuid); 221 | } 222 | 223 | if (this._allowDuplicates || !previouslyDiscoverd) { 224 | this.emit('discover', peripheral); 225 | } 226 | }; 227 | 228 | Able.prototype.connect = function (peripheralUuid) { 229 | this._bindings.connect(peripheralUuid); 230 | }; 231 | 232 | Able.prototype.onConnect = function (peripheralUuid, error) { 233 | var peripheral = this._peripherals[peripheralUuid]; 234 | 235 | if (peripheral) { 236 | peripheral.state = error ? 'error' : 'connected'; 237 | peripheral.emit('connect', error); 238 | } else { 239 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ' connected!'); 240 | } 241 | }; 242 | 243 | Able.prototype.disconnect = function (peripheralUuid) { 244 | this._bindings.disconnect(peripheralUuid); 245 | }; 246 | 247 | Able.prototype.onDisconnect = function (peripheralUuid) { 248 | var peripheral = this._peripherals[peripheralUuid]; 249 | debug("recieved disconnect"); 250 | this.emit('disconnect'); 251 | if (peripheral) { 252 | peripheral.state = 'disconnected'; 253 | peripheral.emit('disconnect'); 254 | } else { 255 | debug('unknown peripheral ' + peripheralUuid + ' disconnected!'); 256 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ' disconnected!'); 257 | } 258 | }; 259 | 260 | Able.prototype.updateRssi = function (peripheralUuid) { 261 | this._bindings.updateRssi(peripheralUuid); 262 | }; 263 | 264 | Able.prototype.onRssiUpdate = function (peripheralUuid, rssi) { 265 | var peripheral = this._peripherals[peripheralUuid]; 266 | 267 | if (peripheral) { 268 | peripheral.rssi = rssi; 269 | 270 | peripheral.emit('rssiUpdate', rssi); 271 | } else { 272 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ' RSSI update!'); 273 | } 274 | }; 275 | 276 | Able.prototype.findHandlesForUuid = function (peripheralUuid, uuid) { 277 | var uuidBuf = new Buffer(uuid, 'hex'); 278 | this._bindings.findByTypeRequest(peripheralUuid, 0x0001, 0xffff, 0x2800, uuidBuf); 279 | } 280 | 281 | Able.prototype.findService = function (peripheralUuid, uuid) { 282 | var uuidBuf = new Buffer(uuid, 'hex'); 283 | this._bindings.findByTypeRequest(peripheralUuid, 0x0001, 0xffff, 0x2800, uuidBuf); 284 | } 285 | 286 | Able.prototype.discoverServices = function (peripheralUuid, uuids) { 287 | this._bindings.discoverServices(peripheralUuid, uuids); 288 | }; 289 | 290 | Able.prototype.onServicesDiscover = function (peripheralUuid, serviceUuids) { 291 | var peripheral = this._peripherals[peripheralUuid]; 292 | 293 | if (peripheral) { 294 | var services = []; 295 | 296 | for (var i = 0; i < serviceUuids.length; i++) { 297 | var serviceUuid = serviceUuids[i]; 298 | var service = new Service(this, peripheralUuid, serviceUuid); 299 | 300 | this._services[peripheralUuid][serviceUuid] = service; 301 | this._characteristics[peripheralUuid][serviceUuid] = {}; 302 | this._descriptors[peripheralUuid][serviceUuid] = {}; 303 | 304 | services.push(service); 305 | } 306 | 307 | peripheral.services = services; 308 | 309 | peripheral.emit('servicesDiscover', services); 310 | } else { 311 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ' services discover!'); 312 | } 313 | }; 314 | 315 | Able.prototype.discoverIncludedServices = function (peripheralUuid, serviceUuid, serviceUuids) { 316 | this._bindings.discoverIncludedServices(peripheralUuid, serviceUuid, serviceUuids); 317 | }; 318 | 319 | Able.prototype.onIncludedServicesDiscover = function (peripheralUuid, serviceUuid, includedServiceUuids) { 320 | var service = this._services[peripheralUuid][serviceUuid]; 321 | 322 | if (service) { 323 | service.includedServiceUuids = includedServiceUuids; 324 | 325 | service.emit('includedServicesDiscover', includedServiceUuids); 326 | } else { 327 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ' included services discover!'); 328 | } 329 | }; 330 | 331 | Able.prototype.discoverCharacteristics = function (peripheralUuid, serviceUuid, characteristicUuids) { 332 | this._bindings.discoverCharacteristics(peripheralUuid, serviceUuid, characteristicUuids); 333 | }; 334 | 335 | Able.prototype.onCharacteristicsDiscover = function (peripheralUuid, serviceUuid, characteristics) { 336 | var service = this._services[peripheralUuid][serviceUuid]; 337 | 338 | if (service) { 339 | var characteristics_ = []; 340 | 341 | for (var i = 0; i < characteristics.length; i++) { 342 | var characteristicUuid = characteristics[i].uuid; 343 | 344 | var characteristic = new RemoteCharacteristic( 345 | this, 346 | peripheralUuid, 347 | serviceUuid, 348 | characteristicUuid, 349 | characteristics[i].properties 350 | ); 351 | 352 | this._characteristics[peripheralUuid][serviceUuid][characteristicUuid] = characteristic; 353 | this._descriptors[peripheralUuid][serviceUuid][characteristicUuid] = {}; 354 | 355 | characteristics_.push(characteristic); 356 | } 357 | 358 | service.characteristics = characteristics_; 359 | 360 | service.emit('characteristicsDiscover', characteristics_); 361 | } else { 362 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ' characteristics discover!'); 363 | } 364 | }; 365 | 366 | Able.prototype.read = function (peripheralUuid, serviceUuid, characteristicUuid) { 367 | this._bindings.read(peripheralUuid, serviceUuid, characteristicUuid); 368 | }; 369 | 370 | Able.prototype.onRead = function (peripheralUuid, serviceUuid, characteristicUuid, data, isNotification) { 371 | var characteristic = this._characteristics[peripheralUuid][serviceUuid][characteristicUuid]; 372 | 373 | if (characteristic) { 374 | characteristic.emit('data', data, isNotification); 375 | 376 | characteristic.emit('read', data, isNotification); // for backwards compatbility 377 | } else { 378 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ', ' + characteristicUuid + ' read!'); 379 | } 380 | }; 381 | 382 | Able.prototype.write = function (peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse) { 383 | this._bindings.write(peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse); 384 | }; 385 | 386 | Able.prototype.onWrite = function (peripheralUuid, serviceUuid, characteristicUuid) { 387 | var characteristic = this._characteristics[peripheralUuid][serviceUuid][characteristicUuid]; 388 | 389 | if (characteristic) { 390 | characteristic.emit('write'); 391 | } else { 392 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ', ' + characteristicUuid + ' write!'); 393 | } 394 | }; 395 | 396 | Able.prototype.broadcast = function (peripheralUuid, serviceUuid, characteristicUuid, broadcast) { 397 | this._bindings.broadcast(peripheralUuid, serviceUuid, characteristicUuid, broadcast); 398 | }; 399 | 400 | Able.prototype.onBroadcast = function (peripheralUuid, serviceUuid, characteristicUuid, state) { 401 | var characteristic = this._characteristics[peripheralUuid][serviceUuid][characteristicUuid]; 402 | 403 | if (characteristic) { 404 | characteristic.emit('broadcast', state); 405 | } else { 406 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ', ' + characteristicUuid + ' broadcast!'); 407 | } 408 | }; 409 | 410 | Able.prototype.notify = function (peripheralUuid, serviceUuid, characteristicUuid, notify) { 411 | this._bindings.notify(peripheralUuid, serviceUuid, characteristicUuid, notify); 412 | }; 413 | 414 | Able.prototype.onNotify = function (peripheralUuid, serviceUuid, characteristicUuid, state) { 415 | var characteristic = this._characteristics[peripheralUuid][serviceUuid][characteristicUuid]; 416 | 417 | if (characteristic) { 418 | characteristic.emit('notify', state); 419 | } else { 420 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ', ' + characteristicUuid + ' notify!'); 421 | } 422 | }; 423 | 424 | Able.prototype.discoverDescriptors = function (peripheralUuid, serviceUuid, characteristicUuid) { 425 | this._bindings.discoverDescriptors(peripheralUuid, serviceUuid, characteristicUuid); 426 | }; 427 | 428 | Able.prototype.onDescriptorsDiscover = function (peripheralUuid, serviceUuid, characteristicUuid, descriptors) { 429 | var characteristic = this._characteristics[peripheralUuid][serviceUuid][characteristicUuid]; 430 | 431 | if (characteristic) { 432 | var descriptors_ = []; 433 | 434 | for (var i = 0; i < descriptors.length; i++) { 435 | var descriptorUuid = descriptors[i]; 436 | 437 | var descriptor = new Descriptor( 438 | this, 439 | peripheralUuid, 440 | serviceUuid, 441 | characteristicUuid, 442 | descriptorUuid 443 | ); 444 | 445 | this._descriptors[peripheralUuid][serviceUuid][characteristicUuid][descriptorUuid] = descriptor; 446 | 447 | descriptors_.push(descriptor); 448 | } 449 | 450 | characteristic.descriptors = descriptors_; 451 | 452 | characteristic.emit('descriptorsDiscover', descriptors_); 453 | } else { 454 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ', ' + characteristicUuid + ' descriptors discover!'); 455 | } 456 | }; 457 | 458 | Able.prototype.readValue = function (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid) { 459 | this._bindings.readValue(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid); 460 | }; 461 | 462 | Able.prototype.onValueRead = function (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data) { 463 | var descriptor = this._descriptors[peripheralUuid][serviceUuid][characteristicUuid][descriptorUuid]; 464 | 465 | if (descriptor) { 466 | descriptor.emit('valueRead', data); 467 | } else { 468 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ', ' + characteristicUuid + ', ' + descriptorUuid + ' value read!'); 469 | } 470 | }; 471 | 472 | Able.prototype.writeValue = function (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data) { 473 | this._bindings.writeValue(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data); 474 | }; 475 | 476 | Able.prototype.onValueWrite = function (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid) { 477 | var descriptor = this._descriptors[peripheralUuid][serviceUuid][characteristicUuid][descriptorUuid]; 478 | 479 | if (descriptor) { 480 | descriptor.emit('valueWrite'); 481 | } else { 482 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ', ' + characteristicUuid + ', ' + descriptorUuid + ' value write!'); 483 | } 484 | }; 485 | 486 | Able.prototype.readHandle = function (peripheralUuid, handle) { 487 | this._bindings.readHandle(peripheralUuid, handle); 488 | }; 489 | 490 | Able.prototype.onHandleRead = function (peripheralUuid, handle, data) { 491 | var peripheral = this._peripherals[peripheralUuid]; 492 | 493 | if (peripheral) { 494 | peripheral.emit('handleRead' + handle, data); 495 | } else { 496 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ' handle read!'); 497 | } 498 | }; 499 | 500 | Able.prototype.writeHandle = function (peripheralUuid, handle, data, withoutResponse) { 501 | this._bindings.writeHandle(peripheralUuid, handle, data, withoutResponse); 502 | }; 503 | 504 | Able.prototype.onHandleWrite = function (peripheralUuid, handle) { 505 | var peripheral = this._peripherals[peripheralUuid]; 506 | 507 | if (peripheral) { 508 | peripheral.emit('handleWrite' + handle); 509 | } else { 510 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ' handle write!'); 511 | } 512 | }; 513 | 514 | Able.prototype.onHandleNotify = function (peripheralUuid, handle, data) { 515 | var peripheral = this._peripherals[peripheralUuid]; 516 | 517 | if (peripheral) { 518 | peripheral.emit('handleNotify', handle, data); 519 | } else { 520 | this.emit('warning', 'unknown peripheral ' + peripheralUuid + ' handle notify!'); 521 | } 522 | }; 523 | 524 | /*Bleno start */ 525 | 526 | Able.prototype.startAdvertising = function (name, serviceUuids, callback) { 527 | if (this.state !== 'poweredOn') { 528 | var error = new Error('Could not start advertising, state is ' + this.state + ' (not poweredOn)'); 529 | 530 | if (typeof callback === 'function') { 531 | callback(error); 532 | } else { 533 | throw error; 534 | } 535 | } else { 536 | if (callback) { 537 | this.once('advertisingStart', callback); 538 | } 539 | 540 | var undashedServiceUuids = []; 541 | 542 | if (serviceUuids && serviceUuids.length) { 543 | for (var i = 0; i < serviceUuids.length; i++) { 544 | undashedServiceUuids[i] = UuidUtil.removeDashes(serviceUuids[i]); 545 | } 546 | } 547 | 548 | this._bindings.startAdvertising(name, undashedServiceUuids); 549 | } 550 | }; 551 | 552 | Able.prototype.startAdvertisingIBeacon = function (uuid, major, minor, measuredPower, callback) { 553 | if (this.state !== 'poweredOn') { 554 | var error = new Error('Could not start advertising, state is ' + this.state + ' (not poweredOn)'); 555 | 556 | if (typeof callback === 'function') { 557 | callback(error); 558 | } else { 559 | throw error; 560 | } 561 | } else { 562 | var undashedUuid = UuidUtil.removeDashes(uuid); 563 | var uuidData = new Buffer(undashedUuid, 'hex'); 564 | var uuidDataLength = uuidData.length; 565 | var iBeaconData = new Buffer(uuidData.length + 5); 566 | 567 | for (var i = 0; i < uuidDataLength; i++) { 568 | iBeaconData[i] = uuidData[i]; 569 | } 570 | 571 | iBeaconData.writeUInt16BE(major, uuidDataLength); 572 | iBeaconData.writeUInt16BE(minor, uuidDataLength + 2); 573 | iBeaconData.writeInt8(measuredPower, uuidDataLength + 4); 574 | 575 | if (callback) { 576 | this.once('advertisingStart', callback); 577 | } 578 | 579 | debug('iBeacon data = ' + iBeaconData.toString('hex')); 580 | 581 | this._bindings.startAdvertisingIBeacon(iBeaconData); 582 | } 583 | }; 584 | 585 | Able.prototype.onAdvertisingStart = function (error) { 586 | debug('advertisingStart: ' + error); 587 | 588 | if (error) { 589 | this.emit('advertisingStartError', error); 590 | } 591 | 592 | this.emit('advertisingStart', error); 593 | }; 594 | 595 | if (platform === 'linux') { 596 | // Linux only API 597 | Able.prototype.startAdvertisingWithEIRData = function (advertisementData, scanData, callback) { 598 | if (this.state !== 'poweredOn') { 599 | var error = new Error('Could not advertising scanning, state is ' + this.state + ' (not poweredOn)'); 600 | 601 | if (typeof callback === 'function') { 602 | callback(error); 603 | } else { 604 | throw error; 605 | } 606 | } else { 607 | if (callback) { 608 | this.once('advertisingStart', callback); 609 | } 610 | this._bindings.startAdvertisingWithEIRData(advertisementData, scanData); 611 | } 612 | }; 613 | } 614 | 615 | 616 | 617 | Able.prototype.stopAdvertising = function (callback) { 618 | if (callback) { 619 | this.once('advertisingStop', callback); 620 | } 621 | this._bindings.stopAdvertising(); 622 | }; 623 | 624 | Able.prototype.onAdvertisingStop = function () { 625 | debug('advertisingStop'); 626 | this.emit('advertisingStop'); 627 | }; 628 | 629 | Able.prototype.setServices = function (services, callback) { 630 | if (callback) { 631 | this.once('servicesSet', callback); 632 | } 633 | this._bindings.setServices(services); 634 | }; 635 | 636 | Able.prototype.onServicesSet = function (error) { 637 | debug('servicesSet'); 638 | 639 | if (error) { 640 | this.emit('servicesSetError', error); 641 | } 642 | 643 | this.emit('servicesSet', error); 644 | }; 645 | 646 | if (platform === 'linux') { 647 | // Linux only API 648 | Able.prototype.disconnect = function () { 649 | debug('disconnect'); 650 | this._bindings.disconnect(); 651 | }; 652 | } 653 | 654 | Able.prototype.updateRssi = function (callback) { 655 | if (callback) { 656 | this.once('rssiUpdate', function (rssi) { 657 | callback(null, rssi); 658 | }); 659 | } 660 | 661 | this._bindings.updateRssi(); 662 | }; 663 | 664 | Able.prototype.onRssiUpdate = function (rssi) { 665 | this.emit('rssiUpdate', rssi); 666 | }; 667 | 668 | /* Able End */ 669 | 670 | module.exports = Able; 671 | -------------------------------------------------------------------------------- /lib/characteristics.json: -------------------------------------------------------------------------------- 1 | { 2 | "2a00" : { "name" : "Device Name" 3 | , "type" : "org.bluetooth.characteristic.gap.device_name" 4 | } 5 | , "2a01" : { "name" : "Appearance" 6 | , "type" : "org.bluetooth.characteristic.gap.appearance" 7 | } 8 | , "2a02" : { "name" : "Peripheral Privacy Flag" 9 | , "type" : "org.bluetooth.characteristic.gap.peripheral_privacy_flag" 10 | } 11 | , "2a03" : { "name" : "Reconnection Address" 12 | , "type" : "org.bluetooth.characteristic.gap.reconnection_address" 13 | } 14 | , "2a04" : { "name" : "Peripheral Preferred Connection Parameters" 15 | , "type" : "org.bluetooth.characteristic.gap.peripheral_preferred_connection_parameters" 16 | } 17 | , "2a05" : { "name" : "Service Changed" 18 | , "type" : "org.bluetooth.characteristic.gatt.service_changed" 19 | } 20 | , "2a06" : { "name" : "Alert Level" 21 | , "type" : "org.bluetooth.characteristic.alert_level" 22 | } 23 | , "2a07" : { "name" : "Tx Power Level" 24 | , "type" : "org.bluetooth.characteristic.tx_power_level" 25 | } 26 | , "2a08" : { "name" : "Date Time" 27 | , "type" : "org.bluetooth.characteristic.date_time" 28 | } 29 | , "2a09" : { "name" : "Day of Week" 30 | , "type" : "org.bluetooth.characteristic.day_of_week" 31 | } 32 | , "2a0a" : { "name" : "Day Date Time" 33 | , "type" : "org.bluetooth.characteristic.day_date_time" 34 | } 35 | , "2a0c" : { "name" : "Exact Time 256" 36 | , "type" : "org.bluetooth.characteristic.exact_time_256" 37 | } 38 | , "2a0d" : { "name" : "DST Offset" 39 | , "type" : "org.bluetooth.characteristic.dst_offset" 40 | } 41 | , "2a0e" : { "name" : "Time Zone" 42 | , "type" : "org.bluetooth.characteristic.time_zone" 43 | } 44 | , "2a0f" : { "name" : "Local Time Information" 45 | , "type" : "org.bluetooth.characteristic.local_time_information" 46 | } 47 | , "2a11" : { "name" : "Time with DST" 48 | , "type" : "org.bluetooth.characteristic.time_with_dst" 49 | } 50 | , "2a12" : { "name" : "Time Accuracy" 51 | , "type" : "org.bluetooth.characteristic.time_accuracy" 52 | } 53 | , "2a13" : { "name" : "Time Source" 54 | , "type" : "org.bluetooth.characteristic.time_source" 55 | } 56 | , "2a14" : { "name" : "Reference Time Information" 57 | , "type" : "org.bluetooth.characteristic.reference_time_information" 58 | } 59 | , "2a16" : { "name" : "Time Update Control Point" 60 | , "type" : "org.bluetooth.characteristic.time_update_control_point" 61 | } 62 | , "2a17" : { "name" : "Time Update State" 63 | , "type" : "org.bluetooth.characteristic.time_update_state" 64 | } 65 | , "2a18" : { "name" : "Glucose Measurement" 66 | , "type" : "org.bluetooth.characteristic.glucose_measurement" 67 | } 68 | , "2a19" : { "name" : "Battery Level" 69 | , "type" : "org.bluetooth.characteristic.battery_level" 70 | } 71 | , "2a1c" : { "name" : "Temperature Measurement" 72 | , "type" : "org.bluetooth.characteristic.temperature_measurement" 73 | } 74 | , "2a1d" : { "name" : "Temperature Type" 75 | , "type" : "org.bluetooth.characteristic.temperature_type" 76 | } 77 | , "2a1e" : { "name" : "Intermediate Temperature" 78 | , "type" : "org.bluetooth.characteristic.intermediate_temperature" 79 | } 80 | , "2a21" : { "name" : "Measurement Interval" 81 | , "type" : "org.bluetooth.characteristic.measurement_interval" 82 | } 83 | , "2a22" : { "name" : "Boot Keyboard Input Report" 84 | , "type" : "org.bluetooth.characteristic.boot_keyboard_input_report" 85 | } 86 | , "2a23" : { "name" : "System ID" 87 | , "type" : "org.bluetooth.characteristic.system_id" 88 | } 89 | , "2a24" : { "name" : "Model Number String" 90 | , "type" : "org.bluetooth.characteristic.model_number_string" 91 | } 92 | , "2a25" : { "name" : "Serial Number String" 93 | , "type" : "org.bluetooth.characteristic.serial_number_string" 94 | } 95 | , "2a26" : { "name" : "Firmware Revision String" 96 | , "type" : "org.bluetooth.characteristic.firmware_revision_string" 97 | } 98 | , "2a27" : { "name" : "Hardware Revision String" 99 | , "type" : "org.bluetooth.characteristic.hardware_revision_string" 100 | } 101 | , "2a28" : { "name" : "Software Revision String" 102 | , "type" : "org.bluetooth.characteristic.software_revision_string" 103 | } 104 | , "2a29" : { "name" : "Manufacturer Name String" 105 | , "type" : "org.bluetooth.characteristic.manufacturer_name_string" 106 | } 107 | , "2a2a" : { "name" : "IEEE 11073-20601 Regulatory Certification Data List" 108 | , "type" : "org.bluetooth.characteristic.ieee_11073-20601_regulatory_certification_data_list" 109 | } 110 | , "2a2b" : { "name" : "Current Time" 111 | , "type" : "org.bluetooth.characteristic.current_time" 112 | } 113 | , "2a2c" : { "name" : "Magnetic Declination" 114 | , "type" : "org.bluetooth.characteristic.magnetic_declination" 115 | } 116 | , "2a31" : { "name" : "Scan Refresh" 117 | , "type" : "org.bluetooth.characteristic.scan_refresh" 118 | } 119 | , "2a32" : { "name" : "Boot Keyboard Output Report" 120 | , "type" : "org.bluetooth.characteristic.boot_keyboard_output_report" 121 | } 122 | , "2a33" : { "name" : "Boot Mouse Input Report" 123 | , "type" : "org.bluetooth.characteristic.boot_mouse_input_report" 124 | } 125 | , "2a34" : { "name" : "Glucose Measurement Context" 126 | , "type" : "org.bluetooth.characteristic.glucose_measurement_context" 127 | } 128 | , "2a35" : { "name" : "Blood Pressure Measurement" 129 | , "type" : "org.bluetooth.characteristic.blood_pressure_measurement" 130 | } 131 | , "2a36" : { "name" : "Intermediate Cuff Pressure" 132 | , "type" : "org.bluetooth.characteristic.intermediate_blood_pressure" 133 | } 134 | , "2a37" : { "name" : "Heart Rate Measurement" 135 | , "type" : "org.bluetooth.characteristic.heart_rate_measurement" 136 | } 137 | , "2a38" : { "name" : "Body Sensor Location" 138 | , "type" : "org.bluetooth.characteristic.body_sensor_location" 139 | } 140 | , "2a39" : { "name" : "Heart Rate Control Point" 141 | , "type" : "org.bluetooth.characteristic.heart_rate_control_point" 142 | } 143 | , "2a3f" : { "name" : "Alert Status" 144 | , "type" : "org.bluetooth.characteristic.alert_status" 145 | } 146 | , "2a40" : { "name" : "Ringer Control Point" 147 | , "type" : "org.bluetooth.characteristic.ringer_control_point" 148 | } 149 | , "2a41" : { "name" : "Ringer Setting" 150 | , "type" : "org.bluetooth.characteristic.ringer_setting" 151 | } 152 | , "2a42" : { "name" : "Alert Category ID Bit Mask" 153 | , "type" : "org.bluetooth.characteristic.alert_category_id_bit_mask" 154 | } 155 | , "2a43" : { "name" : "Alert Category ID" 156 | , "type" : "org.bluetooth.characteristic.alert_category_id" 157 | } 158 | , "2a44" : { "name" : "Alert Notification Control Point" 159 | , "type" : "org.bluetooth.characteristic.alert_notification_control_point" 160 | } 161 | , "2a45" : { "name" : "Unread Alert Status" 162 | , "type" : "org.bluetooth.characteristic.unread_alert_status" 163 | } 164 | , "2a46" : { "name" : "New Alert" 165 | , "type" : "org.bluetooth.characteristic.new_alert" 166 | } 167 | , "2a47" : { "name" : "Supported New Alert Category" 168 | , "type" : "org.bluetooth.characteristic.supported_new_alert_category" 169 | } 170 | , "2a48" : { "name" : "Supported Unread Alert Category" 171 | , "type" : "org.bluetooth.characteristic.supported_unread_alert_category" 172 | } 173 | , "2a49" : { "name" : "Blood Pressure Feature" 174 | , "type" : "org.bluetooth.characteristic.blood_pressure_feature" 175 | } 176 | , "2a4a" : { "name" : "HID Information" 177 | , "type" : "org.bluetooth.characteristic.hid_information" 178 | } 179 | , "2a4b" : { "name" : "Report Map" 180 | , "type" : "org.bluetooth.characteristic.report_map" 181 | } 182 | , "2a4c" : { "name" : "HID Control Point" 183 | , "type" : "org.bluetooth.characteristic.hid_control_point" 184 | } 185 | , "2a4d" : { "name" : "Report" 186 | , "type" : "org.bluetooth.characteristic.report" 187 | } 188 | , "2a4e" : { "name" : "Protocol Mode" 189 | , "type" : "org.bluetooth.characteristic.protocol_mode" 190 | } 191 | , "2a4f" : { "name" : "Scan Interval Window" 192 | , "type" : "org.bluetooth.characteristic.scan_interval_window" 193 | } 194 | , "2a50" : { "name" : "PnP ID" 195 | , "type" : "org.bluetooth.characteristic.pnp_id" 196 | } 197 | , "2a51" : { "name" : "Glucose Feature" 198 | , "type" : "org.bluetooth.characteristic.glucose_feature" 199 | } 200 | , "2a52" : { "name" : "Record Access Control Point" 201 | , "type" : "org.bluetooth.characteristic.record_access_control_point" 202 | } 203 | , "2a53" : { "name" : "RSC Measurement" 204 | , "type" : "org.bluetooth.characteristic.rsc_measurement" 205 | } 206 | , "2a54" : { "name" : "RSC Feature" 207 | , "type" : "org.bluetooth.characteristic.rsc_feature" 208 | } 209 | , "2a55" : { "name" : "SC Control Point" 210 | , "type" : "org.bluetooth.characteristic.sc_control_point" 211 | } 212 | , "2a56" : { "name" : "Digital" 213 | , "type" : "org.bluetooth.characteristic.digital" 214 | } 215 | , "2a58" : { "name" : "Analog" 216 | , "type" : "org.bluetooth.characteristic.analog" 217 | } 218 | , "2a5a" : { "name" : "Aggregate" 219 | , "type" : "org.bluetooth.characteristic.aggregate" 220 | } 221 | , "2a5b" : { "name" : "CSC Measurement" 222 | , "type" : "org.bluetooth.characteristic.csc_measurement" 223 | } 224 | , "2a5c" : { "name" : "CSC Feature" 225 | , "type" : "org.bluetooth.characteristic.csc_feature" 226 | } 227 | , "2a5d" : { "name" : "Sensor Location" 228 | , "type" : "org.bluetooth.characteristic.sensor_location" 229 | } 230 | , "2a63" : { "name" : "Cycling Power Measurement" 231 | , "type" : "org.bluetooth.characteristic.cycling_power_measurement" 232 | } 233 | , "2a64" : { "name" : "Cycling Power Vector" 234 | , "type" : "org.bluetooth.characteristic.cycling_power_vector" 235 | } 236 | , "2a65" : { "name" : "Cycling Power Feature" 237 | , "type" : "org.bluetooth.characteristic.cycling_power_feature" 238 | } 239 | , "2a66" : { "name" : "Cycling Power Control Point" 240 | , "type" : "org.bluetooth.characteristic.cycling_power_control_point" 241 | } 242 | , "2a67" : { "name" : "Location and Speed" 243 | , "type" : "org.bluetooth.characteristic.location_and_speed" 244 | } 245 | , "2a68" : { "name" : "Navigation" 246 | , "type" : "org.bluetooth.characteristic.navigation" 247 | } 248 | , "2a69" : { "name" : "Position Quality" 249 | , "type" : "org.bluetooth.characteristic.position_quality" 250 | } 251 | , "2a6a" : { "name" : "LN Feature" 252 | , "type" : "org.bluetooth.characteristic.ln_feature" 253 | } 254 | , "2a6b" : { "name" : "LN Control Point" 255 | , "type" : "org.bluetooth.characteristic.ln_control_point" 256 | } 257 | , "2a6c" : { "name" : "Elevation" 258 | , "type" : "org.bluetooth.characteristic.elevation" 259 | } 260 | , "2a6d" : { "name" : "Pressure" 261 | , "type" : "org.bluetooth.characteristic.pressure" 262 | } 263 | , "2a6e" : { "name" : "Temperature" 264 | , "type" : "org.bluetooth.characteristic.temperature" 265 | } 266 | , "2a6f" : { "name" : "Humidity" 267 | , "type" : "org.bluetooth.characteristic.humidity" 268 | } 269 | , "2a70" : { "name" : "True Wind Speed" 270 | , "type" : "org.bluetooth.characteristic.true_wind_speed" 271 | } 272 | , "2a71" : { "name" : "True Wind Direction" 273 | , "type" : "org.bluetooth.characteristic.true_wind_direction" 274 | } 275 | , "2a72" : { "name" : "Apparent Wind Speed" 276 | , "type" : "org.bluetooth.characteristic.apparent_wind_speed" 277 | } 278 | , "2a73" : { "name" : "Apparent Wind Direction" 279 | , "type" : "org.bluetooth.characteristic.apparent_wind_direction" 280 | } 281 | , "2a74" : { "name" : "Gust Factor" 282 | , "type" : "org.bluetooth.characteristic.gust_factor" 283 | } 284 | , "2a75" : { "name" : "Pollen Concentration" 285 | , "type" : "org.bluetooth.characteristic.pollen_concentration" 286 | } 287 | , "2a76" : { "name" : "UV Index" 288 | , "type" : "org.bluetooth.characteristic.uv_index" 289 | } 290 | , "2a77" : { "name" : "Irradiance" 291 | , "type" : "org.bluetooth.characteristic.irradiance" 292 | } 293 | , "2a78" : { "name" : "Rainfall" 294 | , "type" : "org.bluetooth.characteristic.rainfall" 295 | } 296 | , "2a79" : { "name" : "Wind Chill" 297 | , "type" : "org.bluetooth.characteristic.wind_chill" 298 | } 299 | , "2a7a" : { "name" : "Heat Index" 300 | , "type" : "org.bluetooth.characteristic.heat_index" 301 | } 302 | , "2a7b" : { "name" : "Dew Point" 303 | , "type" : "org.bluetooth.characteristic.dew_point" 304 | } 305 | , "2a7d" : { "name" : "Descriptor Value Changed" 306 | , "type" : "org.bluetooth.characteristic.descriptor_value_change" 307 | } 308 | , "2a7e" : { "name" : "Aerobic Heart Rate Lower Limit" 309 | , "type" : "org.bluetooth.characteristic.aerobic_heart_rate_lower_limit" 310 | } 311 | , "2a7f" : { "name" : "Aerobic Threshold" 312 | , "type" : "org.bluetooth.characteristic.aerobic_threshold" 313 | } 314 | , "2a80" : { "name" : "Age" 315 | , "type" : "org.bluetooth.characteristic.age" 316 | } 317 | , "2a81" : { "name" : "Anaerobic Heart Rate Lower Limit" 318 | , "type" : "org.bluetooth.characteristic.anaerobic_heart_rate_lower_limit" 319 | } 320 | , "2a82" : { "name" : "Anaerobic Heart Rate Upper Limit" 321 | , "type" : "org.bluetooth.characteristic.anaerobic_heart_rate_upper_limit" 322 | } 323 | , "2a83" : { "name" : "Anaerobic Threshold" 324 | , "type" : "org.bluetooth.characteristic.anaerobic_threshold" 325 | } 326 | , "2a84" : { "name" : "Aerobic Heart Rate Upper Limit" 327 | , "type" : "org.bluetooth.characteristic.aerobic_heart_rate_upper_limit" 328 | } 329 | , "2a85" : { "name" : "Date of Birth" 330 | , "type" : "org.bluetooth.characteristic.date_of_birth" 331 | } 332 | , "2a86" : { "name" : "Date of Threshold Assessment" 333 | , "type" : "org.bluetooth.characteristic.date_of_threshold_assessment" 334 | } 335 | , "2a87" : { "name" : "Email Address" 336 | , "type" : "org.bluetooth.characteristic.email_address" 337 | } 338 | , "2a88" : { "name" : "Fat Burn Heart Rate Lower Limit" 339 | , "type" : "org.bluetooth.characteristic.fat_burn_heart_lower_limit" 340 | } 341 | , "2a89" : { "name" : "Fat Burn Heart Rate Upper Limit" 342 | , "type" : "org.bluetooth.characteristic.fat_burn_heart_upper_limit" 343 | } 344 | , "2a8a" : { "name" : "First Name" 345 | , "type" : "org.bluetooth.characteristic.first_name" 346 | } 347 | , "2a8b" : { "name" : "Five Zone Heart Rate Limits" 348 | , "type" : "org.bluetooth.characteristic.five_zone_heart_rate_limits" 349 | } 350 | , "2a8c" : { "name" : "Gender" 351 | , "type" : "org.bluetooth.characteristic.gender" 352 | } 353 | , "2a8d" : { "name" : "Heart Rate Max" 354 | , "type" : "org.bluetooth.characteristic.heart_rate_max" 355 | } 356 | , "2a8e" : { "name" : "Height" 357 | , "type" : "org.bluetooth.characteristic.height" 358 | } 359 | , "2a8f" : { "name" : "Hip Circumference" 360 | , "type" : "org.bluetooth.characteristic.hip_circumference" 361 | } 362 | , "2a90" : { "name" : "Last Name" 363 | , "type" : "org.bluetooth.characteristic.last_name" 364 | } 365 | , "2a91" : { "name" : "Maximum Recommended Heart Rate" 366 | , "type" : "org.bluetooth.characteristic.maximum_recommended_heart_rate" 367 | } 368 | , "2a92" : { "name" : "Resting Heart Rate" 369 | , "type" : "org.bluetooth.characteristic.resting_heart_rate" 370 | } 371 | , "2a93" : { "name" : "Sport Type for Aerobic and Anaerobic Threshold" 372 | , "type" : "org.bluetooth.characteristic.sport_type_for_aerobic_and_anaerobic_threshold" 373 | } 374 | , "2a94" : { "name" : "Three Zone Heart Rate Limits" 375 | , "type" : "org.bluetooth.characteristic.three_zone_heart_rate_limits" 376 | } 377 | , "2a95" : { "name" : "Two Zone Heart Rate Limit" 378 | , "type" : "org.bluetooth.characteristic.two_zone_heart_rate_limit" 379 | } 380 | , "2a96" : { "name" : "VO2 Max" 381 | , "type" : "org.bluetooth.characteristic.vo2_max" 382 | } 383 | , "2a97" : { "name" : "Waist Circumference" 384 | , "type" : "org.bluetooth.characteristic.waist_circumference" 385 | } 386 | , "2a98" : { "name" : "Weight" 387 | , "type" : "org.bluetooth.characteristic.weight" 388 | } 389 | , "2a99" : { "name" : "Database Change Increment" 390 | , "type" : "org.bluetooth.characteristic.database_change_increment" 391 | } 392 | , "2a9a" : { "name" : "User Index" 393 | , "type" : "org.bluetooth.characteristic.user_index" 394 | } 395 | , "2a9b" : { "name" : "Body Composition Feature" 396 | , "type" : "org.bluetooth.characteristic.body_composition_feature" 397 | } 398 | , "2a9c" : { "name" : "Body Composition Measurement" 399 | , "type" : "org.bluetooth.characteristic.body_composition_measurement" 400 | } 401 | , "2a9d" : { "name" : "Weight Measurement" 402 | , "type" : "org.bluetooth.characteristic.weight_measurement" 403 | } 404 | , "2a9e" : { "name" : "Weight Scale Feature" 405 | , "type" : "org.bluetooth.characteristic.weight_scale_feature" 406 | } 407 | , "2a9f" : { "name" : "User Control Point" 408 | , "type" : "org.bluetooth.characteristic.user_control_point" 409 | } 410 | , "2aa0" : { "name" : "Magnetic Flux Density - 2D" 411 | , "type" : "org.bluetooth.characteristic.magnetic_flux_density_2d" 412 | } 413 | , "2aa1" : { "name" : "Magnetic Flux Density - 3D" 414 | , "type" : "org.bluetooth.characteristic.magnetic_flux_density_3d" 415 | } 416 | , "2aa2" : { "name" : "Language" 417 | , "type" : "org.bluetooth.characteristic.language" 418 | } 419 | , "2aa3" : { "name" : "Barometric Pressure Trend" 420 | , "type" : "org.bluetooth.characteristic.barometric_pressure_trend" 421 | } 422 | , "2aa4" : { "name" : "Bond Management Control Point" 423 | , "type" : "org.bluetooth.characteristic.bond_management_control_point" 424 | } 425 | , "2aa5" : { "name" : "Bond Management Feature" 426 | , "type" : "org.bluetooth.characteristic.bond_management_feature" 427 | } 428 | , "2aa6" : { "name" : "Central Address Resolution" 429 | , "type" : "org.bluetooth.characteristic.central_address_resolution" 430 | } 431 | , "2aa7" : { "name" : "CGM Measurement" 432 | , "type" : "org.bluetooth.characteristic.cgm_measurement" 433 | } 434 | , "2aa8" : { "name" : "CGM Feature" 435 | , "type" : "org.bluetooth.characteristic.cgm_feature" 436 | } 437 | , "2aa9" : { "name" : "CGM Status" 438 | , "type" : "org.bluetooth.characteristic.cgm_status" 439 | } 440 | , "2aaa" : { "name" : "CGM Session Start Time" 441 | , "type" : "org.bluetooth.characteristic.cgm_session_start_time" 442 | } 443 | , "2aab" : { "name" : "CGM Session Run Time" 444 | , "type" : "org.bluetooth.characteristic.cgm_session_run_time" 445 | } 446 | , "2aac" : { "name" : "CGM Specific Ops Control Point" 447 | , "type" : "org.bluetooth.characteristic.cgm_specific_ops_control_point" 448 | } 449 | } -------------------------------------------------------------------------------- /lib/descriptors.json: -------------------------------------------------------------------------------- 1 | { 2 | "2900" : { "name" : "Characteristic Extended Properties" 3 | , "type" : "org.bluetooth.descriptor.gatt.characteristic_extended_properties" 4 | } 5 | , "2901" : { "name" : "Characteristic User Description" 6 | , "type" : "org.bluetooth.descriptor.gatt.characteristic_user_description" 7 | } 8 | , "2902" : { "name" : "Client Characteristic Configuration" 9 | , "type" : "org.bluetooth.descriptor.gatt.client_characteristic_configuration" 10 | } 11 | , "2903" : { "name" : "Server Characteristic Configuration" 12 | , "type" : "org.bluetooth.descriptor.gatt.server_characteristic_configuration" 13 | } 14 | , "2904" : { "name" : "Characteristic Presentation Format" 15 | , "type" : "org.bluetooth.descriptor.gatt.characteristic_presentation_format" 16 | } 17 | , "2905" : { "name" : "Characteristic Aggregate Format" 18 | , "type" : "org.bluetooth.descriptor.gatt.characteristic_aggregate_format" 19 | } 20 | , "2906" : { "name" : "Valid Range" 21 | , "type" : "org.bluetooth.descriptor.valid_range" 22 | } 23 | , "2907" : { "name" : "External Report Reference" 24 | , "type" : "org.bluetooth.descriptor.external_report_reference" 25 | } 26 | , "2908" : { "name" : "Report Reference" 27 | , "type" : "org.bluetooth.descriptor.report_reference" 28 | } 29 | , "2909" : { "name" : "Number of Digitals" 30 | , "type" : "org.bluetooth.descriptor.number_of_digitals" 31 | } 32 | , "290a" : { "name" : "Value Trigger Setting" 33 | , "type" : "org.bluetooth.descriptor.value_trigger_setting" 34 | } 35 | , "290b" : { "name" : "Environmental Sensing Configuration" 36 | , "type" : "org.bluetooth.descriptor.environmental_sensing_configuration" 37 | } 38 | , "290c" : { "name" : "Environmental Sensing Measurement" 39 | , "type" : "org.bluetooth.descriptor.environmental_sensing_measurement" 40 | } 41 | , "290d" : { "name" : "Environmental Sensing Trigger Setting" 42 | , "type" : "org.bluetooth.descriptor.environmental_sensing_trigger_setting" 43 | } 44 | , "290e" : { "name" : "Time Trigger Setting" 45 | , "type" : "org.bluetooth.descriptor.time_trigger_setting" 46 | } 47 | } -------------------------------------------------------------------------------- /lib/hci-socket/acl-stream.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('acl-att-stream'); 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | 6 | var crypto = require('./crypto'); 7 | var Smp = require('./smp'); 8 | 9 | var AclStream = function(hci, handle, localAddressType, localAddress, remoteAddressType, remoteAddress) { 10 | this._hci = hci; 11 | this._handle = handle; 12 | this.encypted = false; 13 | 14 | this._smp = new Smp(this, localAddressType, localAddress, remoteAddressType, remoteAddress); 15 | 16 | this.onSmpStkBinded = this.onSmpStk.bind(this); 17 | this.onSmpFailBinded = this.onSmpFail.bind(this); 18 | this.onSmpEndBinded = this.onSmpEnd.bind(this); 19 | 20 | this._smp.on('stk', this.onSmpStkBinded); 21 | this._smp.on('fail', this.onSmpFailBinded); 22 | this._smp.on('end', this.onSmpEndBinded); 23 | }; 24 | 25 | util.inherits(AclStream, events.EventEmitter); 26 | 27 | AclStream.prototype.encrypt = function() { 28 | this._smp.sendPairingRequest(); 29 | }; 30 | 31 | AclStream.prototype.pushLtkNegReply = function() { 32 | this.emit('ltkNegReply'); 33 | }; 34 | 35 | AclStream.prototype.write = function(cid, data) { 36 | this._hci.writeAclDataPkt(this._handle, cid, data); 37 | }; 38 | 39 | AclStream.prototype.push = function(cid, data) { 40 | if (data) { 41 | this.emit('data', cid, data); 42 | } else { 43 | this.emit('end'); 44 | } 45 | }; 46 | 47 | AclStream.prototype.pushEncrypt = function(encrypt) { 48 | 49 | this.encrypted = encrypt ? true : false; 50 | 51 | //bleno 52 | this.emit('encryptChange', this.encrypted); 53 | 54 | //noble 55 | this.emit('encrypt', encrypt); 56 | }; 57 | 58 | AclStream.prototype.onSmpStk = function(stk) { 59 | var random = new Buffer('0000000000000000', 'hex'); 60 | var diversifier = new Buffer('0000', 'hex'); 61 | 62 | this._hci.startLeEncryption(this._handle, random, diversifier, stk); 63 | }; 64 | 65 | AclStream.prototype.onSmpFail = function() { 66 | this.emit('encryptFail', this); 67 | 68 | }; 69 | 70 | AclStream.prototype.onSmpEnd = function() { 71 | this._smp.removeListener('stk', this.onSmpStkBinded); 72 | this._smp.removeListener('fail', this.onSmpFailBinded); 73 | this._smp.removeListener('end', this.onSmpEndBinded); 74 | }; 75 | 76 | module.exports = AclStream; 77 | -------------------------------------------------------------------------------- /lib/hci-socket/bindings.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('bindings'); 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | 6 | var AclStream = require('./acl-stream'); 7 | var Gatt = require('./gatt'); 8 | var LocalGatt = require('./local-gatt'); 9 | var Gap = require('./gap'); 10 | var Hci = require('./hci'); 11 | 12 | 13 | var AbleBindings = function () { 14 | this._state = null; 15 | 16 | this._addresses = {}; 17 | this._addresseTypes = {}; 18 | this._connectable = {}; 19 | 20 | this._pendingConnectionUuid = null; 21 | this._connectionQueue = []; 22 | 23 | this._handles = {}; 24 | this._gatts = {}; 25 | this._aclStreams = {}; 26 | 27 | this._hci = new Hci(); 28 | this._hci.init(); 29 | this._gatt = new LocalGatt(this._hci); 30 | this._gap = new Gap(this._hci); 31 | this.onSigIntBinded = this.onSigInt.bind(this); 32 | 33 | process.on('SIGINT', this.onSigIntBinded); 34 | process.on('exit', this.onExit.bind(this)); 35 | 36 | this._gatt.on('handleMtuRequest', this.onMtu.bind(this)); 37 | 38 | this._gap.on('advertisingStart', this.onAdvertisingStart.bind(this)); 39 | this._gap.on('advertisingStop', this.onAdvertisingStop.bind(this)); 40 | 41 | this._hci.on('addressChange', this.onAddressChange.bind(this)); 42 | 43 | 44 | this._gap.on('scanStart', this.onScanStart.bind(this)); 45 | this._gap.on('scanStop', this.onScanStop.bind(this)); 46 | this._gap.on('discover', this.onDiscover.bind(this)); 47 | this._hci.on('addressChange', this.onAddressChange.bind(this)); 48 | this._hci.on('stateChange', this.onStateChange.bind(this)); 49 | this._hci.on('leConnComplete', this.onLeConnComplete.bind(this)); 50 | this._hci.on('leConnUpdateComplete', this.onLeConnUpdateComplete.bind(this)); 51 | this._hci.on('rssiRead', this.onRssiRead.bind(this)); 52 | this._hci.on('disconnComplete', this.onDisconnComplete.bind(this)); 53 | this._hci.on('encryptChange', this.onEncryptChange.bind(this)); 54 | this._hci.on('leLtkNegReply', this.onLeLtkNegReply.bind(this)); 55 | this._hci.on('aclDataPkt', this.onAclDataPkt.bind(this)); 56 | //debug('registered listeners for disconnComplete: ' + this._hci.listenerCount('disconnComplete')); 57 | 58 | 59 | }; 60 | 61 | util.inherits(AbleBindings, events.EventEmitter); 62 | 63 | 64 | 65 | 66 | AbleBindings.prototype.startScanning = function (serviceUuids, allowDuplicates) { 67 | this._scanServiceUuids = serviceUuids || []; 68 | 69 | this._gap.startScanning(allowDuplicates); 70 | }; 71 | 72 | AbleBindings.prototype.stopScanning = function () { 73 | this._gap.stopScanning(); 74 | }; 75 | 76 | AbleBindings.prototype.connect = function (peripheralUuid) { 77 | var address = this._addresses[peripheralUuid]; 78 | var addressType = this._addresseTypes[peripheralUuid]; 79 | 80 | console.log("Connect - uuid: " + peripheralUuid + " addres: " + address); 81 | if (!this._pendingConnectionUuid) { 82 | this._pendingConnectionUuid = peripheralUuid; 83 | 84 | this._hci.createLeConn(address, addressType); 85 | } else { 86 | this._connectionQueue.push(peripheralUuid); 87 | } 88 | }; 89 | 90 | AbleBindings.prototype.disconnect = function (peripheralUuid) { 91 | this._hci.disconnect(this._handles[peripheralUuid]); 92 | }; 93 | 94 | AbleBindings.prototype.updateRssi = function (peripheralUuid) { 95 | this._hci.readRssi(this._handles[peripheralUuid]); 96 | }; 97 | 98 | AbleBindings.prototype.init = function () { 99 | // no-op 100 | }; 101 | 102 | 103 | AbleBindings.prototype.onSigInt = function () { 104 | var sigIntListeners = process.listeners('SIGINT'); 105 | 106 | if (sigIntListeners[sigIntListeners.length - 1] === this.onSigIntBinded) { 107 | // we are the last listener, so exit 108 | // this will trigger onExit, and clean up 109 | process.exit(1); 110 | } 111 | }; 112 | 113 | AbleBindings.prototype.onExit = function () { 114 | this.stopScanning(); 115 | 116 | for (var handle in this._aclStreams) { 117 | this._hci.disconnect(handle); 118 | } 119 | //Bleno 120 | this._gap.stopAdvertising(); 121 | 122 | this.disconnect(); 123 | 124 | 125 | }; 126 | 127 | 128 | AbleBindings.prototype.onStateChange = function (state) { 129 | if (this._state === state) { 130 | return; 131 | } 132 | this._state = state; 133 | 134 | 135 | if (state === 'unauthorized') { 136 | console.log('able warning: adapter state unauthorized, please run as root or with sudo'); 137 | console.log(' or see README for information on running without root/sudo:'); 138 | console.log(' https://github.com/sandeepmistry/able#running-on-linux'); 139 | } else if (state === 'unsupported') { 140 | console.log('able warning: adapter does not support Bluetooth Low Energy (BLE, Bluetooth Smart).'); 141 | console.log(' Try to run with environment variable:'); 142 | console.log(' [sudo] NOBLE_HCI_DEVICE_ID=x node ...'); 143 | } 144 | 145 | this.emit('stateChange', state); 146 | }; 147 | 148 | AbleBindings.prototype.onAddressChange = function (address) { 149 | this.emit('addressChange', address); 150 | }; 151 | 152 | AbleBindings.prototype.onScanStart = function () { 153 | this.emit('scanStart'); 154 | }; 155 | 156 | AbleBindings.prototype.onScanStop = function () { 157 | this.emit('scanStop'); 158 | }; 159 | 160 | AbleBindings.prototype.onDiscover = function (status, address, addressType, connectable, advertisement, rssi) { 161 | if (this._scanServiceUuids === undefined) { 162 | return; 163 | } 164 | 165 | var serviceUuids = advertisement.serviceUuids || []; 166 | var serviceData = advertisement.serviceData || []; 167 | var hasScanServiceUuids = (this._scanServiceUuids.length === 0); 168 | 169 | if (!hasScanServiceUuids) { 170 | var i; 171 | 172 | serviceUuids = serviceUuids.slice(); 173 | 174 | for (i in serviceData) { 175 | serviceUuids.push(serviceData[i].uuid); 176 | } 177 | 178 | for (i in serviceUuids) { 179 | hasScanServiceUuids = (this._scanServiceUuids.indexOf(serviceUuids[i]) !== -1); 180 | 181 | if (hasScanServiceUuids) { 182 | break; 183 | } 184 | } 185 | } 186 | 187 | if (hasScanServiceUuids) { 188 | var uuid = address.split(':').join(''); 189 | this._addresses[uuid] = address; 190 | this._addresseTypes[uuid] = addressType; 191 | this._connectable[uuid] = connectable; 192 | 193 | this.emit('discover', uuid, address, addressType, connectable, advertisement, rssi); 194 | } 195 | }; 196 | 197 | 198 | 199 | AbleBindings.prototype.onLeConnComplete = function (status, handle, role, addressType, address, interval, latency, supervisionTimeout, masterClockAccuracy) { 200 | 201 | var uuid = null; 202 | var error = null; 203 | 204 | debug("onLeConnComplete - status: " + status + " role: " + role + " Address: " + address + " Type: " + addressType); 205 | 206 | 207 | if (status === 0) { 208 | uuid = address.split(':').join('').toLowerCase(); 209 | var aclStream = new AclStream(this._hci, handle, this._hci.addressType, this._hci.address, addressType, address); 210 | var gatt = new Gatt(address, aclStream); 211 | 212 | // Bleno Code 213 | 214 | this._address = address; 215 | this._handle = handle; 216 | 217 | //end bleno code 218 | 219 | this._gatts[uuid] = this._gatts[handle] = gatt; 220 | this._aclStreams[handle] = aclStream; 221 | this._handles[uuid] = handle; 222 | this._handles[handle] = uuid; 223 | 224 | this._gatts[handle].on('mtu', this.onMtu.bind(this)); 225 | this._gatts[handle].on('servicesDiscover', this.onServicesDiscovered.bind(this)); 226 | this._gatts[handle].on('includedServicesDiscover', this.onIncludedServicesDiscovered.bind(this)); 227 | this._gatts[handle].on('characteristicsDiscover', this.onCharacteristicsDiscovered.bind(this)); 228 | this._gatts[handle].on('read', this.onRead.bind(this)); 229 | this._gatts[handle].on('write', this.onWrite.bind(this)); 230 | this._gatts[handle].on('broadcast', this.onBroadcast.bind(this)); 231 | this._gatts[handle].on('notify', this.onNotify.bind(this)); 232 | this._gatts[handle].on('notification', this.onNotification.bind(this)); 233 | this._gatts[handle].on('descriptorsDiscover', this.onDescriptorsDiscovered.bind(this)); 234 | this._gatts[handle].on('valueRead', this.onValueRead.bind(this)); 235 | this._gatts[handle].on('valueWrite', this.onValueWrite.bind(this)); 236 | this._gatts[handle].on('handleRead', this.onHandleRead.bind(this)); 237 | this._gatts[handle].on('handleWrite', this.onHandleWrite.bind(this)); 238 | this._gatts[handle].on('handleNotify', this.onHandleNotify.bind(this)); 239 | this._gatts[handle].on('encryptFail', this.onEncryptFail.bind(this)); 240 | this._gatts[handle].exchangeMtu(256); 241 | 242 | if (role == 1) { 243 | //bleno 244 | //this._gatts[handle].setServices([]); 245 | uuid = address.split(':').join('').toLowerCase(); 246 | this._gatt.setAclStream(aclStream); 247 | this._gatts[handle]._handles = []; 248 | console.log("Bleno: Handle: " + handle + " UUID: " + uuid + " address " + address); 249 | if (this._connectionQueue.length > 0) { 250 | var peripheralUuid = this._connectionQueue.shift(); 251 | console.log("Perh: " + peripheralUuid + " address: " + address); 252 | } 253 | 254 | this._addresses[uuid] = address; 255 | this._addresseTypes[uuid] = addressType; 256 | 257 | this.emit('accept', uuid, addressType, address); 258 | return; 259 | } else { 260 | this.emit('connect', uuid, error); 261 | 262 | if (this._connectionQueue.length > 0) { 263 | var peripheralUuid = this._connectionQueue.shift(); 264 | 265 | address = this._addresses[peripheralUuid]; 266 | addressType = this._addresseTypes[peripheralUuid]; 267 | 268 | this._pendingConnectionUuid = peripheralUuid; 269 | 270 | this._hci.createLeConn(address, addressType); 271 | } else { 272 | this._pendingConnectionUuid = null; 273 | } 274 | } 275 | } else { 276 | uuid = this._pendingConnectionUuid; 277 | error = new Error(Hci.STATUS_MAPPER[status] || ('Unknown (' + status + ')')); 278 | console.log("onLeConnComplete Error: " + error); 279 | } 280 | 281 | 282 | }; 283 | 284 | AbleBindings.prototype.onLeConnUpdateComplete = function (status, handle, interval, latency, supervisionTimeout) { 285 | // no-op 286 | debug("onLeConnUpdateComplete - Not sure what to do... not doing anything"); 287 | 288 | if (status === 0) { 289 | //end bleno code 290 | debug('\t\taddress: ' + this._address); 291 | debug('\t\thandle: ' + this._handle); 292 | debug('\t\tgatt: ' + this._gatts[handle]); 293 | debug('\t\thandles: ' + this._handles[handle]); 294 | } else { 295 | error = new Error(Hci.STATUS_MAPPER[status] || ('Unknown (' + status + ')')); 296 | debug("Error: " + error); 297 | } 298 | }; 299 | 300 | AbleBindings.prototype.onDisconnComplete = function (handle, reason) { 301 | debug('disconnComplete'); 302 | var uuid = this._handles[handle]; 303 | console.log('\t\tHande:' + handle + '\tuuid: ' + uuid); 304 | if (uuid) { 305 | this._aclStreams[handle].push(null, null); 306 | this._gatts[handle].removeAllListeners(); 307 | 308 | delete this._gatts[uuid]; 309 | delete this._gatts[handle]; 310 | delete this._aclStreams[handle]; 311 | delete this._handles[uuid]; 312 | delete this._handles[handle]; 313 | debug('\t\tEmmitting disconnect'); 314 | this.emit('disconnect', uuid); // TODO: handle reason? 315 | } else { 316 | debug('unknown handle ' + handle + ' disconnected!'); 317 | } 318 | //This is for Bleno 319 | if (this._aclStream) { 320 | this._aclStream.push(null, null); 321 | } 322 | 323 | var address = this._address; 324 | 325 | this._address = null; 326 | this._handle = null; 327 | this._aclStream = null; 328 | 329 | if (address) { 330 | debug('\t\tEmmitting disconnect'); 331 | this.emit('disconnect', address); // TODO: use reason 332 | } 333 | 334 | if (this._advertising) { 335 | debug('\t\tRestarting adverstising'); 336 | this._gap.restartAdvertising(); 337 | } 338 | 339 | }; 340 | 341 | 342 | AbleBindings.prototype.onLeLtkNegReply = function (handle) { 343 | if (this._handle === handle && this._aclStream) { 344 | this._aclStream.pushLtkNegReply(); 345 | } 346 | }; 347 | 348 | AbleBindings.prototype.onEncryptFail = function (aclStream) { 349 | /* if (this._handle === handle && this._aclStream) { 350 | this._aclStream.pushEncrypt(encrypt); 351 | }*/ 352 | debug("onEncryptFail"); 353 | 354 | /* 355 | if (aclStream) { 356 | debug("Trying Pairing Again"); 357 | aclStream.pushEncrypt(true); 358 | }*/ 359 | }; 360 | 361 | 362 | AbleBindings.prototype.onEncryptChange = function (handle, encrypt) { 363 | /* if (this._handle === handle && this._aclStream) { 364 | this._aclStream.pushEncrypt(encrypt); 365 | }*/ 366 | 367 | 368 | var aclStream = this._aclStreams[handle]; 369 | debug("onEncryptChange: " + handle); 370 | this.emit("encryptChange"); 371 | if (aclStream) { 372 | aclStream.pushEncrypt(encrypt); 373 | } 374 | }; 375 | 376 | AbleBindings.prototype.onMtu = function (address, mtu) { 377 | this.emit('mtuChange', mtu); 378 | }; 379 | 380 | 381 | 382 | 383 | AbleBindings.prototype.onRssiRead = function (handle, rssi) { 384 | this.emit('rssiUpdate', this._handles[handle], rssi); 385 | 386 | /* Bleno 387 | this.emit('rssiUpdate', rssi); 388 | */ 389 | }; 390 | 391 | 392 | AbleBindings.prototype.onAclDataPkt = function (handle, cid, data) { 393 | var aclStream = this._aclStreams[handle]; 394 | 395 | if (aclStream) { 396 | aclStream.push(cid, data); 397 | } 398 | /* Bleno 399 | if (this._handle === handle && this._aclStream) { 400 | this._aclStream.push(cid, data); 401 | } 402 | */ 403 | }; 404 | 405 | AbleBindings.prototype.findByTypeRequest = function (peripheralUuid, startHandle, endHandle, uuid, value) { 406 | var handle = this._handles[peripheralUuid]; 407 | var gatt = this._gatts[handle]; 408 | if (gatt) { 409 | gatt.findByTypeRequest(startHandle, endHandle, uuid, value); 410 | } else { 411 | console.warn('able warning: FindByTypeRequest unknown peripheral ' + peripheralUuid); 412 | } 413 | 414 | } 415 | 416 | 417 | AbleBindings.prototype.discoverServices = function (peripheralUuid, uuids) { 418 | var handle = this._handles[peripheralUuid]; 419 | var gatt = this._gatts[handle]; 420 | 421 | if (gatt) { 422 | gatt.discoverServices(uuids || []); 423 | } else { 424 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 425 | } 426 | }; 427 | 428 | AbleBindings.prototype.onServicesDiscovered = function (address, serviceUuids) { 429 | var uuid = address.split(':').join('').toLowerCase(); 430 | 431 | this.emit('servicesDiscover', uuid, serviceUuids); 432 | }; 433 | 434 | AbleBindings.prototype.discoverIncludedServices = function (peripheralUuid, serviceUuid, serviceUuids) { 435 | var handle = this._handles[peripheralUuid]; 436 | var gatt = this._gatts[handle]; 437 | 438 | if (gatt) { 439 | gatt.discoverIncludedServices(serviceUuid, serviceUuids || []); 440 | } else { 441 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 442 | } 443 | }; 444 | 445 | AbleBindings.prototype.onIncludedServicesDiscovered = function (address, serviceUuid, includedServiceUuids) { 446 | var uuid = address.split(':').join('').toLowerCase(); 447 | 448 | this.emit('includedServicesDiscover', uuid, serviceUuid, includedServiceUuids); 449 | }; 450 | 451 | AbleBindings.prototype.discoverCharacteristics = function (peripheralUuid, serviceUuid, characteristicUuids) { 452 | var handle = this._handles[peripheralUuid]; 453 | var gatt = this._gatts[handle]; 454 | 455 | if (gatt) { 456 | gatt.discoverCharacteristics(serviceUuid, characteristicUuids || []); 457 | } else { 458 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 459 | } 460 | }; 461 | 462 | AbleBindings.prototype.onCharacteristicsDiscovered = function (address, serviceUuid, characteristics) { 463 | var uuid = address.split(':').join('').toLowerCase(); 464 | 465 | this.emit('characteristicsDiscover', uuid, serviceUuid, characteristics); 466 | }; 467 | 468 | AbleBindings.prototype.read = function (peripheralUuid, serviceUuid, characteristicUuid) { 469 | var handle = this._handles[peripheralUuid]; 470 | var gatt = this._gatts[handle]; 471 | 472 | if (gatt) { 473 | gatt.read(serviceUuid, characteristicUuid); 474 | } else { 475 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 476 | } 477 | }; 478 | 479 | AbleBindings.prototype.onRead = function (address, serviceUuid, characteristicUuid, data) { 480 | var uuid = address.split(':').join('').toLowerCase(); 481 | 482 | this.emit('read', uuid, serviceUuid, characteristicUuid, data, false); 483 | }; 484 | 485 | AbleBindings.prototype.write = function (peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse) { 486 | var handle = this._handles[peripheralUuid]; 487 | var gatt = this._gatts[handle]; 488 | 489 | if (gatt) { 490 | gatt.write(serviceUuid, characteristicUuid, data, withoutResponse); 491 | } else { 492 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 493 | } 494 | }; 495 | 496 | AbleBindings.prototype.onWrite = function (address, serviceUuid, characteristicUuid) { 497 | var uuid = address.split(':').join('').toLowerCase(); 498 | 499 | this.emit('write', uuid, serviceUuid, characteristicUuid); 500 | }; 501 | 502 | AbleBindings.prototype.broadcast = function (peripheralUuid, serviceUuid, characteristicUuid, broadcast) { 503 | var handle = this._handles[peripheralUuid]; 504 | var gatt = this._gatts[handle]; 505 | 506 | if (gatt) { 507 | gatt.broadcast(serviceUuid, characteristicUuid, broadcast); 508 | } else { 509 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 510 | } 511 | }; 512 | 513 | AbleBindings.prototype.onBroadcast = function (address, serviceUuid, characteristicUuid, state) { 514 | var uuid = address.split(':').join('').toLowerCase(); 515 | 516 | this.emit('broadcast', uuid, serviceUuid, characteristicUuid, state); 517 | }; 518 | 519 | AbleBindings.prototype.notify = function (peripheralUuid, serviceUuid, characteristicUuid, notify) { 520 | var handle = this._handles[peripheralUuid]; 521 | var gatt = this._gatts[handle]; 522 | 523 | if (gatt) { 524 | gatt.notify(serviceUuid, characteristicUuid, notify); 525 | } else { 526 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 527 | } 528 | }; 529 | 530 | AbleBindings.prototype.onNotify = function (address, serviceUuid, characteristicUuid, state) { 531 | var uuid = address.split(':').join('').toLowerCase(); 532 | 533 | this.emit('notify', uuid, serviceUuid, characteristicUuid, state); 534 | }; 535 | 536 | AbleBindings.prototype.onNotification = function (address, serviceUuid, characteristicUuid, data) { 537 | var uuid = address.split(':').join('').toLowerCase(); 538 | 539 | this.emit('read', uuid, serviceUuid, characteristicUuid, data, true); 540 | }; 541 | 542 | AbleBindings.prototype.discoverDescriptors = function (peripheralUuid, serviceUuid, characteristicUuid) { 543 | var handle = this._handles[peripheralUuid]; 544 | var gatt = this._gatts[handle]; 545 | 546 | if (gatt) { 547 | gatt.discoverDescriptors(serviceUuid, characteristicUuid); 548 | } else { 549 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 550 | } 551 | }; 552 | 553 | AbleBindings.prototype.onDescriptorsDiscovered = function (address, serviceUuid, characteristicUuid, descriptorUuids) { 554 | var uuid = address.split(':').join('').toLowerCase(); 555 | 556 | this.emit('descriptorsDiscover', uuid, serviceUuid, characteristicUuid, descriptorUuids); 557 | }; 558 | 559 | AbleBindings.prototype.readValue = function (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid) { 560 | var handle = this._handles[peripheralUuid]; 561 | var gatt = this._gatts[handle]; 562 | 563 | if (gatt) { 564 | gatt.readValue(serviceUuid, characteristicUuid, descriptorUuid); 565 | } else { 566 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 567 | } 568 | }; 569 | 570 | AbleBindings.prototype.onValueRead = function (address, serviceUuid, characteristicUuid, descriptorUuid, data) { 571 | var uuid = address.split(':').join('').toLowerCase(); 572 | 573 | this.emit('valueRead', uuid, serviceUuid, characteristicUuid, descriptorUuid, data); 574 | }; 575 | 576 | AbleBindings.prototype.writeValue = function (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data) { 577 | var handle = this._handles[peripheralUuid]; 578 | var gatt = this._gatts[handle]; 579 | 580 | if (gatt) { 581 | gatt.writeValue(serviceUuid, characteristicUuid, descriptorUuid, data); 582 | } else { 583 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 584 | } 585 | }; 586 | 587 | AbleBindings.prototype.onValueWrite = function (address, serviceUuid, characteristicUuid, descriptorUuid) { 588 | var uuid = address.split(':').join('').toLowerCase(); 589 | 590 | this.emit('valueWrite', uuid, serviceUuid, characteristicUuid, descriptorUuid); 591 | }; 592 | 593 | AbleBindings.prototype.readHandle = function (peripheralUuid, attHandle) { 594 | var handle = this._handles[peripheralUuid]; 595 | var gatt = this._gatts[handle]; 596 | 597 | if (gatt) { 598 | gatt.readHandle(attHandle); 599 | } else { 600 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 601 | } 602 | }; 603 | 604 | AbleBindings.prototype.onHandleRead = function (address, handle, data) { 605 | var uuid = address.split(':').join('').toLowerCase(); 606 | 607 | this.emit('handleRead', uuid, handle, data); 608 | }; 609 | 610 | AbleBindings.prototype.writeHandle = function (peripheralUuid, attHandle, data, withoutResponse) { 611 | var handle = this._handles[peripheralUuid]; 612 | var gatt = this._gatts[handle]; 613 | 614 | if (gatt) { 615 | gatt.writeHandle(attHandle, data, withoutResponse); 616 | } else { 617 | console.warn('able warning: unknown peripheral ' + peripheralUuid); 618 | } 619 | }; 620 | 621 | AbleBindings.prototype.onHandleWrite = function (address, handle) { 622 | var uuid = address.split(':').join('').toLowerCase(); 623 | 624 | this.emit('handleWrite', uuid, handle); 625 | }; 626 | 627 | AbleBindings.prototype.onHandleNotify = function (address, handle, data) { 628 | var uuid = address.split(':').join('').toLowerCase(); 629 | 630 | this.emit('handleNotify', uuid, handle, data); 631 | }; 632 | 633 | AbleBindings.prototype.startAdvertising = function (name, serviceUuids) { 634 | this._advertising = true; 635 | 636 | this._gap.startAdvertising(name, serviceUuids); 637 | }; 638 | 639 | AbleBindings.prototype.startAdvertisingIBeacon = function (data) { 640 | this._advertising = true; 641 | 642 | this._gap.startAdvertisingIBeacon(data); 643 | }; 644 | 645 | AbleBindings.prototype.startAdvertisingWithEIRData = function (advertisementData, scanData) { 646 | this._advertising = true; 647 | 648 | this._gap.startAdvertisingWithEIRData(advertisementData, scanData); 649 | }; 650 | 651 | AbleBindings.prototype.stopAdvertising = function () { 652 | this._advertising = false; 653 | 654 | this._gap.stopAdvertising(); 655 | }; 656 | 657 | AbleBindings.prototype.setServices = function (services) { 658 | this._gatt.setServices(services); 659 | debug('Trying to set services'); 660 | 661 | this.emit('servicesSet'); 662 | }; 663 | 664 | AbleBindings.prototype.disconnect = function () { 665 | if (this._handle) { 666 | debug('disconnect by server'); 667 | 668 | this._hci.disconnect(this._handle); 669 | } 670 | }; 671 | 672 | AbleBindings.prototype.updateRssi = function () { 673 | if (this._handle) { 674 | this._hci.readRssi(this._handle); 675 | } 676 | }; 677 | 678 | 679 | 680 | 681 | 682 | AbleBindings.prototype.onAddressChange = function (address) { 683 | this.emit('addressChange', address); 684 | }; 685 | 686 | AbleBindings.prototype.onAdvertisingStart = function (error) { 687 | this.emit('advertisingStart', error); 688 | }; 689 | 690 | AbleBindings.prototype.onAdvertisingStop = function () { 691 | this.emit('advertisingStop'); 692 | }; 693 | 694 | 695 | 696 | module.exports = new AbleBindings(); 697 | -------------------------------------------------------------------------------- /lib/hci-socket/crypto.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | function r() { 4 | return crypto.randomBytes(16); 5 | } 6 | 7 | function c1(k, r, pres, preq, iat, ia, rat, ra) { 8 | var p1 = Buffer.concat([ 9 | iat, 10 | rat, 11 | preq, 12 | pres 13 | ]); 14 | 15 | var p2 = Buffer.concat([ 16 | ra, 17 | ia, 18 | new Buffer('00000000', 'hex') 19 | ]); 20 | 21 | var res = xor(r, p1); 22 | res = e(k, res); 23 | res = xor(res, p2); 24 | res = e(k, res); 25 | 26 | return res; 27 | } 28 | 29 | function s1(k, r1, r2) { 30 | return e(k, Buffer.concat([ 31 | r2.slice(0, 8), 32 | r1.slice(0, 8) 33 | ])); 34 | } 35 | 36 | function e(key, data) { 37 | key = swap(key); 38 | data = swap(data); 39 | 40 | var cipher = crypto.createCipheriv('aes-128-ecb', key, ''); 41 | cipher.setAutoPadding(false); 42 | 43 | return swap(Buffer.concat([ 44 | cipher.update(data), 45 | cipher.final() 46 | ])); 47 | } 48 | 49 | function xor(b1, b2) { 50 | var result = new Buffer(b1.length); 51 | 52 | for (var i = 0; i < b1.length; i++) { 53 | result[i] = b1[i] ^ b2[i]; 54 | } 55 | 56 | return result; 57 | } 58 | 59 | function swap(input) { 60 | var output = new Buffer(input.length); 61 | 62 | for (var i = 0; i < output.length; i++) { 63 | output[i] = input[input.length - i - 1]; 64 | } 65 | 66 | return output; 67 | } 68 | 69 | module.exports = { 70 | r: r, 71 | c1: c1, 72 | s1: s1, 73 | e: e 74 | }; 75 | -------------------------------------------------------------------------------- /lib/hci-socket/gap.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('gap'); 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | 6 | var Gap = function (hci) { 7 | this._hci = hci; 8 | 9 | this._advertiseState = null; 10 | 11 | this._hci.on('error', this.onHciError.bind(this)); 12 | 13 | this._scanState = null; 14 | this._scanFilterDuplicates = null; 15 | this._discoveries = {}; 16 | 17 | this._hci.on('error', this.onHciError.bind(this)); 18 | this._hci.on('leScanParametersSet', this.onHciLeScanParametersSet.bind(this)); 19 | this._hci.on('leScanEnableSet', this.onHciLeScanEnableSet.bind(this)); 20 | this._hci.on('leAdvertisingReport', this.onHciLeAdvertisingReport.bind(this)); 21 | 22 | 23 | //Bleno 24 | this._hci.on('leAdvertisingParametersSet', this.onHciLeAdvertisingParametersSet.bind(this)); 25 | this._hci.on('leAdvertisingDataSet', this.onHciLeAdvertisingDataSet.bind(this)); 26 | this._hci.on('leScanResponseDataSet', this.onHciLeScanResponseDataSet.bind(this)); 27 | this._hci.on('leAdvertiseEnableSet', this.onHciLeAdvertiseEnableSet.bind(this)); 28 | this._hci.on('cmdLeScanEnableSet', this.onCmdHciLeScanEnableSet.bind(this)); 29 | }; 30 | 31 | util.inherits(Gap, events.EventEmitter); 32 | 33 | Gap.prototype.startScanning = function (allowDuplicates) { 34 | this._scanState = 'starting'; 35 | this._scanFilterDuplicates = !allowDuplicates; 36 | this._hci.setScanEnabled(true, !allowDuplicates); 37 | }; 38 | 39 | Gap.prototype.stopScanning = function () { 40 | this._scanState = 'stopping'; 41 | 42 | this._hci.setScanEnabled(false, true); 43 | }; 44 | 45 | Gap.prototype.onHciError = function (error) { 46 | 47 | }; 48 | 49 | Gap.prototype.onHciLeScanParametersSet = function () { 50 | 51 | }; 52 | // Called when receive an event "Command Complete" for "LE Set Scan Enable" 53 | Gap.prototype.onHciLeScanEnableSet = function (status) { 54 | // Check the status we got from the command complete function. 55 | // If it is nonzero there was an error, and we should not change 56 | // our status as a result. 57 | if (status != 0) { 58 | return; 59 | } 60 | if (this._scanState === 'starting') { 61 | this._scanState = 'started'; 62 | 63 | this.emit('scanStart'); 64 | } else if (this._scanState === 'stopping') { 65 | this._scanState = 'stopped'; 66 | 67 | this.emit('scanStop'); 68 | } 69 | }; 70 | 71 | // Called when we see the actual command "LE Set Scan Enable" 72 | Gap.prototype.onCmdHciLeScanEnableSet = function (enable, filter_dups) { 73 | // Check to see if the new settings differ from what we expect. 74 | // If we are scanning, then a change happens if the new command stops 75 | // scanning or if duplicate filtering changes. 76 | // If we are not scanning, then a change happens if scanning was enabled. 77 | if ((this._scanState == 'starting' || this._scanState == 'started') && 78 | (!enable || this._scanFilterDuplicates != filter_dups)) { 79 | // We think we are scanning with particular settings and these changed. 80 | // Notify the user that scanning may have stopped. 81 | this.emit('scanStop'); 82 | } else if ((this._scanState == 'stopping' || this._scanState == 'stopped') && 83 | (enable)) { 84 | // Someone started scanning on us. 85 | this.emit('scanStart'); 86 | } 87 | }; 88 | 89 | Gap.prototype.onHciLeAdvertisingReport = function (status, type, address, addressType, eir, rssi) { 90 | var previouslyDiscovered = !!this._discoveries[address]; 91 | var advertisement = previouslyDiscovered ? this._discoveries[address].advertisement : { 92 | localName: undefined, 93 | txPowerLevel: undefined, 94 | manufacturerData: undefined, 95 | serviceData: [], 96 | serviceUuids: [] 97 | }; 98 | 99 | var discoveryCount = previouslyDiscovered ? this._discoveries[address].count : 0; 100 | var hasScanResponse = previouslyDiscovered ? this._discoveries[address].hasScanResponse : false; 101 | 102 | if (type === 0x04) { 103 | hasScanResponse = true; 104 | } else { 105 | // reset service data every non-scan response event 106 | advertisement.serviceData = []; 107 | advertisement.serviceUuids = []; 108 | } 109 | 110 | discoveryCount++; 111 | 112 | var i = 0; 113 | var j = 0; 114 | var serviceUuid = null; 115 | 116 | while ((i + 1) < eir.length) { 117 | var length = eir.readUInt8(i); 118 | 119 | if (length < 1) { 120 | debug('invalid EIR data, length = ' + length); 121 | break; 122 | } 123 | 124 | var eirType = eir.readUInt8(i + 1); // https://www.bluetooth.org/en-us/specification/assigned-numbers/generic-access-profile 125 | 126 | if ((i + length + 1) > eir.length) { 127 | debug('invalid EIR data, out of range of buffer length'); 128 | break; 129 | } 130 | 131 | var bytes = eir.slice(i + 2).slice(0, length - 1); 132 | 133 | switch (eirType) { 134 | case 0x02: // Incomplete List of 16-bit Service Class UUID 135 | case 0x03: // Complete List of 16-bit Service Class UUIDs 136 | for (j = 0; j < bytes.length; j += 2) { 137 | serviceUuid = bytes.readUInt16LE(j).toString(16); 138 | if (advertisement.serviceUuids.indexOf(serviceUuid) === -1) { 139 | advertisement.serviceUuids.push(serviceUuid); 140 | } 141 | } 142 | break; 143 | 144 | case 0x06: // Incomplete List of 128-bit Service Class UUIDs 145 | case 0x07: // Complete List of 128-bit Service Class UUIDs 146 | for (j = 0; j < bytes.length; j += 16) { 147 | serviceUuid = bytes.slice(j, j + 16).toString('hex').match(/.{1,2}/g).reverse().join(''); 148 | if (advertisement.serviceUuids.indexOf(serviceUuid) === -1) { 149 | advertisement.serviceUuids.push(serviceUuid); 150 | } 151 | } 152 | break; 153 | 154 | case 0x08: // Shortened Local Name 155 | case 0x09: // Complete Local Name» 156 | advertisement.localName = bytes.toString('utf8'); 157 | break; 158 | 159 | case 0x0a: // Tx Power Level 160 | advertisement.txPowerLevel = bytes.readInt8(0); 161 | break; 162 | 163 | case 0x16: // Service Data, there can be multiple occurences 164 | var serviceDataUuid = bytes.slice(0, 2).toString('hex').match(/.{1,2}/g).reverse().join(''); 165 | var serviceData = bytes.slice(2, bytes.length); 166 | 167 | advertisement.serviceData.push({ 168 | uuid: serviceDataUuid, 169 | data: serviceData 170 | }); 171 | break; 172 | 173 | case 0xff: // Manufacturer Specific Data 174 | advertisement.manufacturerData = bytes; 175 | break; 176 | } 177 | 178 | i += (length + 1); 179 | } 180 | 181 | debug('advertisement = ' + JSON.stringify(advertisement, null, 0)); 182 | 183 | var connectable = (type === 0x04) ? this._discoveries[address].connectable : (type !== 0x03); 184 | 185 | this._discoveries[address] = { 186 | address: address, 187 | addressType: addressType, 188 | connectable: connectable, 189 | advertisement: advertisement, 190 | rssi: rssi, 191 | count: discoveryCount, 192 | hasScanResponse: hasScanResponse 193 | }; 194 | 195 | // only report after a scan response event or more than one discovery without a scan response, so more data can be collected 196 | if (type === 0x04 || (discoveryCount > 1 && !hasScanResponse) || process.env.NOBLE_REPORT_ALL_HCI_EVENTS) { 197 | this.emit('discover', status, address, addressType, connectable, advertisement, rssi); 198 | } 199 | }; 200 | 201 | /* BLENO Start*/ 202 | Gap.prototype.startAdvertising = function (name, serviceUuids) { 203 | debug('startAdvertising: name = ' + name + ', serviceUuids = ' + JSON.stringify(serviceUuids, null, 2)); 204 | 205 | var advertisementDataLength = 3; 206 | var scanDataLength = 0; 207 | 208 | var serviceUuids16bit = []; 209 | var serviceUuids128bit = []; 210 | var i = 0; 211 | 212 | if (name && name.length) { 213 | scanDataLength += 2 + name.length; 214 | } 215 | 216 | if (serviceUuids && serviceUuids.length) { 217 | for (i = 0; i < serviceUuids.length; i++) { 218 | var serviceUuid = new Buffer(serviceUuids[i].match(/.{1,2}/g).reverse().join(''), 'hex'); 219 | 220 | if (serviceUuid.length === 2) { 221 | serviceUuids16bit.push(serviceUuid); 222 | } else if (serviceUuid.length === 16) { 223 | serviceUuids128bit.push(serviceUuid); 224 | } 225 | } 226 | } 227 | 228 | if (serviceUuids16bit.length) { 229 | advertisementDataLength += 2 + 2 * serviceUuids16bit.length; 230 | } 231 | 232 | if (serviceUuids128bit.length) { 233 | advertisementDataLength += 2 + 16 * serviceUuids128bit.length; 234 | } 235 | 236 | var advertisementData = new Buffer(advertisementDataLength); 237 | var scanData = new Buffer(scanDataLength); 238 | 239 | // flags 240 | advertisementData.writeUInt8(2, 0); 241 | advertisementData.writeUInt8(0x01, 1); 242 | advertisementData.writeUInt8(0x06, 2); 243 | 244 | var advertisementDataOffset = 3; 245 | 246 | if (serviceUuids16bit.length) { 247 | advertisementData.writeUInt8(1 + 2 * serviceUuids16bit.length, advertisementDataOffset); 248 | advertisementDataOffset++; 249 | 250 | advertisementData.writeUInt8(0x03, advertisementDataOffset); 251 | advertisementDataOffset++; 252 | 253 | for (i = 0; i < serviceUuids16bit.length; i++) { 254 | serviceUuids16bit[i].copy(advertisementData, advertisementDataOffset); 255 | advertisementDataOffset += serviceUuids16bit[i].length; 256 | } 257 | } 258 | 259 | if (serviceUuids128bit.length) { 260 | advertisementData.writeUInt8(1 + 16 * serviceUuids128bit.length, advertisementDataOffset); 261 | advertisementDataOffset++; 262 | 263 | advertisementData.writeUInt8(0x06, advertisementDataOffset); 264 | advertisementDataOffset++; 265 | 266 | for (i = 0; i < serviceUuids128bit.length; i++) { 267 | serviceUuids128bit[i].copy(advertisementData, advertisementDataOffset); 268 | advertisementDataOffset += serviceUuids128bit[i].length; 269 | } 270 | } 271 | 272 | // name 273 | if (name && name.length) { 274 | var nameBuffer = new Buffer(name); 275 | 276 | scanData.writeUInt8(1 + nameBuffer.length, 0); 277 | scanData.writeUInt8(0x08, 1); 278 | nameBuffer.copy(scanData, 2); 279 | } 280 | 281 | this.startAdvertisingWithEIRData(advertisementData, scanData); 282 | }; 283 | 284 | 285 | Gap.prototype.startAdvertisingIBeacon = function (data) { 286 | debug('startAdvertisingIBeacon: data = ' + data.toString('hex')); 287 | 288 | var dataLength = data.length; 289 | var manufacturerDataLength = 4 + dataLength; 290 | var advertisementDataLength = 5 + manufacturerDataLength; 291 | var scanDataLength = 0; 292 | 293 | var advertisementData = new Buffer(advertisementDataLength); 294 | var scanData = new Buffer(0); 295 | 296 | // flags 297 | advertisementData.writeUInt8(2, 0); 298 | advertisementData.writeUInt8(0x01, 1); 299 | advertisementData.writeUInt8(0x06, 2); 300 | 301 | advertisementData.writeUInt8(manufacturerDataLength + 1, 3); 302 | advertisementData.writeUInt8(0xff, 4); 303 | advertisementData.writeUInt16LE(0x004c, 5); // Apple Company Identifier LE (16 bit) 304 | advertisementData.writeUInt8(0x02, 7); // type, 2 => iBeacon 305 | advertisementData.writeUInt8(dataLength, 8); 306 | 307 | data.copy(advertisementData, 9); 308 | 309 | this.startAdvertisingWithEIRData(advertisementData, scanData); 310 | }; 311 | 312 | Gap.prototype.startAdvertisingWithEIRData = function (advertisementData, scanData) { 313 | advertisementData = advertisementData || new Buffer(0); 314 | scanData = scanData || new Buffer(0); 315 | 316 | debug('startAdvertisingWithEIRData: advertisement data = ' + advertisementData.toString('hex') + ', scan data = ' + scanData.toString('hex')); 317 | 318 | var error = null; 319 | 320 | if (advertisementData.length > 31) { 321 | error = new Error('Advertisement data is over maximum limit of 31 bytes'); 322 | } else if (scanData.length > 31) { 323 | error = new Error('Scan data is over maximum limit of 31 bytes'); 324 | } 325 | 326 | if (error) { 327 | this.emit('advertisingStart', error); 328 | } else { 329 | this._advertiseState = 'starting'; 330 | 331 | this._hci.setScanResponseData(scanData); 332 | this._hci.setAdvertisingData(advertisementData); 333 | this._hci.setAdvertiseEnable(true); 334 | //this._hci.setScanResponseData(scanData); 335 | //this._hci.setAdvertisingData(advertisementData); 336 | } 337 | }; 338 | 339 | Gap.prototype.restartAdvertising = function () { 340 | this._advertiseState = 'restarting'; 341 | 342 | this._hci.setAdvertiseEnable(true); 343 | }; 344 | 345 | Gap.prototype.stopAdvertising = function () { 346 | this._advertiseState = 'stopping'; 347 | 348 | this._hci.setAdvertiseEnable(false); 349 | }; 350 | 351 | Gap.prototype.onHciLeAdvertisingParametersSet = function (status) {}; 352 | 353 | Gap.prototype.onHciLeAdvertisingDataSet = function (status) {}; 354 | 355 | Gap.prototype.onHciLeScanResponseDataSet = function (status) {}; 356 | 357 | Gap.prototype.onHciLeAdvertiseEnableSet = function (status) { 358 | if (this._advertiseState === 'starting') { 359 | this._advertiseState = 'started'; 360 | 361 | var error = null; 362 | 363 | if (status) { 364 | error = new Error('Unknown (' + status + ')'); 365 | } 366 | 367 | this.emit('advertisingStart', error); 368 | } else if (this._advertiseState === 'stopping') { 369 | this._advertiseState = 'stopped'; 370 | 371 | this.emit('advertisingStop'); 372 | } 373 | }; 374 | 375 | /* BLENO Stop */ 376 | 377 | 378 | module.exports = Gap; 379 | -------------------------------------------------------------------------------- /lib/hci-socket/gatt.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('gatt'); 2 | 3 | var events = require('events'); 4 | var os = require('os'); 5 | var util = require('util'); 6 | 7 | var ATT_OP_ERROR = 0x01; 8 | var ATT_OP_MTU_REQ = 0x02; 9 | var ATT_OP_MTU_RESP = 0x03; 10 | var ATT_OP_FIND_INFO_REQ = 0x04; 11 | var ATT_OP_FIND_INFO_RESP = 0x05; 12 | var ATT_OP_FIND_BY_TYPE_REQ = 0x06; 13 | var ATT_OP_FIND_BY_TYPE_RESP = 0x07; 14 | var ATT_OP_READ_BY_TYPE_REQ = 0x08; 15 | var ATT_OP_READ_BY_TYPE_RESP = 0x09; 16 | var ATT_OP_READ_REQ = 0x0a; 17 | var ATT_OP_READ_RESP = 0x0b; 18 | var ATT_OP_READ_BLOB_REQ = 0x0c; 19 | var ATT_OP_READ_BLOB_RESP = 0x0d; 20 | var ATT_OP_READ_MULTI_REQ = 0x0e; 21 | var ATT_OP_READ_MULTI_RESP = 0x0f; 22 | var ATT_OP_READ_BY_GROUP_REQ = 0x10; 23 | var ATT_OP_READ_BY_GROUP_RESP = 0x11; 24 | var ATT_OP_WRITE_REQ = 0x12; 25 | var ATT_OP_WRITE_RESP = 0x13; 26 | var ATT_OP_WRITE_CMD = 0x52; 27 | var ATT_OP_PREP_WRITE_REQ = 0x16; 28 | var ATT_OP_PREP_WRITE_RESP = 0x17; 29 | var ATT_OP_EXEC_WRITE_REQ = 0x18; 30 | var ATT_OP_EXEC_WRITE_RESP = 0x19; 31 | var ATT_OP_HANDLE_NOTIFY = 0x1b; 32 | var ATT_OP_HANDLE_IND = 0x1d; 33 | var ATT_OP_HANDLE_CNF = 0x1e; 34 | var ATT_OP_WRITE_CMD = 0x52; 35 | var ATT_OP_SIGNED_WRITE_CMD = 0xd2; 36 | 37 | var GATT_PRIM_SVC_UUID = 0x2800; 38 | var GATT_INCLUDE_UUID = 0x2802; 39 | var GATT_CHARAC_UUID = 0x2803; 40 | 41 | var GATT_CLIENT_CHARAC_CFG_UUID = 0x2902; 42 | var GATT_SERVER_CHARAC_CFG_UUID = 0x2903; 43 | 44 | var ATT_ECODE_SUCCESS = 0x00; 45 | var ATT_ECODE_INVALID_HANDLE = 0x01; 46 | var ATT_ECODE_READ_NOT_PERM = 0x02; 47 | var ATT_ECODE_WRITE_NOT_PERM = 0x03; 48 | var ATT_ECODE_INVALID_PDU = 0x04; 49 | var ATT_ECODE_AUTHENTICATION = 0x05; 50 | var ATT_ECODE_REQ_NOT_SUPP = 0x06; 51 | var ATT_ECODE_INVALID_OFFSET = 0x07; 52 | var ATT_ECODE_AUTHORIZATION = 0x08; 53 | var ATT_ECODE_PREP_QUEUE_FULL = 0x09; 54 | var ATT_ECODE_ATTR_NOT_FOUND = 0x0a; 55 | var ATT_ECODE_ATTR_NOT_LONG = 0x0b; 56 | var ATT_ECODE_INSUFF_ENCR_KEY_SIZE = 0x0c; 57 | var ATT_ECODE_INVAL_ATTR_VALUE_LEN = 0x0d; 58 | var ATT_ECODE_UNLIKELY = 0x0e; 59 | var ATT_ECODE_INSUFF_ENC = 0x0f; 60 | var ATT_ECODE_UNSUPP_GRP_TYPE = 0x10; 61 | var ATT_ECODE_INSUFF_RESOURCES = 0x11; 62 | var ATT_CID = 0x0004; 63 | 64 | var Gatt = function(address, aclStream) { 65 | this._address = address; 66 | this._aclStream = aclStream; 67 | 68 | this._services = {}; 69 | this._characteristics = {}; 70 | this._descriptors = {}; 71 | 72 | this._currentCommand = null; 73 | this._commandQueue = []; 74 | 75 | this._mtu = 23; 76 | this._security = 'low'; 77 | 78 | this.onAclStreamDataBinded = this.onAclStreamData.bind(this); 79 | this.onAclStreamEncryptBinded = this.onAclStreamEncrypt.bind(this); 80 | this.onAclStreamEncryptFailBinded = this.onAclStreamEncryptFail.bind(this); 81 | this.onAclStreamEndBinded = this.onAclStreamEnd.bind(this); 82 | 83 | this._aclStream.on('data', this.onAclStreamDataBinded); 84 | this._aclStream.on('encrypt', this.onAclStreamEncryptBinded); 85 | this._aclStream.on('encryptFail', this.onAclStreamEncryptFailBinded); 86 | this._aclStream.on('end', this.onAclStreamEndBinded); 87 | }; 88 | 89 | util.inherits(Gatt, events.EventEmitter); 90 | 91 | Gatt.prototype.fromJson = function(json) { 92 | this._address = json.address; 93 | this._services = json.services; 94 | this._characteristics = json.characteristics; 95 | this._descriptors = json.descriptors; 96 | this._mtu = json.mtu; 97 | this._security = json.security; 98 | } 99 | 100 | Gatt.prototype.toJson = function() { 101 | var output; 102 | output.address = this._address; 103 | output.services = this._services; 104 | output.characteristics = this._characteristics; 105 | output.descriptors = this._descriptors; 106 | output.mtu = this._mtu; 107 | output.security = this._security; 108 | 109 | return output; 110 | } 111 | 112 | 113 | 114 | 115 | Gatt.prototype.onAclStreamData = function(cid, data) { 116 | if (cid !== ATT_CID) { 117 | return; 118 | } 119 | 120 | var request = data; 121 | 122 | 123 | var requestType = request[0]; 124 | var response = null; 125 | 126 | switch(requestType) { 127 | case ATT_OP_ERROR: 128 | debug('ATT_OP_ERROR: ' + request.toString('hex')); 129 | this.handleOpError(request); 130 | break; 131 | case ATT_OP_READ_BY_TYPE_RESP: 132 | debug('ATT_OP_READ_BY_TYPE_RESP: ' + request.toString('hex')); 133 | break; 134 | case ATT_OP_READ_BY_GROUP_RESP: 135 | debug('ATT_OP_READ_BY_GROUP_RESP: ' + request.toString('hex')); 136 | break; 137 | case ATT_OP_HANDLE_NOTIFY: 138 | case ATT_OP_HANDLE_IND: 139 | debug('ATT_OP_HANDLE_NOTIFY: ' + request.toString('hex')); 140 | var valueHandle = data.readUInt16LE(1); 141 | var valueData = data.slice(3); 142 | 143 | this.emit('handleNotify', this._address, valueHandle, valueData); 144 | if (data[0] === ATT_OP_HANDLE_IND) { 145 | this._queueCommand(this.handleConfirmation(), null, function() { 146 | this.emit('handleConfirmation', this._address, valueHandle); 147 | }.bind(this)); 148 | } 149 | 150 | for (var serviceUuid in this._services) { 151 | for (var characteristicUuid in this._characteristics[serviceUuid]) { 152 | if (this._characteristics[serviceUuid][characteristicUuid].valueHandle === valueHandle) { 153 | this.emit('notification', this._address, serviceUuid, characteristicUuid, valueData); 154 | } 155 | } 156 | } 157 | break; 158 | 159 | default: 160 | case ATT_OP_READ_MULTI_REQ: 161 | case ATT_OP_PREP_WRITE_REQ: 162 | case ATT_OP_EXEC_WRITE_REQ: 163 | case ATT_OP_SIGNED_WRITE_CMD: 164 | //response = this.errorResponse(requestType, 0x0000, ATT_ECODE_REQ_NOT_SUPP); 165 | break; 166 | 167 | 168 | } 169 | 170 | if (response) { 171 | debug('response: ' + response.toString('hex')); 172 | 173 | this.writeAtt(response); 174 | } 175 | 176 | 177 | if (this._currentCommand && data.toString('hex') === this._currentCommand.buffer.toString('hex')) { 178 | debug(this._address + ': echo ... echo ... echo ...'); 179 | } else if (data[0] % 2 === 0) { 180 | /* var requestType = data[0]; 181 | debug(this._address + ': replying with REQ_NOT_SUPP to 0x' + requestType.toString(16)); 182 | this.writeAtt(this.errorResponse(requestType, 0x0000, ATT_ECODE_REQ_NOT_SUPP));*/ 183 | } else if (!this._currentCommand) { 184 | debug(this._address + ': uh oh, no current command'); 185 | } else { 186 | debug(this._address + ': There is a current command'); 187 | 188 | if (data[0] === ATT_OP_ERROR && 189 | (data[4] === ATT_ECODE_AUTHENTICATION || data[4] === ATT_ECODE_AUTHORIZATION || data[4] === ATT_ECODE_INSUFF_ENC) && 190 | this._security !== 'medium') { 191 | 192 | debug('\t!! Need to encrypt!'); 193 | this._aclStream.encrypt(); 194 | return; 195 | } 196 | 197 | this._currentCommand.callback(data); 198 | 199 | this._currentCommand = null; 200 | 201 | while(this._commandQueue.length) { 202 | this._currentCommand = this._commandQueue.shift(); 203 | debug('\tUnqueueing and sending command'); 204 | this.writeAtt(this._currentCommand.buffer); 205 | 206 | if (this._currentCommand.callback) { 207 | break; 208 | } else if (this._currentCommand.writeCallback) { 209 | this._currentCommand.writeCallback(); 210 | 211 | this._currentCommand = null; 212 | } 213 | } 214 | } 215 | }; 216 | 217 | Gatt.prototype.onAclStreamEncrypt = function(encrypt) { 218 | if (encrypt) { 219 | this._security = 'medium'; 220 | debug("Trying to Encrypt"); 221 | //this.writeAtt(this._currentCommand.buffer); 222 | } 223 | }; 224 | 225 | Gatt.prototype.onAclStreamEncryptFail = function(aclStream) { 226 | debug("onAclStreamEncryptFail: "+ aclStream); 227 | this._security = 'low'; 228 | this.emit('encryptFail',aclStream); 229 | }; 230 | 231 | 232 | Gatt.prototype.onAclStreamEnd = function() { 233 | this._aclStream.removeListener('data', this.onAclStreamDataBinded); 234 | this._aclStream.removeListener('encrypt', this.onAclStreamEncryptBinded); 235 | this._aclStream.removeListener('encryptFail', this.onAclStreamEncryptFailBinded); 236 | this._aclStream.removeListener('end', this.onAclStreamEndBinded); 237 | }; 238 | 239 | Gatt.prototype.errorResponse = function(opcode, handle, status) { 240 | var buf = new Buffer(5); 241 | 242 | debug('\tSending ATT_OP_ERROR'); 243 | debug('\t\topcode: 0x' + opcode.toString(16)); 244 | debug('\t\thandle 0x' + handle.toString(16)); 245 | debug('\t\tstatus: 0x' + status.toString(16)); 246 | buf.writeUInt8(ATT_OP_ERROR, 0); 247 | buf.writeUInt8(opcode, 1); 248 | buf.writeUInt16LE(handle, 2); 249 | buf.writeUInt8(status, 4); 250 | 251 | return buf; 252 | }; 253 | 254 | Gatt.prototype.writeAtt = function(data) { 255 | debug(this._address + ': write: ' + data.toString('hex')); 256 | 257 | this._aclStream.write(ATT_CID, data); 258 | }; 259 | 260 | Gatt.prototype._queueCommand = function(buffer, callback, writeCallback) { 261 | this._commandQueue.push({ 262 | buffer: buffer, 263 | callback: callback, 264 | writeCallback: writeCallback 265 | }); 266 | 267 | debug(this._commandQueue.length + ' Commands in Queue'); 268 | 269 | if (this._currentCommand === null) { 270 | while (this._commandQueue.length) { 271 | this._currentCommand = this._commandQueue.shift(); 272 | 273 | this.writeAtt(this._currentCommand.buffer); 274 | 275 | if (this._currentCommand.callback) { 276 | break; 277 | } else if (this._currentCommand.writeCallback) { 278 | this._currentCommand.writeCallback(); 279 | 280 | this._currentCommand = null; 281 | } 282 | } 283 | } 284 | }; 285 | 286 | Gatt.prototype.mtuRequest = function(mtu) { 287 | var buf = new Buffer(3); 288 | 289 | debug("Sending ATT_OP_MTU_REQ: " + mtu); 290 | buf.writeUInt8(ATT_OP_MTU_REQ, 0); 291 | buf.writeUInt16LE(mtu, 1); 292 | 293 | return buf; 294 | }; 295 | 296 | Gatt.prototype.readByGroupRequest = function(startHandle, endHandle, groupUuid) { 297 | var buf = new Buffer(7); 298 | var printBuf = new Buffer(2); 299 | debug('\tSending ATT_OP_READ_BY_GROUP_REQ ID'); 300 | debug('\t\tstartHandle: 0x' + startHandle.toString(16)); 301 | debug('\t\tendHandle: 0x' + endHandle.toString(16)); 302 | debug('\t\tgroupUuid: 0x' + groupUuid.toString(16)); 303 | buf.writeUInt8(ATT_OP_READ_BY_GROUP_REQ, 0); 304 | buf.writeUInt16LE(startHandle, 1); 305 | buf.writeUInt16LE(endHandle, 3); 306 | buf.writeUInt16LE(groupUuid, 5); 307 | 308 | return buf; 309 | }; 310 | 311 | 312 | /* 313 | Gatt.prototype.readByTypeResponse = function(startHandle, endHandle, groupUuid) { 314 | var buf = new Buffer(7); 315 | 316 | buf.writeUInt8(ATT_OP_READ_BY_TYPE_REQ, 0); 317 | buf.writeUInt16LE(startHandle, 1); 318 | buf.writeUInt16LE(endHandle, 3); 319 | buf.writeUInt16LE(groupUuid, 5); 320 | 321 | return buf; 322 | }*/ 323 | 324 | Gatt.prototype.readByTypeRequest = function(startHandle, endHandle, groupUuid) { 325 | var buf = new Buffer(7); 326 | 327 | debug('\tSending ATT_OP_READ_BY_TYPE_REQ'); 328 | debug('\t\tstartHandle: 0x' + startHandle.toString(16)); 329 | debug('\t\tendHandle: 0x' + endHandle.toString(16)); 330 | debug('\t\tgroupUuid: 0x' + groupUuid.toString(16)); 331 | 332 | buf.writeUInt8(ATT_OP_READ_BY_TYPE_REQ, 0); 333 | buf.writeUInt16LE(startHandle, 1); 334 | buf.writeUInt16LE(endHandle, 3); 335 | buf.writeUInt16LE(groupUuid, 5); 336 | 337 | return buf; 338 | }; 339 | 340 | Gatt.prototype.readRequest = function(handle) { 341 | var buf = new Buffer(3); 342 | 343 | debug('\tSending ATT_OP_READ_REQ'); 344 | debug('\t\thandle: 0x' + handle.toString(16)); 345 | buf.writeUInt8(ATT_OP_READ_REQ, 0); 346 | buf.writeUInt16LE(handle, 1); 347 | 348 | return buf; 349 | }; 350 | 351 | Gatt.prototype.readBlobRequest = function(handle, offset) { 352 | var buf = new Buffer(5); 353 | 354 | debug('\tSending ATT_OP_READ_BLOB_REQ'); 355 | debug('\t\thandle: 0x' + handle.toString(16)); 356 | debug('\t\toffset: 0x' + offset.toString(16)); 357 | buf.writeUInt8(ATT_OP_READ_BLOB_REQ, 0); 358 | buf.writeUInt16LE(handle, 1); 359 | buf.writeUInt16LE(offset, 3); 360 | 361 | return buf; 362 | }; 363 | 364 | Gatt.prototype.findInfoRequest = function(startHandle, endHandle) { 365 | var buf = new Buffer(5); 366 | 367 | debug('\tSending ATT_OP_FIND_INFO_REQ'); 368 | debug('\t\tstartHandle: 0x' + startHandle.toString(16)); 369 | debug('\t\tendHandle: 0x' + endHandle.toString(16)); 370 | 371 | buf.writeUInt8(ATT_OP_FIND_INFO_REQ, 0); 372 | buf.writeUInt16LE(startHandle, 1); 373 | buf.writeUInt16LE(endHandle, 3); 374 | 375 | return buf; 376 | }; 377 | 378 | Gatt.prototype.findByTypeRequest = function(startHandle, endHandle, uuid, value) { 379 | var buf = new Buffer(7 + value.length); 380 | 381 | debug('\tSending ATT_OP_READ_BY_TYPE_REQ'); 382 | debug('\t\tstartHandle: 0x' + startHandle.toString(16)); 383 | debug('\t\tendHandle: 0x' + endHandle.toString(16)); 384 | debug('\t\tuuid: 0x' + uuid.toString(16)); 385 | buf.writeUInt8(ATT_OP_FIND_BY_TYPE_REQ , 0); 386 | buf.writeUInt16LE(startHandle, 1); 387 | buf.writeUInt16LE(endHandle, 3); 388 | buf.writeUInt16LE(uuid, 5); 389 | 390 | for (var i = 0; i < value.length; i++) { 391 | buf.writeUInt8(value.readUInt8(i), (value.length - i - 1) + 7); 392 | } 393 | 394 | var callback = function(data) { 395 | var opcode = data[0]; 396 | var i = 0; 397 | 398 | debug("\tATT_OP_FIND_BY_TYPE_RESP Callback"); 399 | 400 | if (opcode === ATT_OP_FIND_BY_TYPE_RESP) { 401 | 402 | var serviceUuids = []; 403 | var services = []; 404 | var start = data.readUInt16LE(1); 405 | var end = data.readUInt16LE(3); 406 | services.push({ 407 | startHandle: start, 408 | endHandle: end, 409 | uuid: uuid 410 | }); 411 | 412 | serviceUuids.push(uuid); 413 | 414 | this._services[services[0].uuid] = services[0]; 415 | 416 | debug('\tATT_OP_FIND_BY_TYPE_RESP Found Service UUID: 0x' + uuid.toString(16) + " start- 0x" + start.toString(16) + " end - 0x" + end.toString(16)); 417 | this.emit('servicesDiscover', this._address, serviceUuids); 418 | 419 | //this.emit('descriptorsDiscover', this._address, start, end); 420 | 421 | } 422 | 423 | }.bind(this); 424 | 425 | 426 | this._queueCommand(buf, callback ); 427 | 428 | return buf; 429 | }; 430 | 431 | 432 | Gatt.prototype.writeRequest = function(handle, data, withoutResponse) { 433 | var buf = new Buffer(3 + data.length); 434 | 435 | if (withoutResponse) { 436 | debug('\tSending ATT_OP_WRITE_CMD'); 437 | } else 438 | { 439 | debug('\tSending ATT_OP_WRITE_REQ'); 440 | } 441 | debug('\t\thandle: 0x' + handle.toString(16)); 442 | 443 | 444 | buf.writeUInt8(withoutResponse ? ATT_OP_WRITE_CMD : ATT_OP_WRITE_REQ , 0); 445 | buf.writeUInt16LE(handle, 1); 446 | 447 | for (var i = 0; i < data.length; i++) { 448 | buf.writeUInt8(data.readUInt8(i), i + 3); 449 | } 450 | 451 | return buf; 452 | }; 453 | 454 | 455 | Gatt.prototype.handleOpError = function(data) { 456 | var opcode = data[0]; 457 | if (opcode === ATT_OP_ERROR) { 458 | var errOpCode = data[1]; 459 | 460 | switch(errOpCode) { 461 | case ATT_OP_ERROR: 462 | debug('\tOpCode in Error: ATT_OP_ERROR: '); 463 | break; 464 | case ATT_OP_READ_BY_TYPE_RESP: 465 | debug('\tOpCode in Error: ATT_OP_READ_BY_TYPE_RESP: '); 466 | break; 467 | case ATT_OP_READ_BY_GROUP_RESP: 468 | debug('\tOpCode in Error: ATT_OP_READ_BY_GROUP_RESP: '); 469 | break; 470 | case ATT_OP_MTU_REQ: 471 | debug('\tOpCode in Error: ATT_OP_MTU_REQ: '); 472 | break; 473 | case ATT_OP_MTU_RESP: 474 | debug("\tOpCode in Error: ATT_OP_MTU_RESP"); 475 | break; 476 | case ATT_OP_FIND_INFO_REQ: 477 | debug('\tOpCode in Error: ATT_OP_FIND_INFO_REQ: '); 478 | break; 479 | case ATT_OP_FIND_BY_TYPE_REQ: 480 | debug('\tOpCode in Error: ATT_OP_FIND_BY_TYPE_REQ: '); 481 | break; 482 | case ATT_OP_READ_BY_TYPE_REQ: 483 | debug('\tOpCode in Error: ATT_OP_READ_BY_TYPE_REQ:'); 484 | break; 485 | case ATT_OP_READ_REQ: 486 | case ATT_OP_READ_BLOB_REQ: 487 | debug('\tOpCode in Error: ATT_OP_READ_REQ: '); 488 | break; 489 | case ATT_OP_READ_BY_GROUP_REQ: 490 | debug('\tOpCode in Error: ATT_OP_READ_BY_GROUP_REQ: '); 491 | break; 492 | case ATT_OP_WRITE_REQ: 493 | case ATT_OP_WRITE_CMD: 494 | debug('\tOpCode in Error: ATT_OP_WRITE_REQ: '); 495 | break; 496 | case ATT_OP_HANDLE_CNF: 497 | debug('\tOpCode in Error: ATT_OP_HANDLE_CNF: '); 498 | break; 499 | case ATT_OP_HANDLE_NOTIFY: 500 | case ATT_OP_HANDLE_IND: 501 | debug('\tOpCode in Error: ATT_OP_HANDLE_NOTIFY: '); 502 | break; 503 | default: 504 | debug("\tOpCode in Error: " + errOpCode) 505 | } 506 | debug('\tHandle: 0x' + data.readUInt16LE(2).toString(16)); 507 | var errorCode = data[4]; 508 | debug('\tError Code: 0x'+errorCode.toString(16)); 509 | switch(errorCode) { 510 | case ATT_ECODE_SUCCESS: 511 | debug('\tError Code: ATT_ECODE_SUCCESS: '); 512 | break; 513 | case ATT_ECODE_INVALID_HANDLE: 514 | debug('\tError Code: ATT_ECODE_INVALID_HANDLE: '); 515 | break; 516 | case ATT_ECODE_READ_NOT_PERM: 517 | debug('\tError Code: ATT_ECODE_READ_NOT_PERM: '); 518 | break; 519 | case ATT_ECODE_WRITE_NOT_PERM: 520 | debug('\tError Code: ATT_ECODE_WRITE_NOT_PERM: '); 521 | break; 522 | case ATT_ECODE_INVALID_PDU: 523 | debug('\tError Code: ATT_ECODE_INVALID_PDU: '); 524 | break; 525 | case ATT_ECODE_AUTHENTICATION: 526 | debug('\tError Code: ATT_ECODE_AUTHENTICATION: '); 527 | break; 528 | case ATT_ECODE_REQ_NOT_SUPP: 529 | debug('\tError Code: ATT_ECODE_REQ_NOT_SUPP: '); 530 | break; 531 | case ATT_ECODE_INVALID_OFFSET: 532 | debug('\tError Code: ATT_ECODE_INVALID_OFFSET: '); 533 | break; 534 | case ATT_ECODE_AUTHORIZATION: 535 | debug('\tError Code: ATT_ECODE_AUTHORIZATION: '); 536 | break; 537 | case ATT_ECODE_PREP_QUEUE_FULL: 538 | debug('\tError Code: ATT_ECODE_PREP_QUEUE_FULL: '); 539 | break; 540 | case ATT_ECODE_ATTR_NOT_FOUND: 541 | debug('\tError Code: ATT_ECODE_ATTR_NOT_FOUND: '); 542 | break; 543 | case ATT_ECODE_ATTR_NOT_LONG: 544 | debug('\tError Code: ATT_ECODE_ATTR_NOT_LONG: '); 545 | break; 546 | case ATT_ECODE_INSUFF_ENCR_KEY_SIZE: 547 | debug('\tError Code: ATT_ECODE_INSUFF_ENCR_KEY_SIZE: '); 548 | break; 549 | case ATT_ECODE_INVAL_ATTR_VALUE_LEN: 550 | debug('\tError Code: ATT_ECODE_INVAL_ATTR_VALUE_LEN: '); 551 | break; 552 | case ATT_ECODE_UNLIKELY: 553 | debug('\tError Code: ATT_ECODE_UNLIKELY: '); 554 | break; 555 | case ATT_ECODE_INSUFF_ENC: 556 | debug('\tError Code: ATT_ECODE_INSUFF_ENC: '); 557 | break; 558 | case ATT_ECODE_UNSUPP_GRP_TYPE: 559 | debug('\tError Code: ATT_ECODE_UNSUPP_GRP_TYPE: '); 560 | break; 561 | case ATT_ECODE_INSUFF_RESOURCES: 562 | debug('\tError Code: ATT_ECODE_INSUFF_RESOURCES: '); 563 | break; 564 | default: 565 | debug('\tError Code: ' + errorCode); 566 | break; 567 | } 568 | } 569 | }; 570 | 571 | Gatt.prototype.handleConfirmation = function(request) { 572 | if (this._lastIndicatedAttribute) { 573 | if (this._lastIndicatedAttribute.emit) { 574 | this._lastIndicatedAttribute.emit('indicate'); 575 | } 576 | 577 | this._lastIndicatedAttribute = null; 578 | } 579 | }; 580 | 581 | 582 | Gatt.prototype.exchangeMtu = function(mtu) { 583 | this._queueCommand(this.mtuRequest(mtu), function(data) { 584 | var opcode = data[0]; 585 | 586 | if (opcode === ATT_OP_MTU_RESP) { 587 | var newMtu = data.readUInt16LE(1); 588 | debug("Echange Mtu: " + newMtu) 589 | debug(this._address + ': new MTU is ' + newMtu); 590 | 591 | this._mtu = newMtu; 592 | 593 | this.emit('mtu', this._address, newMtu); 594 | } else { 595 | this.emit('mtu', this._address, 23); 596 | } 597 | }.bind(this)); 598 | }; 599 | 600 | Gatt.prototype.discoverServices = function(uuids) { 601 | var services = []; 602 | 603 | debug("Discover Services: " + uuids); 604 | var callback = function(data) { 605 | var opcode = data[0]; 606 | var i = 0; 607 | 608 | if (opcode === ATT_OP_READ_BY_GROUP_RESP) { 609 | var type = data[1]; 610 | var num = (data.length - 2) / type; 611 | 612 | debug("ATT_OP_READ_BY_GROUP_RESP: "); 613 | for (i = 0; i < num; i++) { 614 | debug("\tService - Start: " + data.readUInt16LE(2 + i * type + 0) + " End: " + data.readUInt16LE(2 + i * type + 2) + " UUID: " + (type == 6) ? data.readUInt16LE(2 + i * type + 4).toString(16) : data.slice(2 + i * type + 4).slice(0, 16).toString('hex').match(/.{1,2}/g).reverse().join('')); 615 | services.push({ 616 | startHandle: data.readUInt16LE(2 + i * type + 0), 617 | endHandle: data.readUInt16LE(2 + i * type + 2), 618 | uuid: (type == 6) ? data.readUInt16LE(2 + i * type + 4).toString(16) : data.slice(2 + i * type + 4).slice(0, 16).toString('hex').match(/.{1,2}/g).reverse().join('') 619 | }); 620 | } 621 | } 622 | 623 | if (opcode !== ATT_OP_READ_BY_GROUP_RESP || services[services.length - 1].endHandle === 0xffff) { 624 | 625 | var serviceUuids = []; 626 | for (i = 0; i < services.length; i++) { 627 | if (uuids.length === 0 || uuids.indexOf(services[i].uuid) !== -1) { 628 | serviceUuids.push(services[i].uuid); 629 | } 630 | 631 | this._services[services[i].uuid] = services[i]; 632 | } 633 | this.emit('servicesDiscover', this._address, serviceUuids); 634 | } else { 635 | this._queueCommand(this.readByGroupRequest(services[services.length - 1].endHandle + 1, 0xffff, GATT_PRIM_SVC_UUID), callback); 636 | } 637 | }.bind(this); 638 | 639 | this._queueCommand(this.readByGroupRequest(0x0001, 0xffff, GATT_PRIM_SVC_UUID), callback); 640 | }; 641 | 642 | Gatt.prototype.discoverIncludedServices = function(serviceUuid, uuids) { 643 | var service = this._services[serviceUuid]; 644 | var includedServices = []; 645 | 646 | var callback = function(data) { 647 | var opcode = data[0]; 648 | var i = 0; 649 | 650 | if (opcode === ATT_OP_READ_BY_TYPE_RESP) { 651 | var type = data[1]; 652 | var num = (data.length - 2) / type; 653 | 654 | for (i = 0; i < num; i++) { 655 | includedServices.push({ 656 | endHandle: data.readUInt16LE(2 + i * type + 0), 657 | startHandle: data.readUInt16LE(2 + i * type + 2), 658 | uuid: (type == 8) ? data.readUInt16LE(2 + i * type + 6).toString(16) : data.slice(2 + i * type + 6).slice(0, 16).toString('hex').match(/.{1,2}/g).reverse().join('') 659 | }); 660 | } 661 | } 662 | 663 | if (opcode !== ATT_OP_READ_BY_TYPE_RESP || includedServices[includedServices.length - 1].endHandle === service.endHandle) { 664 | var includedServiceUuids = []; 665 | 666 | for (i = 0; i < includedServices.length; i++) { 667 | if (uuids.length === 0 || uuids.indexOf(includedServices[i].uuid) !== -1) { 668 | includedServiceUuids.push(includedServices[i].uuid); 669 | } 670 | } 671 | 672 | this.emit('includedServicesDiscover', this._address, service.uuid, includedServiceUuids); 673 | } else { 674 | this._queueCommand(this.readByTypeRequest(includedServices[includedServices.length - 1].endHandle + 1, service.endHandle, GATT_INCLUDE_UUID), callback); 675 | } 676 | }.bind(this); 677 | 678 | this._queueCommand(this.readByTypeRequest(service.startHandle, service.endHandle, GATT_INCLUDE_UUID), callback); 679 | }; 680 | 681 | Gatt.prototype.discoverCharacteristics = function(serviceUuid, characteristicUuids) { 682 | var service = this._services[serviceUuid]; 683 | var characteristics = []; 684 | 685 | this._characteristics[serviceUuid] = {}; 686 | this._descriptors[serviceUuid] = {}; 687 | debug('Discovering Characteristics for Service: ' + serviceUuid.toString(16)); 688 | 689 | var callback = function(data) { 690 | var opcode = data[0]; 691 | var i = 0; 692 | 693 | debug("Recieved response for Read By Type Resp"); 694 | if (opcode === ATT_OP_READ_BY_TYPE_RESP) { 695 | debug('\tATT_OP_READ_BY_TYPE_RESP: 0x' + opcode.toString(16)); 696 | var type = data[1]; 697 | var num = (data.length - 2) / type; 698 | 699 | for (i = 0; i < num; i++) { 700 | characteristics.push({ 701 | startHandle: data.readUInt16LE(2 + i * type + 0), 702 | properties: data.readUInt8(2 + i * type + 2), 703 | valueHandle: data.readUInt16LE(2 + i * type + 3), 704 | uuid: (type == 7) ? data.readUInt16LE(2 + i * type + 5).toString(16) : data.slice(2 + i * type + 5).slice(0, 16).toString('hex').match(/.{1,2}/g).reverse().join('') 705 | }); 706 | debug('\tuuid: 0x' + characteristics[characteristics.length - 1].uuid); 707 | debug('\t\tstartHandle: 0x' + data.readUInt16LE(2 + i * type + 0).toString(16)); 708 | debug('\t\tproperties: 0x' + data.readUInt8(2 + i * type + 2)); 709 | debug('\t\tvalueHandle: 0x' + data.readUInt16LE(2 + i * type + 3)); 710 | 711 | } 712 | } 713 | 714 | if (opcode !== ATT_OP_READ_BY_TYPE_RESP || characteristics[characteristics.length - 1].valueHandle === service.endHandle) { 715 | debug('\tNot ATT_OP_READ_BY_TYPE_RESP: 0x' + opcode.toString(16)); 716 | var characteristicsDiscovered = []; 717 | for (i = 0; i < characteristics.length; i++) { 718 | var properties = characteristics[i].properties; 719 | 720 | var characteristic = { 721 | properties: [], 722 | uuid: characteristics[i].uuid 723 | }; 724 | 725 | if (i !== 0) { 726 | characteristics[i - 1].endHandle = characteristics[i].startHandle - 1; 727 | } 728 | 729 | if (i === (characteristics.length - 1)) { 730 | characteristics[i].endHandle = service.endHandle; 731 | } 732 | 733 | this._characteristics[serviceUuid][characteristics[i].uuid] = characteristics[i]; 734 | 735 | if (properties & 0x01) { 736 | characteristic.properties.push('broadcast'); 737 | } 738 | 739 | if (properties & 0x02) { 740 | characteristic.properties.push('read'); 741 | } 742 | 743 | if (properties & 0x04) { 744 | characteristic.properties.push('writeWithoutResponse'); 745 | } 746 | 747 | if (properties & 0x08) { 748 | characteristic.properties.push('write'); 749 | } 750 | 751 | if (properties & 0x10) { 752 | characteristic.properties.push('notify'); 753 | } 754 | 755 | if (properties & 0x20) { 756 | characteristic.properties.push('indicate'); 757 | } 758 | 759 | if (properties & 0x40) { 760 | characteristic.properties.push('authenticatedSignedWrites'); 761 | } 762 | 763 | if (properties & 0x80) { 764 | characteristic.properties.push('extendedProperties'); 765 | } 766 | 767 | if (characteristicUuids.length === 0 || characteristicUuids.indexOf(characteristic.uuid) !== -1) { 768 | characteristicsDiscovered.push(characteristic); 769 | } 770 | } 771 | 772 | this.emit('characteristicsDiscover', this._address, serviceUuid, characteristicsDiscovered); 773 | } else { 774 | this._queueCommand(this.readByTypeRequest(characteristics[characteristics.length - 1].valueHandle + 1, service.endHandle, GATT_CHARAC_UUID), callback); 775 | } 776 | }.bind(this); 777 | 778 | this._queueCommand(this.readByTypeRequest(service.startHandle, service.endHandle, GATT_CHARAC_UUID), callback); 779 | }; 780 | 781 | Gatt.prototype.read = function(serviceUuid, characteristicUuid) { 782 | var characteristic = this._characteristics[serviceUuid][characteristicUuid]; 783 | 784 | var readData = new Buffer(0); 785 | 786 | var callback = function(data) { 787 | var opcode = data[0]; 788 | 789 | if (opcode === ATT_OP_READ_RESP || opcode === ATT_OP_READ_BLOB_RESP) { 790 | readData = new Buffer(readData.toString('hex') + data.slice(1).toString('hex'), 'hex'); 791 | 792 | if (data.length === this._mtu) { 793 | this._queueCommand(this.readBlobRequest(characteristic.valueHandle, readData.length), callback); 794 | } else { 795 | this.emit('read', this._address, serviceUuid, characteristicUuid, readData); 796 | } 797 | } else { 798 | this.emit('read', this._address, serviceUuid, characteristicUuid, readData); 799 | } 800 | }.bind(this); 801 | 802 | this._queueCommand(this.readRequest(characteristic.valueHandle), callback); 803 | }; 804 | 805 | Gatt.prototype.write = function(serviceUuid, characteristicUuid, data, withoutResponse) { 806 | var characteristic = this._characteristics[serviceUuid][characteristicUuid]; 807 | 808 | if (withoutResponse) { 809 | this._queueCommand(this.writeRequest(characteristic.valueHandle, data, true), null, function() { 810 | this.emit('write', this._address, serviceUuid, characteristicUuid); 811 | }.bind(this)); 812 | } else { 813 | this._queueCommand(this.writeRequest(characteristic.valueHandle, data, false), function(data) { 814 | var opcode = data[0]; 815 | 816 | if (opcode === ATT_OP_WRITE_RESP) { 817 | this.emit('write', this._address, serviceUuid, characteristicUuid); 818 | } 819 | }.bind(this)); 820 | } 821 | }; 822 | 823 | Gatt.prototype.broadcast = function(serviceUuid, characteristicUuid, broadcast) { 824 | var characteristic = this._characteristics[serviceUuid][characteristicUuid]; 825 | 826 | this._queueCommand(this.readByTypeRequest(characteristic.startHandle, characteristic.endHandle, GATT_SERVER_CHARAC_CFG_UUID), function(data) { 827 | var opcode = data[0]; 828 | if (opcode === ATT_OP_READ_BY_TYPE_RESP) { 829 | var type = data[1]; 830 | var handle = data.readUInt16LE(2); 831 | var value = data.readUInt16LE(4); 832 | 833 | if (broadcast) { 834 | value |= 0x0001; 835 | } else { 836 | value &= 0xfffe; 837 | } 838 | 839 | var valueBuffer = new Buffer(2); 840 | valueBuffer.writeUInt16LE(value, 0); 841 | 842 | this._queueCommand(this.writeRequest(handle, valueBuffer, false), function(data) { 843 | var opcode = data[0]; 844 | 845 | if (opcode === ATT_OP_WRITE_RESP) { 846 | this.emit('broadcast', this._address, serviceUuid, characteristicUuid, broadcast); 847 | } 848 | }.bind(this)); 849 | } 850 | }.bind(this)); 851 | }; 852 | 853 | Gatt.prototype.notify = function(serviceUuid, characteristicUuid, notify) { 854 | var characteristic = this._characteristics[serviceUuid][characteristicUuid]; 855 | 856 | this._queueCommand(this.readByTypeRequest(characteristic.startHandle, characteristic.endHandle, GATT_CLIENT_CHARAC_CFG_UUID), function(data) { 857 | var opcode = data[0]; 858 | if (opcode === ATT_OP_READ_BY_TYPE_RESP) { 859 | var type = data[1]; 860 | var handle = data.readUInt16LE(2); 861 | var value = data.readUInt16LE(4); 862 | 863 | var useNotify = characteristic.properties & 0x10; 864 | var useIndicate = characteristic.properties & 0x20; 865 | 866 | if (notify) { 867 | if (useNotify) { 868 | value |= 0x0001; 869 | } else if (useIndicate) { 870 | value |= 0x0002; 871 | } 872 | } else { 873 | if (useNotify) { 874 | value &= 0xfffe; 875 | } else if (useIndicate) { 876 | value &= 0xfffd; 877 | } 878 | } 879 | 880 | var valueBuffer = new Buffer(2); 881 | valueBuffer.writeUInt16LE(value, 0); 882 | 883 | this._queueCommand(this.writeRequest(handle, valueBuffer, false), function(data) { 884 | var opcode = data[0]; 885 | 886 | if (opcode === ATT_OP_WRITE_RESP) { 887 | this.emit('notify', this._address, serviceUuid, characteristicUuid, notify); 888 | } 889 | }.bind(this)); 890 | } 891 | }.bind(this)); 892 | }; 893 | 894 | Gatt.prototype.discoverDescriptors = function(serviceUuid, characteristicUuid) { 895 | var characteristic = this._characteristics[serviceUuid][characteristicUuid]; 896 | var descriptors = []; 897 | 898 | this._descriptors[serviceUuid][characteristicUuid] = {}; 899 | 900 | var callback = function(data) { 901 | var opcode = data[0]; 902 | var i = 0; 903 | 904 | if (opcode === ATT_OP_FIND_INFO_RESP) { 905 | var num = data[1]; 906 | 907 | for (i = 0; i < num; i++) { 908 | descriptors.push({ 909 | handle: data.readUInt16LE(2 + i * 4 + 0), 910 | uuid: data.readUInt16LE(2 + i * 4 + 2).toString(16) 911 | }); 912 | } 913 | } 914 | 915 | if (opcode !== ATT_OP_FIND_INFO_RESP || descriptors[descriptors.length - 1].handle === characteristic.endHandle) { 916 | var descriptorUuids = []; 917 | for (i = 0; i < descriptors.length; i++) { 918 | descriptorUuids.push(descriptors[i].uuid); 919 | 920 | this._descriptors[serviceUuid][characteristicUuid][descriptors[i].uuid] = descriptors[i]; 921 | } 922 | 923 | this.emit('descriptorsDiscover', this._address, serviceUuid, characteristicUuid, descriptorUuids); 924 | } else { 925 | this._queueCommand(this.findInfoRequest(descriptors[descriptors.length - 1].handle + 1, characteristic.endHandle), callback); 926 | } 927 | }.bind(this); 928 | 929 | this._queueCommand(this.findInfoRequest(characteristic.valueHandle + 1, characteristic.endHandle), callback); 930 | }; 931 | 932 | Gatt.prototype.readValue = function(serviceUuid, characteristicUuid, descriptorUuid) { 933 | var descriptor = this._descriptors[serviceUuid][characteristicUuid][descriptorUuid]; 934 | 935 | this._queueCommand(this.readRequest(descriptor.handle), function(data) { 936 | var opcode = data[0]; 937 | 938 | if (opcode === ATT_OP_READ_RESP) { 939 | this.emit('valueRead', this._address, serviceUuid, characteristicUuid, descriptorUuid, data.slice(1)); 940 | } 941 | }.bind(this)); 942 | }; 943 | 944 | Gatt.prototype.writeValue = function(serviceUuid, characteristicUuid, descriptorUuid, data) { 945 | var descriptor = this._descriptors[serviceUuid][characteristicUuid][descriptorUuid]; 946 | 947 | this._queueCommand(this.writeRequest(descriptor.handle, data, false), function(data) { 948 | var opcode = data[0]; 949 | 950 | if (opcode === ATT_OP_WRITE_RESP) { 951 | this.emit('valueWrite', this._address, serviceUuid, characteristicUuid, descriptorUuid); 952 | } 953 | }.bind(this)); 954 | }; 955 | 956 | Gatt.prototype.readHandle = function(handle) { 957 | this._queueCommand(this.readRequest(handle), function(data) { 958 | var opcode = data[0]; 959 | 960 | if (opcode === ATT_OP_READ_RESP) { 961 | this.emit('handleRead', this._address, handle, data.slice(1)); 962 | } 963 | }.bind(this)); 964 | }; 965 | 966 | Gatt.prototype.writeHandle = function(handle, data, withoutResponse) { 967 | if (withoutResponse) { 968 | this._queueCommand(this.writeRequest(handle, data, true), null, function() { 969 | this.emit('handleWrite', this._address, handle); 970 | }.bind(this)); 971 | } else { 972 | this._queueCommand(this.writeRequest(handle, data, false), function(data) { 973 | var opcode = data[0]; 974 | 975 | if (opcode === ATT_OP_WRITE_RESP) { 976 | this.emit('handleWrite', this._address, handle); 977 | } 978 | }.bind(this)); 979 | } 980 | }; 981 | 982 | module.exports = Gatt; 983 | -------------------------------------------------------------------------------- /lib/hci-socket/hci-status.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Success", 3 | "Unknown HCI Command", 4 | "Unknown Connection Identifier", 5 | "Hardware Failure", 6 | "Page Timeout", 7 | "Authentication Failure", 8 | "PIN or Key Missing", 9 | "Memory Capacity Exceeded", 10 | "Connection Timeout", 11 | "Connection Limit Exceeded", 12 | "Synchronous Connection Limit to a Device Exceeded", 13 | "ACL Connection Already Exists", 14 | "Command Disallowed", 15 | "Connection Rejected due to Limited Resources", 16 | "Connection Rejected due to Security Reasons", 17 | "Connection Rejected due to Unacceptable BD_ADDR", 18 | "Connection Accept Timeout Exceeded", 19 | "Unsupported Feature or Parameter Value", 20 | "Invalid HCI Command Parameters", 21 | "Remote User Terminated Connection", 22 | "Remote Device Terminated due to Low Resources", 23 | "Remote Device Terminated due to Power Off", 24 | "Connection Terminated By Local Host", 25 | "Repeated Attempts", 26 | "Pairing Not Allowed", 27 | "Unknown LMP PDU", 28 | "Unsupported Remote Feature / Unsupported LMP Feature", 29 | "SCO Offset Rejected", 30 | "SCO Interval Rejected", 31 | "SCO Air Mode Rejected", 32 | "Invalid LMP Parameters / Invalid LL Parameters", 33 | "Unspecified Error", 34 | "Unsupported LMP Parameter Value / Unsupported LL Parameter Value", 35 | "Role Change Not Allowed", 36 | "LMP Response Timeout / LL Response Timeout", 37 | "LMP Error Transaction Collision", 38 | "LMP PDU Not Allowed", 39 | "Encryption Mode Not Acceptable", 40 | "Link Key cannot be Changed", 41 | "Requested QoS Not Supported", 42 | "Instant Passed", 43 | "Pairing With Unit Key Not Supported", 44 | "Different Transaction Collision", 45 | "Reserved", 46 | "QoS Unacceptable Parameter", 47 | "QoS Rejected", 48 | "Channel Classification Not Supported", 49 | "Insufficient Security", 50 | "Parameter Out Of Manadatory Range", 51 | "Reserved", 52 | "Role Switch Pending", 53 | "Reserved", 54 | "Reserved Slot Violation", 55 | "Role Switch Failed", 56 | "Extended Inquiry Response Too Large", 57 | "Secure Simple Pairing Not Supported By Host", 58 | "Host Busy - Pairing", 59 | "Connection Rejected due to No Suitable Channel Found", 60 | "Controller Busy", 61 | "Unacceptable Connection Parameters" , 62 | "Directed Advertising Timeout", 63 | "Connection Terminated due to MIC Failure", 64 | "Connection Failed to be Established", 65 | "MAC Connection Failed", 66 | "Coarse Clock Adjustment Rejected but Will Try to Adjust Using Clock Dragging" 67 | ] -------------------------------------------------------------------------------- /lib/hci-socket/hci.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('hci'); 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | 6 | var BluetoothHciSocket = require('bluetooth-hci-socket'); 7 | 8 | 9 | var ATT_OP_ERROR = 0x01; 10 | var ATT_OP_MTU_REQ = 0x02; 11 | var ATT_OP_MTU_RESP = 0x03; 12 | var ATT_OP_FIND_INFO_REQ = 0x04; 13 | var ATT_OP_FIND_INFO_RESP = 0x05; 14 | var ATT_OP_FIND_BY_TYPE_REQ = 0x06; 15 | var ATT_OP_FIND_BY_TYPE_RESP = 0x07; 16 | var ATT_OP_READ_BY_TYPE_REQ = 0x08; 17 | var ATT_OP_READ_BY_TYPE_RESP = 0x09; 18 | var ATT_OP_READ_REQ = 0x0a; 19 | var ATT_OP_READ_RESP = 0x0b; 20 | var ATT_OP_READ_BLOB_REQ = 0x0c; 21 | var ATT_OP_READ_BLOB_RESP = 0x0d; 22 | var ATT_OP_READ_MULTI_REQ = 0x0e; 23 | var ATT_OP_READ_MULTI_RESP = 0x0f; 24 | var ATT_OP_READ_BY_GROUP_REQ = 0x10; 25 | var ATT_OP_READ_BY_GROUP_RESP = 0x11; 26 | var ATT_OP_WRITE_REQ = 0x12; 27 | var ATT_OP_WRITE_RESP = 0x13; 28 | var ATT_OP_WRITE_CMD = 0x52; 29 | var ATT_OP_PREP_WRITE_REQ = 0x16; 30 | var ATT_OP_PREP_WRITE_RESP = 0x17; 31 | var ATT_OP_EXEC_WRITE_REQ = 0x18; 32 | var ATT_OP_EXEC_WRITE_RESP = 0x19; 33 | var ATT_OP_HANDLE_NOTIFY = 0x1b; 34 | var ATT_OP_HANDLE_IND = 0x1d; 35 | var ATT_OP_HANDLE_CNF = 0x1e; 36 | var ATT_OP_WRITE_CMD = 0x52; 37 | var ATT_OP_SIGNED_WRITE_CMD = 0xd2; 38 | 39 | var HCI_COMMAND_PKT = 0x01; 40 | var HCI_ACLDATA_PKT = 0x02; 41 | var HCI_EVENT_PKT = 0x04; 42 | 43 | var ACL_START_NO_FLUSH = 0x00; 44 | var ACL_CONT = 0x01; 45 | var ACL_START = 0x02; 46 | 47 | var EVT_DISCONN_COMPLETE = 0x05; 48 | var EVT_ENCRYPT_CHANGE = 0x08; 49 | var EVT_CMD_COMPLETE = 0x0e; 50 | var EVT_CMD_STATUS = 0x0f; 51 | var EVT_LE_META_EVENT = 0x3e; 52 | 53 | var EVT_LE_CONN_COMPLETE = 0x01; 54 | var EVT_LE_ADVERTISING_REPORT = 0x02; 55 | var EVT_LE_CONN_UPDATE_COMPLETE = 0x03; 56 | 57 | var OGF_LINK_CTL = 0x01; 58 | var OCF_DISCONNECT = 0x0006; 59 | 60 | var OGF_HOST_CTL = 0x03; 61 | var OCF_SET_EVENT_MASK = 0x0001; 62 | 63 | var OGF_INFO_PARAM = 0x04; 64 | var OCF_READ_LOCAL_VERSION = 0x0001; 65 | var OCF_READ_BD_ADDR = 0x0009; 66 | 67 | var OGF_STATUS_PARAM = 0x05; 68 | var OCF_READ_RSSI = 0x0005; 69 | 70 | var OGF_LE_CTL = 0x08; 71 | var OCF_LE_SET_EVENT_MASK = 0x0001; 72 | var OCF_LE_SET_ADVERTISING_PARAMETERS = 0x0006; 73 | var OCF_LE_SET_ADVERTISING_DATA = 0x0008; 74 | var OCF_LE_SET_SCAN_RESPONSE_DATA = 0x0009; 75 | var OCF_LE_SET_ADVERTISE_ENABLE = 0x000a; 76 | var OCF_LE_LTK_NEG_REPLY = 0x001B; 77 | var OCF_LE_SET_SCAN_PARAMETERS = 0x000b; 78 | var OCF_LE_SET_SCAN_ENABLE = 0x000c; 79 | var OCF_LE_CREATE_CONN = 0x000d; 80 | var OCF_LE_START_ENCRYPTION = 0x0019; 81 | 82 | var DISCONNECT_CMD = OCF_DISCONNECT | OGF_LINK_CTL << 10; 83 | 84 | var SET_EVENT_MASK_CMD = OCF_SET_EVENT_MASK | OGF_HOST_CTL << 10; 85 | 86 | var READ_LOCAL_VERSION_CMD = OCF_READ_LOCAL_VERSION | (OGF_INFO_PARAM << 10); 87 | var READ_BD_ADDR_CMD = OCF_READ_BD_ADDR | (OGF_INFO_PARAM << 10); 88 | 89 | var READ_RSSI_CMD = OCF_READ_RSSI | OGF_STATUS_PARAM << 10; 90 | 91 | var LE_SET_EVENT_MASK_CMD = OCF_SET_EVENT_MASK | OGF_LE_CTL << 10; 92 | var LE_SET_SCAN_PARAMETERS_CMD = OCF_LE_SET_SCAN_PARAMETERS | OGF_LE_CTL << 10; 93 | var LE_SET_SCAN_ENABLE_CMD = OCF_LE_SET_SCAN_ENABLE | OGF_LE_CTL << 10; 94 | var LE_CREATE_CONN_CMD = OCF_LE_CREATE_CONN | OGF_LE_CTL << 10; 95 | var LE_START_ENCRYPTION_CMD = OCF_LE_START_ENCRYPTION | OGF_LE_CTL << 10; 96 | var LE_SET_ADVERTISING_PARAMETERS_CMD = OCF_LE_SET_ADVERTISING_PARAMETERS | OGF_LE_CTL << 10; 97 | var LE_SET_ADVERTISING_DATA_CMD = OCF_LE_SET_ADVERTISING_DATA | OGF_LE_CTL << 10; 98 | var LE_SET_SCAN_RESPONSE_DATA_CMD = OCF_LE_SET_SCAN_RESPONSE_DATA | OGF_LE_CTL << 10; 99 | var LE_SET_ADVERTISE_ENABLE_CMD = OCF_LE_SET_ADVERTISE_ENABLE | OGF_LE_CTL << 10; 100 | var LE_LTK_NEG_REPLY_CMD = OCF_LE_LTK_NEG_REPLY | OGF_LE_CTL << 10; 101 | 102 | var HCI_OE_USER_ENDED_CONNECTION = 0x13; 103 | 104 | var STATUS_MAPPER = require('./hci-status'); 105 | 106 | var Hci = function () { 107 | this._socket = new BluetoothHciSocket(); 108 | this._isDevUp = null; 109 | this._state = null; 110 | this._handleBuffers = {}; 111 | this.on('stateChange', this.onStateChange.bind(this)); 112 | }; 113 | 114 | util.inherits(Hci, events.EventEmitter); 115 | 116 | Hci.STATUS_MAPPER = STATUS_MAPPER; 117 | 118 | Hci.prototype.init = function () { 119 | this._socket.on('data', this.onSocketData.bind(this)); 120 | this._socket.on('error', this.onSocketError.bind(this)); 121 | 122 | var deviceId = process.env.ABLE_HCI_DEVICE_ID ? parseInt(process.env.ABLE_HCI_DEVICE_ID, 10) : undefined; 123 | 124 | this._socket.bindRaw(deviceId); 125 | //this._socket.bindUser(deviceId); 126 | this._socket.start(); 127 | 128 | this.pollIsDevUp(); 129 | }; 130 | 131 | Hci.prototype.pollIsDevUp = function () { 132 | var isDevUp = this._socket.isDevUp(); 133 | 134 | if (this._isDevUp !== isDevUp) { 135 | if (isDevUp) { 136 | this.setSocketFilter(); 137 | this.setEventMask(); 138 | this.setLeEventMask(); 139 | this.readLocalVersion(); 140 | this.readBdAddr(); 141 | } else { 142 | this.emit('stateChange', 'poweredOff'); 143 | } 144 | 145 | this._isDevUp = isDevUp; 146 | } 147 | 148 | setTimeout(this.pollIsDevUp.bind(this), 1000); 149 | }; 150 | 151 | Hci.prototype.setSocketFilter = function () { 152 | var filter = new Buffer(14); 153 | var typeMask = (1 << HCI_EVENT_PKT) | (1 << HCI_ACLDATA_PKT) | (1 << HCI_COMMAND_PKT); 154 | var eventMask1 = (1 << EVT_DISCONN_COMPLETE) | (1 << EVT_ENCRYPT_CHANGE) | (1 << EVT_CMD_COMPLETE) | (1 << EVT_CMD_STATUS); 155 | var eventMask2 = (1 << (EVT_LE_META_EVENT - 32)); 156 | var opcode = 0; 157 | 158 | filter.writeUInt32LE(typeMask, 0); 159 | filter.writeUInt32LE(eventMask1, 4); 160 | filter.writeUInt32LE(eventMask2, 8); 161 | filter.writeUInt16LE(opcode, 12); 162 | 163 | debug('setting filter to: ' + filter.toString('hex')); 164 | this._socket.setFilter(filter); 165 | }; 166 | 167 | Hci.prototype.setEventMask = function () { 168 | var cmd = new Buffer(12); 169 | var eventMask = new Buffer('fffffbff07f8bf3d', 'hex'); 170 | 171 | // header 172 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 173 | cmd.writeUInt16LE(SET_EVENT_MASK_CMD, 1); 174 | 175 | // length 176 | cmd.writeUInt8(eventMask.length, 3); 177 | 178 | eventMask.copy(cmd, 4); 179 | 180 | debug('set event mask - writing: ' + cmd.toString('hex')); 181 | this._socket.write(cmd); 182 | }; 183 | 184 | Hci.prototype.readLocalVersion = function () { 185 | var cmd = new Buffer(4); 186 | 187 | // header 188 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 189 | cmd.writeUInt16LE(READ_LOCAL_VERSION_CMD, 1); 190 | 191 | // length 192 | cmd.writeUInt8(0x0, 3); 193 | 194 | debug('read local version - writing: ' + cmd.toString('hex')); 195 | this._socket.write(cmd); 196 | }; 197 | 198 | Hci.prototype.readBdAddr = function () { 199 | var cmd = new Buffer(4); 200 | 201 | // header 202 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 203 | cmd.writeUInt16LE(READ_BD_ADDR_CMD, 1); 204 | 205 | // length 206 | cmd.writeUInt8(0x0, 3); 207 | 208 | debug('read bd addr - writing: ' + cmd.toString('hex')); 209 | this._socket.write(cmd); 210 | }; 211 | 212 | Hci.prototype.setLeEventMask = function () { 213 | var cmd = new Buffer(12); 214 | var leEventMask = new Buffer('1f00000000000000', 'hex'); 215 | 216 | // header 217 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 218 | cmd.writeUInt16LE(LE_SET_EVENT_MASK_CMD, 1); 219 | 220 | // length 221 | cmd.writeUInt8(leEventMask.length, 3); 222 | 223 | leEventMask.copy(cmd, 4); 224 | 225 | debug('set le event mask - writing: ' + cmd.toString('hex')); 226 | this._socket.write(cmd); 227 | }; 228 | 229 | Hci.prototype.setAdvertisingParameters = function () { 230 | var cmd = new Buffer(19); 231 | 232 | // header 233 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 234 | cmd.writeUInt16LE(LE_SET_ADVERTISING_PARAMETERS_CMD, 1); 235 | 236 | // length 237 | cmd.writeUInt8(15, 3); 238 | 239 | var advertisementInterval = Math.floor((process.env.BLENO_ADVERTISING_INTERVAL ? parseInt(process.env.BLENO_ADVERTISING_INTERVAL) : 20) * 1.6); 240 | 241 | // data 242 | cmd.writeUInt16LE(advertisementInterval, 4); // min interval 243 | cmd.writeUInt16LE(advertisementInterval, 6); // max interval 244 | cmd.writeUInt8(0x00, 8); // adv type 245 | cmd.writeUInt8(0x00, 9); // own addr typ 246 | cmd.writeUInt8(0x00, 10); // direct addr type 247 | (new Buffer('000000000000', 'hex')).copy(cmd, 11); // direct addr 248 | cmd.writeUInt8(0x07, 17); 249 | cmd.writeUInt8(0x00, 18); 250 | 251 | debug('set advertisement parameters - writing: ' + cmd.toString('hex')); 252 | this._socket.write(cmd); 253 | }; 254 | 255 | Hci.prototype.setAdvertisingData = function (data) { 256 | var cmd = new Buffer(36); 257 | 258 | cmd.fill(0x00); 259 | 260 | // header 261 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 262 | cmd.writeUInt16LE(LE_SET_ADVERTISING_DATA_CMD, 1); 263 | 264 | // length 265 | cmd.writeUInt8(32, 3); 266 | 267 | // data 268 | cmd.writeUInt8(data.length, 4); 269 | data.copy(cmd, 5); 270 | 271 | debug('set advertisement data - writing: ' + cmd.toString('hex')); 272 | this._socket.write(cmd); 273 | }; 274 | 275 | Hci.prototype.setScanResponseData = function (data) { 276 | var cmd = new Buffer(36); 277 | 278 | cmd.fill(0x00); 279 | 280 | // header 281 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 282 | cmd.writeUInt16LE(LE_SET_SCAN_RESPONSE_DATA_CMD, 1); 283 | 284 | // length 285 | cmd.writeUInt8(32, 3); 286 | 287 | // data 288 | cmd.writeUInt8(data.length, 4); 289 | data.copy(cmd, 5); 290 | 291 | debug('set scan response data - writing: ' + cmd.toString('hex')); 292 | this._socket.write(cmd); 293 | }; 294 | 295 | Hci.prototype.setAdvertiseEnable = function (enabled) { 296 | var cmd = new Buffer(5); 297 | 298 | // header 299 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 300 | cmd.writeUInt16LE(LE_SET_ADVERTISE_ENABLE_CMD, 1); 301 | 302 | // length 303 | cmd.writeUInt8(0x01, 3); 304 | 305 | // data 306 | cmd.writeUInt8(enabled ? 0x01 : 0x00, 4); // enable: 0 -> disabled, 1 -> enabled 307 | 308 | debug('set advertise enable - writing: ' + cmd.toString('hex')); 309 | this._socket.write(cmd); 310 | }; 311 | 312 | Hci.prototype.setScanParameters = function () { 313 | var cmd = new Buffer(11); 314 | 315 | // header 316 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 317 | cmd.writeUInt16LE(LE_SET_SCAN_PARAMETERS_CMD, 1); 318 | 319 | // length 320 | cmd.writeUInt8(0x07, 3); 321 | 322 | // data 323 | cmd.writeUInt8(0x01, 4); // type: 0 -> passive, 1 -> active 324 | cmd.writeUInt16LE(0x0010, 5); // internal, ms * 1.6 325 | cmd.writeUInt16LE(0x0010, 7); // window, ms * 1.6 326 | cmd.writeUInt8(0x00, 9); // own address type: 0 -> public, 1 -> random 327 | cmd.writeUInt8(0x00, 10); // filter: 0 -> all event types 328 | 329 | debug('set scan parameters - writing: ' + cmd.toString('hex')); 330 | this._socket.write(cmd); 331 | }; 332 | 333 | Hci.prototype.setScanEnabled = function (enabled, filterDuplicates) { 334 | var cmd = new Buffer(6); 335 | 336 | // header 337 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 338 | cmd.writeUInt16LE(LE_SET_SCAN_ENABLE_CMD, 1); 339 | 340 | // length 341 | cmd.writeUInt8(0x02, 3); 342 | 343 | // data 344 | cmd.writeUInt8(enabled ? 0x01 : 0x00, 4); // enable: 0 -> disabled, 1 -> enabled 345 | cmd.writeUInt8(filterDuplicates ? 0x01 : 0x00, 5); // duplicates: 0 -> duplicates, 0 -> duplicates 346 | 347 | debug('set scan enabled - writing: ' + cmd.toString('hex')); 348 | this._socket.write(cmd); 349 | }; 350 | 351 | Hci.prototype.createLeConn = function (address, addressType) { 352 | var cmd = new Buffer(29); 353 | 354 | // header 355 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 356 | cmd.writeUInt16LE(LE_CREATE_CONN_CMD, 1); 357 | 358 | // length 359 | cmd.writeUInt8(0x19, 3); 360 | 361 | // data 362 | cmd.writeUInt16LE(0x0060, 4); // interval 363 | cmd.writeUInt16LE(0x0030, 6); // window 364 | cmd.writeUInt8(0x00, 8); // initiator filter 365 | 366 | cmd.writeUInt8(addressType === 'random' ? 0x01 : 0x00, 9); // peer address type 367 | (new Buffer(address.split(':').reverse().join(''), 'hex')).copy(cmd, 10); // peer address 368 | 369 | cmd.writeUInt8(0x00, 16); // own address type 370 | 371 | cmd.writeUInt16LE(0x0006, 17); // min interval 372 | cmd.writeUInt16LE(0x000c, 19); // max interval 373 | cmd.writeUInt16LE(0x0000, 21); // latency 374 | cmd.writeUInt16LE(0x00c8, 23); // supervision timeout 375 | cmd.writeUInt16LE(0x0004, 25); // min ce length 376 | cmd.writeUInt16LE(0x0006, 27); // max ce length 377 | 378 | debug('create le conn - writing: ' + cmd.toString('hex')); 379 | this._socket.write(cmd); 380 | }; 381 | 382 | 383 | Hci.prototype.startLeEncryption = function (handle, random, diversifier, key) { 384 | var cmd = new Buffer(32); 385 | 386 | // header 387 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 388 | cmd.writeUInt16LE(LE_START_ENCRYPTION_CMD, 1); 389 | 390 | // length 391 | cmd.writeUInt8(0x1c, 3); 392 | 393 | // data 394 | cmd.writeUInt16LE(handle, 4); // handle 395 | random.copy(cmd, 6); 396 | diversifier.copy(cmd, 14); 397 | key.copy(cmd, 16); 398 | 399 | debug('start le encryption - writing: ' + cmd.toString('hex')); 400 | this._socket.write(cmd); 401 | }; 402 | 403 | Hci.prototype.disconnect = function (handle, reason) { 404 | var cmd = new Buffer(7); 405 | 406 | reason = reason || HCI_OE_USER_ENDED_CONNECTION; 407 | 408 | // header 409 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 410 | cmd.writeUInt16LE(DISCONNECT_CMD, 1); 411 | 412 | // length 413 | cmd.writeUInt8(0x03, 3); 414 | 415 | // data 416 | cmd.writeUInt16LE(handle, 4); // handle 417 | cmd.writeUInt8(reason, 6); // reason 418 | 419 | debug('disconnect - writing: ' + cmd.toString('hex')); 420 | this._socket.write(cmd); 421 | }; 422 | 423 | Hci.prototype.readRssi = function (handle) { 424 | var cmd = new Buffer(6); 425 | 426 | // header 427 | cmd.writeUInt8(HCI_COMMAND_PKT, 0); 428 | cmd.writeUInt16LE(READ_RSSI_CMD, 1); 429 | 430 | // length 431 | cmd.writeUInt8(0x02, 3); 432 | 433 | // data 434 | cmd.writeUInt16LE(handle, 4); // handle 435 | 436 | debug('read rssi - writing: ' + cmd.toString('hex')); 437 | this._socket.write(cmd); 438 | }; 439 | 440 | Hci.prototype.writeAclDataPkt = function (handle, cid, data) { 441 | var pkt = new Buffer(9 + data.length); 442 | 443 | // header 444 | pkt.writeUInt8(HCI_ACLDATA_PKT, 0); 445 | pkt.writeUInt16LE(handle | ACL_START_NO_FLUSH << 12, 1); 446 | pkt.writeUInt16LE(data.length + 4, 3); // data length 1 447 | pkt.writeUInt16LE(data.length, 5); // data length 2 448 | pkt.writeUInt16LE(cid, 7); 449 | 450 | data.copy(pkt, 9); 451 | 452 | debug('write acl data pkt - writing: ' + pkt.toString('hex')); 453 | this._socket.write(pkt); 454 | }; 455 | 456 | 457 | Hci.prototype.debugPacketType = function (type) { 458 | switch (type) { 459 | case ATT_OP_ERROR: 460 | debug('\t\ttype = ATT_OP_ERROR: '); 461 | break; 462 | 463 | case ATT_OP_MTU_REQ: 464 | debug('\t\ttype = ATT_OP_MTU_REQ: '); 465 | break; 466 | 467 | case ATT_OP_FIND_INFO_REQ: 468 | debug('\t\ttype = ATT_OP_FIND_INFO_REQ: '); 469 | break; 470 | 471 | case ATT_OP_FIND_BY_TYPE_REQ: 472 | debug('\t\ttype = ATT_OP_FIND_BY_TYPE_REQ: '); 473 | break; 474 | 475 | case ATT_OP_READ_BY_TYPE_REQ: 476 | debug('\t\ttype = ATT_OP_READ_BY_TYPE_REQ:'); 477 | break; 478 | 479 | case ATT_OP_READ_REQ: 480 | case ATT_OP_READ_BLOB_REQ: 481 | debug('\t\ttype = ATT_OP_READ_REQ: '); 482 | break; 483 | 484 | case ATT_OP_READ_BY_GROUP_REQ: 485 | debug('\t\ttype = ATT_OP_READ_BY_GROUP_REQ: '); 486 | break; 487 | 488 | case ATT_OP_WRITE_REQ: 489 | case ATT_OP_WRITE_CMD: 490 | debug('\t\ttype = ATT_OP_WRITE_REQ: '); 491 | break; 492 | 493 | case ATT_OP_HANDLE_CNF: 494 | debug('\t\ttype = ATT_OP_HANDLE_CNF: '); 495 | break; 496 | case ATT_OP_ERROR: 497 | debug('\t\ttype = ATT_OP_ERROR: '); 498 | 499 | break; 500 | case ATT_OP_READ_BY_TYPE_RESP: 501 | debug('\t\ttype = ATT_OP_READ_BY_TYPE_RESP: '); 502 | break; 503 | case ATT_OP_READ_BY_GROUP_RESP: 504 | debug('\t\ttype = ATT_OP_READ_BY_GROUP_RESP: '); 505 | break; 506 | case ATT_OP_HANDLE_NOTIFY: 507 | case ATT_OP_HANDLE_IND: 508 | debug('\t\ttype = ATT_OP_HANDLE_NOTIFY: '); 509 | 510 | default: 511 | case ATT_OP_READ_MULTI_REQ: 512 | case ATT_OP_PREP_WRITE_REQ: 513 | case ATT_OP_EXEC_WRITE_REQ: 514 | case ATT_OP_SIGNED_WRITE_CMD: 515 | debug('\t\ttype = Unhandled: ' + type); 516 | break; 517 | } 518 | }; 519 | Hci.prototype.onSocketData = function (data) { 520 | 521 | 522 | var eventType = data.readUInt8(0); 523 | var handle; 524 | var cmd; 525 | var status; 526 | 527 | debug('onSocketData: ' + data.toString('hex') + '\tevent type = ' + eventType); 528 | 529 | if (HCI_EVENT_PKT === eventType) { 530 | var subEventType = data.readUInt8(1); 531 | 532 | debug('\tsub event type = ' + subEventType); 533 | 534 | if (subEventType === EVT_DISCONN_COMPLETE) { 535 | handle = data.readUInt16LE(4); 536 | var reason = data.readUInt8(6); 537 | 538 | debug('\t\tEVT_DISCONN_COMPLETE'); 539 | debug('\t\thandle = ' + handle); 540 | debug('\t\treason = ' + reason); 541 | debug('emitting disconnComplete'); 542 | var listened = this.emit('disconnComplete', handle, reason); 543 | debug('Did someone hear: ' + listened); 544 | } else if (subEventType === EVT_ENCRYPT_CHANGE) { 545 | handle = data.readUInt16LE(4); 546 | var encrypt = data.readUInt8(6); 547 | 548 | debug('\t\tEVT_ENCRYPT_CHANGE'); 549 | debug('\t\thandle = ' + handle); 550 | debug('\t\tencrypt = ' + encrypt); 551 | 552 | this.emit('encryptChange', handle, encrypt); 553 | } else if (subEventType === EVT_CMD_COMPLETE) { 554 | cmd = data.readUInt16LE(4); 555 | status = data.readUInt8(6); 556 | var result = data.slice(7); 557 | 558 | debug('\t\tEVT_CMD_COMPLETE'); 559 | debug('\t\tcmd = ' + cmd); 560 | debug('\t\tstatus = ' + status); 561 | debug('\t\tresult = ' + result.toString('hex')); 562 | 563 | this.processCmdCompleteEvent(cmd, status, result); 564 | } else if (subEventType === EVT_CMD_STATUS) { 565 | status = data.readUInt8(3); 566 | cmd = data.readUInt16LE(5); 567 | 568 | debug('\t\tstatus = ' + status); 569 | debug('\t\tcmd = ' + cmd); 570 | 571 | this.processCmdStatusEvent(cmd, status); 572 | } else if (subEventType === EVT_LE_META_EVENT) { 573 | var leMetaEventType = data.readUInt8(3); 574 | var leMetaEventStatus = data.readUInt8(4); 575 | var leMetaEventData = data.slice(5); 576 | 577 | debug('\t\tEVT_LE_META_EVENT'); 578 | debug('\t\tLE meta event type = ' + leMetaEventType); 579 | debug('\t\tLE meta event status = ' + leMetaEventStatus); 580 | debug('\t\tLE meta event data = ' + leMetaEventData.toString('hex')); 581 | 582 | this.processLeMetaEvent(leMetaEventType, leMetaEventStatus, leMetaEventData); 583 | } else { 584 | debug('\t\tUnknown Command'); 585 | } 586 | } else if (HCI_ACLDATA_PKT === eventType) { 587 | var flags = data.readUInt16LE(1) >> 12; 588 | handle = data.readUInt16LE(1) & 0x0fff; 589 | 590 | if (ACL_START === flags) { 591 | var cid = data.readUInt16LE(7); 592 | 593 | var length = data.readUInt16LE(5); 594 | var pktData = data.slice(9); 595 | 596 | debug('\t\tHCI_ACLDATA_PKT - ACL_START'); 597 | debug('\t\tcid = ' + cid); 598 | this.debugPacketType(pktData[0]); 599 | if (length === pktData.length) { 600 | debug('\t\thandle = ' + handle); 601 | debug('\t\tdata = ' + pktData.toString('hex')); 602 | 603 | this.emit('aclDataPkt', handle, cid, pktData); 604 | } else { 605 | this._handleBuffers[handle] = { 606 | length: length, 607 | cid: cid, 608 | data: pktData 609 | }; 610 | } 611 | } 612 | /*else if (ACL_START_NO_FLUSH === flags) { 613 | var cid = data.readUInt16LE(7); 614 | 615 | var length = data.readUInt16LE(5); 616 | var pktData = data.slice(9); 617 | 618 | debug('\t\tHCI_ACLDATA_PKT - ACL_START_NO_FLUSH'); 619 | debug('\t\tcid = ' + cid); 620 | this.debugPacketType(pktData[0]); 621 | if (length === pktData.length) { 622 | debug('\t\thandle = ' + handle); 623 | debug('\t\tdata = ' + pktData.toString('hex')); 624 | 625 | this.emit('aclDataPkt', handle, cid, pktData); 626 | } else { 627 | this._handleBuffers[handle] = { 628 | length: length, 629 | cid: cid, 630 | data: pktData 631 | }; 632 | } 633 | 634 | } */ 635 | else if (ACL_CONT === flags) { 636 | debug('\t\tHCI_ACLDATA_PKT - ACL_CONT'); 637 | if (!this._handleBuffers[handle] || !this._handleBuffers[handle].data) { 638 | debug('!\tUnable to find previous packets'); 639 | return; 640 | } 641 | 642 | this._handleBuffers[handle].data = Buffer.concat([ 643 | this._handleBuffers[handle].data, 644 | data.slice(5) 645 | ]); 646 | 647 | if (this._handleBuffers[handle].data.length === this._handleBuffers[handle].length) { 648 | debug('\t\tCOMPLETE'); 649 | this.emit('aclDataPkt', handle, this._handleBuffers[handle].cid, this._handleBuffers[handle].data); 650 | delete this._handleBuffers[handle]; 651 | } 652 | } 653 | } else if (HCI_COMMAND_PKT === eventType) { 654 | cmd = data.readUInt16LE(1); 655 | var len = data.readUInt8(3); 656 | 657 | debug('\t\tcmd = ' + cmd); 658 | debug('\t\tdata len = ' + len); 659 | 660 | if (cmd === LE_SET_SCAN_ENABLE_CMD) { 661 | var enable = (data.readUInt8(4) === 0x1); 662 | var filter_dups = (data.readUInt8(5) === 0x1); 663 | 664 | debug('\t\t\tLE enable scan command'); 665 | debug('\t\t\tenable scanning = ' + enable); 666 | debug('\t\t\tfilter duplicates = ' + filter_dups); 667 | 668 | this.emit('cmdLeScanEnableSet', enable, filter_dups); 669 | 670 | debug('\t\tUnknown Flags for HCI_ACLDATA_PKT: ' + flags) 671 | } 672 | } else { 673 | debug('!\tPacket unhandled'); 674 | } 675 | }; 676 | 677 | Hci.prototype.onSocketError = function (error) { 678 | debug('onSocketError: ' + error.message); 679 | 680 | if (error.message === 'Operation not permitted') { 681 | this.emit('stateChange', 'unauthorized'); 682 | } else if (error.message === 'Network is down') { 683 | // no-op 684 | } 685 | }; 686 | 687 | Hci.prototype.processCmdCompleteEvent = function (cmd, status, result) { 688 | var handle; 689 | 690 | if (cmd === READ_LOCAL_VERSION_CMD) { 691 | var hciVer = result.readUInt8(0); 692 | var hciRev = result.readUInt16LE(1); 693 | var lmpVer = result.readInt8(3); 694 | var manufacturer = result.readUInt16LE(4); 695 | var lmpSubVer = result.readUInt16LE(6); 696 | 697 | if (hciVer < 0x06) { 698 | this.emit('stateChange', 'unsupported'); 699 | } else if (this._state !== 'poweredOn') { 700 | this.setScanEnabled(false, true); 701 | this.setScanParameters(); 702 | } 703 | debug('\t\tREAD_LOCAL_VERSION_CMD'); 704 | this.emit('readLocalVersion', hciVer, hciRev, lmpVer, manufacturer, lmpSubVer); 705 | } else if (cmd === READ_BD_ADDR_CMD) { 706 | this.addressType = 'public'; 707 | this.address = result.toString('hex').match(/.{1,2}/g).reverse().join(':'); 708 | 709 | 710 | debug('\t\tREAD_BD_ADDR_CMD'); 711 | this.emit('addressChange', this.address); 712 | } else if (cmd === LE_SET_ADVERTISING_PARAMETERS_CMD) { 713 | this.emit('stateChange', 'poweredOn'); 714 | 715 | debug('\t\tLE_SET_ADVERTISING_PARAMETERS_CMD'); 716 | this.emit('leAdvertisingParametersSet', status); 717 | } else if (cmd === LE_SET_SCAN_PARAMETERS_CMD) { 718 | this.emit('stateChange', 'poweredOn'); 719 | 720 | debug('\t\tLE_SET_SCAN_PARAMETERS_CMD'); 721 | this.emit('leScanParametersSet'); 722 | } else if (cmd === LE_SET_SCAN_ENABLE_CMD) { 723 | debug('\t\tLE_SET_SCAN_ENABLE_CMD'); 724 | this.emit('leScanEnableSet', status); 725 | } else if (cmd === LE_SET_ADVERTISING_DATA_CMD) { 726 | debug('\t\tLE_SET_ADVERTISING_DATA_CMD'); 727 | this.emit('leAdvertisingDataSet', status); 728 | } else if (cmd === LE_SET_SCAN_RESPONSE_DATA_CMD) { 729 | debug('\t\tLE_SET_SCAN_RESPONSE_DATA_CMD'); 730 | this.emit('leScanResponseDataSet', status); 731 | } else if (cmd === LE_SET_ADVERTISE_ENABLE_CMD) { 732 | debug('\t\tLE_SET_ADVERTISE_ENABLE_CMD'); 733 | this.emit('leAdvertiseEnableSet', status); 734 | } else if (cmd === READ_RSSI_CMD) { 735 | handle = result.readUInt16LE(0); 736 | var rssi = result.readInt8(2); 737 | debug('\t\tREAD_RSSI_CMD'); 738 | debug('\t\t\thandle = ' + handle); 739 | debug('\t\t\trssi = ' + rssi); 740 | 741 | this.emit('rssiRead', handle, rssi); 742 | } else if (cmd === LE_LTK_NEG_REPLY_CMD) { 743 | handle = result.readUInt16LE(0); 744 | debug('\t\tLE_LTK_NEG_REPLY_CMD'); 745 | debug('\t\t\thandle = ' + handle); 746 | this.emit('leLtkNegReply', handle); 747 | } else { 748 | debug('!\tUnhandled Command: ' + cmd); 749 | } 750 | }; 751 | 752 | Hci.prototype.onStateChange = function (state) { 753 | this._state = state; 754 | }; 755 | 756 | Hci.prototype.processLeMetaEvent = function (eventType, status, data) { 757 | if (eventType === EVT_LE_CONN_COMPLETE) { 758 | this.processLeConnComplete(status, data); 759 | } else if (eventType === EVT_LE_ADVERTISING_REPORT) { 760 | this.processLeAdvertisingReport(status, data); 761 | } else if (eventType === EVT_LE_CONN_UPDATE_COMPLETE) { 762 | this.processLeConnUpdateComplete(status, data); 763 | } 764 | }; 765 | 766 | Hci.prototype.processLeConnComplete = function (status, data) { 767 | var handle = data.readUInt16LE(0); 768 | var role = data.readUInt8(2); 769 | var addressType = data.readUInt8(3) === 0x01 ? 'random' : 'public'; 770 | var address = data.slice(4, 10).toString('hex').match(/.{1,2}/g).reverse().join(':'); 771 | var interval = data.readUInt16LE(10) * 1.25; 772 | var latency = data.readUInt16LE(12); // TODO: multiplier? 773 | var supervisionTimeout = data.readUInt16LE(14) * 10; 774 | var masterClockAccuracy = data.readUInt8(16); // TODO: multiplier? 775 | 776 | debug('\t\t\thandle = ' + handle); 777 | debug('\t\t\trole = ' + role); 778 | debug('\t\t\taddress type = ' + addressType); 779 | debug('\t\t\taddress = ' + address); 780 | debug('\t\t\tinterval = ' + interval); 781 | debug('\t\t\tlatency = ' + latency); 782 | debug('\t\t\tsupervision timeout = ' + supervisionTimeout); 783 | debug('\t\t\tmaster clock accuracy = ' + masterClockAccuracy); 784 | 785 | this.emit('leConnComplete', status, handle, role, addressType, address, interval, latency, supervisionTimeout, masterClockAccuracy); 786 | }; 787 | 788 | Hci.prototype.processLeAdvertisingReport = function (status, data) { 789 | var type = data.readUInt8(0); // ignore for now 790 | var addressType = data.readUInt8(1) === 0x01 ? 'random' : 'public'; 791 | var address = data.slice(2, 8).toString('hex').match(/.{1,2}/g).reverse().join(':'); 792 | var eir = data.slice(9, data.length - 1); 793 | var rssi = data.readInt8(data.length - 1); 794 | 795 | debug('\t\t\ttype = ' + type); 796 | debug('\t\t\taddress = ' + address); 797 | debug('\t\t\taddress type = ' + addressType); 798 | debug('\t\t\teir = ' + eir.toString('hex')); 799 | debug('\t\t\trssi = ' + rssi); 800 | 801 | this.emit('leAdvertisingReport', status, type, address, addressType, eir, rssi); 802 | }; 803 | 804 | Hci.prototype.processLeConnUpdateComplete = function (status, data) { 805 | var handle = data.readUInt16LE(0); 806 | var interval = data.readUInt16LE(2) * 1.25; 807 | var latency = data.readUInt16LE(4); // TODO: multiplier? 808 | var supervisionTimeout = data.readUInt16LE(6) * 10; 809 | 810 | debug('\t\t\thandle = ' + handle); 811 | debug('\t\t\tinterval = ' + interval); 812 | debug('\t\t\tlatency = ' + latency); 813 | debug('\t\t\tsupervision timeout = ' + supervisionTimeout); 814 | 815 | this.emit('leConnUpdateComplete', status, handle, interval, latency, supervisionTimeout); 816 | }; 817 | Hci.prototype.processCmdStatusEvent = function (cmd, status) { 818 | if (cmd === LE_CREATE_CONN_CMD) { 819 | if (status !== 0) { 820 | this.emit('leConnComplete', status); 821 | } 822 | } 823 | }; 824 | 825 | module.exports = Hci; 826 | -------------------------------------------------------------------------------- /lib/hci-socket/mgmt.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('mgmt'); 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | 6 | var BluetoothHciSocket = require('bluetooth-hci-socket'); 7 | 8 | var LTK_INFO_SIZE = 36; 9 | 10 | var MGMT_OP_LOAD_LONG_TERM_KEYS = 0x0013; 11 | 12 | function Mgmt() { 13 | this._socket = new BluetoothHciSocket(); 14 | this._ltkInfos = []; 15 | 16 | this._socket.on('data', this.onSocketData.bind(this)); 17 | this._socket.on('error', this.onSocketError.bind(this)); 18 | 19 | this._socket.bindControl(); 20 | this._socket.start(); 21 | } 22 | 23 | Mgmt.prototype.onSocketData = function(data) { 24 | debug('on data ->' + data.toString('hex')); 25 | }; 26 | 27 | Mgmt.prototype.onSocketError = function(error) { 28 | debug('on error ->' + error.message); 29 | }; 30 | 31 | Mgmt.prototype.addLongTermKey = function(address, addressType, authenticated, master, ediv, rand, key) { 32 | var ltkInfo = new Buffer(LTK_INFO_SIZE); 33 | 34 | address.copy(ltkInfo, 0); 35 | ltkInfo.writeUInt8(addressType.readUInt8(0) + 1, 6); // BDADDR_LE_PUBLIC = 0x01, BDADDR_LE_RANDOM 0x02, so add one 36 | 37 | ltkInfo.writeUInt8(authenticated, 7); 38 | ltkInfo.writeUInt8(master, 8); 39 | ltkInfo.writeUInt8(key.length, 9); 40 | 41 | ediv.copy(ltkInfo, 10); 42 | rand.copy(ltkInfo, 12); 43 | key.copy(ltkInfo, 20); 44 | 45 | this._ltkInfos.push(ltkInfo); 46 | 47 | this.loadLongTermKeys(); 48 | }; 49 | 50 | Mgmt.prototype.clearLongTermKeys = function() { 51 | this._ltkInfos = []; 52 | 53 | this.loadLongTermKeys(); 54 | }; 55 | 56 | Mgmt.prototype.loadLongTermKeys = function() { 57 | var numLongTermKeys = this._ltkInfos.length; 58 | var op = new Buffer(2 + numLongTermKeys * LTK_INFO_SIZE); 59 | 60 | op.writeUInt16LE(numLongTermKeys, 0); 61 | 62 | for (var i = 0; i < numLongTermKeys; i++) { 63 | this._ltkInfos[i].copy(op, 2 + i * LTK_INFO_SIZE); 64 | } 65 | 66 | this.write(MGMT_OP_LOAD_LONG_TERM_KEYS, 0, op); 67 | }; 68 | 69 | Mgmt.prototype.write = function(opcode, index, data) { 70 | var length = 0; 71 | 72 | if (data) { 73 | length = data.length; 74 | } 75 | 76 | var pkt = new Buffer(6 + length); 77 | 78 | pkt.writeUInt16LE(opcode, 0); 79 | pkt.writeUInt16LE(index, 2); 80 | pkt.writeUInt16LE(length, 4); 81 | 82 | if (length) { 83 | data.copy(pkt, 6); 84 | } 85 | 86 | debug('writing -> ' + pkt.toString('hex')); 87 | this._socket.write(pkt); 88 | }; 89 | 90 | module.exports = new Mgmt(); 91 | -------------------------------------------------------------------------------- /lib/hci-socket/smp.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('smp'); 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | 6 | var crypto = require('./crypto'); 7 | 8 | 9 | var SMP_CID = 0x0006; 10 | 11 | var SMP_PAIRING_REQUEST = 0x01; 12 | var SMP_PAIRING_RESPONSE = 0x02; 13 | var SMP_PAIRING_CONFIRM = 0x03; 14 | var SMP_PAIRING_RANDOM = 0x04; 15 | var SMP_PAIRING_FAILED = 0x05; 16 | var SMP_ENCRYPT_INFO = 0x06; 17 | var SMP_MASTER_IDENT = 0x07; 18 | 19 | 20 | var LTK_INFO_SIZE = 36; 21 | 22 | var MGMT_OP_LOAD_LONG_TERM_KEYS = 0x0013; 23 | 24 | var Smp = function(aclStream, localAddressType, localAddress, remoteAddressType, remoteAddress) { 25 | this._aclStream = aclStream; 26 | this._ltkInfos = []; 27 | 28 | //Bleno 29 | this._iat = new Buffer([(remoteAddressType === 'random') ? 0x01 : 0x00]); 30 | this._ia = new Buffer(remoteAddress.split(':').reverse().join(''), 'hex'); 31 | this._rat = new Buffer([(localAddressType === 'random') ? 0x01 : 0x00]); 32 | this._ra = new Buffer(localAddress.split(':').reverse().join(''), 'hex'); 33 | 34 | /* 35 | this._iat = new Buffer([(localAddressType === 'random') ? 0x01 : 0x00]); 36 | this._ia = new Buffer(localAddress.split(':').reverse().join(''), 'hex'); 37 | this._rat = new Buffer([(remoteAddressType === 'random') ? 0x01 : 0x00]); 38 | this._ra = new Buffer(remoteAddress.split(':').reverse().join(''), 'hex'); 39 | */ 40 | 41 | this.onAclStreamDataBinded = this.onAclStreamData.bind(this); 42 | this.onAclStreamEncryptChangeBinded = this.onAclStreamEncryptChange.bind(this); 43 | this.onAclStreamLtkNegReplyBinded = this.onAclStreamLtkNegReply.bind(this); 44 | this.onAclStreamEndBinded = this.onAclStreamEnd.bind(this); 45 | 46 | this._aclStream.on('data', this.onAclStreamDataBinded); 47 | this._aclStream.on('encryptChange', this.onAclStreamEncryptChangeBinded); 48 | this._aclStream.on('ltkNegReply', this.onAclStreamLtkNegReplyBinded); 49 | this._aclStream.on('end', this.onAclStreamEndBinded); 50 | }; 51 | 52 | util.inherits(Smp, events.EventEmitter); 53 | 54 | 55 | 56 | 57 | 58 | Smp.prototype.sendPairingRequest = function() { 59 | console.log("Sending pairing request"); 60 | this._preq = new Buffer([ 61 | SMP_PAIRING_REQUEST, 62 | 0x03, //0x04, // IO capability: NoInputNoOutput 63 | 0x00, // OOB data: Authentication data not present 64 | 0x01, //0x05, // Authentication requirement: Bonding - No MITM 65 | 0x10, // Max encryption key size 66 | 0x00, //0x03, // Initiator key distribution: 67 | 0x01 //0x03 // Responder key distribution: EncKey 68 | ]); 69 | 70 | this.write(this._preq); 71 | }; 72 | 73 | 74 | Smp.prototype.handlePairingRequest = function(data) { 75 | this._preq = data; 76 | console.log("Recieved a Pairing Request"); 77 | this._pres = new Buffer([ 78 | SMP_PAIRING_RESPONSE, 79 | 0x03, // IO capability: NoInputNoOutput 80 | 0x00, // OOB data: Authentication data not present 81 | 0x05, //0x01, // Authentication requirement: Bonding - No MITM 82 | 0x10, // Max encryption key size 83 | 0x03, //0x00, // Initiator key distribution: 84 | 0x01 //0x01 // Responder key distribution: EncKey 85 | ]); 86 | debug('\tShould be Sending: ' + this._pres.toString('hex')); 87 | //this.write(this._pres); 88 | }; 89 | 90 | 91 | /* 92 | Smp.prototype.handlePairingRequest = function(data) { 93 | this._preq = data; 94 | console.log("Recieved a Pairing Request"); 95 | this._pres = new Buffer([ 96 | SMP_PAIRING_RESPONSE, 97 | 0x03, // IO capability: NoInputNoOutput 98 | 0x00, // OOB data: Authentication data not present 99 | 0x01, // Authentication requirement: Bonding - No MITM 100 | 0x10, // Max encryption key size 101 | 0x00, // Initiator key distribution: 102 | 0x01 // Responder key distribution: EncKey 103 | ]); 104 | 105 | this.write(this._pres); 106 | }; 107 | */ 108 | 109 | Smp.prototype.onAclStreamLtkNegReply = function() { 110 | debug("SMP recieved LtkNegReply"); 111 | this.write(new Buffer([ 112 | SMP_PAIRING_FAILED, 113 | SMP_UNSPECIFIED 114 | ])); 115 | 116 | this.emit('fail'); 117 | }; 118 | 119 | Smp.prototype.onAclStreamData = function(cid, data) { 120 | if (cid !== SMP_CID) { 121 | return; 122 | } 123 | 124 | var code = data.readUInt8(0); 125 | debug("SMP Data - code: 0x" + code.toString(16)); 126 | if (SMP_PAIRING_REQUEST === code) { 127 | this.handlePairingRequest(data); 128 | } else if (SMP_PAIRING_RESPONSE === code) { 129 | this.handlePairingResponse(data); 130 | } else if (SMP_PAIRING_CONFIRM === code) { 131 | this.handlePairingConfirm(data); 132 | } else if (SMP_PAIRING_RANDOM === code) { 133 | this.handlePairingRandom(data); 134 | } else if (SMP_PAIRING_FAILED === code) { 135 | this.handlePairingFailed(data); 136 | } else if (SMP_ENCRYPT_INFO === code) { 137 | this.handleEncryptInfo(data); 138 | } else if (SMP_MASTER_IDENT === code) { 139 | this.handleMasterIdent(data); 140 | } 141 | }; 142 | 143 | Smp.prototype.onAclStreamEnd = function() { 144 | this._aclStream.removeListener('data', this.onAclStreamDataBinded); 145 | this._aclStream.removeListener('end', this.onAclStreamEndBinded); 146 | this._aclStream.removeListener('encryptChange', this.onAclStreamEncryptChangeBinded); 147 | this._aclStream.removeListener('ltkNegReply', this.onAclStreamLtkNegReplyBinded); 148 | 149 | this.emit('end'); 150 | }; 151 | 152 | Smp.prototype.onAclStreamEncryptChange = function(encrypted) { 153 | if (encrypted) { 154 | if (this._stk && this._diversifier && this._random) { 155 | this.write(Buffer.concat([ 156 | new Buffer([SMP_ENCRYPT_INFO]), 157 | this._stk 158 | ])); 159 | 160 | this.write(Buffer.concat([ 161 | new Buffer([SMP_MASTER_IDENT]), 162 | this._diversifier, 163 | this._random 164 | ])); 165 | } 166 | } 167 | }; 168 | 169 | Smp.prototype.handlePairingResponse = function(data) { 170 | this._pres = data; 171 | debug("Recieved a Pairing Response"); 172 | debug('\t\tIO Capability: 0x' + data[1].toString(16)); 173 | debug('\t\tOOB Data: 0x' + data[2].toString(16)); 174 | debug('\t\tAuthentication Requirement: 0x' + data[3].toString(16)); 175 | debug('\t\tMax Encryption Size: 0x' + data[4].toString(16)); 176 | debug('\t\tInitiator Key: 0x' + data[5].toString(16)); 177 | debug('\t\tResponder Key: 0x' + data[6].toString(16)); 178 | 179 | 180 | this._tk = new Buffer('00000000000000000000000000000000', 'hex'); 181 | this._r = crypto.r(); 182 | /* 183 | this.write(Buffer.concat([ 184 | new Buffer([SMP_PAIRING_CONFIRM]), 185 | crypto.c1(this._tk, this._r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra) 186 | ]));*/ 187 | }; 188 | 189 | 190 | 191 | //Bleno 192 | 193 | Smp.prototype.handlePairingConfirm = function(data) { 194 | this._pcnf = data; 195 | 196 | this._tk = new Buffer('00000000000000000000000000000000', 'hex'); 197 | this._r = crypto.r(); 198 | debug("Recieved a Pairing Confirm"); 199 | 200 | /*this.write(Buffer.concat([ 201 | new Buffer([SMP_PAIRING_CONFIRM]), 202 | crypto.c1(this._tk, this._r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra) 203 | ]));*/ 204 | }; 205 | /* 206 | 207 | //Noble 208 | Smp.prototype.handlePairingConfirm = function(data) { 209 | this._pcnf = data; 210 | 211 | this.write(Buffer.concat([ 212 | new Buffer([SMP_PAIRING_RANDOM]), 213 | this._r 214 | ])); 215 | };*/ 216 | 217 | 218 | //Bleno 219 | 220 | Smp.prototype.addLongTermKey = function(address, addressType, authenticated, master, ediv, rand, key) { 221 | var ltkInfo = new Buffer(LTK_INFO_SIZE); 222 | 223 | address.copy(ltkInfo, 0); 224 | ltkInfo.writeUInt8(addressType.readUInt8(0) + 1, 6); // BDADDR_LE_PUBLIC = 0x01, BDADDR_LE_RANDOM 0x02, so add one 225 | 226 | ltkInfo.writeUInt8(authenticated, 7); 227 | ltkInfo.writeUInt8(master, 8); 228 | ltkInfo.writeUInt8(key.length, 9); 229 | 230 | ediv.copy(ltkInfo, 10); 231 | rand.copy(ltkInfo, 12); 232 | key.copy(ltkInfo, 20); 233 | 234 | this._ltkInfos.push(ltkInfo); 235 | 236 | 237 | this.loadLongTermKeys(); 238 | }; 239 | 240 | Smp.prototype.clearLongTermKeys = function() { 241 | this._ltkInfos = []; 242 | 243 | this.loadLongTermKeys(); 244 | }; 245 | 246 | Smp.prototype.loadLongTermKeys = function() { 247 | var numLongTermKeys = this._ltkInfos.length; 248 | var op = new Buffer(2 + numLongTermKeys * LTK_INFO_SIZE); 249 | 250 | op.writeUInt16LE(numLongTermKeys, 0); 251 | 252 | debug("Adding Long Term Keys") 253 | for (var i = 0; i < numLongTermKeys; i++) { 254 | debug('\t\t'+this._ltkInfos[i]); 255 | this._ltkInfos[i].copy(op, 2 + i * LTK_INFO_SIZE); 256 | } 257 | 258 | //this.write(MGMT_OP_LOAD_LONG_TERM_KEYS, 0, op); 259 | this.mgmtWrite(MGMT_OP_LOAD_LONG_TERM_KEYS, 0, op); 260 | }; 261 | 262 | Smp.prototype.mgmtWrite = function(opcode, index, data) { 263 | var length = 0; 264 | 265 | if (data) { 266 | length = data.length; 267 | } 268 | 269 | var pkt = new Buffer(6 + length); 270 | 271 | pkt.writeUInt16LE(opcode, 0); 272 | pkt.writeUInt16LE(index, 2); 273 | pkt.writeUInt16LE(length, 4); 274 | 275 | if (length) { 276 | data.copy(pkt, 6); 277 | } 278 | 279 | debug('Mgmt writing -> ' + pkt.toString('hex')); 280 | this.write(pkt); 281 | }; 282 | 283 | 284 | Smp.prototype.handlePairingRandom = function(data) { 285 | var r = data.slice(1); 286 | 287 | debug("Handle Pairing Random: "); 288 | /* 289 | var pcnf = Buffer.concat([ 290 | new Buffer([SMP_PAIRING_CONFIRM]), 291 | crypto.c1(this._tk, r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra) 292 | ]); 293 | 294 | if (this._pcnf.toString('hex') === pcnf.toString('hex')) { 295 | debug('\t\tRandom Worked: '); 296 | this._diversifier = new Buffer('0000', 'hex'); 297 | this._random = new Buffer('0000000000000000', 'hex'); 298 | this._stk = crypto.s1(this._tk, this._r, r); 299 | 300 | this.addLongTermKey(this._ia, this._iat, 0, 0, this._diversifier, this._random, this._stk); 301 | 302 | this.write(Buffer.concat([ 303 | new Buffer([SMP_PAIRING_RANDOM]), 304 | this._r 305 | ])); 306 | } else { 307 | debug('\t\tRandom failed: '); 308 | this.write(new Buffer([ 309 | SMP_PAIRING_FAILED, 310 | SMP_PAIRING_CONFIRM 311 | ])); 312 | 313 | this.emit('fail'); 314 | } 315 | */ 316 | }; 317 | 318 | /* 319 | //Noble 320 | Smp.prototype.handlePairingRandom = function(data) { 321 | var r = data.slice(1); 322 | 323 | var pcnf = Buffer.concat([ 324 | new Buffer([SMP_PAIRING_CONFIRM]), 325 | crypto.c1(this._tk, r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra) 326 | ]); 327 | 328 | if (this._pcnf.toString('hex') === pcnf.toString('hex')) { 329 | var stk = crypto.s1(this._tk, r, this._r); 330 | 331 | this.emit('stk', stk); 332 | } else { 333 | this.write(new Buffer([ 334 | SMP_PAIRING_RANDOM, 335 | SMP_PAIRING_CONFIRM 336 | ])); 337 | 338 | this.emit('fail'); 339 | } 340 | }; 341 | 342 | */ 343 | 344 | 345 | Smp.prototype.handlePairingFailed = function(data) { 346 | debug('Pairing Failed!'); 347 | debug('\t\tReason: ' + data[1].toString(16)); 348 | this.emit('fail'); 349 | }; 350 | 351 | 352 | Smp.prototype.handleEncryptInfo = function(data) { 353 | var ltk = data.slice(1); 354 | debug("Encrypt Info") 355 | this.emit('ltk', ltk); 356 | }; 357 | 358 | Smp.prototype.handleMasterIdent = function(data) { 359 | var ediv = data.slice(1, 3); 360 | var rand = data.slice(3); 361 | 362 | this.emit('masterIdent', ediv, rand); 363 | }; 364 | 365 | Smp.prototype.write = function(data) { 366 | this._aclStream.write(SMP_CID, data); 367 | }; 368 | 369 | module.exports = Smp; 370 | -------------------------------------------------------------------------------- /lib/local-characteristic.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | 4 | var debug = require('debug')('characteristic'); 5 | 6 | var UuidUtil = require('./uuid-util'); 7 | 8 | function Characteristic(options) { 9 | this.uuid = UuidUtil.removeDashes(options.uuid); 10 | this.properties = options.properties || []; 11 | this.secure = options.secure || []; 12 | this.value = options.value || null; 13 | this.descriptors = options.descriptors || []; 14 | 15 | if (options.onReadRequest) { 16 | this.onReadRequest = options.onReadRequest; 17 | } 18 | 19 | if (options.onWriteRequest) { 20 | this.onWriteRequest = options.onWriteRequest; 21 | } 22 | 23 | if (options.onSubscribe) { 24 | this.onSubscribe = options.onSubscribe; 25 | } 26 | 27 | if (options.onUnsubscribe) { 28 | this.onUnsubscribe = options.onUnsubscribe; 29 | } 30 | 31 | if (options.onNotify) { 32 | this.onNotify = options.onNotify; 33 | } 34 | 35 | if (options.onIndicate) { 36 | this.onIndicate = options.onIndicate; 37 | } 38 | 39 | this.on('readRequest', this.onReadRequest.bind(this)); 40 | this.on('writeRequest', this.onWriteRequest.bind(this)); 41 | this.on('subscribe', this.onSubscribe.bind(this)); 42 | this.on('unsubscribe', this.onUnsubscribe.bind(this)); 43 | this.on('notify', this.onNotify.bind(this)); 44 | this.on('indicate', this.onIndicate.bind(this)); 45 | } 46 | 47 | util.inherits(Characteristic, events.EventEmitter); 48 | 49 | Characteristic.RESULT_SUCCESS = Characteristic.prototype.RESULT_SUCCESS = 0x00; 50 | Characteristic.RESULT_INVALID_OFFSET = Characteristic.prototype.RESULT_INVALID_OFFSET = 0x07; 51 | Characteristic.RESULT_ATTR_NOT_LONG = Characteristic.prototype.RESULT_ATTR_NOT_LONG = 0x0b; 52 | Characteristic.RESULT_INVALID_ATTRIBUTE_LENGTH = Characteristic.prototype.RESULT_INVALID_ATTRIBUTE_LENGTH = 0x0d; 53 | Characteristic.RESULT_UNLIKELY_ERROR = Characteristic.prototype.RESULT_UNLIKELY_ERROR = 0x0e; 54 | 55 | Characteristic.prototype.toString = function() { 56 | return JSON.stringify({ 57 | uuid: this.uuid, 58 | properties: this.properties, 59 | secure: this.secure, 60 | value: this.value, 61 | descriptors: this.descriptors 62 | }); 63 | }; 64 | 65 | Characteristic.prototype.onReadRequest = function(offset, callback) { 66 | callback(this.RESULT_UNLIKELY_ERROR, null); 67 | }; 68 | 69 | Characteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) { 70 | callback(this.RESULT_UNLIKELY_ERROR); 71 | }; 72 | 73 | Characteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback) { 74 | this.maxValueSize = maxValueSize; 75 | this.updateValueCallback = updateValueCallback; 76 | }; 77 | 78 | Characteristic.prototype.onUnsubscribe = function() { 79 | this.maxValueSize = null; 80 | this.updateValueCallback = null; 81 | }; 82 | 83 | Characteristic.prototype.onNotify = function() { 84 | }; 85 | 86 | Characteristic.prototype.onIndicate = function() { 87 | }; 88 | 89 | module.exports = Characteristic; 90 | -------------------------------------------------------------------------------- /lib/local-descriptor.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('descriptor'); 2 | 3 | var UuidUtil = require('./uuid-util'); 4 | 5 | function LocalDescriptor(options) { 6 | this.uuid = UuidUtil.removeDashes(options.uuid); 7 | this.value = options.value || new Buffer(0); 8 | } 9 | 10 | LocalDescriptor.prototype.toString = function() { 11 | return JSON.stringify({ 12 | uuid: this.uuid, 13 | value: Buffer.isBuffer(this.value) ? this.value.toString('hex') : this.value 14 | }); 15 | }; 16 | 17 | module.exports = LocalDescriptor; 18 | -------------------------------------------------------------------------------- /lib/peripheral.js: -------------------------------------------------------------------------------- 1 | /*jshint loopfunc: true */ 2 | var debug = require('debug')('peripheral'); 3 | 4 | var events = require('events'); 5 | var util = require('util'); 6 | 7 | function Peripheral(able, id, address, addressType, connectable, advertisement, rssi) { 8 | this._able = able; 9 | 10 | this.id = id; 11 | this.uuid = id; // for legacy 12 | this.address = address; 13 | this.addressType = addressType; 14 | this.connectable = connectable; 15 | this.advertisement = advertisement; 16 | this.rssi = rssi; 17 | this.services = null; 18 | this.state = 'disconnected'; 19 | } 20 | 21 | util.inherits(Peripheral, events.EventEmitter); 22 | 23 | Peripheral.prototype.toString = function() { 24 | return JSON.stringify({ 25 | id: this.id, 26 | address: this.address, 27 | addressType: this.addressType, 28 | connectable: this.connectable, 29 | advertisement: this.advertisement, 30 | rssi: this.rssi, 31 | state: this.state 32 | }); 33 | }; 34 | 35 | Peripheral.prototype.connect = function(callback) { 36 | if (callback) { 37 | this.once('connect', function(error) { 38 | callback(error); 39 | }); 40 | } 41 | 42 | if (this.state === 'connected') { 43 | this.emit('connect', new Error('Peripheral already connected')); 44 | } else { 45 | this.state = 'connecting'; 46 | this._able.connect(this.id); 47 | } 48 | }; 49 | 50 | Peripheral.prototype.disconnect = function(callback) { 51 | if (callback) { 52 | this.once('disconnect', function() { 53 | callback(null); 54 | }); 55 | } 56 | this.state = 'disconnecting'; 57 | this._able.disconnect(this.id); 58 | }; 59 | 60 | Peripheral.prototype.updateRssi = function(callback) { 61 | if (callback) { 62 | this.once('rssiUpdate', function(rssi) { 63 | callback(null, rssi); 64 | }); 65 | } 66 | 67 | this._able.updateRssi(this.id); 68 | }; 69 | 70 | Peripheral.prototype.discoverServices = function(uuids, callback) { 71 | if (callback) { 72 | this.once('servicesDiscover', function(services) { 73 | callback(null, services); 74 | }); 75 | } 76 | 77 | this._able.discoverServices(this.id, uuids); 78 | }; 79 | 80 | Peripheral.prototype.findService = function(uuid, callback) { 81 | if (callback) { 82 | this.once('servicesDiscover', function(services) { 83 | callback(null, services); 84 | }); 85 | } 86 | 87 | this._able.findService(this.id, uuid); 88 | }; 89 | 90 | Peripheral.prototype.findServiceAndCharacteristics = function(serviceUuid, characteristicsUuids, callback) { 91 | this.findService(serviceUuid, function(err, services) { 92 | var numDiscovered = 0; 93 | var allCharacteristics = []; 94 | 95 | for (var i in services) { 96 | var service = services[i]; 97 | 98 | service.discoverCharacteristics(characteristicsUuids, function(error, characteristics) { 99 | numDiscovered++; 100 | 101 | if (error === null) { 102 | for (var j in characteristics) { 103 | var characteristic = characteristics[j]; 104 | 105 | allCharacteristics.push(characteristic); 106 | } 107 | } 108 | 109 | if (numDiscovered === services.length) { 110 | if (callback) { 111 | callback(null, services, allCharacteristics); 112 | } 113 | } 114 | }.bind(this)); 115 | } 116 | }.bind(this)); 117 | }; 118 | 119 | 120 | 121 | 122 | 123 | Peripheral.prototype.discoverSomeServicesAndCharacteristics = function(serviceUuids, characteristicsUuids, callback) { 124 | this.discoverServices(serviceUuids, function(err, services) { 125 | var numDiscovered = 0; 126 | var allCharacteristics = []; 127 | 128 | for (var i in services) { 129 | var service = services[i]; 130 | 131 | service.discoverCharacteristics(characteristicsUuids, function(error, characteristics) { 132 | numDiscovered++; 133 | 134 | if (error === null) { 135 | for (var j in characteristics) { 136 | var characteristic = characteristics[j]; 137 | 138 | allCharacteristics.push(characteristic); 139 | } 140 | } 141 | 142 | if (numDiscovered === services.length) { 143 | if (callback) { 144 | callback(null, services, allCharacteristics); 145 | } 146 | } 147 | }.bind(this)); 148 | } 149 | }.bind(this)); 150 | }; 151 | 152 | Peripheral.prototype.discoverAllServicesAndCharacteristics = function(callback) { 153 | this.discoverSomeServicesAndCharacteristics([], [], callback); 154 | }; 155 | 156 | Peripheral.prototype.readHandle = function(handle, callback) { 157 | if (callback) { 158 | this.once('handleRead' + handle, function(data) { 159 | callback(null, data); 160 | }); 161 | } 162 | 163 | this._able.readHandle(this.id, handle); 164 | }; 165 | 166 | Peripheral.prototype.writeHandle = function(handle, data, withoutResponse, callback) { 167 | if (!(data instanceof Buffer)) { 168 | throw new Error('data must be a Buffer'); 169 | } 170 | 171 | if (callback) { 172 | this.once('handleWrite' + handle, function() { 173 | callback(null); 174 | }); 175 | } 176 | 177 | this._able.writeHandle(this.id, handle, data, withoutResponse); 178 | }; 179 | 180 | module.exports = Peripheral; 181 | -------------------------------------------------------------------------------- /lib/primary-service.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | 4 | var debug = require('debug')('primary-service'); 5 | 6 | var UuidUtil = require('./uuid-util'); 7 | 8 | function PrimaryService(options) { 9 | this.uuid = UuidUtil.removeDashes(options.uuid); 10 | this.characteristics = options.characteristics || []; 11 | } 12 | 13 | util.inherits(PrimaryService, events.EventEmitter); 14 | 15 | PrimaryService.prototype.toString = function() { 16 | return JSON.stringify({ 17 | uuid: this.uuid, 18 | characteristics: this.characteristics 19 | }); 20 | }; 21 | 22 | module.exports = PrimaryService; 23 | -------------------------------------------------------------------------------- /lib/remote-characteristic.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('characteristic'); 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | 6 | var characteristics = require('./characteristics.json'); 7 | 8 | function RemoteCharacteristic(able, peripheralId, serviceUuid, uuid, properties) { 9 | this._able = able; 10 | this._peripheralId = peripheralId; 11 | this._serviceUuid = serviceUuid; 12 | 13 | this.uuid = uuid; 14 | this.name = null; 15 | this.type = null; 16 | this.properties = properties; 17 | this.descriptors = null; 18 | 19 | var characteristic = characteristics[uuid]; 20 | if (characteristic) { 21 | this.name = characteristic.name; 22 | this.type = characteristic.type; 23 | } 24 | } 25 | 26 | util.inherits(RemoteCharacteristic, events.EventEmitter); 27 | 28 | RemoteCharacteristic.prototype.toString = function() { 29 | return JSON.stringify({ 30 | uuid: this.uuid, 31 | name: this.name, 32 | type: this.type, 33 | properties: this.properties 34 | }); 35 | }; 36 | 37 | RemoteCharacteristic.prototype.read = function(callback) { 38 | if (callback) { 39 | this.once('read', function(data) { 40 | callback(null, data); 41 | }); 42 | } 43 | 44 | this._able.read( 45 | this._peripheralId, 46 | this._serviceUuid, 47 | this.uuid 48 | ); 49 | }; 50 | 51 | RemoteCharacteristic.prototype.write = function(data, withoutResponse, callback) { 52 | if (process.title !== 'browser') { 53 | if (!(data instanceof Buffer)) { 54 | throw new Error('data must be a Buffer'); 55 | } 56 | } 57 | 58 | if (callback) { 59 | this.once('write', function() { 60 | callback(null); 61 | }); 62 | } 63 | 64 | this._able.write( 65 | this._peripheralId, 66 | this._serviceUuid, 67 | this.uuid, 68 | data, 69 | withoutResponse 70 | ); 71 | }; 72 | 73 | RemoteCharacteristic.prototype.broadcast = function(broadcast, callback) { 74 | if (callback) { 75 | this.once('broadcast', function() { 76 | callback(null); 77 | }); 78 | } 79 | 80 | this._able.broadcast( 81 | this._peripheralId, 82 | this._serviceUuid, 83 | this.uuid, 84 | broadcast 85 | ); 86 | }; 87 | 88 | RemoteCharacteristic.prototype.notify = function(notify, callback) { 89 | if (callback) { 90 | this.once('notify', function() { 91 | callback(null); 92 | }); 93 | } 94 | 95 | this._able.notify( 96 | this._peripheralId, 97 | this._serviceUuid, 98 | this.uuid, 99 | notify 100 | ); 101 | }; 102 | 103 | RemoteCharacteristic.prototype.discoverDescriptors = function(callback) { 104 | if (callback) { 105 | this.once('descriptorsDiscover', function(descriptors) { 106 | callback(null, descriptors); 107 | }); 108 | } 109 | 110 | this._able.discoverDescriptors( 111 | this._peripheralId, 112 | this._serviceUuid, 113 | this.uuid 114 | ); 115 | }; 116 | 117 | module.exports = RemoteCharacteristic; 118 | -------------------------------------------------------------------------------- /lib/remote-descriptor.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('descriptor'); 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | 6 | var descriptors = require('./descriptors.json'); 7 | 8 | function RemoteDescriptor(able, peripheralId, serviceUuid, characteristicUuid, uuid) { 9 | this._able = able; 10 | this._peripheralId = peripheralId; 11 | this._serviceUuid = serviceUuid; 12 | this._characteristicUuid = characteristicUuid; 13 | 14 | this.uuid = uuid; 15 | this.name = null; 16 | this.type = null; 17 | 18 | var descriptor = descriptors[uuid]; 19 | if (descriptor) { 20 | this.name = descriptor.name; 21 | this.type = descriptor.type; 22 | } 23 | } 24 | 25 | util.inherits(RemoteDescriptor, events.EventEmitter); 26 | 27 | RemoteDescriptor.prototype.toString = function() { 28 | return JSON.stringify({ 29 | uuid: this.uuid, 30 | name: this.name, 31 | type: this.type 32 | }); 33 | }; 34 | 35 | RemoteDescriptor.prototype.readValue = function(callback) { 36 | if (callback) { 37 | this.once('valueRead', function(data) { 38 | callback(null, data); 39 | }); 40 | } 41 | this._able.readValue( 42 | this._peripheralId, 43 | this._serviceUuid, 44 | this._characteristicUuid, 45 | this.uuid 46 | ); 47 | }; 48 | 49 | RemoteDescriptor.prototype.writeValue = function(data, callback) { 50 | if (!(data instanceof Buffer)) { 51 | throw new Error('data must be a Buffer'); 52 | } 53 | 54 | if (callback) { 55 | this.once('valueWrite', function() { 56 | callback(null); 57 | }); 58 | } 59 | this._able.writeValue( 60 | this._peripheralId, 61 | this._serviceUuid, 62 | this._characteristicUuid, 63 | this.uuid, 64 | data 65 | ); 66 | }; 67 | 68 | module.exports = RemoteDescriptor; 69 | -------------------------------------------------------------------------------- /lib/service.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('service'); 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | 6 | var services = require('./services.json'); 7 | 8 | function Service(able, peripheralId, uuid) { 9 | this._able = able; 10 | this._peripheralId = peripheralId; 11 | 12 | this.uuid = uuid; 13 | this.name = null; 14 | this.type = null; 15 | this.includedServiceUuids = null; 16 | this.characteristics = null; 17 | 18 | var service = services[uuid]; 19 | if (service) { 20 | this.name = service.name; 21 | this.type = service.type; 22 | } 23 | } 24 | 25 | util.inherits(Service, events.EventEmitter); 26 | 27 | Service.prototype.toString = function() { 28 | return JSON.stringify({ 29 | uuid: this.uuid, 30 | name: this.name, 31 | type: this.type, 32 | includedServiceUuids: this.includedServiceUuids 33 | }); 34 | }; 35 | 36 | Service.prototype.discoverIncludedServices = function(serviceUuids, callback) { 37 | if (callback) { 38 | this.once('includedServicesDiscover', function(includedServiceUuids) { 39 | callback(null, includedServiceUuids); 40 | }); 41 | } 42 | 43 | this._able.discoverIncludedServices( 44 | this._peripheralId, 45 | this.uuid, 46 | serviceUuids 47 | ); 48 | }; 49 | 50 | Service.prototype.discoverCharacteristics = function(characteristicUuids, callback) { 51 | if (callback) { 52 | this.once('characteristicsDiscover', function(characteristics) { 53 | callback(null, characteristics); 54 | }); 55 | } 56 | 57 | this._able.discoverCharacteristics( 58 | this._peripheralId, 59 | this.uuid, 60 | characteristicUuids 61 | ); 62 | }; 63 | 64 | module.exports = Service; 65 | -------------------------------------------------------------------------------- /lib/services.json: -------------------------------------------------------------------------------- 1 | { 2 | "1800" : { "name" : "Generic Access" 3 | , "type" : "org.bluetooth.service.generic_access" 4 | } 5 | , "1801" : { "name" : "Generic Attribute" 6 | , "type" : "org.bluetooth.service.generic_attribute" 7 | } 8 | , "1802" : { "name" : "Immediate Alert" 9 | , "type" : "org.bluetooth.service.immediate_alert" 10 | } 11 | , "1803" : { "name" : "Link Loss" 12 | , "type" : "org.bluetooth.service.link_loss" 13 | } 14 | , "1804" : { "name" : "Tx Power" 15 | , "type" : "org.bluetooth.service.tx_power" 16 | } 17 | , "1805" : { "name" : "Current Time Service" 18 | , "type" : "org.bluetooth.service.current_time" 19 | } 20 | , "1806" : { "name" : "Reference Time Update Service" 21 | , "type" : "org.bluetooth.service.reference_time_update" 22 | } 23 | , "1807" : { "name" : "Next DST Change Service" 24 | , "type" : "org.bluetooth.service.next_dst_change" 25 | } 26 | , "1808" : { "name" : "Glucose" 27 | , "type" : "org.bluetooth.service.glucose" 28 | } 29 | , "1809" : { "name" : "Health Thermometer" 30 | , "type" : "org.bluetooth.service.health_thermometer" 31 | } 32 | , "180a" : { "name" : "Device Information" 33 | , "type" : "org.bluetooth.service.device_information" 34 | } 35 | , "180d" : { "name" : "Heart Rate" 36 | , "type" : "org.bluetooth.service.heart_rate" 37 | } 38 | , "180e" : { "name" : "Phone Alert Status Service" 39 | , "type" : "org.bluetooth.service.phone_alert_service" 40 | } 41 | , "180f" : { "name" : "Battery Service" 42 | , "type" : "org.bluetooth.service.battery_service" 43 | } 44 | , "1810" : { "name" : "Blood Pressure" 45 | , "type" : "org.bluetooth.service.blood_pressuer" 46 | } 47 | , "1811" : { "name" : "Alert Notification Service" 48 | , "type" : "org.bluetooth.service.alert_notification" 49 | } 50 | , "1812" : { "name" : "Human Interface Device" 51 | , "type" : "org.bluetooth.service.human_interface_device" 52 | } 53 | , "1813" : { "name" : "Scan Parameters" 54 | , "type" : "org.bluetooth.service.scan_parameters" 55 | } 56 | , "1814" : { "name" : "Running Speed and Cadence" 57 | , "type" : "org.bluetooth.service.running_speed_and_cadence" 58 | } 59 | , "1815" : { "name" : "Automation IO" 60 | , "type" : "org.bluetooth.service.automation_io" 61 | } 62 | , "1816" : { "name" : "Cycling Speed and Cadence" 63 | , "type" : "org.bluetooth.service.cycling_speed_and_cadence" 64 | } 65 | , "1818" : { "name" : "Cycling Power" 66 | , "type" : "org.bluetooth.service.cycling_power" 67 | } 68 | , "1819" : { "name" : "Location and Navigation" 69 | , "type" : "org.bluetooth.service.location_and_navigation" 70 | } 71 | , "181a" : { "name" : "Environmental Sensing" 72 | , "type" : "org.bluetooth.service.environmental_sensing" 73 | } 74 | , "181b" : { "name" : "Body Composition" 75 | , "type" : "org.bluetooth.service.body_composition" 76 | } 77 | , "181c" : { "name" : "User Data" 78 | , "type" : "org.bluetooth.service.user_data" 79 | } 80 | , "181d" : { "name" : "Weight Scale" 81 | , "type" : "org.bluetooth.service.weight_scale" 82 | } 83 | , "181e" : { "name" : "Bond Management" 84 | , "type" : "org.bluetooth.service.bond_management" 85 | } 86 | , "181f" : { "name" : "Continuous Glucose Monitoring" 87 | , "type" : "org.bluetooth.service.continuous_glucose_monitoring" 88 | } 89 | , "1820" : { "name" : "Internet Protocol Support" 90 | , "type" : "org.bluetooth.service.internet_protocol_support" 91 | } 92 | } -------------------------------------------------------------------------------- /lib/uuid-util.js: -------------------------------------------------------------------------------- 1 | module.exports.removeDashes = function(uuid) { 2 | if (uuid) { 3 | uuid = uuid.replace(/-/g, ''); 4 | } 5 | 6 | return uuid; 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ble-ancs", 3 | "version": "0.0.1", 4 | "description": "An Apple ANCS reciever from Linux. It is a combination of the Bleno, Noble and ANCS projects from Sandeep Mistry ", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/robotastic/ble-ancs.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/robotastic/ble-ancs/issues" 15 | }, 16 | "keywords": [ 17 | "Apple", 18 | "ANCS", 19 | "iOS", 20 | "Notification", 21 | "Bluetooth", 22 | "BLE" 23 | ], 24 | "os": [ 25 | "linux" 26 | ], 27 | "engines": { 28 | "node": ">=0.8" 29 | }, 30 | "author": "Luke Berndt ", 31 | "license": "MIT", 32 | "dependencies": { 33 | "debug": "~2.2.0" 34 | }, 35 | "optionalDependencies": { 36 | "bluetooth-hci-socket": "~0.4.3", 37 | "bplist-parser": "0.0.6", 38 | "xpc-connection": "~0.1.4" 39 | }, 40 | "devDependencies": { 41 | "jshint": "latest", 42 | "mocha": "~1.8.2", 43 | "should": "~1.2.2", 44 | "sinon": "~1.6.0", 45 | "async": "~0.2.9", 46 | "ws": "~0.4.31" 47 | } 48 | } 49 | --------------------------------------------------------------------------------