├── .editorconfig ├── .env.example ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── ble-test-device ├── README.md ├── platformio.ini └── src │ └── main.cpp ├── docs ├── api.md ├── documentation-testing.md └── templates │ └── api.hbs ├── example.js ├── jest.config.js ├── node-ble.conf ├── package-lock.json ├── package.json ├── src ├── Adapter.js ├── Bluetooth.js ├── BusHelper.js ├── Device.js ├── GattCharacteristic.js ├── GattServer.js ├── GattService.js ├── buildTypedValue.js ├── index.d.ts ├── index.js └── parseDict.js ├── test-e2e ├── e2e-test-utils.js ├── e2e.spec.js └── multiple-attemps.spec.js └── test ├── Adapter.spec.js ├── Bluetooth.spec.js ├── BusHelper.spec.js ├── Device.spec.js ├── GattCharacteristic.spec.js ├── GattServer.spec.js ├── GattService.spec.js ├── __interfaces └── TestInterface.js ├── buildTypedValue.spec.js └── parseDict.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TEST_DEVICE=00:00:00:00:00:00 2 | 3 | TEST_SERVICE=12345678-1234-5678-1234-56789abcdef0 4 | TEST_CHARACTERISTIC=12345678-1234-5678-1234-56789abcdef1 5 | 6 | TEST_NOTIFY_SERVICE=12345678-1234-5678-1234-56789abcdef0 7 | TEST_NOTIFY_CHARACTERISTIC=12345678-1234-5678-1234-56789abcdef2 8 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | NEXT_VERSION: 7 | description: Define next version (major/minor/patch) 8 | required: true 9 | REPOSITORY_NAME: 10 | description: Repository full name (e.g. chrvadala/hello ) 11 | required: true 12 | 13 | jobs: 14 | build_and_release: 15 | runs-on: ubuntu-24.04 16 | if: ${{ github.event.inputs.REPOSITORY_NAME == github.repository }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Setup dbus permissions 20 | run: sudo sh -c 'sed "s/my_username/runner/g" node-ble.conf > /etc/dbus-1/system.d/dbus.conf' 21 | - name: Release library 22 | uses: chrvadala/github-actions/nodejs-release-library-action@v1 23 | with: 24 | NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} 25 | NPM_TOKEN: ${{ secrets.npm_token }} 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | - workflow_dispatch 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [ ubuntu-24.04 ] 13 | node: [ 20, 22 ] 14 | name: Test Nodejs v${{ matrix.node }} on ${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup dbus permissions 19 | run: sudo sh -c 'sed "s/my_username/runner/g" node-ble.conf > /etc/dbus-1/system.d/dbus.conf' 20 | - uses: chrvadala/github-actions/nodejs-test-library-action@v1 21 | with: 22 | NODE_VERSION: ${{ matrix.node }} 23 | - name: Publish Coveralls 24 | uses: coverallsapp/github-action@v2.3.3 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | flag-name: run-nodejs-v${{ matrix.node }}-${{ matrix.os }} 28 | parallel: true 29 | 30 | finish: 31 | name: Finish 32 | needs: build 33 | runs-on: ubuntu-24.04 34 | steps: 35 | - name: Coveralls Finished 36 | uses: coverallsapp/github-action@v2.3.3 37 | with: 38 | github-token: ${{ secrets.GITHUB_TOKEN }} 39 | parallel-finished: true 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .env 4 | coverage 5 | *.log 6 | .pio -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 https://github.com/chrvadala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-ble 2 | 3 | Bluetooth Low Energy (BLE) library written with pure Node.js (no bindings) - baked by Bluez via DBus 4 | 5 | [![chrvadala](https://img.shields.io/badge/website-chrvadala-orange.svg)](https://chrvadala.github.io) 6 | [![Donate](https://img.shields.io/badge/donate-Paypal-lightgrey.svg)](https://www.paypal.com/paypalme/chrvadala/15) 7 | 8 | [![Test](https://github.com/chrvadala/node-ble/workflows/Test/badge.svg)](https://github.com/chrvadala/node-ble/actions) 9 | [![Coverage Status](https://coveralls.io/repos/github/chrvadala/node-ble/badge.svg?branch=master)](https://coveralls.io/github/chrvadala/node-ble?branch=master) 10 | [![npm](https://img.shields.io/npm/v/node-ble.svg?maxAge=2592000?style=plastic)](https://www.npmjs.com/package/node-ble) 11 | [![Downloads](https://img.shields.io/npm/dm/node-ble.svg)](https://www.npmjs.com/package/node-ble) 12 | 13 | 14 | 15 | 16 | # Documentation 17 | - [Documentation testing](https://github.com/chrvadala/node-ble/blob/main/docs/documentation-testing.md) 18 | - [Quick start guide](#quick-start-guide) 19 | - [APIs](https://github.com/chrvadala/node-ble/blob/main/docs/api.md) 20 | - [createBluetooth](https://github.com/chrvadala/node-ble/blob/main/docs/api.md#createBluetooth) 21 | - [Bluetooth](https://github.com/chrvadala/node-ble/blob/main/docs/api.md#Bluetooth) 22 | - [Adapter](https://github.com/chrvadala/node-ble/blob/main/docs/api.md#Adapter) 23 | - [Device](https://github.com/chrvadala/node-ble/blob/main/docs/api.md#Device) 24 | - [GattServer](https://github.com/chrvadala/node-ble/blob/main/docs/api.md#GattServer) 25 | - [GattService](https://github.com/chrvadala/node-ble/blob/main/docs/api.md#GattService) 26 | - [GattCharacteristic](https://github.com/chrvadala/node-ble/blob/main/docs/api.md#GattCharacteristic) 27 | 28 | # Pre-requisites 29 | This library works on many architectures supported by Linux. However Windows and Mac OS are [*not* supported](https://github.com/chrvadala/node-ble/issues/31). 30 | 31 | It leverages the `bluez` driver, a component supported by the following platforms and distributions . 32 | 33 | *node-ble* has been tested on the following operating systems: 34 | - Raspbian 35 | - Ubuntu 36 | - Debian 37 | 38 | # Install 39 | ```sh 40 | npm install node-ble 41 | ``` 42 | 43 | # Quick start guide 44 | 45 | ## Provide permissions 46 | In order to allow a connection with the DBus daemon, you have to set up right permissions. 47 | 48 | Execute the following command, in order to create the file `/etc/dbus-1/system.d/node-ble.conf`, configured with the current *user id* (Note: You may need to manually change the *user id*). 49 | 50 | ```sh 51 | echo ' 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ' | sed "s/__USERID__/$(id -un)/" | sudo tee /etc/dbus-1/system.d/node-ble.conf > /dev/null 63 | ``` 64 | 65 | ## STEP 1: Get Adapter 66 | To start a Bluetooth Low Energy (BLE) connection you need a Bluetooth adapter instance. 67 | 68 | ```javascript 69 | const {createBluetooth} = require('node-ble') 70 | const {bluetooth, destroy} = createBluetooth() 71 | const adapter = await bluetooth.defaultAdapter() 72 | ``` 73 | 74 | ## STEP 2: Start discovering 75 | In order to find a Bluetooth Low Energy device out, you have to start a discovery operation. 76 | ```javascript 77 | if (! await adapter.isDiscovering()) 78 | await adapter.startDiscovery() 79 | ``` 80 | 81 | ## STEP 3: Get a device, Connect and Get GATT Server 82 | Use the adapter instance in order to get a remote Bluetooth device, then connect and interact with the GATT (Generic Attribute Profile) server. 83 | 84 | ```javascript 85 | const device = await adapter.waitDevice('00:00:00:00:00:00') 86 | await device.connect() 87 | const gattServer = await device.gatt() 88 | ``` 89 | 90 | ## STEP 4a: Read and write a characteristic. 91 | ```javascript 92 | const service1 = await gattServer.getPrimaryService('uuid') 93 | const characteristic1 = await service1.getCharacteristic('uuid') 94 | await characteristic1.writeValue(Buffer.from("Hello world")) 95 | const buffer = await characteristic1.readValue() 96 | console.log(buffer) 97 | ``` 98 | 99 | ## STEP 4b: Subscribe to a characteristic. 100 | ```javascript 101 | const service2 = await gattServer.getPrimaryService('uuid') 102 | const characteristic2 = await service2.getCharacteristic('uuid') 103 | characteristic2.on('valuechanged', buffer => { 104 | console.log(buffer) 105 | }) 106 | await characteristic2.startNotifications() 107 | ``` 108 | 109 | ## STEP 5: Disconnect 110 | When you have done you can stop notifications, disconnect and destroy the session. 111 | ```javascript 112 | await characteristic2.stopNotifications() 113 | await device.disconnect() 114 | destroy() 115 | ``` 116 | 117 | # Changelog 118 | - **0.x** - Beta version 119 | - **1.0** - First official version 120 | - **1.1** - Migrates to gh-workflows 121 | - **1.2** - Upgrades deps 122 | - **1.3** - Adds typescript definitions [#10](https://github.com/chrvadala/node-ble/pull/10) 123 | - **1.4** - Upgrades deps 124 | - **1.5** - Adds write options configuration `async writeValue (value, optionsOrOffset = {})` [#20](https://github.com/chrvadala/node-ble/pull/20); Upgrades deps 125 | - **1.6** - Upgrades deps and removes some dependencies; migrates to npm; improves gh-actions 126 | - **1.7** - Fixes compatibility issue [#30](https://github.com/chrvadala/node-ble/issues/30); Adds JSdoc; Deprecates NodeJS 10 and 12; Upgrades deps; 127 | - **1.8** - Upgrades deps and gh-actions os; Adds `Bluetooth.activeAdapters()` func [#45](https://github.com/chrvadala/node-ble/pull/45); 128 | - **1.9** - Upgrades deps; Adds `writeValueWithoutResponse()` and `writeValueWithResponse` methods [#47](https://github.com/chrvadala/node-ble/pull/47); Improves typescript definition [#48](https://github.com/chrvadala/node-ble/pull/48) 129 | - **1.10** - Upgrades deps and gh-actions; Fixes memory leak [#37](https://github.com/chrvadala/node-ble/pull/37); Makes MAC Address case insensitive 130 | - **1.11** - Upgrades deps; Fixes doc [#69](https://github.com/chrvadala/node-ble/pull/69); Adds `getManufacturerData` and `getAdvertisingData` functions on `Device` [#67](https://github.com/chrvadala/node-ble/pull/67); Adds `getServiceData` functions on `Device`; Improves pre-requisite doc section [#68](https://github.com/chrvadala/node-ble/pull/68) 131 | - **1.12** - Upgrades deps and actions; Fixes memory leak [#75](https://github.com/chrvadala/node-ble/pull/75); Improved docs with copy-and-paste configuration scripts. 132 | - **1.13** - Upgrades deps; Fixes race condition [#77](https://github.com/chrvadala/node-ble/pull/77) 133 | 134 | # Contributors 135 | - [chrvadala](https://github.com/chrvadala) (author) 136 | - [pascalopitz](https://github.com/pascalopitz) 137 | - [lupol](https://github.com/lupol) 138 | - [altaircunhajr](https://github.com/altaircunhajr) 139 | - [derwehr](https://github.com/derwehr) 140 | - [mxc42](https://github.com/mxc42) 141 | - [tuxedoxt](https://github.com/tuxedoxt) 142 | - [raffone17](https://github.com/Raffone17) 143 | - [gmacario](https://github.com/gmacario) 144 | - [ianchanning](https://github.com/ianchanning) 145 | - [nmasse-itix](https://github.com/nmasse-itix) 146 | 147 | # References 148 | - https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/adapter-api.txt?h=5.64 149 | - https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt?h=5.64 150 | - https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/gatt-api.txt?h=5.64 151 | - https://webbluetoothcg.github.io/web-bluetooth - method signatures follow, when possible, WebBluetooth standards 152 | - https://developers.google.com/web/updates/2015/07/interact-with-ble-devices-on-the-web - method signatures follow, when possible, WebBluetooth standards 153 | 154 | # Similar libraries 155 | - https://github.com/noble/noble 156 | - https://github.com/abandonware/noble (noble fork) 157 | - https://www.npmjs.com/package/node-web-bluetooth 158 | 159 | # Useful commands 160 | | Command | Description | 161 | | --- | --- | 162 | | rm -r /var/lib/bluetooth/* | Clean Bluetooth cache | 163 | | hciconfig -a | Adapter info | 164 | | hcitool dev | Adapter info (through Bluez) | 165 | | d-feet | DBus debugging tool | 166 | | nvram bluetoothHostControllerSwitchBehavior=never | Only on Parallels | 167 | | inxi --bluetooth -z | Bluetooth device info | 168 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | '@babel/plugin-proposal-class-properties', 4 | [ 5 | '@babel/plugin-proposal-decorators', 6 | { 7 | decoratorsBeforeExport: true, 8 | legacy: false 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /ble-test-device/README.md: -------------------------------------------------------------------------------- 1 | # Node BLE Testing device 2 | This code allows you to flash an ESP32 device and create a BLE device that can be used for e2e testing. 3 | 4 | Read [Testing](https://github.com/chrvadala/node-ble/blob/main/docs/documentation-testing.md) to see how to use it in a test. 5 | 6 | ## How to flash a test device 7 | 8 | ### 1. PlatformIO Setup 9 | Install PlatformIO Core by following the instructions on the [official PlatformIO website](https://platformio.org/install/cli). 10 | ```bash 11 | curl -fsSL -o get-platformio.py https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py 12 | python3 get-platformio.py 13 | ``` 14 | 15 | ### 2. Build firmware and flash device 16 | ```bash 17 | cd ble-test-device/ 18 | platformio run -t upload 19 | ``` 20 | 21 | ### 3. Read MAC Address and watch logs 22 | ```bash 23 | platformio device monitor -b 115200 24 | ``` 25 | When everything works fine, you can see a log like that, generated by your test device. 26 | ``` 27 | Boot 28 | Device UUID: 00:00:00:00:00:00 29 | Ready 30 | Client connected 31 | Notifications and indications are disabled 32 | Characteristic1 value written: hello_world_%{ISO_TS} 33 | Characteristic1 value read: ECHO>hello_world_%{ISO_TS} 34 | Notifications or indications are enabled 35 | Client disconnected 36 | ``` 37 | 38 | --- 39 | 40 | _References:_ 41 | - https://en.wikipedia.org/wiki/ESP32 42 | - https://github.com/arduino-libraries/ArduinoBLE 43 | 44 | -------------------------------------------------------------------------------- /ble-test-device/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp32dev] 12 | platform = espressif32 13 | framework = arduino 14 | board = esp32dev 15 | monitor_speed = 115200 16 | lib_deps = arduino-libraries/ArduinoBLE@^1.3.6 17 | -------------------------------------------------------------------------------- /ble-test-device/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define DEVICE_NAME "EchoBLE" 8 | #define SERVICE_UUID "12345678-1234-5678-1234-56789abcdef0" 9 | #define CHARACTERISTIC_UUID1 "12345678-1234-5678-1234-56789abcdef1" 10 | #define CHARACTERISTIC_UUID2 "12345678-1234-5678-1234-56789abcdef2" 11 | 12 | BLEService *pService; 13 | BLECharacteristic *pCharacteristic1; 14 | BLECharacteristic *pCharacteristic2; 15 | BLEServer *pServer; 16 | bool deviceConnected = false; 17 | 18 | /** 19 | * @brief Callbacks for BLE server events. 20 | */ 21 | class MyServerCallbacks : public BLEServerCallbacks 22 | { 23 | void onConnect(BLEServer *pServer) 24 | { 25 | Serial.println("Client connected"); 26 | deviceConnected = true; 27 | } 28 | 29 | void onDisconnect(BLEServer *pServer) 30 | { 31 | Serial.println("Client disconnected"); 32 | // Restart advertising after disconnection 33 | pServer->getAdvertising()->start(); 34 | deviceConnected = false; 35 | } 36 | }; 37 | 38 | /** 39 | * @brief Callbacks for characteristic1 events. 40 | */ 41 | class MyCallbacks1 : public BLECharacteristicCallbacks 42 | { 43 | std::string storedValue; // Variable to store the value of characteristic1 44 | 45 | void onWrite(BLECharacteristic *pCharacteristic) 46 | { 47 | std::string value = pCharacteristic->getValue(); 48 | Serial.print("Characteristic1 value written: "); 49 | printValue(value); 50 | storedValue = value; 51 | } 52 | 53 | void onRead(BLECharacteristic *pCharacteristic) 54 | { 55 | Serial.print("Characteristic1 value read: "); 56 | std::string res = "ECHO>" + storedValue; 57 | printValue(res); 58 | pCharacteristic->setValue(res); 59 | } 60 | 61 | void printValue(const std::string& value) 62 | { 63 | for (int i = 0; i < value.length(); i++) 64 | { 65 | Serial.print(value[i]); 66 | } 67 | Serial.println(); 68 | } 69 | }; 70 | 71 | void setupBLE() 72 | { 73 | BLEDevice::init(DEVICE_NAME); // Initialize BLEDevice with the device name 74 | 75 | // Print device UUID 76 | Serial.print("Device UUID: "); 77 | Serial.println(BLEDevice::getAddress().toString().c_str()); 78 | 79 | pServer = BLEDevice::createServer(); // Create BLE server 80 | pServer->setCallbacks(new MyServerCallbacks()); // Set server callbacks 81 | 82 | pService = pServer->createService(SERVICE_UUID); // Initialize pService 83 | 84 | pCharacteristic1 = pService->createCharacteristic( 85 | CHARACTERISTIC_UUID1, 86 | BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE 87 | ); 88 | pCharacteristic1->setCallbacks(new MyCallbacks1()); 89 | 90 | pCharacteristic2 = pService->createCharacteristic( 91 | CHARACTERISTIC_UUID2, 92 | BLECharacteristic::PROPERTY_NOTIFY 93 | ); 94 | pCharacteristic2->addDescriptor(new BLE2902()); 95 | 96 | pService->start(); // Start the service 97 | 98 | BLEAdvertising *pAdvertising = pServer->getAdvertising(); // Get advertising from server 99 | pAdvertising->addServiceUUID(SERVICE_UUID); 100 | pAdvertising->start(); // Start advertising from server 101 | pAdvertising->setScanResponse(true); 102 | pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue 103 | pAdvertising->setMinPreferred(0x12); 104 | pServer->startAdvertising(); // Start advertising from server 105 | } 106 | 107 | void setup() 108 | { 109 | Serial.begin(115200); 110 | Serial.println("Boot"); 111 | setupBLE(); 112 | Serial.println("Ready"); 113 | } 114 | 115 | void loop() 116 | { 117 | if (deviceConnected) 118 | { 119 | static unsigned long lastNotifyTime = 0; 120 | if (millis() - lastNotifyTime > 3000) 121 | { 122 | lastNotifyTime = millis(); 123 | 124 | // Check if notifications are enabled 125 | BLE2902* p2902Descriptor = (BLE2902*)pCharacteristic2->getDescriptorByUUID(BLEUUID((uint16_t)0x2902)); 126 | if (p2902Descriptor->getNotifications() || p2902Descriptor->getIndications()) 127 | { 128 | // Notifications or indications are enabled 129 | Serial.println("Notifications or indications are enabled"); 130 | 131 | std::string notificationData = "Notification data " + std::to_string(millis()); 132 | pCharacteristic2->setValue(notificationData.c_str()); 133 | pCharacteristic2->notify(); 134 | } 135 | else 136 | { 137 | // Notifications and indications are disabled 138 | Serial.println("Notifications and indications are disabled"); 139 | } 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | # Node BLE APIs 12 | ## Classes 13 | 14 |
15 |
Adapter
16 |

Adapter class interacts with the local bluetooth adapter

17 |
18 |
Bluetooth
19 |

Top level object that represent a bluetooth session

20 |
21 |
DeviceEventEmitter
22 |

Device class interacts with a remote device.

23 |
24 |
GattCharacteristicEventEmitter
25 |

GattCharacteristic class interacts with a GATT characteristic.

26 |
27 |
GattServer
28 |

GattServer class that provides interaction with device GATT profile.

29 |
30 |
GattService
31 |

GattService class interacts with a remote GATT service.

32 |
33 |
34 | 35 | ## Functions 36 | 37 |
38 |
createBluetooth()NodeBleInit
39 |

Init bluetooth session and return

40 |
41 |
42 | 43 | ## Typedefs 44 | 45 |
46 |
WritingMode
47 |
48 |
NodeBleSession : Object
49 |
50 |
51 | 52 | # Specs 53 | 54 | 55 | ## Adapter 56 | Adapter class interacts with the local bluetooth adapter 57 | 58 | **Kind**: global class 59 | **See**: You can construct an Adapter session via [getAdapter](#Bluetooth+getAdapter) method 60 | 61 | * [Adapter](#Adapter) 62 | * [.getAddress()](#Adapter+getAddress) ⇒ string 63 | * [.getAddressType()](#Adapter+getAddressType) ⇒ string 64 | * [.getName()](#Adapter+getName) ⇒ string 65 | * [.getAlias()](#Adapter+getAlias) ⇒ string 66 | * [.isPowered()](#Adapter+isPowered) ⇒ boolean 67 | * [.isDiscovering()](#Adapter+isDiscovering) ⇒ boolean 68 | * [.startDiscovery()](#Adapter+startDiscovery) 69 | * [.stopDiscovery()](#Adapter+stopDiscovery) 70 | * [.devices()](#Adapter+devices) ⇒ Array.<string> 71 | * [.getDevice(uuid)](#Adapter+getDevice) ⇒ [Device](#Device) 72 | * [.waitDevice(uuid, [timeout], [discoveryInterval])](#Adapter+waitDevice) ⇒ [Device](#Device) 73 | * [.toString()](#Adapter+toString) ⇒ string 74 | 75 | 76 | 77 | ### adapter.getAddress() ⇒ string 78 | The Bluetooth device address. 79 | 80 | **Kind**: instance method of [Adapter](#Adapter) 81 | 82 | 83 | ### adapter.getAddressType() ⇒ string 84 | The Bluetooth device Address Type. (public, random) 85 | 86 | **Kind**: instance method of [Adapter](#Adapter) 87 | 88 | 89 | ### adapter.getName() ⇒ string 90 | The Bluetooth system name 91 | 92 | **Kind**: instance method of [Adapter](#Adapter) 93 | 94 | 95 | ### adapter.getAlias() ⇒ string 96 | The Bluetooth friendly name. 97 | 98 | **Kind**: instance method of [Adapter](#Adapter) 99 | 100 | 101 | ### adapter.isPowered() ⇒ boolean 102 | Current adapter state. 103 | 104 | **Kind**: instance method of [Adapter](#Adapter) 105 | 106 | 107 | ### adapter.isDiscovering() ⇒ boolean 108 | Indicates that a device discovery procedure is active. 109 | 110 | **Kind**: instance method of [Adapter](#Adapter) 111 | 112 | 113 | ### adapter.startDiscovery() 114 | This method starts the device discovery session. 115 | 116 | **Kind**: instance method of [Adapter](#Adapter) 117 | 118 | 119 | ### adapter.stopDiscovery() 120 | This method will cancel any previous StartDiscovery transaction. 121 | 122 | **Kind**: instance method of [Adapter](#Adapter) 123 | 124 | 125 | ### adapter.devices() ⇒ Array.<string> 126 | List of found device names (uuid). 127 | 128 | **Kind**: instance method of [Adapter](#Adapter) 129 | 130 | 131 | ### adapter.getDevice(uuid) ⇒ [Device](#Device) 132 | Init a device instance and returns it. 133 | 134 | **Kind**: instance method of [Adapter](#Adapter) 135 | 136 | | Param | Type | Description | 137 | | --- | --- | --- | 138 | | uuid | string | Device Name. | 139 | 140 | 141 | 142 | ### adapter.waitDevice(uuid, [timeout], [discoveryInterval]) ⇒ [Device](#Device) 143 | Wait that a specific device is found, then init a device instance and returns it. 144 | 145 | **Kind**: instance method of [Adapter](#Adapter) 146 | 147 | | Param | Type | Default | Description | 148 | | --- | --- | --- | --- | 149 | | uuid | string | | Device Name. | 150 | | [timeout] | number | 120000 | Time (ms) to wait before throwing a timeout expection. | 151 | | [discoveryInterval] | number | 1000 | Interval (ms) frequency that verifies device availability. | 152 | 153 | 154 | 155 | ### adapter.toString() ⇒ string 156 | Human readable class identifier. 157 | 158 | **Kind**: instance method of [Adapter](#Adapter) 159 | 160 | 161 | ## Bluetooth 162 | Top level object that represent a bluetooth session 163 | 164 | **Kind**: global class 165 | **See**: You can construct a Bluetooth session via [createBluetooth](#createBluetooth) function 166 | 167 | * [Bluetooth](#Bluetooth) 168 | * [.adapters()](#Bluetooth+adapters) ⇒ Array.<string> 169 | * [.defaultAdapter()](#Bluetooth+defaultAdapter) ⇒ [Adapter](#Adapter) 170 | * [.getAdapter(adapter)](#Bluetooth+getAdapter) ⇒ [Adapter](#Adapter) 171 | * [.activeAdapters()](#Bluetooth+activeAdapters) ⇒ Promise.<Array.<Adapter>> 172 | 173 | 174 | 175 | ### bluetooth.adapters() ⇒ Array.<string> 176 | List of available adapter names 177 | 178 | **Kind**: instance method of [Bluetooth](#Bluetooth) 179 | 180 | 181 | ### bluetooth.defaultAdapter() ⇒ [Adapter](#Adapter) 182 | Get first available adapter 183 | 184 | **Kind**: instance method of [Bluetooth](#Bluetooth) 185 | **Throws**: 186 | 187 | - Will throw an error if there aren't available adapters. 188 | 189 | 190 | 191 | ### bluetooth.getAdapter(adapter) ⇒ [Adapter](#Adapter) 192 | Init an adapter instance and returns it 193 | 194 | **Kind**: instance method of [Bluetooth](#Bluetooth) 195 | **Throw**: Will throw adapter not found if provided name isn't valid. 196 | 197 | | Param | Type | Description | 198 | | --- | --- | --- | 199 | | adapter | string | Name of an adapter | 200 | 201 | 202 | 203 | ### bluetooth.activeAdapters() ⇒ Promise.<Array.<Adapter>> 204 | List all available (powered) adapters 205 | 206 | **Kind**: instance method of [Bluetooth](#Bluetooth) 207 | 208 | 209 | ## Device ⇐ EventEmitter 210 | Device class interacts with a remote device. 211 | 212 | **Kind**: global class 213 | **Extends**: EventEmitter 214 | **See**: You can construct a Device object via [getDevice](#Adapter+getDevice) method 215 | 216 | * [Device](#Device) ⇐ EventEmitter 217 | * [.getName()](#Device+getName) ⇒ string 218 | * [.getAddress()](#Device+getAddress) ⇒ string 219 | * [.getAddressType()](#Device+getAddressType) ⇒ string 220 | * [.getAlias()](#Device+getAlias) ⇒ string 221 | * [.getRSSI()](#Device+getRSSI) ⇒ number 222 | * [.getTXPower()](#Device+getTXPower) ⇒ number 223 | * [.getManufacturerData()](#Device+getManufacturerData) ⇒ Object.<string, any> 224 | * [.getAdvertisingData()](#Device+getAdvertisingData) ⇒ Object.<string, any> 225 | * [.getServiceData()](#Device+getServiceData) ⇒ Object.<string, any> 226 | * [.isPaired()](#Device+isPaired) ⇒ boolean 227 | * [.isConnected()](#Device+isConnected) ⇒ boolean 228 | * [.pair()](#Device+pair) 229 | * [.cancelPair()](#Device+cancelPair) 230 | * [.connect()](#Device+connect) 231 | * [.disconnect()](#Device+disconnect) 232 | * [.gatt()](#Device+gatt) ⇒ [GattServer](#GattServer) 233 | * [.toString()](#Device+toString) ⇒ string 234 | * ["connect"](#Device+event_connect) 235 | * ["disconnect"](#Device+event_disconnect) 236 | 237 | 238 | 239 | ### device.getName() ⇒ string 240 | The Bluetooth remote name. 241 | 242 | **Kind**: instance method of [Device](#Device) 243 | 244 | 245 | ### device.getAddress() ⇒ string 246 | The Bluetooth device address of the remote device. 247 | 248 | **Kind**: instance method of [Device](#Device) 249 | 250 | 251 | ### device.getAddressType() ⇒ string 252 | The Bluetooth device Address Type (public, random). 253 | 254 | **Kind**: instance method of [Device](#Device) 255 | 256 | 257 | ### device.getAlias() ⇒ string 258 | The name alias for the remote device. 259 | 260 | **Kind**: instance method of [Device](#Device) 261 | 262 | 263 | ### device.getRSSI() ⇒ number 264 | Received Signal Strength Indicator of the remote device 265 | 266 | **Kind**: instance method of [Device](#Device) 267 | 268 | 269 | ### device.getTXPower() ⇒ number 270 | Advertised transmitted power level. 271 | 272 | **Kind**: instance method of [Device](#Device) 273 | 274 | 275 | ### device.getManufacturerData() ⇒ Object.<string, any> 276 | Advertised transmitted manufacturer data. 277 | 278 | **Kind**: instance method of [Device](#Device) 279 | 280 | 281 | ### device.getAdvertisingData() ⇒ Object.<string, any> 282 | Advertised transmitted data. (experimental: this feature might not be fully supported by bluez) 283 | 284 | **Kind**: instance method of [Device](#Device) 285 | 286 | 287 | ### device.getServiceData() ⇒ Object.<string, any> 288 | Advertised transmitted data. 289 | 290 | **Kind**: instance method of [Device](#Device) 291 | 292 | 293 | ### device.isPaired() ⇒ boolean 294 | Indicates if the remote device is paired. 295 | 296 | **Kind**: instance method of [Device](#Device) 297 | 298 | 299 | ### device.isConnected() ⇒ boolean 300 | Indicates if the remote device is currently connected. 301 | 302 | **Kind**: instance method of [Device](#Device) 303 | 304 | 305 | ### device.pair() 306 | This method will connect to the remote device 307 | 308 | **Kind**: instance method of [Device](#Device) 309 | 310 | 311 | ### device.cancelPair() 312 | This method can be used to cancel a pairing operation initiated by the Pair method. 313 | 314 | **Kind**: instance method of [Device](#Device) 315 | 316 | 317 | ### device.connect() 318 | Connect to remote device 319 | 320 | **Kind**: instance method of [Device](#Device) 321 | 322 | 323 | ### device.disconnect() 324 | Disconnect remote device 325 | 326 | **Kind**: instance method of [Device](#Device) 327 | 328 | 329 | ### device.gatt() ⇒ [GattServer](#GattServer) 330 | Init a GattServer instance and return it 331 | 332 | **Kind**: instance method of [Device](#Device) 333 | 334 | 335 | ### device.toString() ⇒ string 336 | Human readable class identifier. 337 | 338 | **Kind**: instance method of [Device](#Device) 339 | 340 | 341 | ### "connect" 342 | Connection event 343 | 344 | **Kind**: event emitted by [Device](#Device) 345 | **Properties** 346 | 347 | | Name | Type | Description | 348 | | --- | --- | --- | 349 | | connected | boolean | Indicates current connection status. | 350 | 351 | 352 | 353 | ### "disconnect" 354 | Disconection event 355 | 356 | **Kind**: event emitted by [Device](#Device) 357 | **Properties** 358 | 359 | | Name | Type | Description | 360 | | --- | --- | --- | 361 | | connected | boolean | Indicates current connection status. | 362 | 363 | 364 | 365 | ## GattCharacteristic ⇐ EventEmitter 366 | GattCharacteristic class interacts with a GATT characteristic. 367 | 368 | **Kind**: global class 369 | **Extends**: EventEmitter 370 | **See**: You can construct a GattCharacteristic object via [getCharacteristic](#GattService+getCharacteristic) method. 371 | 372 | * [GattCharacteristic](#GattCharacteristic) ⇐ EventEmitter 373 | * [.getUUID()](#GattCharacteristic+getUUID) ⇒ string 374 | * [.getFlags()](#GattCharacteristic+getFlags) ⇒ Array.<string> 375 | * [.isNotifying()](#GattCharacteristic+isNotifying) ⇒ boolean 376 | * [.readValue([offset])](#GattCharacteristic+readValue) ⇒ Buffer 377 | * [.writeValue(value, [optionsOrOffset])](#GattCharacteristic+writeValue) 378 | * [.writeValueWithoutResponse(value, [offset])](#GattCharacteristic+writeValueWithoutResponse) ⇒ Promise 379 | * [.writeValueWithResponse(value, [offset])](#GattCharacteristic+writeValueWithResponse) ⇒ Promise 380 | * [.startNotifications()](#GattCharacteristic+startNotifications) 381 | * ["valuechanged"](#GattCharacteristic+event_valuechanged) 382 | 383 | 384 | 385 | ### gattCharacteristic.getUUID() ⇒ string 386 | 128-bit characteristic UUID. 387 | 388 | **Kind**: instance method of [GattCharacteristic](#GattCharacteristic) 389 | 390 | 391 | ### gattCharacteristic.getFlags() ⇒ Array.<string> 392 | Defines how the characteristic value can be used. 393 | 394 | **Kind**: instance method of [GattCharacteristic](#GattCharacteristic) 395 | 396 | 397 | ### gattCharacteristic.isNotifying() ⇒ boolean 398 | True, if notifications or indications on this characteristic are currently enabled. 399 | 400 | **Kind**: instance method of [GattCharacteristic](#GattCharacteristic) 401 | 402 | 403 | ### gattCharacteristic.readValue([offset]) ⇒ Buffer 404 | Read the value of the characteristic 405 | 406 | **Kind**: instance method of [GattCharacteristic](#GattCharacteristic) 407 | 408 | | Param | Type | Default | 409 | | --- | --- | --- | 410 | | [offset] | number | 0 | 411 | 412 | 413 | 414 | ### gattCharacteristic.writeValue(value, [optionsOrOffset]) 415 | Write the value of the characteristic. 416 | 417 | **Kind**: instance method of [GattCharacteristic](#GattCharacteristic) 418 | 419 | | Param | Type | Default | Description | 420 | | --- | --- | --- | --- | 421 | | value | Buffer | | Buffer containing the characteristic value. | 422 | | [optionsOrOffset] | number \| Object | 0 | Starting offset or writing options. | 423 | | [optionsOrOffset.offset] | number | 0 | Starting offset. | 424 | | [optionsOrOffset.type] | [WritingMode](#WritingMode) | reliable | Writing mode | 425 | 426 | 427 | 428 | ### gattCharacteristic.writeValueWithoutResponse(value, [offset]) ⇒ Promise 429 | Write the value of the characteristic without waiting for the response. 430 | 431 | **Kind**: instance method of [GattCharacteristic](#GattCharacteristic) 432 | 433 | | Param | Type | Default | Description | 434 | | --- | --- | --- | --- | 435 | | value | Buffer | | Buffer containing the characteristic value. | 436 | | [offset] | number | 0 | Starting offset. | 437 | 438 | 439 | 440 | ### gattCharacteristic.writeValueWithResponse(value, [offset]) ⇒ Promise 441 | Write the value of the characteristic and wait for the response. 442 | 443 | **Kind**: instance method of [GattCharacteristic](#GattCharacteristic) 444 | 445 | | Param | Type | Default | Description | 446 | | --- | --- | --- | --- | 447 | | value | Buffer | | Buffer containing the characteristic value. | 448 | | [offset] | number | 0 | Starting offset. | 449 | 450 | 451 | 452 | ### gattCharacteristic.startNotifications() 453 | Starts a notification session from this characteristic. 454 | It emits valuechanged event when receives a notification. 455 | 456 | **Kind**: instance method of [GattCharacteristic](#GattCharacteristic) 457 | 458 | 459 | ### "valuechanged" 460 | Notification event 461 | 462 | **Kind**: event emitted by [GattCharacteristic](#GattCharacteristic) 463 | 464 | 465 | ## GattServer 466 | GattServer class that provides interaction with device GATT profile. 467 | 468 | **Kind**: global class 469 | **See**: You can construct a Device object via [gatt](#Device+gatt) method 470 | 471 | * [GattServer](#GattServer) 472 | * [.services()](#GattServer+services) ⇒ Array.<string> 473 | * [.getPrimaryService(uuid)](#GattServer+getPrimaryService) ⇒ [GattService](#GattService) 474 | 475 | 476 | 477 | ### gattServer.services() ⇒ Array.<string> 478 | List of available services 479 | 480 | **Kind**: instance method of [GattServer](#GattServer) 481 | 482 | 483 | ### gattServer.getPrimaryService(uuid) ⇒ [GattService](#GattService) 484 | Init a GattService instance and return it 485 | 486 | **Kind**: instance method of [GattServer](#GattServer) 487 | 488 | | Param | Type | 489 | | --- | --- | 490 | | uuid | string | 491 | 492 | 493 | 494 | ## GattService 495 | GattService class interacts with a remote GATT service. 496 | 497 | **Kind**: global class 498 | **See**: You can construct a GattService object via [getPrimaryService](#GattServer+getPrimaryService) method. 499 | 500 | * [GattService](#GattService) 501 | * [.isPrimary()](#GattService+isPrimary) ⇒ boolean 502 | * [.getUUID()](#GattService+getUUID) ⇒ string 503 | * [.characteristics()](#GattService+characteristics) ⇒ Array.<string> 504 | * [.getCharacteristic(uuid)](#GattService+getCharacteristic) ⇒ [GattCharacteristic](#GattCharacteristic) 505 | * [.toString()](#GattService+toString) ⇒ string 506 | 507 | 508 | 509 | ### gattService.isPrimary() ⇒ boolean 510 | Indicates whether or not this GATT service is a primary service. 511 | 512 | **Kind**: instance method of [GattService](#GattService) 513 | 514 | 515 | ### gattService.getUUID() ⇒ string 516 | 128-bit service UUID. 517 | 518 | **Kind**: instance method of [GattService](#GattService) 519 | 520 | 521 | ### gattService.characteristics() ⇒ Array.<string> 522 | List of available characteristic names. 523 | 524 | **Kind**: instance method of [GattService](#GattService) 525 | 526 | 527 | ### gattService.getCharacteristic(uuid) ⇒ [GattCharacteristic](#GattCharacteristic) 528 | Init a GattCharacteristic instance and return it 529 | 530 | **Kind**: instance method of [GattService](#GattService) 531 | 532 | | Param | Type | Description | 533 | | --- | --- | --- | 534 | | uuid | string | Characteristic UUID. | 535 | 536 | 537 | 538 | ### gattService.toString() ⇒ string 539 | Human readable class identifier. 540 | 541 | **Kind**: instance method of [GattService](#GattService) 542 | 543 | 544 | ## createBluetooth() ⇒ NodeBleInit 545 | Init bluetooth session and return 546 | 547 | **Kind**: global function 548 | **Example** 549 | ```js 550 | const { createBluetooth } = require('node-ble') 551 | 552 | async function main () { 553 | const { bluetooth, destroy } = createBluetooth() 554 | const adapter = await bluetooth.defaultAdapter() 555 | // do here your staff 556 | destroy() 557 | } 558 | ``` 559 | 560 | 561 | ## WritingMode 562 | **Kind**: global typedef 563 | **Properties** 564 | 565 | | Name | Type | Description | 566 | | --- | --- | --- | 567 | | command | string | Write without response | 568 | | request | string | Write with response | 569 | | reliable | string | Reliable Write | 570 | 571 | 572 | 573 | ## NodeBleSession : Object 574 | **Kind**: global typedef 575 | **Properties** 576 | 577 | | Name | Type | Description | 578 | | --- | --- | --- | 579 | | bluetooth | [Bluetooth](#Bluetooth) | Bluetooth session | 580 | | destroy | func | Close bluetooth session | 581 | 582 | -------------------------------------------------------------------------------- /docs/documentation-testing.md: -------------------------------------------------------------------------------- 1 | # Running tests 2 | 3 | This library provides two test suite types: 4 | - Unit tests: They are available in the `/test` folder and they test every single component (class/function). 5 | - End to end tests: They are available in the `/test-e2e` folder and they test the interaction with a real bluetooth device that you spawn on your own. 6 | 7 | 8 | ## Pre-requisite 9 | 10 | In order to run the available test suites you have to set up right D-Bus permissions. 11 | Execute the following script on a bash terminal 12 | 13 | ```sh 14 | echo ' 16 | 17 | 18 | 19 | 20 | 21 | 22 | ' | sed "s/__USERID__/$(id -un)/" | sudo tee /etc/dbus-1/system.d/node-ble-test.conf > /dev/null 23 | ``` 24 | 25 | ## Run unit tests 26 | ``` 27 | npm test 28 | ``` 29 | 30 | ## Run end to end (e2e) tests 31 | 32 | The end to end test will try to connect to a real bluetooth device and read some characteristics. To do that, you need two different devices a central and a peripheral. 33 | The test suite will act as a central, but you to need to create a fake peripheral (test device). You can follow [fake device setup guide](https://github.com/chrvadala/node-ble/blob/main/ble-test-device). 34 | 35 | After you have prepared the device, you have to read via USB its mac address, then launch the test suite. 36 | 37 | ```shell script 38 | TEST_DEVICE=00:00:00:00:00:00 npm run test:e2e 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/templates/api.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | # Node BLE APIs 12 | {{>main-index~}} 13 | 14 | # Specs 15 | {{>all-docs~}} -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | const { createBluetooth } = require('.') 4 | const { TEST_DEVICE, TEST_SERVICE, TEST_CHARACTERISTIC, TEST_NOTIFY_SERVICE, TEST_NOTIFY_CHARACTERISTIC } = process.env 5 | 6 | async function main () { 7 | const { bluetooth, destroy } = createBluetooth() 8 | 9 | // get bluetooth adapter 10 | const adapter = await bluetooth.defaultAdapter() 11 | await adapter.startDiscovery() 12 | console.log('discovering') 13 | 14 | // get device and connect 15 | const device = await adapter.waitDevice(TEST_DEVICE) 16 | console.log('got device', await device.getAddress(), await device.getName()) 17 | await device.connect() 18 | console.log('connected') 19 | 20 | const gattServer = await device.gatt() 21 | 22 | // read write characteristic 23 | const service1 = await gattServer.getPrimaryService(TEST_SERVICE) 24 | const characteristic1 = await service1.getCharacteristic(TEST_CHARACTERISTIC) 25 | await characteristic1.writeValue(Buffer.from('Hello world')) 26 | const buffer = await characteristic1.readValue() 27 | console.log('read', buffer, buffer.toString()) 28 | 29 | // subscribe characteristic 30 | const service2 = await gattServer.getPrimaryService(TEST_NOTIFY_SERVICE) 31 | const characteristic2 = await service2.getCharacteristic(TEST_NOTIFY_CHARACTERISTIC) 32 | await characteristic2.startNotifications() 33 | await new Promise(done => { 34 | characteristic2.once('valuechanged', buffer => { 35 | console.log('subscription', buffer) 36 | done() 37 | }) 38 | }) 39 | 40 | await characteristic2.stopNotifications() 41 | destroy() 42 | } 43 | 44 | main() 45 | .then(console.log) 46 | .catch(console.error) 47 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coveragePathIgnorePatterns: ['test-e2e/e2e-test-utils.js'] 3 | } 4 | -------------------------------------------------------------------------------- /node-ble.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-ble", 3 | "description": "Bluetooth Low Energy (BLE) library written with pure Node.js (no bindings) - baked by Bluez via DBus", 4 | "version": "1.13.0", 5 | "repository": "https://github.com/chrvadala/node-ble.git", 6 | "author": "chrvadala", 7 | "license": "MIT", 8 | "main": "./src/index.js", 9 | "typings": "./src/index.d.ts", 10 | "files": [ 11 | "*.md", 12 | "src", 13 | "test", 14 | "test-e2e", 15 | "example.js" 16 | ], 17 | "keywords": [ 18 | "bluetooth-low-energy", 19 | "ble", 20 | "bluetooth-peripherals", 21 | "bluez", 22 | "bluez-dbus", 23 | "bluetooth", 24 | "bluetooth-le" 25 | ], 26 | "homepage": "https://github.com/chrvadala/node-ble", 27 | "scripts": { 28 | "build": "npm run docs:api", 29 | "test": "npm run test:standard && npm run test:coverage && npm run test:typescript", 30 | "test:jest": "jest --testPathIgnorePatterns=test-e2e/", 31 | "test:coverage": "jest --testPathIgnorePatterns=test-e2e/ --coverage", 32 | "test:e2e": "jest --runInBand", 33 | "test:standard": "standard", 34 | "test:typescript": "tsc --strict src/index.d.ts", 35 | "docs:api": "jsdoc2md --template docs/templates/api.hbs --example-lang js --heading-depth 2 src/*.js > docs/api.md" 36 | }, 37 | "dependencies": { 38 | "dbus-next": "^0.10.2" 39 | }, 40 | "devDependencies": { 41 | "@babel/plugin-proposal-class-properties": "^7.18.6", 42 | "@babel/plugin-proposal-decorators": "^7.25.9", 43 | "@types/jest": "^29.5.14", 44 | "@types/node": "^22.10.7", 45 | "jest": "^29.7.0", 46 | "jsdoc-to-markdown": "^9.1.1", 47 | "standard": "^17.1.2", 48 | "typescript": "^5.7.3" 49 | }, 50 | "standard": { 51 | "ignore": [ 52 | "example.js", 53 | "test/__interfaces/*.js" 54 | ] 55 | }, 56 | "funding": "https://github.com/sponsors/chrvadala" 57 | } 58 | -------------------------------------------------------------------------------- /src/Adapter.js: -------------------------------------------------------------------------------- 1 | const Device = require('./Device') 2 | const BusHelper = require('./BusHelper') 3 | const buildTypedValue = require('./buildTypedValue') 4 | 5 | const DEFAULT_TIMEOUT = 2 * 60 * 1000 6 | const DEFAULT_DISCOVERY_INTERVAL = 1000 7 | 8 | /** 9 | * @classdesc Adapter class interacts with the local bluetooth adapter 10 | * @class Adapter 11 | * @see You can construct an Adapter session via {@link Bluetooth#getAdapter} method 12 | */ 13 | class Adapter { 14 | constructor (dbus, adapter) { 15 | this.dbus = dbus 16 | this.adapter = adapter 17 | this.helper = new BusHelper(dbus, 'org.bluez', `/org/bluez/${adapter}`, 'org.bluez.Adapter1') 18 | } 19 | 20 | /** 21 | * The Bluetooth device address. 22 | * @async 23 | * @returns {string} 24 | */ 25 | async getAddress () { 26 | return this.helper.prop('Address') 27 | } 28 | 29 | /** 30 | * The Bluetooth device Address Type. (public, random) 31 | * @async 32 | * @returns {string} 33 | */ 34 | async getAddressType () { 35 | return this.helper.prop('AddressType') 36 | } 37 | 38 | /** 39 | * The Bluetooth system name 40 | * @async 41 | * @returns {string} 42 | */ 43 | async getName () { 44 | return this.helper.prop('Name') 45 | } 46 | 47 | /** 48 | * The Bluetooth friendly name. 49 | * @async 50 | * @returns {string} 51 | */ 52 | async getAlias () { 53 | return this.helper.prop('Alias') 54 | } 55 | 56 | /** 57 | * Current adapter state. 58 | * @async 59 | * @returns {boolean} 60 | */ 61 | async isPowered () { 62 | return this.helper.prop('Powered') 63 | } 64 | 65 | /** 66 | * Indicates that a device discovery procedure is active. 67 | * @async 68 | * @returns {boolean} 69 | */ 70 | async isDiscovering () { 71 | return this.helper.prop('Discovering') 72 | } 73 | 74 | /** 75 | * This method starts the device discovery session. 76 | * @async 77 | */ 78 | async startDiscovery () { 79 | if (await this.isDiscovering()) { 80 | throw new Error('Discovery already in progress') 81 | } 82 | 83 | await this.helper.callMethod('SetDiscoveryFilter', { 84 | Transport: buildTypedValue('string', 'le') 85 | }) 86 | await this.helper.callMethod('StartDiscovery') 87 | } 88 | 89 | /** 90 | * This method will cancel any previous StartDiscovery transaction. 91 | * @async 92 | */ 93 | async stopDiscovery () { 94 | if (!await this.isDiscovering()) { 95 | throw new Error('No discovery started') 96 | } 97 | await this.helper.callMethod('StopDiscovery') 98 | } 99 | 100 | /** 101 | * List of found device names (uuid). 102 | * @async 103 | * @returns {string[]} 104 | */ 105 | async devices () { 106 | const devices = await this.helper.children() 107 | return devices.map(Adapter.deserializeUUID) 108 | } 109 | 110 | /** 111 | * Init a device instance and returns it. 112 | * @param {string} uuid - Device Name. 113 | * @async 114 | * @returns {Device} 115 | */ 116 | async getDevice (uuid) { 117 | const serializedUUID = Adapter.serializeUUID(uuid) 118 | 119 | const devices = await this.helper.children() 120 | if (!devices.includes(serializedUUID)) { 121 | throw new Error('Device not found') 122 | } 123 | 124 | return new Device(this.dbus, this.adapter, serializedUUID) 125 | } 126 | 127 | /** 128 | * Wait that a specific device is found, then init a device instance and returns it. 129 | * @param {string} uuid - Device Name. 130 | * @param {number} [timeout = 120000] - Time (ms) to wait before throwing a timeout expection. 131 | * @param {number} [discoveryInterval = 1000] - Interval (ms) frequency that verifies device availability. 132 | * @async 133 | * @returns {Device} 134 | */ 135 | async waitDevice (uuid, timeout = DEFAULT_TIMEOUT, discoveryInterval = DEFAULT_DISCOVERY_INTERVAL) { 136 | // this should be optimized subscribing InterfacesAdded signal 137 | 138 | const cancellable = [] 139 | const discoveryHandler = new Promise((resolve, reject) => { 140 | const check = () => { 141 | this.getDevice(uuid) 142 | .then(device => { 143 | resolve(device) 144 | }) 145 | .catch(e => { 146 | if (e.message !== 'Device not found') { 147 | return e 148 | } 149 | }) 150 | } 151 | 152 | const handler = setInterval(check, discoveryInterval) 153 | cancellable.push(() => clearInterval(handler)) 154 | }) 155 | 156 | const timeoutHandler = new Promise((resolve, reject) => { 157 | const handler = setTimeout(() => { 158 | reject(new Error('operation timed out')) 159 | }, timeout) 160 | 161 | cancellable.push(() => clearTimeout(handler)) 162 | }) 163 | 164 | try { 165 | const device = await Promise.race([discoveryHandler, timeoutHandler]) 166 | return device 167 | } finally { 168 | for (const cancel of cancellable) { 169 | cancel() 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * Human readable class identifier. 176 | * @async 177 | * @returns {string} 178 | */ 179 | async toString () { 180 | const name = await this.getName() 181 | const address = await this.getAddress() 182 | 183 | return `${name} [${address}]` 184 | } 185 | 186 | static serializeUUID (uuid) { 187 | return `dev_${uuid.replace(/:/g, '_').toUpperCase()}` 188 | } 189 | 190 | static deserializeUUID (uuid) { 191 | return uuid.substring(4).replace(/_/g, ':') 192 | } 193 | } 194 | 195 | module.exports = Adapter 196 | -------------------------------------------------------------------------------- /src/Bluetooth.js: -------------------------------------------------------------------------------- 1 | const BusHelper = require('./BusHelper') 2 | const Adapter = require('./Adapter') 3 | 4 | /** 5 | * @classdesc Top level object that represent a bluetooth session 6 | * @class Bluetooth 7 | * @see You can construct a Bluetooth session via {@link createBluetooth} function 8 | */ 9 | class Bluetooth { 10 | constructor (dbus) { 11 | this.dbus = dbus 12 | this.helper = new BusHelper(dbus, 'org.bluez', '/org/bluez', 'org.bluez.AgentManager1', { useProps: false }) 13 | } 14 | 15 | /** 16 | * List of available adapter names 17 | * @async 18 | * @returns {string[]} 19 | */ 20 | async adapters () { 21 | return this.helper.children() 22 | } 23 | 24 | /** 25 | * Get first available adapter 26 | * @async 27 | * @throws Will throw an error if there aren't available adapters. 28 | * @returns {Adapter} 29 | */ 30 | async defaultAdapter () { 31 | const adapters = await this.adapters() 32 | if (!adapters.length) { 33 | throw new Error('No available adapters found') 34 | } 35 | 36 | return this.getAdapter(adapters[0]) 37 | } 38 | 39 | /** 40 | * Init an adapter instance and returns it 41 | * @async 42 | * @param {string} adapter - Name of an adapter 43 | * @throw Will throw adapter not found if provided name isn't valid. 44 | * @returns {Adapter} 45 | */ 46 | async getAdapter (adapter) { 47 | const adapters = await this.adapters() 48 | if (!adapters.includes(adapter)) { 49 | throw new Error('Adapter not found') 50 | } 51 | 52 | return new Adapter(this.dbus, adapter) 53 | } 54 | 55 | /** 56 | * List all available (powered) adapters 57 | * @async 58 | * @returns {Promise} 59 | */ 60 | async activeAdapters () { 61 | const adapterNames = await this.adapters() 62 | const allAdapters = await Promise.allSettled(adapterNames.map(async name => { 63 | const adapter = await this.getAdapter(name) 64 | const isPowered = await adapter.isPowered() 65 | return { adapter, isPowered } 66 | })) 67 | 68 | return allAdapters 69 | .filter(item => item.status === 'fulfilled' && item.value.isPowered) 70 | .map(item => item.value.adapter) 71 | } 72 | } 73 | 74 | module.exports = Bluetooth 75 | -------------------------------------------------------------------------------- /src/BusHelper.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events') 2 | 3 | const DEFAULT_OPTIONS = { 4 | useProps: true, 5 | usePropsEvents: false 6 | } 7 | 8 | class BusHelper extends EventEmitter { 9 | constructor (dbus, service, object, iface, options = {}) { 10 | super() 11 | 12 | this.service = service 13 | this.object = object 14 | this.iface = iface 15 | 16 | this.dbus = dbus 17 | 18 | this.options = { 19 | ...DEFAULT_OPTIONS, 20 | ...options 21 | } 22 | 23 | this._ready = false 24 | this._objectProxy = null 25 | this._ifaceProxy = null 26 | this._propsProxy = null 27 | } 28 | 29 | async _prepare () { 30 | if (this._ready) return 31 | const objectProxy = this._objectProxy = await this.dbus.getProxyObject(this.service, this.object) 32 | this._ifaceProxy = await objectProxy.getInterface(this.iface) 33 | 34 | if (this.options.useProps) { 35 | this._propsProxy = await objectProxy.getInterface('org.freedesktop.DBus.Properties') 36 | } 37 | 38 | if (this.options.useProps && this.options.usePropsEvents) { 39 | this._propsProxy.on('PropertiesChanged', (iface, changedProps, invalidated) => { 40 | if (iface === this.iface) { 41 | this.emit('PropertiesChanged', changedProps) 42 | } 43 | }) 44 | } 45 | 46 | this._ready = true 47 | } 48 | 49 | async props () { 50 | if (!this.options.useProps) throw new Error('props not available') 51 | await this._prepare() 52 | const rawProps = await this._propsProxy.GetAll(this.iface) 53 | const props = {} 54 | for (const propKey in rawProps) { 55 | props[propKey] = rawProps[propKey].value 56 | } 57 | return props 58 | } 59 | 60 | async prop (propName) { 61 | if (!this.options.useProps) throw new Error('props not available') 62 | await this._prepare() 63 | const rawProp = await this._propsProxy.Get(this.iface, propName) 64 | return rawProp.value 65 | } 66 | 67 | async set (propName, value) { 68 | if (!this.options.useProps) throw new Error('props not available') 69 | await this._prepare() 70 | await this._propsProxy.Set(this.iface, propName, value) 71 | } 72 | 73 | async waitPropChange (propName) { 74 | await this._prepare() 75 | return new Promise((resolve) => { 76 | const cb = (iface, changedProps, invalidated) => { 77 | // console.log('changed props on %s -> %o', iface, changedProps) 78 | 79 | if (!(iface === this.iface && (propName in changedProps))) return 80 | 81 | resolve(changedProps[propName].value) 82 | this._propsProxy.off('PropertiesChanged', cb) 83 | } 84 | 85 | this._propsProxy.on('PropertiesChanged', cb) 86 | }) 87 | } 88 | 89 | async children () { 90 | this._ready = false // WORKAROUND: it forces to construct a new ProxyObject 91 | await this._prepare() 92 | return BusHelper.buildChildren(this.object, this._objectProxy.nodes) 93 | } 94 | 95 | async callMethod (methodName, ...args) { 96 | await this._prepare() 97 | return this._ifaceProxy[methodName](...args) 98 | } 99 | 100 | removeListeners () { 101 | this.removeAllListeners('PropertiesChanged') 102 | if (this._propsProxy !== null) { 103 | this._propsProxy.removeAllListeners('PropertiesChanged') 104 | this._ready = false 105 | } 106 | } 107 | 108 | static buildChildren (path, nodes) { 109 | if (path === '/') path = '' 110 | const children = new Set() 111 | for (const node of nodes) { 112 | if (!node.startsWith(path)) continue 113 | 114 | const end = node.indexOf('/', path.length + 1) 115 | const sub = (end >= 0) ? node.substring(path.length + 1, end) : node.substring(path.length + 1) 116 | if (sub.length < 1) continue 117 | 118 | children.add(sub) 119 | } 120 | return Array.from(children.values()) 121 | } 122 | } 123 | 124 | module.exports = BusHelper 125 | -------------------------------------------------------------------------------- /src/Device.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events') 2 | const BusHelper = require('./BusHelper') 3 | const GattServer = require('./GattServer') 4 | const parseDict = require('./parseDict') 5 | 6 | /** 7 | * @classdesc Device class interacts with a remote device. 8 | * @class Device 9 | * @extends EventEmitter 10 | * @see You can construct a Device object via {@link Adapter#getDevice} method 11 | */ 12 | class Device extends EventEmitter { 13 | constructor (dbus, adapter, device) { 14 | super() 15 | this.dbus = dbus 16 | this.adapter = adapter 17 | this.device = device 18 | this.helper = new BusHelper(dbus, 'org.bluez', `/org/bluez/${adapter}/${device}`, 'org.bluez.Device1', { usePropsEvents: true }) 19 | } 20 | 21 | /** 22 | * The Bluetooth remote name. 23 | * @returns {string} 24 | */ 25 | async getName () { 26 | return this.helper.prop('Name') 27 | } 28 | 29 | /** 30 | * The Bluetooth device address of the remote device. 31 | * @returns {string} 32 | */ 33 | async getAddress () { 34 | return this.helper.prop('Address') 35 | } 36 | 37 | /** 38 | * The Bluetooth device Address Type (public, random). 39 | * @returns {string} 40 | */ 41 | async getAddressType () { 42 | return this.helper.prop('AddressType') 43 | } 44 | 45 | /** 46 | * The name alias for the remote device. 47 | * @returns {string} 48 | */ 49 | async getAlias () { 50 | return this.helper.prop('Alias') 51 | } 52 | 53 | /** 54 | * Received Signal Strength Indicator of the remote device 55 | * @returns {number} 56 | */ 57 | async getRSSI () { 58 | return this.helper.prop('RSSI') 59 | } 60 | 61 | /** 62 | * Advertised transmitted power level. 63 | * @returns {number} 64 | */ 65 | async getTXPower () { 66 | return this.helper.prop('TxPower') 67 | } 68 | 69 | /** 70 | * Advertised transmitted manufacturer data. 71 | * @returns {Object.} 72 | */ 73 | async getManufacturerData () { 74 | return parseDict(await this.helper.prop('ManufacturerData')) 75 | } 76 | 77 | /** 78 | * Advertised transmitted data. (experimental: this feature might not be fully supported by bluez) 79 | * @returns {Object.} 80 | */ 81 | async getAdvertisingData () { 82 | return parseDict(await this.helper.prop('AdvertisingData')) 83 | } 84 | 85 | /** 86 | * Advertised transmitted data. 87 | * @returns {Object.} 88 | */ 89 | async getServiceData () { 90 | return parseDict(await this.helper.prop('ServiceData')) 91 | } 92 | 93 | /** 94 | * Indicates if the remote device is paired. 95 | * @returns {boolean} 96 | */ 97 | async isPaired () { 98 | return this.helper.prop('Paired') 99 | } 100 | 101 | /** 102 | * Indicates if the remote device is currently connected. 103 | * @returns {boolean} 104 | */ 105 | async isConnected () { 106 | return this.helper.prop('Connected') 107 | } 108 | 109 | /** 110 | * This method will connect to the remote device 111 | */ 112 | async pair () { 113 | return this.helper.callMethod('Pair') 114 | } 115 | 116 | /** 117 | * This method can be used to cancel a pairing operation initiated by the Pair method. 118 | */ 119 | async cancelPair () { 120 | return this.helper.callMethod('CancelPair') 121 | } 122 | 123 | /** 124 | * Connect to remote device 125 | */ 126 | async connect () { 127 | const cb = (propertiesChanged) => { 128 | if ('Connected' in propertiesChanged) { 129 | const { value } = propertiesChanged.Connected 130 | if (value) { 131 | this.emit('connect', { connected: true }) 132 | } else { 133 | this.emit('disconnect', { connected: false }) 134 | } 135 | } 136 | } 137 | 138 | this.helper.on('PropertiesChanged', cb) 139 | await this.helper.callMethod('Connect') 140 | } 141 | 142 | /** 143 | * Disconnect remote device 144 | */ 145 | async disconnect () { 146 | await this.helper.callMethod('Disconnect') 147 | this.helper.removeListeners() 148 | } 149 | 150 | /** 151 | * Init a GattServer instance and return it 152 | * @returns {GattServer} 153 | */ 154 | async gatt () { 155 | const gattServer = new GattServer(this.dbus, this.adapter, this.device) 156 | await gattServer.init() 157 | return gattServer 158 | } 159 | 160 | /** 161 | * Human readable class identifier. 162 | * @returns {string} 163 | */ 164 | async toString () { 165 | const name = await this.getName() 166 | const address = await this.getAddress() 167 | 168 | return `${name} [${address}]` 169 | } 170 | } 171 | 172 | /** 173 | * Connection event 174 | * 175 | * @event Device#connect 176 | * @type {object} 177 | * @property {boolean} connected - Indicates current connection status. 178 | */ 179 | 180 | /** 181 | * Disconection event 182 | * 183 | * @event Device#disconnect 184 | * @type {object} 185 | * @property {boolean} connected - Indicates current connection status. 186 | */ 187 | 188 | module.exports = Device 189 | -------------------------------------------------------------------------------- /src/GattCharacteristic.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events') 2 | const BusHelper = require('./BusHelper') 3 | const buildTypedValue = require('./buildTypedValue') 4 | 5 | /** 6 | * @classdesc GattCharacteristic class interacts with a GATT characteristic. 7 | * @class GattCharacteristic 8 | * @extends EventEmitter 9 | * @see You can construct a GattCharacteristic object via {@link GattService#getCharacteristic} method. 10 | */ 11 | class GattCharacteristic extends EventEmitter { 12 | constructor (dbus, adapter, device, service, characteristic) { 13 | super() 14 | this.dbus = dbus 15 | this.adapter = adapter 16 | this.device = device 17 | this.service = service 18 | this.characteristic = characteristic 19 | this.helper = new BusHelper(dbus, 'org.bluez', `/org/bluez/${adapter}/${device}/${service}/${characteristic}`, 'org.bluez.GattCharacteristic1', { usePropsEvents: true }) 20 | } 21 | 22 | /** 23 | * 128-bit characteristic UUID. 24 | * @returns {string} 25 | */ 26 | async getUUID () { 27 | return this.helper.prop('UUID') 28 | } 29 | 30 | /** 31 | * Defines how the characteristic value can be used. 32 | * @returns {string[]} 33 | */ 34 | async getFlags () { 35 | return this.helper.prop('Flags') 36 | } 37 | 38 | /** 39 | * True, if notifications or indications on this characteristic are currently enabled. 40 | * @returns {boolean} 41 | */ 42 | async isNotifying () { 43 | return this.helper.prop('Notifying') 44 | } 45 | 46 | /** 47 | * Read the value of the characteristic 48 | * @param {number} [offset = 0] 49 | * @returns {Buffer} 50 | */ 51 | async readValue (offset = 0) { 52 | const options = { 53 | offset: buildTypedValue('uint16', offset) 54 | } 55 | const payload = await this.helper.callMethod('ReadValue', options) 56 | return Buffer.from(payload) 57 | } 58 | 59 | /** 60 | * Write the value of the characteristic. 61 | * @param {Buffer} value - Buffer containing the characteristic value. 62 | * @param {number|Object} [optionsOrOffset = 0] - Starting offset or writing options. 63 | * @param {number} [optionsOrOffset.offset = 0] - Starting offset. 64 | * @param {WritingMode} [optionsOrOffset.type = reliable] - Writing mode 65 | */ 66 | async writeValue (value, optionsOrOffset = {}) { 67 | if (!Buffer.isBuffer(value)) { 68 | throw new Error('Only buffers can be wrote') 69 | } 70 | 71 | const options = typeof optionsOrOffset === 'number' ? { offset: optionsOrOffset } : optionsOrOffset 72 | const mergedOptions = Object.assign({ offset: 0, type: 'reliable' }, options) 73 | 74 | const callOptions = { 75 | offset: buildTypedValue('uint16', mergedOptions.offset), 76 | type: buildTypedValue('string', mergedOptions.type) 77 | } 78 | 79 | const { data } = value.toJSON() 80 | await this.helper.callMethod('WriteValue', data, callOptions) 81 | } 82 | 83 | /** 84 | * Write the value of the characteristic without waiting for the response. 85 | * @param {Buffer} value - Buffer containing the characteristic value. 86 | * @param {number} [offset = 0] - Starting offset. 87 | * @returns {Promise} 88 | */ 89 | async writeValueWithoutResponse (value, offset = 0) { 90 | return this.writeValue(value, { offset, type: 'command' }) 91 | } 92 | 93 | /** 94 | * Write the value of the characteristic and wait for the response. 95 | * @param {Buffer} value - Buffer containing the characteristic value. 96 | * @param {number} [offset = 0] - Starting offset. 97 | * @returns {Promise} 98 | */ 99 | async writeValueWithResponse (value, offset = 0) { 100 | return this.writeValue(value, { offset, type: 'request' }) 101 | } 102 | 103 | /** 104 | * Starts a notification session from this characteristic. 105 | * It emits valuechanged event when receives a notification. 106 | */ 107 | async startNotifications () { 108 | const cb = (propertiesChanged) => { 109 | if ('Value' in propertiesChanged) { 110 | const { value } = propertiesChanged.Value 111 | this.emit('valuechanged', Buffer.from(value)) 112 | } 113 | } 114 | 115 | this.helper.on('PropertiesChanged', cb) 116 | 117 | await this.helper.callMethod('StartNotify') 118 | } 119 | 120 | async stopNotifications () { 121 | await this.helper.callMethod('StopNotify') 122 | this.helper.removeAllListeners('PropertiesChanged') // might be improved 123 | } 124 | 125 | async toString () { 126 | return this.getUUID() 127 | } 128 | } 129 | 130 | module.exports = GattCharacteristic 131 | 132 | /** 133 | * @typedef WritingMode 134 | * @property {string} command Write without response 135 | * @property {string} request Write with response 136 | * @property {string} reliable Reliable Write 137 | */ 138 | 139 | /** 140 | * Notification event 141 | * 142 | * @event GattCharacteristic#valuechanged 143 | * @type {Buffer} 144 | */ 145 | -------------------------------------------------------------------------------- /src/GattServer.js: -------------------------------------------------------------------------------- 1 | const BusHelper = require('./BusHelper') 2 | const GattService = require('./GattService') 3 | 4 | /** 5 | * @classdesc GattServer class that provides interaction with device GATT profile. 6 | * @class GattServer 7 | * @see You can construct a Device object via {@link Device#gatt} method 8 | */ 9 | class GattServer { 10 | constructor (dbus, adapter, device) { 11 | this.dbus = dbus 12 | this.adapter = adapter 13 | this.device = device 14 | this.helper = new BusHelper(dbus, 'org.bluez', `/org/bluez/${adapter}/${device}`, 'org.bluez.Device1') 15 | 16 | this._services = {} 17 | } 18 | 19 | async init () { 20 | // TODO add lock to avoid race conditions 21 | this._services = {} 22 | 23 | const servicesResolved = await this.helper.prop('ServicesResolved') 24 | if (!servicesResolved) { 25 | await this.helper.waitPropChange('ServicesResolved') 26 | } 27 | 28 | const children = await this.helper.children() 29 | for (const s of children) { 30 | const service = new GattService(this.dbus, this.adapter, this.device, s) 31 | const uuid = await service.getUUID() 32 | await service.init() 33 | this._services[uuid] = service 34 | } 35 | } 36 | 37 | /** 38 | * List of available services 39 | * @returns {string[]} 40 | */ 41 | async services () { 42 | return Object.keys(this._services) 43 | } 44 | 45 | /** 46 | * Init a GattService instance and return it 47 | * @param {string} uuid 48 | * @returns {GattService} 49 | */ 50 | async getPrimaryService (uuid) { 51 | if (uuid in this._services) { 52 | return this._services[uuid] 53 | } 54 | 55 | throw new Error('Service not available') 56 | } 57 | } 58 | 59 | module.exports = GattServer 60 | -------------------------------------------------------------------------------- /src/GattService.js: -------------------------------------------------------------------------------- 1 | const BusHelper = require('./BusHelper') 2 | const GattCharacteristic = require('./GattCharacteristic') 3 | 4 | /** 5 | * @classdesc GattService class interacts with a remote GATT service. 6 | * @class GattService 7 | * @see You can construct a GattService object via {@link GattServer#getPrimaryService} method. 8 | */ 9 | class GattService { 10 | constructor (dbus, adapter, device, service) { 11 | this.dbus = dbus 12 | this.adapter = adapter 13 | this.device = device 14 | this.service = service 15 | this.helper = new BusHelper(dbus, 'org.bluez', `/org/bluez/${adapter}/${device}/${service}`, 'org.bluez.GattService1') 16 | 17 | this._characteristics = {} 18 | } 19 | 20 | async init () { 21 | this._characteristics = {} 22 | 23 | const children = await this.helper.children() 24 | for (const c of children) { 25 | const characteristic = new GattCharacteristic(this.dbus, this.adapter, this.device, this.service, c) 26 | const uuid = await characteristic.getUUID() 27 | this._characteristics[uuid] = characteristic 28 | } 29 | } 30 | 31 | /** 32 | * Indicates whether or not this GATT service is a primary service. 33 | * @returns {boolean} 34 | */ 35 | async isPrimary () { 36 | return this.helper.prop('Primary') 37 | } 38 | 39 | /** 40 | * 128-bit service UUID. 41 | * @returns {string} 42 | */ 43 | async getUUID () { 44 | return this.helper.prop('UUID') 45 | } 46 | 47 | /** 48 | * List of available characteristic names. 49 | * @returns {string[]} 50 | */ 51 | async characteristics () { 52 | return Object.keys(this._characteristics) 53 | } 54 | 55 | /** 56 | * Init a GattCharacteristic instance and return it 57 | * @param {string} uuid - Characteristic UUID. 58 | * @returns {GattCharacteristic} 59 | */ 60 | async getCharacteristic (uuid) { 61 | if (uuid in this._characteristics) { 62 | return this._characteristics[uuid] 63 | } 64 | 65 | throw new Error('Characteristic not available') 66 | } 67 | 68 | /** 69 | * Human readable class identifier. 70 | * @returns {string} 71 | */ 72 | async toString () { 73 | return this.getUUID() 74 | } 75 | } 76 | 77 | module.exports = GattService 78 | -------------------------------------------------------------------------------- /src/buildTypedValue.js: -------------------------------------------------------------------------------- 1 | const { Variant } = require('dbus-next') 2 | 3 | function buildTypedValue (type, value) { 4 | const dbusType = MAPPINGS[type] 5 | if (!dbusType) throw new Error('Unrecognized type') 6 | 7 | return new Variant(dbusType, value) 8 | } 9 | 10 | module.exports = buildTypedValue 11 | 12 | // https://dbus.freedesktop.org/doc/dbus-specification.html 13 | const MAPPINGS = { 14 | string: 's', 15 | int16: 'n', 16 | boolean: 'b', 17 | uint16: 'q', 18 | dict: 'e' 19 | } 20 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import events = require('events'); 2 | 3 | declare namespace NodeBle { 4 | interface GattCharacteristic extends events.EventEmitter { 5 | getUUID(): Promise; 6 | getFlags(): Promise; 7 | isNotifying(): Promise; 8 | readValue(offset?: number): Promise; 9 | writeValue(buffer: Buffer, optionsOrOffset?: number | WriteValueOptions): Promise; 10 | writeValueWithoutResponse(buffer: Buffer, offset?: number): Promise; 11 | writeValueWithResponse(buffer: Buffer, offset?: number): Promise; 12 | startNotifications(): Promise; 13 | stopNotifications(): Promise; 14 | toString(): Promise; 15 | 16 | on(event: 'valuechanged', listener: (buffer: Buffer) => void): this; 17 | } 18 | 19 | interface GattService { 20 | isPrimary(): Promise; 21 | getUUID(): Promise; 22 | characteristics(): Promise; 23 | toString(): Promise; 24 | getCharacteristic(uuid: string): Promise; 25 | } 26 | 27 | interface GattServer { 28 | services(): Promise; 29 | getPrimaryService(uuid: string): Promise; 30 | } 31 | 32 | interface ConnectionState { 33 | connected: boolean; 34 | } 35 | 36 | interface Device extends events.EventEmitter { 37 | getName(): Promise; 38 | getAddress(): Promise; 39 | getAddressType(): Promise; 40 | getAlias(): Promise; 41 | getRSSI(): Promise; 42 | getManufacturerData(): Promise<{[key:string]:any}>; 43 | getAdvertisingData(): Promise<{[key:string]:any}>; 44 | getServiceData(): Promise<{[key:string]:any}>; 45 | isPaired(): Promise; 46 | isConnected(): Promise; 47 | pair(): Promise; 48 | cancelPair(): Promise; 49 | connect(): Promise; 50 | disconnect(): Promise; 51 | gatt(): Promise; 52 | toString(): Promise; 53 | 54 | on(event: 'connect', listener: (state: ConnectionState) => void): this; 55 | on(event: 'disconnect', listener: (state: ConnectionState) => void): this; 56 | } 57 | 58 | interface Adapter { 59 | getAddress(): Promise; 60 | getAddressType(): Promise; 61 | getName(): Promise; 62 | getAlias(): Promise; 63 | isPowered(): Promise; 64 | isDiscovering(): Promise; 65 | startDiscovery(): Promise; 66 | stopDiscovery(): Promise; 67 | devices(): Promise; 68 | getDevice(uuid: string): Promise; 69 | waitDevice(uuid: string, timeout?: number, discoveryInterval?: number): Promise; 70 | toString(): Promise; 71 | } 72 | 73 | interface Bluetooth { 74 | adapters(): Promise; 75 | defaultAdapter(): Promise; 76 | getAdapter(adapter: string): Promise; 77 | } 78 | 79 | function createBluetooth(): { 80 | destroy(): void; 81 | bluetooth: Bluetooth; 82 | }; 83 | 84 | interface WriteValueOptions { 85 | offset?: number; 86 | type?: 'reliable' | 'request' | 'command'; 87 | } 88 | } 89 | 90 | export = NodeBle; 91 | 92 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { systemBus: createSystemBus } = require('dbus-next') 2 | const Bluetooth = require('./Bluetooth') 3 | 4 | /** 5 | * @typedef {Object} NodeBleSession 6 | * @property {Bluetooth} bluetooth - Bluetooth session 7 | * @property {func} destroy - Close bluetooth session 8 | */ 9 | 10 | /** 11 | * @function createBluetooth 12 | * @description Init bluetooth session and return 13 | * @returns {NodeBleInit} 14 | * @example 15 | * const { createBluetooth } = require('node-ble') 16 | * 17 | * async function main () { 18 | * const { bluetooth, destroy } = createBluetooth() 19 | * const adapter = await bluetooth.defaultAdapter() 20 | * // do here your staff 21 | * destroy() 22 | * } 23 | */ 24 | function createBluetooth () { 25 | const dbus = createSystemBus() 26 | 27 | const bluetooth = new Bluetooth(dbus) 28 | const destroy = () => dbus.disconnect() 29 | 30 | return { bluetooth, destroy } 31 | } 32 | 33 | module.exports.createBluetooth = createBluetooth 34 | -------------------------------------------------------------------------------- /src/parseDict.js: -------------------------------------------------------------------------------- 1 | const { Variant } = require('dbus-next') 2 | 3 | const isVariant = o => o instanceof Variant 4 | 5 | function parseDict (dict) { 6 | const o = {} 7 | for (const id in dict) { 8 | o[id] = isVariant(dict[id]) ? dict[id].value : dict[id] 9 | } 10 | return o 11 | } 12 | 13 | module.exports = parseDict 14 | -------------------------------------------------------------------------------- /test-e2e/e2e-test-utils.js: -------------------------------------------------------------------------------- 1 | function getTestDevice () { 2 | const TEST_DEVICE = process.env.TEST_DEVICE 3 | if (!TEST_DEVICE) { 4 | console.error('TEST_DEVICE environment variable not found') 5 | process.exit(1) 6 | } 7 | 8 | return TEST_DEVICE 9 | } 10 | 11 | module.exports = { 12 | getTestDevice 13 | } 14 | -------------------------------------------------------------------------------- /test-e2e/e2e.spec.js: -------------------------------------------------------------------------------- 1 | /* global test, describe, expect, beforeAll, afterAll */ 2 | const { getTestDevice } = require('./e2e-test-utils.js') 3 | const { createBluetooth } = require('..') 4 | 5 | const TEST_SERVICE = '12345678-1234-5678-1234-56789abcdef0' // FOR READ/WRITE TESTING 6 | const TEST_CHARACTERISTIC = '12345678-1234-5678-1234-56789abcdef1' // FOR READ/WRITE TESTING 7 | 8 | const TEST_NOTIFY_SERVICE = '12345678-1234-5678-1234-56789abcdef0' // FOR NOTIFY TESTING 9 | const TEST_NOTIFY_CHARACTERISTIC = '12345678-1234-5678-1234-56789abcdef2' // FOR NOTIFY TESTING 10 | 11 | const TEST_DEVICE = getTestDevice() 12 | 13 | let bluetooth, destroy 14 | 15 | beforeAll(() => ({ bluetooth, destroy } = createBluetooth())) 16 | afterAll(() => destroy()) 17 | 18 | test('check properly configured', () => { 19 | expect(TEST_DEVICE).not.toBeUndefined() 20 | }) 21 | 22 | describe('gatt e2e', () => { 23 | test('get adapters', async () => { 24 | const adapters = await bluetooth.adapters() 25 | console.log({ adapters }) 26 | }) 27 | 28 | let adapter 29 | test('get adapter', async () => { 30 | adapter = await bluetooth.defaultAdapter() 31 | }) 32 | 33 | test('discovery', async () => { 34 | if (!await adapter.isDiscovering()) { 35 | await adapter.startDiscovery() 36 | } 37 | }) 38 | 39 | let device 40 | test('get device', async () => { 41 | device = await adapter.waitDevice(TEST_DEVICE) 42 | const deviceName = await device.toString() 43 | expect(typeof deviceName).toBe('string') 44 | console.log({ deviceName }) 45 | }, 20 * 1000) // increases test secs 46 | 47 | test('connect', async () => { 48 | device.on('connect', () => console.log('connect')) 49 | device.on('disconnect', () => console.log('disconnect')) 50 | const isConnected = await device.isConnected() 51 | console.log({ isConnected }) 52 | await device.connect() 53 | }, 20 * 1000) 54 | 55 | let gattServer 56 | test('get gatt', async () => { 57 | gattServer = await device.gatt() 58 | const services = await gattServer.services() 59 | console.log({ services }) 60 | }, 20 * 1000) // increases test secs 61 | 62 | let service 63 | test('get service', async () => { 64 | service = await gattServer.getPrimaryService(TEST_SERVICE) 65 | const uuid = await service.getUUID() 66 | expect(uuid).toEqual(TEST_SERVICE) 67 | console.log({ 68 | serviceUUID: uuid, 69 | service: service.service, 70 | characteristics: await service.characteristics() 71 | }) 72 | }) 73 | 74 | let characteristic 75 | test('get characteristic', async () => { 76 | characteristic = await service.getCharacteristic(TEST_CHARACTERISTIC) 77 | const uuid = await characteristic.getUUID() 78 | expect(uuid).toEqual(TEST_CHARACTERISTIC) 79 | console.log({ 80 | characteristic: characteristic.characteristic, 81 | characteristicUUID: uuid 82 | }) 83 | }) 84 | 85 | test('read/write value', async () => { 86 | const now = new Date().toISOString() 87 | const string = Buffer.from(`hello_world_${now}`) 88 | const expected = Buffer.from(`ECHO>hello_world_${now}`) 89 | 90 | await characteristic.writeValue(string) 91 | const value = await characteristic.readValue() 92 | expect(value).toEqual(expected) 93 | console.log({ value: value.toString() }) 94 | }) 95 | 96 | test('notify', async () => { 97 | const notifiableService = await gattServer.getPrimaryService(TEST_NOTIFY_SERVICE) 98 | const notifiableCharacteristic = await notifiableService.getCharacteristic(TEST_NOTIFY_CHARACTERISTIC) 99 | 100 | console.log({ 101 | notifiable: { 102 | service: notifiableService.service, 103 | serviceUUID: await notifiableService.getUUID(), 104 | 105 | characteristic: notifiableCharacteristic.characteristic, 106 | characteristicUUID: await notifiableCharacteristic.getUUID() 107 | } 108 | }) 109 | await notifiableCharacteristic.startNotifications() 110 | 111 | const res = await new Promise(resolve => { 112 | notifiableCharacteristic.on('valuechanged', buffer => { 113 | console.log({ notifiedBuffer: buffer, string: buffer.toString() }) 114 | resolve(buffer) 115 | }) 116 | }) 117 | 118 | console.log({ notifiedString: res.toString() }) 119 | expect(res).toBeInstanceOf(Buffer) 120 | expect(res.toString().startsWith('Notification data')).toBeTruthy() 121 | 122 | await notifiableCharacteristic.stopNotifications() 123 | }) 124 | 125 | test('disconnect', async () => { 126 | await adapter.stopDiscovery() 127 | await device.disconnect() 128 | await new Promise((resolve, reject) => setTimeout(resolve, 100)) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /test-e2e/multiple-attemps.spec.js: -------------------------------------------------------------------------------- 1 | /* global test, expect, beforeAll, afterAll */ 2 | const { getTestDevice } = require('./e2e-test-utils.js') 3 | const { createBluetooth } = require('..') 4 | 5 | const TEST_DEVICE = getTestDevice() 6 | 7 | let bluetooth, destroy, adapter, device 8 | 9 | beforeAll(async () => { 10 | ({ bluetooth, destroy } = createBluetooth()) 11 | adapter = await bluetooth.defaultAdapter() 12 | if (!await adapter.isDiscovering()) await adapter.startDiscovery() 13 | }, 20 * 1000) 14 | 15 | afterAll(async () => { 16 | await adapter.stopDiscovery() 17 | destroy() 18 | }) 19 | 20 | test.each(['#1', '#2', '#3'])('gatt e2e %s', async (attempt) => { 21 | expect(TEST_DEVICE).not.toBeUndefined() 22 | device = await adapter.waitDevice(TEST_DEVICE) 23 | device.on('connect', () => console.log({ attempt, event: 'connect' })) 24 | device.on('disconnect', () => console.log({ attempt, event: 'disconnect' })) 25 | await device.connect() 26 | 27 | const dbus = bluetooth.dbus 28 | const name = `/org/bluez/${device.adapter}/${device.device}` 29 | // console.log(dbus._signals._events) 30 | const listenerCount = dbus._signals.listenerCount(`{"path":"${name}","interface":"org.freedesktop.DBus.Properties","member":"PropertiesChanged"}`) 31 | expect(listenerCount).toBe(1) 32 | await device.disconnect() 33 | }, 10 * 1000) 34 | -------------------------------------------------------------------------------- /test/Adapter.spec.js: -------------------------------------------------------------------------------- 1 | /* global test, describe, expect, jest */ 2 | 3 | jest.mock('../src/BusHelper') 4 | jest.mock('../src/Device') 5 | 6 | const dbus = Symbol('dbus') 7 | 8 | const Adapter = require('../src/Adapter') 9 | const Device = require('../src/Device') 10 | 11 | test('serializeUUID', () => { 12 | expect(Adapter.serializeUUID('00:00:00:00:00:00')).toEqual('dev_00_00_00_00_00_00') 13 | expect(Adapter.serializeUUID('aa:BB:cc:DD:ee:FF')).toEqual('dev_AA_BB_CC_DD_EE_FF') 14 | }) 15 | 16 | test('deserializeUUID', () => { 17 | expect(Adapter.deserializeUUID('dev_00_00_00_00_00_00')).toEqual('00:00:00:00:00:00') 18 | }) 19 | 20 | test('props', async () => { 21 | const adapter = new Adapter(dbus, 'hci0') 22 | adapter.helper.prop.mockImplementation((value) => Promise.resolve(({ 23 | Address: '00:00:00:00:00:00', 24 | AddressType: 'public', 25 | Name: '_name_', 26 | Alias: '_alias_', 27 | Powered: true, 28 | Discovering: true 29 | }[value]))) 30 | 31 | await expect(adapter.getAddress()).resolves.toEqual('00:00:00:00:00:00') 32 | await expect(adapter.getAddressType()).resolves.toEqual('public') 33 | await expect(adapter.getName()).resolves.toEqual('_name_') 34 | await expect(adapter.getAlias()).resolves.toEqual('_alias_') 35 | await expect(adapter.isPowered()).resolves.toEqual(true) 36 | await expect(adapter.isDiscovering()).resolves.toEqual(true) 37 | 38 | await expect(adapter.toString()).resolves.toEqual('_name_ [00:00:00:00:00:00]') 39 | }) 40 | 41 | test('discovering methods', async () => { 42 | const adapter = new Adapter(dbus, 'hci0') 43 | 44 | let isDiscovering 45 | adapter.helper.prop.mockImplementation(() => Promise.resolve(isDiscovering)) 46 | 47 | isDiscovering = false 48 | await expect(adapter.startDiscovery()).resolves.toBeUndefined() 49 | expect(adapter.helper.callMethod).toHaveBeenLastCalledWith('StartDiscovery') 50 | await expect(adapter.stopDiscovery()).rejects.toThrow('No discovery started') 51 | expect(adapter.helper.callMethod).toHaveBeenCalledTimes(2) 52 | adapter.helper.callMethod.mockClear() 53 | 54 | isDiscovering = true 55 | await expect(adapter.stopDiscovery()).resolves.toBeUndefined() 56 | expect(adapter.helper.callMethod).toHaveBeenLastCalledWith('StopDiscovery') 57 | await expect(adapter.startDiscovery()).rejects.toThrow('Discovery already in progress') 58 | 59 | expect(adapter.helper.callMethod).toHaveBeenCalledTimes(1) 60 | }) 61 | 62 | test('devices', async () => { 63 | const adapter = new Adapter(dbus, 'hci0') 64 | 65 | adapter.helper.children.mockResolvedValueOnce([ 66 | 'dev_11_11_11_11_11_11', 67 | 'dev_22_22_22_22_22_22', 68 | 'dev_33_33_33_33_33_33' 69 | ]) 70 | 71 | await expect(adapter.devices()).resolves.toEqual([ 72 | '11:11:11:11:11:11', 73 | '22:22:22:22:22:22', 74 | '33:33:33:33:33:33' 75 | ]) 76 | }) 77 | 78 | test('getDevice', async () => { 79 | const adapter = new Adapter(dbus, 'hci0') 80 | 81 | adapter.helper.children.mockResolvedValue([ 82 | 'dev_11_11_11_11_11_11', 83 | 'dev_22_22_22_22_22_22', 84 | 'dev_33_33_33_33_33_33' 85 | ]) 86 | 87 | await expect(adapter.getDevice('00:00:00:00:00:00')).rejects.toThrow('Device not found') 88 | await expect(adapter.getDevice('11:11:11:11:11:11')).resolves.toBeInstanceOf(Device) 89 | expect(Device).toHaveBeenCalledWith(dbus, 'hci0', 'dev_11_11_11_11_11_11') 90 | }) 91 | 92 | describe('waitDevice', () => { 93 | test('immediately found', async () => { 94 | const adapter = new Adapter(dbus, 'hci0') 95 | 96 | adapter.helper.children.mockResolvedValue([ 97 | 'dev_11_11_11_11_11_11', 98 | 'dev_22_22_22_22_22_22', 99 | 'dev_33_33_33_33_33_33' 100 | ]) 101 | 102 | await expect(adapter.waitDevice('11:11:11:11:11:11')).resolves.toBeInstanceOf(Device) 103 | }) 104 | 105 | test('found after a while', async () => { 106 | jest.useFakeTimers() 107 | 108 | const adapter = new Adapter(dbus, 'hci0') 109 | 110 | adapter.helper.children.mockResolvedValueOnce([]) 111 | adapter.helper.children.mockResolvedValueOnce([]) 112 | adapter.helper.children.mockResolvedValueOnce([]) 113 | adapter.helper.children.mockResolvedValueOnce([ 114 | 'dev_11_11_11_11_11_11', 115 | 'dev_22_22_22_22_22_22', 116 | 'dev_33_33_33_33_33_33' 117 | ]) 118 | 119 | const res = expect(adapter.waitDevice('22:22:22:22:22:22', 90 * 1000, 500)).resolves.toBeInstanceOf(Device) 120 | 121 | jest.advanceTimersByTime(2000) 122 | expect(adapter.helper.children).toHaveBeenCalledTimes(4) 123 | 124 | return res 125 | }) 126 | 127 | test('fail for timeout', async () => { 128 | jest.useFakeTimers() 129 | 130 | const adapter = new Adapter(dbus, 'hci0') 131 | 132 | adapter.helper.children.mockResolvedValue([ 133 | 'dev_11_11_11_11_11_11', 134 | 'dev_22_22_22_22_22_22', 135 | 'dev_33_33_33_33_33_33' 136 | ]) 137 | 138 | const res = expect(adapter.waitDevice('44:44:44:44:44:44', 2 * 1000, 500)).rejects.toThrow() 139 | 140 | jest.advanceTimersByTime(2000) 141 | expect(adapter.helper.children).toHaveBeenCalledTimes(4) 142 | 143 | return res 144 | }) 145 | 146 | test('clear intervals and timeouts after fail', async () => { 147 | jest.useFakeTimers() 148 | 149 | const adapter = new Adapter(dbus, 'hci0') 150 | 151 | adapter.helper.children.mockResolvedValue([ 152 | 'dev_11_11_11_11_11_11', 153 | 'dev_22_22_22_22_22_22', 154 | 'dev_33_33_33_33_33_33' 155 | ]) 156 | 157 | const timeout = 500 158 | const discoveryInterval = 100 159 | 160 | const spyClearInterval = jest.spyOn(global, 'clearInterval') 161 | const spyClearTimeout = jest.spyOn(global, 'clearTimeout') 162 | 163 | const waitDevicePromise = adapter.waitDevice('44:44:44:44:44:44', timeout, discoveryInterval) 164 | 165 | jest.advanceTimersByTime(timeout) 166 | 167 | await expect(waitDevicePromise).rejects.toThrow('operation timed out') 168 | 169 | expect(spyClearInterval).toHaveBeenCalled() 170 | expect(spyClearTimeout).toHaveBeenCalled() 171 | }) 172 | }) 173 | -------------------------------------------------------------------------------- /test/Bluetooth.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect, it, jest */ 2 | 3 | jest.mock('../src/BusHelper') 4 | jest.mock('../src/Adapter') 5 | 6 | const Bluetooth = require('../src/Bluetooth') 7 | const Adapter = require('../src/Adapter') 8 | 9 | const dbus = Symbol('dbus') 10 | 11 | test('adapters', async () => { 12 | const bluetooth = new Bluetooth(dbus) 13 | bluetooth.helper.children.mockReturnValue(['hci0', 'hci1', 'hci2']) 14 | 15 | const adapters = await bluetooth.adapters() 16 | expect(adapters).toEqual(['hci0', 'hci1', 'hci2']) 17 | }) 18 | 19 | test('getAdapter', async () => { 20 | const bluetooth = new Bluetooth(dbus) 21 | bluetooth.helper.children.mockReturnValue(['hci0', 'hci1']) 22 | 23 | await expect(bluetooth.getAdapter('hci5')).rejects.toThrowError('Adapter not found') 24 | 25 | const adapter = await bluetooth.getAdapter('hci0') 26 | expect(adapter).toBeInstanceOf(Adapter) 27 | expect(Adapter).toHaveBeenCalledWith(dbus, 'hci0') 28 | }) 29 | 30 | describe('defaultAdapter', () => { 31 | it('should not found adapters', async () => { 32 | const bluetooth = new Bluetooth(dbus) 33 | bluetooth.helper.children.mockReturnValue([]) 34 | 35 | await expect(bluetooth.defaultAdapter()).rejects.toThrowError('No available adapters found') 36 | }) 37 | 38 | it('should be able to get an adapter', async () => { 39 | const bluetooth = new Bluetooth(dbus) 40 | bluetooth.helper.children.mockReturnValue(['hci0']) 41 | 42 | const adapter = await bluetooth.defaultAdapter() 43 | expect(adapter).toBeInstanceOf(Adapter) 44 | expect(Adapter).toHaveBeenCalledWith(dbus, 'hci0') 45 | }) 46 | }) 47 | 48 | describe('getActiveAdapters', () => { 49 | it('should return only active adapters', async () => { 50 | const hci0 = new Adapter(dbus, 'hci0') 51 | hci0.isPowered = async () => false 52 | hci0.getName = async () => 'hci0' 53 | 54 | const hci1 = new Adapter(dbus, 'hci1') 55 | hci1.isPowered = async () => true 56 | hci1.getName = async () => 'hci1' 57 | 58 | const bluetooth = new Bluetooth(dbus) 59 | 60 | const adapters = { hci0, hci1 } 61 | bluetooth.getAdapter = async name => adapters[name] 62 | bluetooth.helper.children.mockReturnValue(['hci0', 'hci1']) 63 | 64 | const result = await bluetooth.activeAdapters() 65 | 66 | expect(result.length).toEqual(1) 67 | await expect(result[0].getName()).resolves.toEqual('hci1') 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /test/BusHelper.spec.js: -------------------------------------------------------------------------------- 1 | /* global test, expect, beforeAll, afterAll */ 2 | 3 | const TEST_NAME = 'org.test' 4 | const TEST_OBJECT = '/org/example' 5 | const TEST_IFACE = 'org.test.iface' 6 | 7 | const { systemBus: createSystemBus } = require('dbus-next') 8 | const BusHelper = require('../src/BusHelper') 9 | const TestInterface = require('./__interfaces/TestInterface') 10 | const buildTypedValue = require('../src/buildTypedValue') 11 | 12 | let dbus, iface 13 | 14 | beforeAll(async () => { 15 | dbus = createSystemBus() 16 | 17 | await dbus.requestName(TEST_NAME) 18 | 19 | iface = new TestInterface(TEST_IFACE) 20 | dbus.export(TEST_OBJECT, iface) 21 | }) 22 | 23 | afterAll(async () => { 24 | dbus.unexport(TEST_OBJECT, iface) 25 | await dbus.releaseName(TEST_NAME) 26 | await dbus.disconnect() 27 | /** 28 | * sometimes disconnect() is slow and throws the following error https://github.com/chrvadala/node-ble/issues/30 29 | * looks like that wait the cb here doesn't solve the issue https://github.com/dbusjs/node-dbus-next/blob/b2a6b89e79de423debb4475452db1cc410beab41/lib/bus.js#L265 30 | */ 31 | await new Promise((resolve, reject) => { 32 | setTimeout(resolve, 50) 33 | }) 34 | }) 35 | 36 | test('props/prop', async () => { 37 | const helper = new BusHelper(dbus, TEST_NAME, TEST_OBJECT, TEST_IFACE) 38 | 39 | const prop = await helper.prop('SimpleProperty') 40 | expect(prop).toEqual('bar') 41 | 42 | const props = await helper.props() 43 | expect(props).toEqual({ 44 | SimpleProperty: 'bar', 45 | VirtualProperty: 'foo' 46 | }) 47 | 48 | await helper.set('SimpleProperty', buildTypedValue('string', 'abc')) 49 | await expect(helper.prop('SimpleProperty')).resolves.toEqual('abc') 50 | }) 51 | 52 | test('callMethod', async () => { 53 | const helper = new BusHelper(dbus, TEST_NAME, TEST_OBJECT, TEST_IFACE) 54 | 55 | const res = await helper.callMethod('Echo', 'hello') 56 | expect(res).toBe('>>hello') 57 | }) 58 | 59 | test('buildChildren', () => { 60 | const nodes = [ 61 | '/foo', 62 | '/foo/a', 63 | '/foo/b/1', 64 | '/foo/c/1', 65 | '/foo/c/2' 66 | ] 67 | 68 | expect(BusHelper.buildChildren('/bar', nodes)).toEqual([]) 69 | expect(BusHelper.buildChildren('/', nodes)).toEqual(['foo']) 70 | expect(BusHelper.buildChildren('/foo', nodes)).toEqual(['a', 'b', 'c']) 71 | expect(BusHelper.buildChildren('/foo/c', nodes)).toEqual(['1', '2']) 72 | }) 73 | 74 | test('children', async () => { 75 | const helper = new BusHelper(dbus, TEST_NAME, TEST_OBJECT, TEST_IFACE) 76 | 77 | const a = new TestInterface(TEST_IFACE) 78 | dbus.export(`${TEST_OBJECT}/bar`, a) 79 | dbus.export(`${TEST_OBJECT}/foo`, a) 80 | dbus.export(`${TEST_OBJECT}/foo/abc`, a) 81 | dbus.export(`${TEST_OBJECT}/foo/abc/def`, a) 82 | 83 | const children = await helper.children() 84 | expect(children).toEqual(['bar', 'foo']) 85 | 86 | dbus.unexport(`${TEST_OBJECT}/bar`, a) 87 | dbus.unexport(`${TEST_OBJECT}/foo`, a) 88 | dbus.unexport(`${TEST_OBJECT}/foo/abc`, a) 89 | dbus.unexport(`${TEST_OBJECT}/foo/abc/def`, a) 90 | }) 91 | 92 | test('disableProps', async () => { 93 | const helper = new BusHelper(dbus, TEST_NAME, TEST_OBJECT, TEST_IFACE, { useProps: false }) 94 | 95 | await expect(helper.callMethod('Echo', 'hello')).resolves.toEqual('>>hello') 96 | 97 | await expect(helper.props()).rejects.toThrow() 98 | await expect(helper.prop('Test')).rejects.toThrow() 99 | }) 100 | 101 | test('waitPropChange', async () => { 102 | const helper = new BusHelper(dbus, TEST_NAME, TEST_OBJECT, TEST_IFACE) 103 | 104 | const res = helper.waitPropChange('VirtualProperty') 105 | await helper.set('VirtualProperty', buildTypedValue('string', 'hello')) 106 | await expect(res).resolves.toEqual('hello') 107 | 108 | const res2 = helper.waitPropChange('VirtualProperty') 109 | await helper.set('VirtualProperty', buildTypedValue('string', 'byebye')) 110 | await expect(res2).resolves.toEqual('byebye') 111 | }) 112 | 113 | test('propsEvents', async () => { 114 | const helper = new BusHelper(dbus, TEST_NAME, TEST_OBJECT, TEST_IFACE, { usePropsEvents: true }) 115 | 116 | const res = new Promise((resolve) => { 117 | const cb = nextProps => { 118 | resolve(nextProps) 119 | helper.off('PropertiesChanged', cb) 120 | } 121 | 122 | helper.on('PropertiesChanged', cb) 123 | }) 124 | 125 | await helper.set('VirtualProperty', buildTypedValue('string', 'bar')) 126 | await expect(res).resolves.toMatchObject({ VirtualProperty: { signature: 's', value: 'bar' } }) 127 | }) 128 | 129 | test('removeListeners', async () => { 130 | const helper = new BusHelper(dbus, TEST_NAME, TEST_OBJECT, TEST_IFACE, { useProps: true, usePropsEvents: true }) 131 | 132 | const dummyCb = () => {} 133 | 134 | // Init with listener on helper (directly attached dummyCb) and _propsProxy (through method call triggered _prepare) 135 | helper.on('PropertiesChanged', dummyCb) 136 | await helper.callMethod('Echo', 'ping') 137 | expect(helper.listenerCount('PropertiesChanged')).toBeGreaterThan(0) 138 | expect(helper._propsProxy.listenerCount('PropertiesChanged')).toBeGreaterThan(0) 139 | 140 | // Test remove 141 | helper.removeListeners() 142 | expect(helper.listenerCount('PropertiesChanged')).toBe(0) 143 | expect(helper._propsProxy.listenerCount('PropertiesChanged')).toBe(0) 144 | 145 | // Test reuse after remove (same initialization as before) 146 | helper.on('PropertiesChanged', dummyCb) 147 | await helper.callMethod('Echo', 'ping') 148 | expect(helper.listenerCount('PropertiesChanged')).toBeGreaterThan(0) 149 | expect(helper._propsProxy.listenerCount('PropertiesChanged')).toBeGreaterThan(0) 150 | 151 | // Remove second time 152 | helper.removeListeners() 153 | expect(helper.listenerCount('PropertiesChanged')).toBe(0) 154 | expect(helper._propsProxy.listenerCount('PropertiesChanged')).toBe(0) 155 | }) 156 | -------------------------------------------------------------------------------- /test/Device.spec.js: -------------------------------------------------------------------------------- 1 | /* global test, expect, jest */ 2 | 3 | jest.doMock('../src/BusHelper', () => { 4 | const EventEmitter = jest.requireActual('events') 5 | 6 | return class BusHelperMock extends EventEmitter { 7 | constructor () { 8 | super() 9 | this._prepare = jest.fn() 10 | this.props = jest.fn() 11 | this.prop = jest.fn() 12 | this.set = jest.fn() 13 | this.waitPropChange = jest.fn() 14 | this.children = jest.fn() 15 | this.callMethod = jest.fn() 16 | this.removeListeners = jest.fn() 17 | } 18 | } 19 | }) 20 | jest.mock('../src/GattServer') 21 | 22 | const dbus = Symbol('dbus') 23 | 24 | const { Variant } = require('dbus-next') 25 | const Device = require('../src/Device') 26 | const GattServer = require('../src/GattServer') 27 | 28 | test('props', async () => { 29 | const device = new Device(dbus, 'hci0', 'dev_00_00_00_00_00_00') 30 | device.helper.prop.mockImplementation((value) => Promise.resolve(({ 31 | Name: '_name_', 32 | Address: '00:00:00:00:00:00', 33 | AddressType: 'public', 34 | Alias: '_alias_', 35 | RSSI: 100, 36 | TxPower: 50, 37 | ManufacturerData: { 1: new Variant('ay', Buffer.from([0x01, 0x02])) }, 38 | AdvertisingData: { 2: new Variant('ay', Buffer.from([0x03, 0x04])) }, 39 | ServiceData: { 3: new Variant('ay', Buffer.from([0x05, 0x6])) }, 40 | Paired: true, 41 | Connected: true 42 | }[value]))) 43 | 44 | await expect(device.getName()).resolves.toEqual('_name_') 45 | await expect(device.getAddress()).resolves.toEqual('00:00:00:00:00:00') 46 | await expect(device.getAddressType()).resolves.toEqual('public') 47 | await expect(device.getAlias()).resolves.toEqual('_alias_') 48 | await expect(device.getRSSI()).resolves.toEqual(100) 49 | await expect(device.getTXPower()).resolves.toEqual(50) 50 | await expect(device.getManufacturerData()).resolves.toMatchObject({ 1: Buffer.from([0x01, 0x02]) }) 51 | await expect(device.getAdvertisingData()).resolves.toMatchObject({ 2: Buffer.from([0x03, 0x04]) }) 52 | await expect(device.getServiceData()).resolves.toMatchObject({ 3: Buffer.from([0x05, 0x06]) }) 53 | await expect(device.isConnected()).resolves.toEqual(true) 54 | await expect(device.isPaired()).resolves.toEqual(true) 55 | 56 | await expect(device.toString()).resolves.toEqual('_name_ [00:00:00:00:00:00]') 57 | }) 58 | 59 | test('pairing', async () => { 60 | const device = new Device(dbus, 'hci0', 'dev_00_00_00_00_00_00') 61 | 62 | await expect(device.pair()).resolves.toBeUndefined() 63 | expect(device.helper.callMethod).toHaveBeenLastCalledWith('Pair') 64 | 65 | await expect(device.cancelPair()).resolves.toBeUndefined() 66 | expect(device.helper.callMethod).toHaveBeenLastCalledWith('CancelPair') 67 | 68 | expect(device.helper.callMethod).toHaveBeenCalledTimes(2) 69 | }) 70 | 71 | test('connection', async () => { 72 | const device = new Device(dbus, 'hci0', 'dev_00_00_00_00_00_00') 73 | 74 | await expect(device.connect()).resolves.toBeUndefined() 75 | expect(device.helper.callMethod).toHaveBeenLastCalledWith('Connect') 76 | 77 | await expect(device.disconnect()).resolves.toBeUndefined() 78 | expect(device.helper.callMethod).toHaveBeenLastCalledWith('Disconnect') 79 | 80 | expect(device.helper.callMethod).toHaveBeenCalledTimes(2) 81 | }) 82 | 83 | test('gatt server initialization', async () => { 84 | const device = new Device(dbus, 'hci0', 'dev_00_00_00_00_00_00') 85 | 86 | const gattServer = await device.gatt() 87 | 88 | expect(GattServer).toHaveBeenCalledWith(dbus, 'hci0', 'dev_00_00_00_00_00_00') 89 | expect(gattServer.init).toHaveBeenCalledTimes(1) 90 | }) 91 | 92 | test('event:valuechanged', async () => { 93 | const device = new Device(dbus, 'hci0', 'dev_00_00_00_00_00_00') 94 | 95 | const connectedFn = jest.fn() 96 | const disconnectedFn = jest.fn() 97 | 98 | await device.connect() 99 | 100 | device.on('connect', connectedFn) 101 | device.on('disconnect', disconnectedFn) 102 | 103 | device.helper.emit('PropertiesChanged', 104 | { Connected: { signature: 'b', value: true } } 105 | ) 106 | 107 | expect(connectedFn).toHaveBeenCalledWith({ connected: true }) 108 | 109 | device.helper.emit('PropertiesChanged', 110 | { Connected: { signature: 'b', value: false } } 111 | ) 112 | 113 | expect(disconnectedFn).toHaveBeenCalledWith({ connected: false }) 114 | 115 | await device.disconnect() 116 | }) 117 | -------------------------------------------------------------------------------- /test/GattCharacteristic.spec.js: -------------------------------------------------------------------------------- 1 | /* global test, expect, jest */ 2 | 3 | jest.doMock('../src/BusHelper', () => { 4 | const EventEmitter = jest.requireActual('events') 5 | 6 | return class BusHelperMock extends EventEmitter { 7 | constructor () { 8 | super() 9 | this._prepare = jest.fn() 10 | this.props = jest.fn() 11 | this.prop = jest.fn() 12 | this.set = jest.fn() 13 | this.waitPropChange = jest.fn() 14 | this.children = jest.fn() 15 | this.callMethod = jest.fn() 16 | } 17 | } 18 | }) 19 | const buildTypedValue = require('../src/buildTypedValue') 20 | const GattCharacteristic = require('../src/GattCharacteristic') 21 | const dbus = Symbol('dbus') 22 | 23 | test('props', async () => { 24 | const characteristic = new GattCharacteristic(dbus, 'hci0', 'dev_00_00_00_00_00_00', 'characteristic0006', 'char008') 25 | characteristic.helper.prop.mockImplementation((value) => Promise.resolve(({ 26 | UUID: 'foobar', 27 | Flags: ['indicate'], 28 | Notifying: true 29 | }[value]))) 30 | 31 | await expect(characteristic.getUUID()).resolves.toEqual('foobar') 32 | await expect(characteristic.isNotifying()).resolves.toEqual(true) 33 | await expect(characteristic.getFlags()).resolves.toEqual(['indicate']) 34 | await expect(characteristic.toString()).resolves.toEqual('foobar') 35 | }) 36 | 37 | test('read/write', async () => { 38 | const characteristic = new GattCharacteristic(dbus, 'hci0', 'dev_00_00_00_00_00_00', 'characteristic0006', 'char008') 39 | const writeValueOptions = (offset = 0, type = 'reliable') => { 40 | return { offset: buildTypedValue('uint16', offset), type: buildTypedValue('string', type) } 41 | } 42 | 43 | await expect(characteristic.writeValue('not_a_buffer')).rejects.toThrow('Only buffers can be wrote') 44 | await expect(characteristic.writeValueWithResponse('not_a_buffer')).rejects.toThrow('Only buffers can be wrote') 45 | await expect(characteristic.writeValueWithoutResponse('not_a_buffer')).rejects.toThrow('Only buffers can be wrote') 46 | 47 | await expect(characteristic.writeValue(Buffer.from('hello'), 5)).resolves.toBeUndefined() 48 | expect(characteristic.helper.callMethod).toHaveBeenCalledWith('WriteValue', expect.anything(), writeValueOptions(5)) 49 | 50 | await expect(characteristic.writeValue(Buffer.from('hello'))).resolves.toBeUndefined() 51 | expect(characteristic.helper.callMethod).toHaveBeenCalledWith('WriteValue', expect.anything(), writeValueOptions()) 52 | 53 | await expect(characteristic.writeValue(Buffer.from('hello'), { type: 'command' })).resolves.toBeUndefined() 54 | expect(characteristic.helper.callMethod).toHaveBeenCalledWith('WriteValue', expect.anything(), writeValueOptions(0, 'command')) 55 | 56 | await expect(characteristic.writeValue(Buffer.from('hello'), { offset: 9, type: 'request' })).resolves.toBeUndefined() 57 | expect(characteristic.helper.callMethod).toHaveBeenCalledWith('WriteValue', expect.anything(), writeValueOptions(9, 'request')) 58 | 59 | await expect(characteristic.writeValue(Buffer.from('hello'), 'incorrect argument')).resolves.toBeUndefined() 60 | expect(characteristic.helper.callMethod).toHaveBeenCalledWith('WriteValue', expect.anything(), writeValueOptions()) 61 | 62 | await expect(characteristic.writeValueWithResponse(Buffer.from('hello'))).resolves.toBeUndefined() 63 | expect(characteristic.helper.callMethod).toHaveBeenCalledWith('WriteValue', expect.anything(), writeValueOptions(0, 'request')) 64 | 65 | await expect(characteristic.writeValueWithoutResponse(Buffer.from('hello'))).resolves.toBeUndefined() 66 | expect(characteristic.helper.callMethod).toHaveBeenCalledWith('WriteValue', expect.anything(), writeValueOptions(0, 'command')) 67 | 68 | characteristic.helper.callMethod.mockResolvedValueOnce([255, 100, 0]) 69 | await expect(characteristic.readValue()).resolves.toEqual(Buffer.from([255, 100, 0])) 70 | }) 71 | 72 | test('notify', async () => { 73 | const characteristic = new GattCharacteristic(dbus, 'hci0', 'dev_00_00_00_00_00_00', 'characteristic0006', 'char008') 74 | 75 | await characteristic.startNotifications() 76 | expect(characteristic.helper.callMethod).toHaveBeenCalledWith('StartNotify') 77 | 78 | await characteristic.stopNotifications() 79 | expect(characteristic.helper.callMethod).toHaveBeenCalledWith('StopNotify') 80 | }) 81 | 82 | test('event:valuechanged', async () => { 83 | const characteristic = new GattCharacteristic(dbus, 'hci0', 'dev_00_00_00_00_00_00', 'characteristic0006', 'char008') 84 | 85 | await characteristic.startNotifications() 86 | 87 | const res = new Promise((resolve) => { 88 | const cb = (value) => { 89 | characteristic.off('valuechanged', cb) 90 | resolve(value) 91 | } 92 | characteristic.on('valuechanged', cb) 93 | }) 94 | 95 | characteristic.helper.emit('PropertiesChanged', 96 | { Value: { signature: 'ay', value: [0x62, 0x61, 0x72] } } // means bar 97 | ) 98 | 99 | await expect(res).resolves.toEqual(Buffer.from('bar')) 100 | 101 | await characteristic.stopNotifications() 102 | }) 103 | 104 | test('race condition between event:valuechanged / startNotification', async () => { 105 | const characteristic = new GattCharacteristic(dbus, 'hci0', 'dev_00_00_00_00_00_00', 'characteristic0006', 'char008') 106 | const cb = jest.fn(value => {}) 107 | characteristic.on('valuechanged', cb) 108 | 109 | // Wrap the call to StartNotify with an early property change to check this event is not lost in a race condition 110 | characteristic.helper.callMethod.mockImplementationOnce(async (method) => { 111 | if (method !== 'StartNotify') { 112 | throw new Error('Unexpected state in unit test') 113 | } 114 | 115 | await characteristic.helper.callMethod('StartNotify') 116 | 117 | // Send the first event right after StartNotify 118 | characteristic.helper.emit('PropertiesChanged', 119 | { Value: { signature: 'ay', value: [0x62, 0x61, 0x72] } } // means bar 120 | ) 121 | }) 122 | 123 | // Start notifications, wait 200ms and send a second event 124 | characteristic.startNotifications() 125 | await new Promise((resolve) => setTimeout(resolve, 200)) 126 | characteristic.helper.emit('PropertiesChanged', 127 | { Value: { signature: 'ay', value: [0x62, 0x61, 0x72] } } // means bar 128 | ) 129 | 130 | // Check the mocked callback function has been called twice 131 | expect(cb.mock.calls).toHaveLength(2) 132 | 133 | // Cleanup 134 | characteristic.off('valuechanged', cb) 135 | }) 136 | -------------------------------------------------------------------------------- /test/GattServer.spec.js: -------------------------------------------------------------------------------- 1 | /* global test, expect, jest */ 2 | 3 | jest.mock('../src/BusHelper') 4 | jest.mock('../src/GattService', () => { 5 | const UUIDs = { 6 | service001: '00000000-0000-1000-8000-000000000001', 7 | service002: '00000000-0000-1000-8000-000000000002', 8 | service003: '00000000-0000-1000-8000-000000000003' 9 | } 10 | 11 | class GattServiceMock { 12 | constructor (dbus, adapter, device, service) { 13 | this._service = service 14 | this.init = jest.fn() 15 | } 16 | 17 | async getUUID () { 18 | return Promise.resolve(UUIDs[this._service]) 19 | } 20 | } 21 | 22 | return GattServiceMock 23 | }) 24 | 25 | const GattServer = require('../src/GattServer') 26 | const GattService = require('../src/GattService') 27 | 28 | const dbus = Symbol('dbus') 29 | 30 | test('init', async () => { 31 | const gattServer = new GattServer(dbus, 'hci0', 'dev_00_00_00_00_00_00') 32 | 33 | gattServer.helper.children.mockResolvedValue([ 34 | 'service001', 35 | 'service002', 36 | 'service003' 37 | ]) 38 | 39 | gattServer.helper.prop.mockImplementation(propName => propName === 'ServicesResolved' ? true : undefined) 40 | 41 | await gattServer.init() 42 | 43 | await expect(gattServer.services()).resolves.toEqual([ 44 | '00000000-0000-1000-8000-000000000001', 45 | '00000000-0000-1000-8000-000000000002', 46 | '00000000-0000-1000-8000-000000000003' 47 | ]) 48 | 49 | const service = await gattServer.getPrimaryService('00000000-0000-1000-8000-000000000002') 50 | await expect(service).toBeInstanceOf(GattService) 51 | await expect(service.getUUID()).resolves.toBe('00000000-0000-1000-8000-000000000002') 52 | await expect(service._service).toBe('service002') 53 | }) 54 | -------------------------------------------------------------------------------- /test/GattService.spec.js: -------------------------------------------------------------------------------- 1 | /* global test, expect, jest */ 2 | 3 | jest.mock('../src/BusHelper') 4 | jest.mock('../src/GattCharacteristic', () => { 5 | const UUIDs = { 6 | char0007: '00000000-0000-1000-8000-000000000007', 7 | char000b: '00000000-0000-1000-8000-000000000008', 8 | char0010: '00000000-0000-1000-8000-000000000009' 9 | } 10 | 11 | class GattCharacteristicMock { 12 | constructor (dbus, adapter, device, service, characteristic) { 13 | this._characteristic = characteristic 14 | this.init = jest.fn() 15 | } 16 | 17 | async getUUID () { 18 | return Promise.resolve(UUIDs[this._characteristic]) 19 | } 20 | } 21 | 22 | return GattCharacteristicMock 23 | }) 24 | 25 | const GattService = require('../src/GattService') 26 | const GattCharacteristic = require('../src/GattCharacteristic') 27 | const dbus = Symbol('dbus') 28 | 29 | test('init', async () => { 30 | const service = new GattService(dbus, 'hci0', 'dev_00_00_00_00_00_00', 'service0006') 31 | service.helper.children.mockResolvedValue([ 32 | 'char0007', 33 | 'char000b', 34 | 'char0010' 35 | ]) 36 | 37 | await service.init() 38 | 39 | await expect(service.characteristics()).resolves.toEqual([ 40 | '00000000-0000-1000-8000-000000000007', 41 | '00000000-0000-1000-8000-000000000008', 42 | '00000000-0000-1000-8000-000000000009' 43 | ]) 44 | 45 | const characteristic = await service.getCharacteristic('00000000-0000-1000-8000-000000000008') 46 | expect(characteristic).toBeInstanceOf(GattCharacteristic) 47 | await expect(characteristic.getUUID()).resolves.toEqual('00000000-0000-1000-8000-000000000008') 48 | await expect(characteristic._characteristic).toEqual('char000b') 49 | }) 50 | 51 | test('props', async () => { 52 | const service = new GattService(dbus, 'hci0', 'dev_00_00_00_00_00_00', 'service0006') 53 | service.helper.prop.mockImplementation((value) => Promise.resolve(({ 54 | Primary: true, 55 | UUID: 'abcdef' 56 | }[value]))) 57 | 58 | await expect(service.getUUID()).resolves.toEqual('abcdef') 59 | await expect(service.isPrimary()).resolves.toEqual(true) 60 | await expect(service.toString()).resolves.toEqual('abcdef') 61 | }) 62 | -------------------------------------------------------------------------------- /test/__interfaces/TestInterface.js: -------------------------------------------------------------------------------- 1 | const {Variant, interface: {Interface, property, method, ACCESS_READWRITE}} = require('dbus-next'); 2 | 3 | class TestInterface extends Interface { 4 | @property({signature: 's', access: ACCESS_READWRITE}) 5 | SimpleProperty = 'bar'; 6 | 7 | _VirtualProperty = 'foo' 8 | 9 | @property({signature: 's'}) 10 | get VirtualProperty() { 11 | return this._VirtualProperty; 12 | } 13 | 14 | set VirtualProperty(value) { 15 | this._VirtualProperty = value; 16 | 17 | Interface.emitPropertiesChanged(this, { 18 | VirtualProperty: value 19 | }) 20 | } 21 | 22 | @method({inSignature: 's', outSignature: 's'}) 23 | Echo(what) { 24 | return `>>${what}`; 25 | } 26 | } 27 | 28 | module.exports = TestInterface 29 | -------------------------------------------------------------------------------- /test/buildTypedValue.spec.js: -------------------------------------------------------------------------------- 1 | /* global test, expect */ 2 | 3 | const buildTypedValue = require('../src/buildTypedValue.js') 4 | const { Variant } = require('dbus-next') 5 | 6 | test('buildTypedValue', () => { 7 | expect(buildTypedValue('string', 'bar')).toEqual(new Variant('s', 'bar')) 8 | expect(buildTypedValue('int16', 100)).toEqual(new Variant('n', 100)) 9 | expect(buildTypedValue('boolean', true)).toEqual(new Variant('b', true)) 10 | 11 | expect(() => buildTypedValue('notvalid', true)).toThrow('Unrecognized type') 12 | }) 13 | -------------------------------------------------------------------------------- /test/parseDict.spec.js: -------------------------------------------------------------------------------- 1 | /* global test, expect */ 2 | 3 | const { Variant } = require('dbus-next') 4 | const parseDict = require('../src/parseDict.js') 5 | 6 | test('parseDict', () => { 7 | const dict = { 8 | 1: new Variant('ay', Buffer.from([0x01, 0x02, 0x03, 0x04])), 9 | 2: new Variant('ay', Buffer.from([0x05, 0x06, 0x07, 0x08])), 10 | 3: 'just a string', 11 | 4: 42 12 | } 13 | 14 | expect(parseDict(dict)).toEqual({ 15 | 1: Buffer.from([0x01, 0x02, 0x03, 0x04]), 16 | 2: Buffer.from([0x05, 0x06, 0x07, 0x08]), 17 | 3: 'just a string', 18 | 4: 42 19 | }) 20 | }) 21 | --------------------------------------------------------------------------------