From f457e0e85c5a9ffa6bba1ba7e90abb4ce1d6cf27 Mon Sep 17 00:00:00 2001 From: Rob Moran Date: Sun, 3 Dec 2017 23:11:27 +0000 Subject: [PATCH] Initial commit --- .gitignore | 7 + .npmignore | 4 + LICENSE | 21 ++ README.md | 16 +- circle.yml | 43 +++ gulpfile.js | 88 +++++ package.json | 43 +++ src/adapter.ts | 342 ++++++++++++++++++ src/characteristic.ts | 138 +++++++ src/descriptor.ts | 62 ++++ src/device.ts | 81 +++++ src/documentation.md | 12 + src/examples/eddystone.ts | 142 ++++++++ src/examples/heartrate.ts | 79 ++++ src/examples/selector.ts | 120 ++++++ src/helpers.ts | 264 ++++++++++++++ src/server.ts | 97 +++++ src/service.ts | 124 +++++++ src/theme/layouts/default.hbs | 45 +++ src/theme/partials/analytics.hbs | 11 + src/theme/partials/breadcrumb.hbs | 16 + src/theme/partials/comment.hbs | 22 ++ src/theme/partials/footer.hbs | 66 ++++ src/theme/partials/header.hbs | 22 ++ src/theme/partials/hierarchy.hbs | 17 + src/theme/partials/index.hbs | 19 + src/theme/partials/member.declaration.hbs | 22 ++ src/theme/partials/member.getterSetter.hbs | 37 ++ src/theme/partials/member.hbs | 22 ++ src/theme/partials/member.signature.body.hbs | 56 +++ src/theme/partials/member.signature.title.hbs | 28 ++ src/theme/partials/member.signatures.hbs | 11 + src/theme/partials/member.sources.hbs | 11 + src/theme/partials/members.group.hbs | 8 + src/theme/partials/members.hbs | 5 + src/theme/partials/navigation.hbs | 26 ++ src/theme/partials/parameter.hbs | 81 +++++ src/theme/partials/toc.hbs | 10 + src/theme/partials/toc.root.hbs | 18 + src/theme/partials/type.hbs | 78 ++++ src/theme/partials/typeAndParent.hbs | 38 ++ src/theme/partials/typeParameters.hbs | 14 + src/theme/templates/index.hbs | 16 + src/webbluetooth.ts | 176 +++++++++ tsconfig.json | 11 + tslint.json | 34 ++ 46 files changed, 2601 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 LICENSE create mode 100644 circle.yml create mode 100644 gulpfile.js create mode 100644 package.json create mode 100644 src/adapter.ts create mode 100644 src/characteristic.ts create mode 100644 src/descriptor.ts create mode 100644 src/device.ts create mode 100644 src/documentation.md create mode 100644 src/examples/eddystone.ts create mode 100644 src/examples/heartrate.ts create mode 100644 src/examples/selector.ts create mode 100644 src/helpers.ts create mode 100644 src/server.ts create mode 100644 src/service.ts create mode 100644 src/theme/layouts/default.hbs create mode 100644 src/theme/partials/analytics.hbs create mode 100644 src/theme/partials/breadcrumb.hbs create mode 100644 src/theme/partials/comment.hbs create mode 100644 src/theme/partials/footer.hbs create mode 100644 src/theme/partials/header.hbs create mode 100644 src/theme/partials/hierarchy.hbs create mode 100644 src/theme/partials/index.hbs create mode 100644 src/theme/partials/member.declaration.hbs create mode 100644 src/theme/partials/member.getterSetter.hbs create mode 100644 src/theme/partials/member.hbs create mode 100644 src/theme/partials/member.signature.body.hbs create mode 100644 src/theme/partials/member.signature.title.hbs create mode 100644 src/theme/partials/member.signatures.hbs create mode 100644 src/theme/partials/member.sources.hbs create mode 100644 src/theme/partials/members.group.hbs create mode 100644 src/theme/partials/members.hbs create mode 100644 src/theme/partials/navigation.hbs create mode 100644 src/theme/partials/parameter.hbs create mode 100644 src/theme/partials/toc.hbs create mode 100644 src/theme/partials/toc.root.hbs create mode 100644 src/theme/partials/type.hbs create mode 100644 src/theme/partials/typeAndParent.hbs create mode 100644 src/theme/partials/typeParameters.hbs create mode 100644 src/theme/templates/index.hbs create mode 100644 src/webbluetooth.ts create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ebb74ff4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.vscode +node_modules +package-lock.json +lib +docs +types diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..f543d597 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +docs +.gitignore +circle.yml +gulpfile.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1f303832 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Rob Moran + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 58d1d11d..75aa7026 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ -# web-bluetooth -Node.js implementation of Web Bluetooth Specification +# Node Web Bluetooth +Node.js implementation of the Web Bluetooth Specification + +[![Circle CI](https://circleci.com/gh/thegecko/webbluetooth.svg?style=shield)](https://circleci.com/gh/thegecko/webbluetooth/) + +## Prerequisites + +[Node.js > v4.0.0](https://nodejs.org), which includes `npm`. + +## Installation + +```bash +$ npm install web-usb +``` diff --git a/circle.yml b/circle.yml new file mode 100644 index 00000000..2910fd64 --- /dev/null +++ b/circle.yml @@ -0,0 +1,43 @@ +general: + artifacts: + - ~/docs + branches: + ignore: + - build + +machine: + node: + version: 4.0.0 + environment: + PROJECT_NAME: ${CIRCLE_PROJECT_REPONAME} + PROJECT_TAG: ${CIRCLE_BRANCH}-${CIRCLE_BUILD_NUM} + PROJECT_LIVE_BRANCH: build + PROJECT_DOCS: ${HOME}/docs + +checkout: + post: + - git config --global user.name thegecko + - git config --global user.email github@thegecko.org + +compile: + override: + - npm run gulp + - mkdir -p ${PROJECT_DOCS} + - cp -r docs/* ${PROJECT_DOCS}/ + +test: + override: + - exit 0 + +deployment: + staging: + branch: master + commands: + - echo Syncing $PROJECT_NAME to $PROJECT_LIVE_BRANCH on GitHub... + - git add --force docs lib types + - git stash save + - git checkout $PROJECT_LIVE_BRANCH + - git merge master --no-commit + - git checkout stash -- . + - git commit --allow-empty --message "Automatic Deployment" + - git push diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..fd34c864 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,88 @@ +var path = require("path"); +var del = require("del"); +var merge = require("merge2"); +var tslint = require("tslint"); +var gulp = require("gulp"); +var sourcemaps = require("gulp-sourcemaps"); +var gulpTs = require("gulp-typescript"); +var gulpTslint = require("gulp-tslint"); +var gulpTypedoc = require("gulp-typedoc"); + +var name = "Node Web Bluetooth"; +var docsToc = ""; + +var srcDir = "src"; +var srcFiles = srcDir + "/**/*.ts"; +var docsDir = "docs"; +var nodeDir = "lib"; +var typesDir = "types"; +var watching = false; + +function handleError() { + if (watching) this.emit("end"); + else process.exit(1); +} + +// Set watching +gulp.task("setWatch", function() { + watching = true; +}); + +// Clear built directories +gulp.task("clean", function() { + return del([nodeDir, typesDir]); +}); + +// Lint the source +gulp.task("lint", function() { + var program = tslint.Linter.createProgram("./"); + + gulp.src(srcFiles) + .pipe(gulpTslint({ + program: program, + formatter: "stylish" + })) + .pipe(gulpTslint.report({ + emitError: !watching + })) +}); + +// Create documentation +gulp.task("doc", function() { + return gulp.src(srcFiles) + .pipe(gulpTypedoc({ + name: name, + readme: "src/documentation.md", + theme: "src/theme", + mode: "file", + target: "es6", + module: "commonjs", + out: docsDir, + excludeExternals: true, + excludePrivate: true, + hideGenerator: true, + toc: docsToc + })) + .on("error", handleError); +}); + +// Build TypeScript source into CommonJS Node modules +gulp.task("compile", ["clean"], function() { + var tsResult = gulp.src(srcFiles) + .pipe(sourcemaps.init()) + .pipe(gulpTs.createProject("tsconfig.json")()) + .on("error", handleError); + + return merge([ + tsResult.js.pipe(sourcemaps.write(".", { + sourceRoot: path.relative(nodeDir, srcDir) + })).pipe(gulp.dest(nodeDir)), + tsResult.dts.pipe(gulp.dest(typesDir)) + ]); +}); + +gulp.task("watch", ["setWatch", "default"], function() { + gulp.watch(srcFiles, ["default"]); +}); + +gulp.task("default", ["lint", "doc", "compile"]); diff --git a/package.json b/package.json new file mode 100644 index 00000000..5d160020 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "webbluetooth", + "version": "0.0.1", + "description": "Node.js implementation of the Web Bluetooth Specification", + "homepage": "https://github.com/thegecko/webbluetooth", + "author": "Rob Moran ", + "license": "MIT", + "types": "./types/index.d.ts", + "main": "./index.js", + "repository": { + "type": "git", + "url": "git://github.com/thegecko/webbluetooth.git" + }, + "keywords": [ + "web-bluetooth", + "ble", + "bluetooth" + ], + "scripts": { + "gulp": "gulp" + }, + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "noble": "^1.8.1" + }, + "devDependencies": { + "@types/noble": "0.0.34", + "@types/node": "^8.0.54", + "del": "^3.0.0", + "gulp": "^3.9.1", + "gulp-sourcemaps": "^2.6.1", + "gulp-tslint": "^8.1.2", + "gulp-typedoc": "^2.1.1", + "gulp-typescript": "^3.2.3", + "merge2": "^1.2.0", + "tslint": "^5.8.0", + "tslint-eslint-rules": "^4.1.1", + "typedoc": "^0.9.0", + "typescript": "^2.6.2" + } +} diff --git a/src/adapter.ts b/src/adapter.ts new file mode 100644 index 00000000..fbb9bd68 --- /dev/null +++ b/src/adapter.ts @@ -0,0 +1,342 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import { getCanonicalUUID } from "./helpers"; +import { BluetoothDevice } from "./device"; +import { BluetoothRemoteGATTService } from "./service"; +import { BluetoothRemoteGATTDescriptor } from "./descriptor"; +import { BluetoothRemoteGATTCharacteristic } from "./characteristic"; +import * as noble from "noble"; + +export interface Adapter { + startScan: (serviceUUIDs: Array, foundFn: (device: Partial) => void, completeFn?: () => void, errorFn?: (errorMsg: string) => void) => void; + stopScan: (errorFn?: (errorMsg: string) => void) => void; + connect: (handle: string, connectFn: () => void, disconnectFn: () => void, errorFn?: (errorMsg: string) => void) => void; + disconnect: (handle: string, errorFn?: (errorMsg: string) => void) => void; + discoverServices: (handle: string, serviceUUIDs: Array, completeFn: (services: Array>) => void, errorFn?: (errorMsg: string) => void) => void; + discoverIncludedServices: (handle: string, serviceUUIDs: Array, completeFn: (services: Array>) => void, errorFn?: (errorMsg: string) => void) => void; + discoverCharacteristics: (handle: string, characteristicUUIDs: Array, completeFn: (characteristics: Array>) => void, errorFn?: (errorMsg: string) => void) => void; + discoverDescriptors: (handle: string, descriptorUUIDs: Array, completeFn: (descriptors: Array>) => void, errorFn?: (errorMsg: string) => void) => void; + readCharacteristic: (handle: string, completeFn: (value: DataView) => void, errorFn?: (errorMsg: string) => void) => void; + writeCharacteristic: (handle: string, value: DataView, completeFn?: () => void, errorFn?: (errorMsg: string) => void) => void; + enableNotify: (handle: string, notifyFn: () => void, completeFn?: () => void, errorFn?: (errorMsg: string) => void) => void; + disableNotify: (handle: string, completeFn?: () => void, errorFn?: (errorMsg: string) => void) => void; + readDescriptor: (handle: string, completeFn: (value: DataView) => void, errorFn?: (errorMsg: string) => void) => void; + writeDescriptor: (handle: string, value: DataView, completeFn?: () => void, errorFn?: (errorMsg: string) => void) => void; +} + +export class NobleAdapter implements Adapter { + + private deviceHandles: {}; + private serviceHandles: {}; + private characteristicHandles: {}; + private descriptorHandles: {}; + private charNotifies: {}; + private foundFn: (device: Partial) => void = null; + private initialised: boolean = false; + + private init(completeFn: () => any) { + if (this.initialised) return completeFn(); + noble.on("discover", this.discover); + this.initialised = true; + completeFn(); + } + + private checkForError(errorFn, continueFn?) { + return function(error) { + if (error) errorFn(error); + else if (typeof continueFn === "function") { + const args = [].slice.call(arguments, 1); + continueFn.apply(this, args); + } + }; + } + + private bufferToDataView(buffer) { + // Buffer to ArrayBuffer + const arrayBuffer = new Uint8Array(buffer).buffer; + return new DataView(arrayBuffer); + } + + private dataViewToBuffer(dataView) { + // DataView to TypedArray + const typedArray = new Uint8Array(dataView.buffer); + return new Buffer(typedArray); + } + + private discover(deviceInfo) { + if (this.foundFn) { + const deviceID = (deviceInfo.address && deviceInfo.address !== "unknown") ? deviceInfo.address : deviceInfo.id; + if (!this.deviceHandles[deviceID]) this.deviceHandles[deviceID] = deviceInfo; + + const serviceUUIDs = []; + if (deviceInfo.advertisement.serviceUuids) { + deviceInfo.advertisement.serviceUuids.forEach(serviceUUID => { + serviceUUIDs.push(getCanonicalUUID(serviceUUID)); + }); + } + + const manufacturerData = new Map(); + if (deviceInfo.advertisement.manufacturerData) { + // First 2 bytes are 16-bit company identifier + let company = deviceInfo.advertisement.manufacturerData.readUInt16LE(0); + company = ("0000" + company.toString(16)).slice(-4); + // Remove company ID + const buffer = deviceInfo.advertisement.manufacturerData.slice(2); + manufacturerData[company] = this.bufferToDataView(buffer); + } + + const serviceData = new Map(); + if (deviceInfo.advertisement.serviceData) { + deviceInfo.advertisement.serviceData.forEach(serviceAdvert => { + serviceData[getCanonicalUUID(serviceAdvert.uuid)] = this.bufferToDataView(serviceAdvert.data); + }); + } + + this.foundFn({ + _handle: deviceID, + id: deviceID, + name: deviceInfo.advertisement.localName, + uuids: serviceUUIDs + // adData: { + // manufacturerData: manufacturerData, + // serviceData: serviceData, + // txPower: deviceInfo.advertisement.txPowerLevel, + // rssi: deviceInfo.rssi + // } + }); + } + } + + public startScan(serviceUUIDs: Array, foundFn: (device: Partial) => void, completeFn?: () => void, errorFn?: (errorMsg: string) => void): void { + + if (serviceUUIDs.length === 0) { + this.foundFn = foundFn; + } else { + this.foundFn = device => { + serviceUUIDs.forEach(serviceUUID => { + if (device.uuids.indexOf(serviceUUID) >= 0) { + foundFn(device); + return; + } + }); + }; + } + + this.init(() => { + this.deviceHandles = {}; + function stateCB(state) { + if (state === "poweredOn") { + noble.startScanning([], false, this.checkForError(errorFn, completeFn)); + } else { + errorFn("adapter not enabled"); + } + } + // tslint:disable-next-line:no-string-literal + if (noble.state === "unknown") noble["once"]("stateChange", stateCB.bind(this)); + else stateCB(noble.state); + }); + } + + public stopScan(_errorFn?: (errorMsg: string) => void): void { + this.foundFn = null; + noble.stopScanning(); + } + + public connect(handle: string, connectFn: () => void, disconnectFn: () => void, errorFn?: (errorMsg: string) => void): void { + const baseDevice = this.deviceHandles[handle]; + baseDevice.once("connect", connectFn); + baseDevice.once("disconnect", function() { + this.serviceHandles = {}; + this.characteristicHandles = {}; + this.descriptorHandles = {}; + this.charNotifies = {}; + disconnectFn(); + }.bind(this)); + baseDevice.connect(this.checkForError(errorFn)); + } + + public disconnect(handle: string, errorFn?: (errorMsg: string) => void): void { + this.deviceHandles[handle].disconnect(this.checkForError(errorFn)); + } + + public discoverServices(handle: string, serviceUUIDs: Array, completeFn: (services: Array>) => void, errorFn?: (errorMsg: string) => void): void { + const baseDevice = this.deviceHandles[handle]; + baseDevice.discoverServices([], this.checkForError(errorFn, function(services) { + + const discovered = []; + services.forEach(function(serviceInfo) { + const serviceUUID = getCanonicalUUID(serviceInfo.uuid); + + if (serviceUUIDs.length === 0 || serviceUUIDs.indexOf(serviceUUID) >= 0) { + if (!this.serviceHandles[serviceUUID]) this.serviceHandles[serviceUUID] = serviceInfo; + + discovered.push({ + _handle: serviceUUID, + uuid: serviceUUID, + primary: true + }); + } + }, this); + + completeFn(discovered); + }.bind(this))); + } + + public discoverIncludedServices(handle: string, serviceUUIDs: Array, completeFn: (services: Array>) => void, errorFn?: (errorMsg: string) => void): void { + const serviceInfo = this.serviceHandles[handle]; + serviceInfo.discoverIncludedServices([], this.checkForError(errorFn, function(services) { + + const discovered = []; + services.forEach(service => { + const serviceUUID = getCanonicalUUID(service.uuid); + + if (serviceUUIDs.length === 0 || serviceUUIDs.indexOf(serviceUUID) >= 0) { + if (!this.serviceHandles[serviceUUID]) this.serviceHandles[serviceUUID] = service; + + discovered.push({ + _handle: serviceUUID, + uuid: serviceUUID, + primary: false + }); + } + }, this); + + completeFn(discovered); + }.bind(this))); + } + + public discoverCharacteristics(handle: string, characteristicUUIDs: Array, completeFn: (characteristics: Array>) => void, errorFn?: (errorMsg: string) => void): void { + const serviceInfo = this.serviceHandles[handle]; + serviceInfo.discoverCharacteristics([], this.checkForError(errorFn, function(characteristics) { + + const discovered = []; + characteristics.forEach(function(characteristicInfo) { + const charUUID = getCanonicalUUID(characteristicInfo.uuid); + + if (characteristicUUIDs.length === 0 || characteristicUUIDs.indexOf(charUUID) >= 0) { + if (!this.characteristicHandles[charUUID]) this.characteristicHandles[charUUID] = characteristicInfo; + + discovered.push({ + _handle: charUUID, + uuid: charUUID, + properties: { + broadcast: (characteristicInfo.properties.indexOf("broadcast") >= 0), + read: (characteristicInfo.properties.indexOf("read") >= 0), + writeWithoutResponse: (characteristicInfo.properties.indexOf("writeWithoutResponse") >= 0), + write: (characteristicInfo.properties.indexOf("write") >= 0), + notify: (characteristicInfo.properties.indexOf("notify") >= 0), + indicate: (characteristicInfo.properties.indexOf("indicate") >= 0), + authenticatedSignedWrites: (characteristicInfo.properties.indexOf("authenticatedSignedWrites") >= 0), + reliableWrite: (characteristicInfo.properties.indexOf("reliableWrite") >= 0), + writableAuxiliaries: (characteristicInfo.properties.indexOf("writableAuxiliaries") >= 0) + } + }); + + characteristicInfo.on("data", function(data, isNotification) { + if (isNotification === true && typeof this.charNotifies[charUUID] === "function") { + const dataView = this.bufferToDataView(data); + this.charNotifies[charUUID](dataView); + } + }.bind(this)); + } + }, this); + + completeFn(discovered); + }.bind(this))); + } + + public discoverDescriptors(handle: string, descriptorUUIDs: Array, completeFn: (descriptors: Array>) => void, errorFn?: (errorMsg: string) => void): void { + const characteristicInfo = this.characteristicHandles[handle]; + characteristicInfo.discoverDescriptors(this.checkForError(errorFn, function(descriptors) { + + const discovered = []; + descriptors.forEach(function(descriptorInfo) { + const descUUID = getCanonicalUUID(descriptorInfo.uuid); + + if (descriptorUUIDs.length === 0 || descriptorUUIDs.indexOf(descUUID) >= 0) { + const descHandle = characteristicInfo.uuid + "-" + descriptorInfo.uuid; + if (!this.descriptorHandles[descHandle]) this.descriptorHandles[descHandle] = descriptorInfo; + + discovered.push({ + _handle: descHandle, + uuid: descUUID + }); + } + }, this); + + completeFn(discovered); + }.bind(this))); + } + + public readCharacteristic(handle: string, completeFn: (value: DataView) => void, errorFn?: (errorMsg: string) => void): void { + this.characteristicHandles[handle].read(this.checkForError(errorFn, data => { + const dataView = this.bufferToDataView(data); + completeFn(dataView); + })); + } + + public writeCharacteristic(handle: string, value: DataView, completeFn?: () => void, errorFn?: (errorMsg: string) => void): void { + const buffer = this.dataViewToBuffer(value); + this.characteristicHandles[handle].write(buffer, true, this.checkForError(errorFn, completeFn)); + } + + public enableNotify(handle: string, notifyFn: () => void, completeFn?: () => void, errorFn?: (errorMsg: string) => void): void { + if (this.charNotifies[handle]) { + this.charNotifies[handle] = notifyFn; + return completeFn(); + } + this.characteristicHandles[handle].once("notify", state => { + if (state !== true) return errorFn("notify failed to enable"); + this.charNotifies[handle] = notifyFn; + completeFn(); + }); + this.characteristicHandles[handle].notify(true, this.checkForError(errorFn)); + } + + public disableNotify(handle: string, completeFn?: () => void, errorFn?: (errorMsg: string) => void): void { + if (!this.charNotifies[handle]) { + return completeFn(); + } + this.characteristicHandles[handle].once("notify", state => { + if (state !== false) return errorFn("notify failed to disable"); + if (this.charNotifies[handle]) delete this.charNotifies[handle]; + completeFn(); + }); + this.characteristicHandles[handle].notify(false, this.checkForError(errorFn)); + } + + public readDescriptor(handle: string, completeFn: (value: DataView) => void, errorFn?: (errorMsg: string) => void): void { + this.descriptorHandles[handle].readValue(this.checkForError(errorFn, data => { + const dataView = this.bufferToDataView(data); + completeFn(dataView); + })); + } + + public writeDescriptor(handle: string, value: DataView, completeFn?: () => void, errorFn?: (errorMsg: string) => void): void { + const buffer = this.dataViewToBuffer(value); + this.descriptorHandles[handle].writeValue(buffer, this.checkForError(errorFn, completeFn)); + } +} diff --git a/src/characteristic.ts b/src/characteristic.ts new file mode 100644 index 00000000..15e008a4 --- /dev/null +++ b/src/characteristic.ts @@ -0,0 +1,138 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +export class BluetoothRemoteGATTCharacteristic { +} + +/* + // BluetoothRemoteGATTCharacteristic Object + var BluetoothRemoteGATTCharacteristic = function(properties) { + this._handle = null; + this._descriptors = null; + + this.service = null; + this.uuid = null; + this.properties = { + broadcast: false, + read: false, + writeWithoutResponse: false, + write: false, + notify: false, + indicate: false, + authenticatedSignedWrites: false, + reliableWrite: false, + writableAuxiliaries: false + }; + this.value = null; + + mergeDictionary(this, properties); + }; + BluetoothRemoteGATTCharacteristic.prototype.getDescriptor = function(descriptorUUID) { + return new Promise(function(resolve, reject) { + if (!this.service.device.gatt.connected) return reject("getDescriptor error: device not connected"); + if (!descriptorUUID) return reject("getDescriptor error: no descriptor specified"); + + this.getDescriptors(descriptorUUID) + .then(function(descriptors) { + if (descriptors.length !== 1) return reject("getDescriptor error: descriptor not found"); + resolve(descriptors[0]); + }) + .catch(function(error) { + reject(error); + }); + }.bind(this)); + }; + BluetoothRemoteGATTCharacteristic.prototype.getDescriptors = function(descriptorUUID) { + return new Promise(function(resolve, reject) { + if (!this.service.device.gatt.connected) return reject("getDescriptors error: device not connected"); + + function complete() { + if (!descriptorUUID) return resolve(this._descriptors); + var filtered = this._descriptors.filter(function(descriptor) { + return (descriptor.uuid === helpers.getDescriptorUUID(descriptorUUID)); + }); + if (filtered.length !== 1) return reject("getDescriptors error: descriptor not found"); + resolve(filtered); + } + if (this._descriptors) return complete.call(this); + adapter.discoverDescriptors(this._handle, [], function(descriptors) { + this._descriptors = descriptors.map(function(descriptorInfo) { + descriptorInfo.characteristic = this; + return new BluetoothRemoteGATTDescriptor(descriptorInfo); + }.bind(this)); + complete.call(this); + }.bind(this), wrapReject(reject, "getDescriptors error")); + }.bind(this)); + }; + BluetoothRemoteGATTCharacteristic.prototype.readValue = function() { + return new Promise(function(resolve, reject) { + if (!this.service.device.gatt.connected) return reject("readValue error: device not connected"); + + adapter.readCharacteristic(this._handle, function(dataView) { + this.value = dataView; + resolve(dataView); + this.dispatchEvent({ type: "characteristicvaluechanged", bubbles: true }); + }.bind(this), wrapReject(reject, "readValue error")); + }.bind(this)); + }; + BluetoothRemoteGATTCharacteristic.prototype.writeValue = function(bufferSource) { + return new Promise(function(resolve, reject) { + if (!this.service.device.gatt.connected) return reject("writeValue error: device not connected"); + + var arrayBuffer = bufferSource.buffer || bufferSource; + var dataView = new DataView(arrayBuffer); + adapter.writeCharacteristic(this._handle, dataView, function() { + this.value = dataView; + resolve(); + }.bind(this), wrapReject(reject, "writeValue error")); + }.bind(this)); + }; + BluetoothRemoteGATTCharacteristic.prototype.startNotifications = function() { + return new Promise(function(resolve, reject) { + if (!this.service.device.gatt.connected) return reject("startNotifications error: device not connected"); + + adapter.enableNotify(this._handle, function(dataView) { + this.value = dataView; + this.dispatchEvent({ type: "characteristicvaluechanged", bubbles: true }); + }.bind(this), function() { + resolve(this); + }.bind(this), wrapReject(reject, "startNotifications error")); + }.bind(this)); + }; + BluetoothRemoteGATTCharacteristic.prototype.stopNotifications = function() { + return new Promise(function(resolve, reject) { + if (!this.service.device.gatt.connected) return reject("stopNotifications error: device not connected"); + + adapter.disableNotify(this._handle, function() { + resolve(this); + }.bind(this), wrapReject(reject, "stopNotifications error")); + }.bind(this)); + }; + BluetoothRemoteGATTCharacteristic.prototype.addEventListener = createListenerFn([ + "characteristicvaluechanged" + ]); + BluetoothRemoteGATTCharacteristic.prototype.removeEventListener = removeEventListener; + BluetoothRemoteGATTCharacteristic.prototype.dispatchEvent = dispatchEvent; +*/ diff --git a/src/descriptor.ts b/src/descriptor.ts new file mode 100644 index 00000000..80e48137 --- /dev/null +++ b/src/descriptor.ts @@ -0,0 +1,62 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +export class BluetoothRemoteGATTDescriptor { +} + +/* + // BluetoothRemoteGATTDescriptor Object + var BluetoothRemoteGATTDescriptor = function(properties) { + this._handle = null; + + this.characteristic = null; + this.uuid = null; + this.value = null; + + mergeDictionary(this, properties); + }; + BluetoothRemoteGATTDescriptor.prototype.readValue = function() { + return new Promise(function(resolve, reject) { + if (!this.characteristic.service.device.gatt.connected) return reject("readValue error: device not connected"); + + adapter.readDescriptor(this._handle, function(dataView) { + this.value = dataView; + resolve(dataView); + }.bind(this), wrapReject(reject, "readValue error")); + }.bind(this)); + }; + BluetoothRemoteGATTDescriptor.prototype.writeValue = function(bufferSource) { + return new Promise(function(resolve, reject) { + if (!this.characteristic.service.device.gatt.connected) return reject("writeValue error: device not connected"); + + var arrayBuffer = bufferSource.buffer || bufferSource; + var dataView = new DataView(arrayBuffer); + adapter.writeDescriptor(this._handle, dataView, function() { + this.value = dataView; + resolve(); + }.bind(this), wrapReject(reject, "writeValue error")); + }.bind(this)); + }; +*/ diff --git a/src/device.ts b/src/device.ts new file mode 100644 index 00000000..616dd92d --- /dev/null +++ b/src/device.ts @@ -0,0 +1,81 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import { BluetoothRemoteGATTServer } from "./server"; + +export class BluetoothDevice { + + public _handle: string = null; + public _allowedServices: Array = []; + public id: string = "unknown"; + public name: string = null; + // public adData: { + // public appearance?: null; + // public txPower?: null; + // rssi?: number; + // manufacturerData = new Map(); + // serviceData = new Map(); + // } + public gatt: BluetoothRemoteGATTServer = new BluetoothRemoteGATTServer(); + public uuids: Array = []; + + constructor(init?: Partial) { + for (const key in init) { + if (init.hasOwnProperty(key)) { + this[key] = init[key]; + } + } + + this.gatt.device = this; + } +} + +/* + // BluetoothDevice Object + var BluetoothDevice = function(properties) { + this._handle = null; + this._allowedServices = []; + + this.id = "unknown"; + this.name = null; + this.adData = { + appearance: null, + txPower: null, + rssi: null, + manufacturerData: new Map(), + serviceData: new Map() + }; + this.gatt = new BluetoothRemoteGATTServer(); + this.gatt.device = this; + this.uuids = []; + + mergeDictionary(this, properties); + }; + BluetoothDevice.prototype.addEventListener = createListenerFn([ + "gattserverdisconnected", + ]); + BluetoothDevice.prototype.removeEventListener = removeEventListener; + BluetoothDevice.prototype.dispatchEvent = dispatchEvent; +*/ diff --git a/src/documentation.md b/src/documentation.md new file mode 100644 index 00000000..d281ee70 --- /dev/null +++ b/src/documentation.md @@ -0,0 +1,12 @@ +# Node Web Bluetooth +Node.js implementation of the Web Bluetooth Specification + +## Prerequisites + +[Node.js > v4.0.0](https://nodejs.org), which includes `npm`. + +## Installation + +```bash +$ npm install web-usb +``` diff --git a/src/examples/eddystone.ts b/src/examples/eddystone.ts new file mode 100644 index 00000000..07a46e96 --- /dev/null +++ b/src/examples/eddystone.ts @@ -0,0 +1,142 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +/* +var bluetooth = require('../index').webbluetooth; +var helpers = require('../index').helpers; +var eddystoneUUID = 0xFEAA; + +var frameTypes = { + "UID": 0x00, + "URL": 0x10, + "TLM": 0x20 +} + +var schemes = { + 0x00: "http://www.", + 0x01: "https://www.", + 0x02: "http://", + 0x03: "https://" +} + +var expansions = { + 0x00: ".com/", + 0x01: ".org/", + 0x02: ".edu/", + 0x03: ".net/", + 0x04: ".info/", + 0x05: ".biz/", + 0x06: ".gov/", + 0x07: ".com", + 0x08: ".org", + 0x09: ".edu", + 0x0a: ".net", + 0x0b: ".info", + 0x0c: ".biz", + 0x0d: ".gov" +} + +function decodeEddystone(view) { + var type = view.getUint8(0); + if (typeof type === "undefined") return null; + + if (type === frameTypes.UID) { + var uidArray = []; + for (var i = 2; i < view.byteLength; i++) { + var hex = view.getUint8(i).toString(16); + uidArray.push(("00" + hex).slice(-2)); + } + return { + type: type, + txPower: view.getInt8(1), + namespace: uidArray.slice(0, 10).join(), + instance: uidArray.slice(10, 16).join() + }; + } + + if (type === frameTypes.URL) { + var url = ""; + for (var i = 2; i < view.byteLength; i++) { + if (i === 2) { + url += schemes[view.getUint8(i)]; + } else { + url += expansions[view.getUint8(i)] || String.fromCharCode(view.getUint8(i)); + } + } + return { + type: type, + txPower: view.getInt8(1), + url: url + }; + } + + if (type === frameTypes.TLM) { + return { + type: type, + version: view.getUint8(1), + battery: view.getUint16(2), + temperature: view.getInt16(4), + advCount: view.getUint32(6), + secCount: view.getUint32(10) + }; + } +} + +function handleDeviceFound(bluetoothDevice) { + var uuid = helpers.getServiceUUID(eddystoneUUID); + var eddyData = bluetoothDevice.adData.serviceData.get(uuid); + if (eddyData) { + var decoded = decodeEddystone(eddyData); + if (decoded) { + switch(decoded.type) { + case frameTypes.UID: + console.log("txPower: " + decoded.txPower); + break; + case frameTypes.URL: + console.log("url: " + decoded.url); + break; + case frameTypes.TLM: + console.log("version: " + decoded.version); + break; + } + } + } +} + +// Recursively scan +function scan() { + console.log("scanning..."); + bluetooth.requestDevice({ + filters:[{ services:[ eddystoneUUID ] }], + deviceFound: handleDeviceFound + }) + .then(scan) + .catch(error => { + console.log(error); + process.exit(); + }); +} +scan(); +*/ diff --git a/src/examples/heartrate.ts b/src/examples/heartrate.ts new file mode 100644 index 00000000..37f1c472 --- /dev/null +++ b/src/examples/heartrate.ts @@ -0,0 +1,79 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +/* +var bluetooth = require('../index').webbluetooth; +var gattServer; +var heartChar; + +function log(message) { + console.log(message); +} + +log('Requesting Bluetooth Devices...'); +bluetooth.requestDevice({ + filters:[{ services:[ "heart_rate" ] }] +}) +.then(device => { + log('Found device: ' + device.name); + return device.gatt.connect(); +}) +.then(server => { + gattServer = server; + log('Gatt server connected: ' + gattServer.connected); + return gattServer.getPrimaryService("heart_rate"); +}) +.then(service => { + log('Primary service: ' + service.uuid); + return service.getCharacteristic("heart_rate_measurement"); +}) +.then(characteristic => { + log('Characteristic: ' + characteristic.uuid); + heartChar = characteristic; + return heartChar.getDescriptors(); +}) +.then(descriptors => { + descriptors.forEach(descriptor => { + log('Descriptor: ' + descriptor.uuid); + }); + + return Array.apply(null, Array(10)).reduce(sequence => { + return sequence.then(() => { + return heartChar.readValue(); + }).then(value => { + log('Value: ' + value.getUint16(0)); + }); + }, Promise.resolve()); +}) +.then(() => { + gattServer.disconnect(); + log('Gatt server connected: ' + gattServer.connected); + process.exit(); +}) +.catch(error => { + log(error); + process.exit(); +}); +*/ diff --git a/src/examples/selector.ts b/src/examples/selector.ts new file mode 100644 index 00000000..85d46368 --- /dev/null +++ b/src/examples/selector.ts @@ -0,0 +1,120 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +/* +var bluetooth = require('../index').webbluetooth; +var bluetoothDevices = []; + +function logError(error) { + console.log(error); + process.exit(); +} + +process.stdin.setEncoding('utf8'); +process.stdin.on('readable', () => { + var input = process.stdin.read(); + if (input === '\u0003') { + process.exit(); + } else { + var index = parseInt(input); + if (index && index <= bluetoothDevices.length) { + process.stdin.setRawMode(false); + selectDevice(index - 1); + } + } +}); + +function enumerateGatt(server) { + return server.getPrimaryServices() + .then(services => { + var sPromises = services.map(service => { + return service.getCharacteristics() + .then(characteristics => { + var cPromises = characteristics.map(characteristic => { + return characteristic.getDescriptors() + .then(descriptors => { + descriptors = descriptors.map(descriptor => `\t\tâ””descriptor: ${descriptor.uuid}`); + descriptors.unshift(`\tâ””characteristic: ${characteristic.uuid}`); + return descriptors.join("\n"); + }); + }); + + return Promise.all(cPromises) + .then(descriptors => { + descriptors.unshift(`service: ${service.uuid}`); + return descriptors.join("\n"); + }); + }); + }); + + return Promise.all(sPromises) + .then(services => { + console.log(services.join("\n")); + }); + }); +} + +function handleDeviceFound(bluetoothDevice, selectFn) { + var discovered = bluetoothDevices.some(device => { + return (device.id === bluetoothDevice.id); + }); + if (discovered) return; + + if (bluetoothDevices.length === 0) { + process.stdin.setRawMode(true); + console.log("select a device:"); + } + + bluetoothDevices.push({ id: bluetoothDevice.id, select: selectFn }); + console.log(bluetoothDevices.length + ": " + bluetoothDevice.name); +} + +function selectDevice(index) { + var device = bluetoothDevices[index]; + device.select(); +} + +var server = null; +console.log("scanning..."); + +bluetooth.requestDevice({ + deviceFound: handleDeviceFound +}) +.then(device => { + console.log("connecting..."); + return device.gatt.connect(); +}) +.then(gattServer => { + console.log("connected"); + server = gattServer; + return enumerateGatt(server); +}) +.then(() => server.disconnect()) +.then(() => { + console.log("\ndisconnected"); + process.exit(); +}) +.catch(logError); +*/ diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 00000000..76ba3deb --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,264 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +export enum bluetoothServices { + "alert_notification" = 0x1811, + "automation_io" = 0x1815, + "battery_service" = 0x180F, + "blood_pressure" = 0x1810, + "body_composition" = 0x181B, + "bond_management" = 0x181E, + "continuous_glucose_monitoring" = 0x181F, + "current_time" = 0x1805, + "cycling_power" = 0x1818, + "cycling_speed_and_cadence" = 0x1816, + "device_information" = 0x180A, + "environmental_sensing" = 0x181A, + "generic_access" = 0x1800, + "generic_attribute" = 0x1801, + "glucose" = 0x1808, + "health_thermometer" = 0x1809, + "heart_rate" = 0x180D, + "human_interface_device" = 0x1812, + "immediate_alert" = 0x1802, + "indoor_positioning" = 0x1821, + "internet_protocol_support" = 0x1820, + "link_loss" = 0x1803, + "location_and_navigation" = 0x1819, + "next_dst_change" = 0x1807, + "phone_alert_status" = 0x180E, + "pulse_oximeter" = 0x1822, + "reference_time_update" = 0x1806, + "running_speed_and_cadence" = 0x1814, + "scan_parameters" = 0x1813, + "tx_power" = 0x1804, + "user_data" = 0x181C, + "weight_scale" = 0x181D +} + +export enum bluetoothCharacteristics { + "aerobic_heart_rate_lower_limit" = 0x2A7E, + "aerobic_heart_rate_upper_limit" = 0x2A84, + "aerobic_threshold" = 0x2A7F, + "age" = 0x2A80, + "aggregate" = 0x2A5A, + "alert_category_id" = 0x2A43, + "alert_category_id_bit_mask" = 0x2A42, + "alert_level" = 0x2A06, + "alert_notification_control_point" = 0x2A44, + "alert_status" = 0x2A3F, + "altitude" = 0x2AB3, + "anaerobic_heart_rate_lower_limit" = 0x2A81, + "anaerobic_heart_rate_upper_limit" = 0x2A82, + "anaerobic_threshold" = 0x2A83, + "analog" = 0x2A58, + "apparent_wind_direction" = 0x2A73, + "apparent_wind_speed" = 0x2A72, + "gap.appearance" = 0x2A01, + "barometric_pressure_trend" = 0x2AA3, + "battery_level" = 0x2A19, + "blood_pressure_feature" = 0x2A49, + "blood_pressure_measurement" = 0x2A35, + "body_composition_feature" = 0x2A9B, + "body_composition_measurement" = 0x2A9C, + "body_sensor_location" = 0x2A38, + "bond_management_control_point" = 0x2AA4, + "bond_management_feature" = 0x2AA5, + "boot_keyboard_input_report" = 0x2A22, + "boot_keyboard_output_report" = 0x2A32, + "boot_mouse_input_report" = 0x2A33, + "gap.central_address_resolution_support" = 0x2AA6, + "cgm_feature" = 0x2AA8, + "cgm_measurement" = 0x2AA7, + "cgm_session_run_time" = 0x2AAB, + "cgm_session_start_time" = 0x2AAA, + "cgm_specific_ops_control_point" = 0x2AAC, + "cgm_status" = 0x2AA9, + "csc_feature" = 0x2A5C, + "csc_measurement" = 0x2A5B, + "current_time" = 0x2A2B, + "cycling_power_control_point" = 0x2A66, + "cycling_power_feature" = 0x2A65, + "cycling_power_measurement" = 0x2A63, + "cycling_power_vector" = 0x2A64, + "database_change_increment" = 0x2A99, + "date_of_birth" = 0x2A85, + "date_of_threshold_assessment" = 0x2A86, + "date_time" = 0x2A08, + "day_date_time" = 0x2A0A, + "day_of_week" = 0x2A09, + "descriptor_value_changed" = 0x2A7D, + "gap.device_name" = 0x2A00, + "dew_point" = 0x2A7B, + "digital" = 0x2A56, + "dst_offset" = 0x2A0D, + "elevation" = 0x2A6C, + "email_address" = 0x2A87, + "exact_time_256" = 0x2A0C, + "fat_burn_heart_rate_lower_limit" = 0x2A88, + "fat_burn_heart_rate_upper_limit" = 0x2A89, + "firmware_revision_string" = 0x2A26, + "first_name" = 0x2A8A, + "five_zone_heart_rate_limits" = 0x2A8B, + "floor_number" = 0x2AB2, + "gender" = 0x2A8C, + "glucose_feature" = 0x2A51, + "glucose_measurement" = 0x2A18, + "glucose_measurement_context" = 0x2A34, + "gust_factor" = 0x2A74, + "hardware_revision_string" = 0x2A27, + "heart_rate_control_point" = 0x2A39, + "heart_rate_max" = 0x2A8D, + "heart_rate_measurement" = 0x2A37, + "heat_index" = 0x2A7A, + "height" = 0x2A8E, + "hid_control_point" = 0x2A4C, + "hid_information" = 0x2A4A, + "hip_circumference" = 0x2A8F, + "humidity" = 0x2A6F, + "ieee_11073-20601_regulatory_certification_data_list" = 0x2A2A, + "indoor_positioning_configuration" = 0x2AAD, + "intermediate_blood_pressure" = 0x2A36, + "intermediate_temperature" = 0x2A1E, + "irradiance" = 0x2A77, + "language" = 0x2AA2, + "last_name" = 0x2A90, + "latitude" = 0x2AAE, + "ln_control_point" = 0x2A6B, + "ln_feature" = 0x2A6A, + "local_east_coordinate.xml" = 0x2AB1, + "local_north_coordinate" = 0x2AB0, + "local_time_information" = 0x2A0F, + "location_and_speed" = 0x2A67, + "location_name" = 0x2AB5, + "longitude" = 0x2AAF, + "magnetic_declination" = 0x2A2C, + "magnetic_flux_density_2D" = 0x2AA0, + "magnetic_flux_density_3D" = 0x2AA1, + "manufacturer_name_string" = 0x2A29, + "maximum_recommended_heart_rate" = 0x2A91, + "measurement_interval" = 0x2A21, + "model_number_string" = 0x2A24, + "navigation" = 0x2A68, + "new_alert" = 0x2A46, + "gap.peripheral_preferred_connection_parameters" = 0x2A04, + "gap.peripheral_privacy_flag" = 0x2A02, + "plx_continuous_measurement" = 0x2A5F, + "plx_features" = 0x2A60, + "plx_spot_check_measurement" = 0x2A5E, + "pnp_id" = 0x2A50, + "pollen_concentration" = 0x2A75, + "position_quality" = 0x2A69, + "pressure" = 0x2A6D, + "protocol_mode" = 0x2A4E, + "rainfall" = 0x2A78, + "gap.reconnection_address" = 0x2A03, + "record_access_control_point" = 0x2A52, + "reference_time_information" = 0x2A14, + "report" = 0x2A4D, + "report_map" = 0x2A4B, + "resting_heart_rate" = 0x2A92, + "ringer_control_point" = 0x2A40, + "ringer_setting" = 0x2A41, + "rsc_feature" = 0x2A54, + "rsc_measurement" = 0x2A53, + "sc_control_point" = 0x2A55, + "scan_interval_window" = 0x2A4F, + "scan_refresh" = 0x2A31, + "sensor_location" = 0x2A5D, + "serial_number_string" = 0x2A25, + "gatt.service_changed" = 0x2A05, + "software_revision_string" = 0x2A28, + "sport_type_for_aerobic_and_anaerobic_thresholds" = 0x2A93, + "supported_new_alert_category" = 0x2A47, + "supported_unread_alert_category" = 0x2A48, + "system_id" = 0x2A23, + "temperature" = 0x2A6E, + "temperature_measurement" = 0x2A1C, + "temperature_type" = 0x2A1D, + "three_zone_heart_rate_limits" = 0x2A94, + "time_accuracy" = 0x2A12, + "time_source" = 0x2A13, + "time_update_control_point" = 0x2A16, + "time_update_state" = 0x2A17, + "time_with_dst" = 0x2A11, + "time_zone" = 0x2A0E, + "true_wind_direction" = 0x2A71, + "true_wind_speed" = 0x2A70, + "two_zone_heart_rate_limit" = 0x2A95, + "tx_power_level" = 0x2A07, + "uncertainty" = 0x2AB4, + "unread_alert_status" = 0x2A45, + "user_control_point" = 0x2A9F, + "user_index" = 0x2A9A, + "uv_index" = 0x2A76, + "vo2_max" = 0x2A96, + "waist_circumference" = 0x2A97, + "weight" = 0x2A98, + "weight_measurement" = 0x2A9D, + "weight_scale_feature" = 0x2A9E, + "wind_chill" = 0x2A79 +} + +export enum bluetoothDescriptors { + "gatt.characteristic_extended_properties" = 0x2900, + "gatt.characteristic_user_description" = 0x2901, + "gatt.client_characteristic_configuration" = 0x2902, + "gatt.server_characteristic_configuration" = 0x2903, + "gatt.characteristic_presentation_format" = 0x2904, + "gatt.characteristic_aggregate_format" = 0x2905, + "valid_range" = 0x2906, + "external_report_reference" = 0x2907, + "report_reference" = 0x2908, + "number_of_digitals" = 0x2909, + "value_trigger_setting" = 0x290A, + "es_configuration" = 0x290B, + "es_measurement" = 0x290C, + "es_trigger_setting" = 0x290D, + "time_trigger_setting" = 0x290E +} + +export function getCanonicalUUID(uuid) { + if (typeof uuid === "number") uuid = uuid.toString(16); + uuid = uuid.toLowerCase(); + if (uuid.length <= 8) uuid = ("00000000" + uuid).slice(-8) + "-0000-1000-8000-00805f9b34fb"; + if (uuid.length === 32) uuid = uuid.match(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/).splice(1).join("-"); + return uuid; +} + +export function getServiceUUID(uuid) { + if (bluetoothServices[uuid]) uuid = bluetoothServices[uuid]; + return getCanonicalUUID(uuid); +} + +export function getCharacteristicUUID(uuid) { + if (bluetoothCharacteristics[uuid]) uuid = bluetoothCharacteristics[uuid]; + return getCanonicalUUID(uuid); +} + +export function getDescriptorUUID(uuid) { + if (bluetoothDescriptors[uuid]) uuid = bluetoothDescriptors[uuid]; + return getCanonicalUUID(uuid); +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 00000000..83eaa2a1 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,97 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import { BluetoothDevice } from "./device"; + +export class BluetoothRemoteGATTServer { + public _services: Array = null; + public device: BluetoothDevice = null; + public connected: boolean = false; +} + +/* + // BluetoothRemoteGATTServer Object + var BluetoothRemoteGATTServer = function() { + this._services = null; + + this.device = null; + this.connected = false; + }; + BluetoothRemoteGATTServer.prototype.connect = function() { + return new Promise(function(resolve, reject) { + if (this.connected) return reject("connect error: device already connected"); + + adapter.connect(this.device._handle, function() { + this.connected = true; + resolve(this); + }.bind(this), function() { + this._services = null; + this.connected = false; + this.device.dispatchEvent({ type: "gattserverdisconnected", bubbles: true }); + }.bind(this), wrapReject(reject, "connect error")); + }.bind(this)); + }; + BluetoothRemoteGATTServer.prototype.disconnect = function() { + adapter.disconnect(this.device._handle); + this.connected = false; + }; + BluetoothRemoteGATTServer.prototype.getPrimaryService = function(serviceUUID) { + return new Promise(function(resolve, reject) { + if (!this.connected) return reject("getPrimaryService error: device not connected"); + if (!serviceUUID) return reject("getPrimaryService error: no service specified"); + + this.getPrimaryServices(serviceUUID) + .then(function(services) { + if (services.length !== 1) return reject("getPrimaryService error: service not found"); + resolve(services[0]); + }) + .catch(function(error) { + reject(error); + }); + }.bind(this)); + }; + BluetoothRemoteGATTServer.prototype.getPrimaryServices = function(serviceUUID) { + return new Promise(function(resolve, reject) { + if (!this.connected) return reject("getPrimaryServices error: device not connected"); + + function complete() { + if (!serviceUUID) return resolve(this._services); + var filtered = this._services.filter(function(service) { + return (service.uuid === helpers.getServiceUUID(serviceUUID)); + }); + if (filtered.length !== 1) return reject("getPrimaryServices error: service not found"); + resolve(filtered); + } + if (this._services) return complete.call(this); + adapter.discoverServices(this.device._handle, this.device._allowedServices, function(services) { + this._services = services.map(function(serviceInfo) { + serviceInfo.device = this.device; + return new BluetoothRemoteGATTService(serviceInfo); + }.bind(this)); + complete.call(this); + }.bind(this), wrapReject(reject, "getPrimaryServices error")); + }.bind(this)); + }; +*/ diff --git a/src/service.ts b/src/service.ts new file mode 100644 index 00000000..072b6328 --- /dev/null +++ b/src/service.ts @@ -0,0 +1,124 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +export class BluetoothRemoteGATTService { +} + +/* + // BluetoothRemoteGATTService Object + var BluetoothRemoteGATTService = function(properties) { + this._handle = null; + this._services = null; + this._characteristics = null; + + this.device = null; + this.uuid = null; + this.isPrimary = false; + + mergeDictionary(this, properties); + this.dispatchEvent({ type: "serviceadded", bubbles: true }); + }; + BluetoothRemoteGATTService.prototype.getCharacteristic = function(characteristicUUID) { + return new Promise(function(resolve, reject) { + if (!this.device.gatt.connected) return reject("getCharacteristic error: device not connected"); + if (!characteristicUUID) return reject("getCharacteristic error: no characteristic specified"); + + this.getCharacteristics(characteristicUUID) + .then(function(characteristics) { + if (characteristics.length !== 1) return reject("getCharacteristic error: characteristic not found"); + resolve(characteristics[0]); + }) + .catch(function(error) { + reject(error); + }); + }.bind(this)); + }; + BluetoothRemoteGATTService.prototype.getCharacteristics = function(characteristicUUID) { + return new Promise(function(resolve, reject) { + if (!this.device.gatt.connected) return reject("getCharacteristics error: device not connected"); + + function complete() { + if (!characteristicUUID) return resolve(this._characteristics); + var filtered = this._characteristics.filter(function(characteristic) { + return (characteristic.uuid === helpers.getCharacteristicUUID(characteristicUUID)); + }); + if (filtered.length !== 1) return reject("getCharacteristics error: characteristic not found"); + resolve(filtered); + } + if (this._characteristics) return complete.call(this); + adapter.discoverCharacteristics(this._handle, [], function(characteristics) { + this._characteristics = characteristics.map(function(characteristicInfo) { + characteristicInfo.service = this; + return new BluetoothRemoteGATTCharacteristic(characteristicInfo); + }.bind(this)); + complete.call(this); + }.bind(this), wrapReject(reject, "getCharacteristics error")); + }.bind(this)); + }; + BluetoothRemoteGATTService.prototype.getIncludedService = function(serviceUUID) { + return new Promise(function(resolve, reject) { + if (!this.device.gatt.connected) return reject("getIncludedService error: device not connected"); + if (!serviceUUID) return reject("getIncludedService error: no service specified"); + + this.getIncludedServices(serviceUUID) + .then(function(services) { + if (services.length !== 1) return reject("getIncludedService error: service not found"); + resolve(services[0]); + }) + .catch(function(error) { + reject(error); + }); + }.bind(this)); + }; + BluetoothRemoteGATTService.prototype.getIncludedServices = function(serviceUUID) { + return new Promise(function(resolve, reject) { + if (!this.device.gatt.connected) return reject("getIncludedServices error: device not connected"); + + function complete() { + if (!serviceUUID) return resolve(this._services); + var filtered = this._services.filter(function(service) { + return (service.uuid === helpers.getServiceUUID(serviceUUID)); + }); + if (filtered.length !== 1) return reject("getIncludedServices error: service not found"); + resolve(filtered); + } + if (this._services) return complete.call(this); + adapter.discoverIncludedServices(this._handle, this.device._allowedServices, function(services) { + this._services = services.map(function(serviceInfo) { + serviceInfo.device = this.device; + return new BluetoothRemoteGATTService(serviceInfo); + }.bind(this)); + complete.call(this); + }.bind(this), wrapReject(reject, "getIncludedServices error")); + }.bind(this)); + }; + BluetoothRemoteGATTService.prototype.addEventListener = createListenerFn([ + "serviceadded", + "servicechanged", + "serviceremoved" + ]); + BluetoothRemoteGATTService.prototype.removeEventListener = removeEventListener; + BluetoothRemoteGATTService.prototype.dispatchEvent = dispatchEvent; +*/ diff --git a/src/theme/layouts/default.hbs b/src/theme/layouts/default.hbs new file mode 100644 index 00000000..4b4a2cf0 --- /dev/null +++ b/src/theme/layouts/default.hbs @@ -0,0 +1,45 @@ + + + + + + {{model.name}} | {{project.name}} + + + + + + + +{{> header}} + + + +
+
+ {{#if model.readme}} +
+ {{#markdown}}{{{model.readme}}}{{/markdown}} +
+ {{/if}} + + {{{contents}}} + {{> footer}} +
+
+ +
+ + + + +{{> analytics}} + + + \ No newline at end of file diff --git a/src/theme/partials/analytics.hbs b/src/theme/partials/analytics.hbs new file mode 100644 index 00000000..2c4ec9bb --- /dev/null +++ b/src/theme/partials/analytics.hbs @@ -0,0 +1,11 @@ +{{#if settings.gaID}} + +{{/if}} \ No newline at end of file diff --git a/src/theme/partials/breadcrumb.hbs b/src/theme/partials/breadcrumb.hbs new file mode 100644 index 00000000..31974183 --- /dev/null +++ b/src/theme/partials/breadcrumb.hbs @@ -0,0 +1,16 @@ +{{#if parent}} + {{#with parent}}{{> breadcrumb}}{{/with}} +
  • + {{#if url}} + {{name}} + {{else}} + {{name}} + {{/if}} +
  • +{{else}} + {{#if url}} +
  • + Globals +
  • + {{/if}} +{{/if}} \ No newline at end of file diff --git a/src/theme/partials/comment.hbs b/src/theme/partials/comment.hbs new file mode 100644 index 00000000..1fbd2d78 --- /dev/null +++ b/src/theme/partials/comment.hbs @@ -0,0 +1,22 @@ +{{#with comment}} + {{#if hasVisibleComponent}} +
    + {{#if shortText}} +
    + {{#markdown}}{{{shortText}}}{{/markdown}} +
    + {{/if}} + {{#if text}} + {{#markdown}}{{{text}}}{{/markdown}} + {{/if}} + {{#if tags}} +
    + {{#each tags}} +
    {{tagName}}
    +
    {{#markdown}}{{{text}}}{{/markdown}}
    + {{/each}} +
    + {{/if}} +
    + {{/if}} +{{/with}} \ No newline at end of file diff --git a/src/theme/partials/footer.hbs b/src/theme/partials/footer.hbs new file mode 100644 index 00000000..76812560 --- /dev/null +++ b/src/theme/partials/footer.hbs @@ -0,0 +1,66 @@ + + +
    +

    Legend

    +
    +
      +
    • Module
    • +
    • Object literal
    • +
    • Variable
    • +
    • Function
    • +
    • Function with type parameter
    • +
    • Index signature
    • +
    • Type alias
    • +
    +
      +
    • Enumeration
    • +
    • Enumeration member
    • +
    • Property
    • +
    • Method
    • +
    +
      +
    • Interface
    • +
    • Interface with type parameter
    • +
    • Constructor
    • +
    • Property
    • +
    • Method
    • +
    • Index signature
    • +
    +
      +
    • Class
    • +
    • Class with type parameter
    • +
    • Constructor
    • +
    • Property
    • +
    • Method
    • +
    • Accessor
    • +
    • Index signature
    • +
    +
      +
    • Inherited constructor
    • +
    • Inherited property
    • +
    • Inherited method
    • +
    • Inherited accessor
    • +
    +
      +
    • Protected property
    • +
    • Protected method
    • +
    • Protected accessor
    • +
    +
      +
    • Private property
    • +
    • Private method
    • +
    • Private accessor
    • +
    +
      +
    • Static property
    • +
    • Static method
    • +
    +
    +
    + + +{{#unless settings.hideGenerator}} +
    +

    Generated using TypeDoc

    +
    +{{/unless}} \ No newline at end of file diff --git a/src/theme/partials/header.hbs b/src/theme/partials/header.hbs new file mode 100644 index 00000000..bbbaf05f --- /dev/null +++ b/src/theme/partials/header.hbs @@ -0,0 +1,22 @@ +
    +
    +
    +
    + + +
    +
    +
    +
    \ No newline at end of file diff --git a/src/theme/partials/hierarchy.hbs b/src/theme/partials/hierarchy.hbs new file mode 100644 index 00000000..9a54d510 --- /dev/null +++ b/src/theme/partials/hierarchy.hbs @@ -0,0 +1,17 @@ +
      + {{#each types}} +
    • + {{#if ../isTarget}} + {{this}} + {{else}} + {{> type}} + {{/if}} + + {{#if @last}} + {{#with ../next}} + {{> hierarchy}} + {{/with}} + {{/if}} +
    • + {{/each}} +
    diff --git a/src/theme/partials/index.hbs b/src/theme/partials/index.hbs new file mode 100644 index 00000000..6224dfea --- /dev/null +++ b/src/theme/partials/index.hbs @@ -0,0 +1,19 @@ +{{#if groups}} +
    +

    Index

    +
    +
    + {{#each groups}} +
    +

    {{title}}

    + +
    + {{/each}} +
    +
    +
    +{{/if}} \ No newline at end of file diff --git a/src/theme/partials/member.declaration.hbs b/src/theme/partials/member.declaration.hbs new file mode 100644 index 00000000..9dbe3f32 --- /dev/null +++ b/src/theme/partials/member.declaration.hbs @@ -0,0 +1,22 @@ +
    {{#compact}} + {{{wbr name}}}{{#if isOptional}}?{{/if}}: {{#with type}}{{>type}}{{/with}} + {{#if defaultValue}} + +  =  + {{defaultValue}} + + {{/if}} +{{/compact}}
    + +{{> member.sources}} + +{{> comment}} + +{{#if type.declaration}} +
    +

    Type declaration

    + {{#with type.declaration}} + {{> parameter}} + {{/with}} +
    +{{/if}} \ No newline at end of file diff --git a/src/theme/partials/member.getterSetter.hbs b/src/theme/partials/member.getterSetter.hbs new file mode 100644 index 00000000..3dede27e --- /dev/null +++ b/src/theme/partials/member.getterSetter.hbs @@ -0,0 +1,37 @@ +
      + {{#if getSignature}} + {{#with getSignature}} +
    • {{#compact}} + get  + {{../name}} + {{> member.signature.title hideName=true }} + {{/compact}}
    • + {{/with}} + {{/if}} + {{#if setSignature}} + {{#with setSignature}} +
    • {{#compact}} + set  + {{../name}} + {{> member.signature.title hideName=true }} + {{/compact}}
    • + {{/with}} + {{/if}} +
    + +
      + {{#if getSignature}} + {{#with getSignature}} +
    • + {{> member.signature.body }} +
    • + {{/with}} + {{/if}} + {{#if setSignature}} + {{#with setSignature}} +
    • + {{> member.signature.body }} +
    • + {{/with}} + {{/if}} +
    \ No newline at end of file diff --git a/src/theme/partials/member.hbs b/src/theme/partials/member.hbs new file mode 100644 index 00000000..01c59e81 --- /dev/null +++ b/src/theme/partials/member.hbs @@ -0,0 +1,22 @@ +
    + + {{#if name}} +

    {{#each flags}}{{this}} {{/each}}{{{wbr name}}}

    + {{/if}} + + {{#if signatures}} + {{> member.signatures}} + {{else}}{{#if hasGetterOrSetter}} + {{> member.getterSetter}} + {{else}} + {{> member.declaration}} + {{/if}}{{/if}} + + {{#each groups}} + {{#each children}} + {{#unless hasOwnDocument}} + {{> member}} + {{/unless}} + {{/each}} + {{/each}} +
    diff --git a/src/theme/partials/member.signature.body.hbs b/src/theme/partials/member.signature.body.hbs new file mode 100644 index 00000000..fdde257d --- /dev/null +++ b/src/theme/partials/member.signature.body.hbs @@ -0,0 +1,56 @@ +{{#unless hideSources}} + {{> member.sources}} +{{/unless}} + +{{> comment}} + +{{#if typeParameters}} +

    Type parameters

    + {{> typeParameters}} +{{/if}} + +{{#if parameters}} +

    Parameters

    +
      + {{#each parameters}} +
    • +
      {{#compact}} + {{#each flags}} + {{this}}  + {{/each}} + {{#if flags.isRest}}...{{/if}} + {{name}}:  + {{#with type}}{{>type}}{{/with}} + {{#if defaultValue}} + +  =  + {{defaultValue}} + + {{/if}} + {{/compact}}
      + + {{> comment}} + + {{#if type.declaration}} + {{#with type.declaration}} + {{> parameter}} + {{/with}} + {{/if}} +
    • + {{/each}} +
    +{{/if}} + +{{#if type}} +

    Returns {{#with type}}{{>type}}{{/with}}

    + + {{#if comment.returns}} + {{#markdown}}{{{comment.returns}}}{{/markdown}} + {{/if}} + + {{#if type.declaration}} + {{#with type.declaration}} + {{> parameter}} + {{/with}} + {{/if}} +{{/if}} \ No newline at end of file diff --git a/src/theme/partials/member.signature.title.hbs b/src/theme/partials/member.signature.title.hbs new file mode 100644 index 00000000..efe9bcc4 --- /dev/null +++ b/src/theme/partials/member.signature.title.hbs @@ -0,0 +1,28 @@ +{{#compact}} + {{#unless hideName}}{{{wbr name}}}{{/unless}} + {{#if typeParameters}} + < + {{#each typeParameters}} + {{#if @index}}, {{/if}} + {{name}} + {{/each}} + > + {{/if}} + ( + {{#each parameters}} + {{#if @index}}, {{/if}} + {{#if flags.isRest}}...{{/if}} + {{name}} + + {{#if flags.isOptional}}?{{/if}} + {{#if defaultValue}}?{{/if}} + :  + + {{#with type}}{{>type}}{{/with}} + {{/each}} + ) + {{#if type}} + + {{#with type}}{{>type}}{{/with}} + {{/if}} +{{/compact}} \ No newline at end of file diff --git a/src/theme/partials/member.signatures.hbs b/src/theme/partials/member.signatures.hbs new file mode 100644 index 00000000..40d53597 --- /dev/null +++ b/src/theme/partials/member.signatures.hbs @@ -0,0 +1,11 @@ +{{#each signatures}} +
      +
    • {{> member.signature.title }}
    • +
    + +
      +
    • + {{> member.signature.body }} +
    • +
    +{{/each}} diff --git a/src/theme/partials/member.sources.hbs b/src/theme/partials/member.sources.hbs new file mode 100644 index 00000000..f3195dae --- /dev/null +++ b/src/theme/partials/member.sources.hbs @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/src/theme/partials/members.group.hbs b/src/theme/partials/members.group.hbs new file mode 100644 index 00000000..574cf147 --- /dev/null +++ b/src/theme/partials/members.group.hbs @@ -0,0 +1,8 @@ +
    +

    {{title}}

    + {{#each children}} + {{#unless hasOwnDocument}} + {{> member}} + {{/unless}} + {{/each}} +
    diff --git a/src/theme/partials/members.hbs b/src/theme/partials/members.hbs new file mode 100644 index 00000000..728fe7b5 --- /dev/null +++ b/src/theme/partials/members.hbs @@ -0,0 +1,5 @@ +{{#each groups}} + {{#unless allChildrenHaveOwnDocument}} + {{> members.group}} + {{/unless}} +{{/each}} \ No newline at end of file diff --git a/src/theme/partials/navigation.hbs b/src/theme/partials/navigation.hbs new file mode 100644 index 00000000..b1f91e4e --- /dev/null +++ b/src/theme/partials/navigation.hbs @@ -0,0 +1,26 @@ +{{#if isVisible}} + {{#if isLabel}} +
  • + {{{wbr title}}} +
  • + {{else}} + {{#if isGlobals}} +
  • + {{{wbr title}}} +
  • + {{else}} +
  • + {{{wbr title}}} + {{#if isInPath}} + {{#if children}} +
      + {{#each children}} + {{> navigation}} + {{/each}} +
    + {{/if}} + {{/if}} +
  • + {{/if}} + {{/if}} +{{/if}} diff --git a/src/theme/partials/parameter.hbs b/src/theme/partials/parameter.hbs new file mode 100644 index 00000000..eb06dc07 --- /dev/null +++ b/src/theme/partials/parameter.hbs @@ -0,0 +1,81 @@ +
      + {{#if signatures}} +
    • +
        + {{#each signatures}} +
      • {{> member.signature.title hideName=true }}
      • + {{/each}} +
      + +
        + {{#each signatures}} +
      • {{> member.signature.body hideSources=true }}
      • + {{/each}} +
      +
    • + {{/if}} + {{#if indexSignature}} +
    • +
      {{#compact}} + [ + {{#each indexSignature.parameters}} + {{#if flags.isRest}}...{{/if}}{{name}}: {{#with type}}{{>type}}{{/with}} + {{/each}} + ]:  + {{#with indexSignature.type}}{{>type}}{{/with}} + {{/compact}}
      + + {{#with indexSignature}} + {{> comment}} + {{/with}} + + {{#if indexSignature.type.declaration}} + {{#with indexSignature.type.declaration}} + {{> parameter}} + {{/with}} + {{/if}} +
    • + {{/if}} + {{#each children}} +
    • + {{#if signatures}} +
      {{#compact}} + {{#if flags.isRest}}...{{/if}} + {{{wbr name}}} + + {{#if isOptional}}?{{/if}} + :  + + function + {{/compact}}
      + + {{> member.signatures}} + {{else}} +
      {{#compact}} + {{#each flags}} + {{this}}  + {{/each}} + {{#if flags.isRest}}...{{/if}} + {{{wbr name}}} + + {{#if flags.isOptional}}?{{/if}} + :  + + {{#with type}}{{>type}}{{/with}} + {{/compact}}
      + + {{> comment}} + + {{#if children}} + {{> parameter}} + {{/if}} + + {{#if type.declaration}} + {{#with type.declaration}} + {{> parameter}} + {{/with}} + {{/if}} + {{/if}} +
    • + {{/each}} +
    diff --git a/src/theme/partials/toc.hbs b/src/theme/partials/toc.hbs new file mode 100644 index 00000000..56b1d748 --- /dev/null +++ b/src/theme/partials/toc.hbs @@ -0,0 +1,10 @@ +
  • + {{{wbr title}}} + {{#if children}} +
      + {{#each children}} + {{> toc}} + {{/each}} +
    + {{/if}} +
  • diff --git a/src/theme/partials/toc.root.hbs b/src/theme/partials/toc.root.hbs new file mode 100644 index 00000000..cba0d663 --- /dev/null +++ b/src/theme/partials/toc.root.hbs @@ -0,0 +1,18 @@ +{{#if isInPath}} + +
      +{{/if}} +
    • + {{{wbr title}}} + {{#if children}} +
        + {{#each children}} + {{> toc}} + {{/each}} +
      + {{/if}} +
    • +{{#if isInPath}} +
    +
      +{{/if}} diff --git a/src/theme/partials/type.hbs b/src/theme/partials/type.hbs new file mode 100644 index 00000000..58717642 --- /dev/null +++ b/src/theme/partials/type.hbs @@ -0,0 +1,78 @@ +{{#if this}} + {{#if reflection}} + {{#compact}} + + {{reflection.name}} + + {{#if typeArguments}} + < + + {{#each typeArguments}} + {{#if @index}} + , + {{/if}}{{> type}} + {{/each}} + + > + {{/if}} + {{#if isArray}}[]{{/if}} + {{/compact}} + {{else}} + {{#if types}} + {{#if isArray}} + ( + {{/if}} + {{#each types}} + {{#if @index}} + | + {{/if}}{{> type}} + {{/each}} + {{#if isArray}} + ) + [] + {{/if}} + {{else}} + {{#if elements}} + {{#compact}} + [ + + {{#each elements}} + {{#if @index}} + , + {{/if}}{{> type}} + {{/each}} + + ] + {{/compact}} + {{else}} + {{#compact}} + + {{#if name}} + {{name}} + {{else}} + {{#if value}} + "{{value}}" + {{else}} + {{this}} + {{/if}} + {{/if}} + + {{#if typeArguments}} + < + + {{#each typeArguments}} + {{#if @index}} + , + {{/if}}{{> type}} + {{/each}} + + > + {{/if}} + {{#if isArray}}[]{{/if}} + {{/compact}} + {{/if}} + {{/if}} + {{/if}} +{{else}} + void +{{/if}} diff --git a/src/theme/partials/typeAndParent.hbs b/src/theme/partials/typeAndParent.hbs new file mode 100644 index 00000000..4c874037 --- /dev/null +++ b/src/theme/partials/typeAndParent.hbs @@ -0,0 +1,38 @@ +{{#compact}} + {{#if this}} + {{#if reflection}} + {{#ifSignature reflection}} + {{#if reflection.parent.parent.url}} + {{reflection.parent.parent.name}} + {{else}} + {{reflection.parent.parent.name}} + {{/if}} + . + {{#if reflection.parent.url}} + {{reflection.parent.name}} + {{else}} + {{reflection.parent.name}} + {{/if}} + {{else}} + {{#if reflection.parent.url}} + {{reflection.parent.name}} + {{else}} + {{reflection.parent.name}} + {{/if}} + . + {{#if reflection.url}} + {{reflection.name}} + {{else}} + {{reflection.name}} + {{/if}} + {{/ifSignature}} + {{#if isArray}} + [] + {{/if}} + {{else}} + {{this}} + {{/if}} + {{else}} + void + {{/if}} +{{/compact}} \ No newline at end of file diff --git a/src/theme/partials/typeParameters.hbs b/src/theme/partials/typeParameters.hbs new file mode 100644 index 00000000..35f89d2c --- /dev/null +++ b/src/theme/partials/typeParameters.hbs @@ -0,0 +1,14 @@ +
        + {{#each typeParameters}} +
      • +

        {{#compact}} + {{name}} + {{#if type}} + + {{#with type}}{{> type}}{{/with}} + {{/if}} + {{/compact}}

        + {{> comment}} +
      • + {{/each}} +
      \ No newline at end of file diff --git a/src/theme/templates/index.hbs b/src/theme/templates/index.hbs new file mode 100644 index 00000000..1cda7d4d --- /dev/null +++ b/src/theme/templates/index.hbs @@ -0,0 +1,16 @@ +{{#with model}} + {{> comment}} +{{/with}} + +{{#if model.typeHierarchy}} +
      +

      Hierarchy

      + {{#with model.typeHierarchy}}{{> hierarchy}}{{/with}} +
      +{{/if}} + +{{#with model}} +
      + {{> index}} + {{> members}} +{{/with}} \ No newline at end of file diff --git a/src/webbluetooth.ts b/src/webbluetooth.ts new file mode 100644 index 00000000..4b2ebc06 --- /dev/null +++ b/src/webbluetooth.ts @@ -0,0 +1,176 @@ +/* +* Node Web Bluetooth +* Copyright (c) 2017 Rob Moran +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import { BluetoothDevice } from "./device"; +import { getServiceUUID } from "./helpers"; +import { Adapter, NobleAdapter } from "./adapter"; + +export interface FilterOptions { + acceptAllDevices: boolean; + deviceFound: (device: BluetoothDevice, selectFn: any) => void; + filters: Array; + optionalServices: Array; + scanTime: any; +} + +export class WebBluetooth { + + private defaultScanTime = 10.24 * 1000; + private scanner = null; + + constructor(private adapter: Adapter = new NobleAdapter()) { + } + + private filterDevice(options, deviceInfo, validServices) { + let valid = false; + + options.filters.forEach(filter => { + // Name + if (filter.name && filter.name !== deviceInfo.name) return; + + // NamePrefix + if (filter.namePrefix) { + if (filter.namePrefix.length > deviceInfo.name.length) return; + if (filter.namePrefix !== deviceInfo.name.substr(0, filter.namePrefix.length)) return; + } + + // Services + if (filter.services) { + const serviceUUIDs = filter.services.map(getServiceUUID); + const servicesValid = serviceUUIDs.every(serviceUUID => { + return (deviceInfo.uuids.indexOf(serviceUUID) > -1); + }); + + if (!servicesValid) return; + validServices = validServices.concat(serviceUUIDs); + } + + valid = true; + }); + + if (!valid) return false; + return deviceInfo; + } + + public requestDevice(options: FilterOptions) { + return new Promise((resolve, reject) => { + if (this.scanner !== null) return reject("requestDevice error: request in progress"); + + if (!options.acceptAllDevices && !options.deviceFound) { + // Must have a filter + if (!options.filters || options.filters.length === 0) { + return reject(new TypeError("requestDevice error: no filters specified")); + } + + // Don't allow empty filters + const emptyFilter = options.filters.some(filter => { + return (Object.keys(filter).length === 0); + }); + if (emptyFilter) { + return reject(new TypeError("requestDevice error: empty filter specified")); + } + + // Don't allow empty namePrefix + const emptyPrefix = options.filters.some(filter => { + return (typeof filter.namePrefix !== "undefined" && filter.namePrefix === ""); + }); + if (emptyPrefix) { + return reject(new TypeError("requestDevice error: empty namePrefix specified")); + } + } + + let searchUUIDs = []; + if (options.filters) { + options.filters.forEach(filter => { + if (filter.services) searchUUIDs = searchUUIDs.concat(filter.services.map(getServiceUUID)); + }); + } + // Unique-ify + searchUUIDs = searchUUIDs.filter((item, index, array) => { + return array.indexOf(item) === index; + }); + + let found = false; + this.adapter.startScan(searchUUIDs, deviceInfo => { + let validServices = []; + + function complete(bluetoothDevice) { + this.cancelRequest() + .then(() => { + resolve(bluetoothDevice); + }); + } + + // filter devices if filters specified + if (options.filters) { + deviceInfo = this.filterDevice(options, deviceInfo, validServices); + } + + if (deviceInfo) { + found = true; + + // Add additional services + if (options.optionalServices) { + validServices = validServices.concat(options.optionalServices.map(getServiceUUID)); + } + + // Set unique list of allowed services + deviceInfo._allowedServices = validServices.filter((item, index, array) => { + return array.indexOf(item) === index; + }); + + const bluetoothDevice = new BluetoothDevice(deviceInfo); + + function selectFn() { + complete(bluetoothDevice); + } + + if (!options.deviceFound || options.deviceFound(bluetoothDevice, selectFn)) { + // If no deviceFound function, or deviceFound returns true, resolve with this device immediately + complete(bluetoothDevice); + } + } + }, () => { + this.scanner = setTimeout(() => { + this.cancelRequest() + .then(() => { + if (!found) reject("requestDevice error: no devices found"); + }); + }, options.scanTime || this.defaultScanTime); + }, error => reject(`requestDevice error: ${error}`)); + }); + } + + public cancelRequest() { + return new Promise((resolve, _reject) => { + if (this.scanner) { + clearTimeout(this.scanner); + this.scanner = null; + this.adapter.stopScan(); + } + resolve(); + }); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..a5600103 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "commonjs", + "alwaysStrict": true, + "noEmitOnError": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "declaration": true + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 00000000..012ec7ff --- /dev/null +++ b/tslint.json @@ -0,0 +1,34 @@ +{ + "extends": [ + "tslint:recommended", + "tslint-eslint-rules" + ], + "rules": { + "array-bracket-spacing": [true, "always"], + "arrow-parens": [true, "ban-single-arg-parens"], + "array-type": [true, "generic"], + "block-spacing": [true, "always"], + "brace-style": [true, "1tbs", { "allowSingleLine": true }], + "curly": [true, "ignore-same-line"], + "eofline": true, + "interface-name": [true, "never-prefix"], + "linebreak-style": [true, "LF"], + "max-line-length": [false], + "member-ordering": [true, { "order": ["static-field", "instance-field", "constructor", "static-method", "instance-method"] }], + "no-bitwise": false, + "no-console": [true], + "no-empty-interface": false, + "no-trailing-whitespace": [true], + "no-unused-variable": [true], + "object-curly-spacing": [true, "always"], + "object-literal-shorthand": false, + "object-literal-sort-keys": false, + "ordered-imports": [true, { "import-sources-order": "any", "named-imports-order": "any" }], + "semicolon": [true, "always"], + "ter-indent": [true, 4], + "ter-no-irregular-whitespace": [true], + "trailing-comma": [true, "never"], + "triple-equals": [true], + "variable-name": [true, "allow-leading-underscore"] + } +}