| 'use strict' |
| |
| const debug = require('debug')('dataparty.comms.ble-peer') |
| const EventEmitter = require('eventemitter3') |
| |
| const BLEMessage = require('./ble/BLEMessage') |
| const BLEOp = require('./ble/BLEOp') |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class BLEPeerClient extends EventEmitter { |
| constructor(bleDevice){ |
| super() |
| this.autoReconnect = true |
| this.reconnectFails = 0 |
| this.reconnectDelay = 500 |
| this.reconnectMaxTries = 5 |
| this.reconnectTimer = undefined |
| |
| this.rxBuffer = {} |
| |
| |
| this.bleDevice = bleDevice |
| this.info = undefined |
| |
| this.characteristics = { |
| owner: undefined, |
| actor: undefined, |
| tx: undefined, |
| rx: undefined |
| } |
| |
| this.bleDevice.addEventListener('gattserverdisconnected', this.handleGATTDisconnected.bind(this)) |
| } |
| |
| static get MTU(){ return 20 } |
| static get DATAPARTY_SERVICE_UUID(){ return 0x31337 } |
| static get OWNER_ID_CHARACTERISTIC_UUID(){ return BluetoothUUID.getCharacteristic('00000001-abcd-42d0-bc79-df8168b55f04') } |
| static get ACTOR_ID_CHARACTERISTIC_UUID(){ return BluetoothUUID.getCharacteristic('00000002-abcd-42d0-bc79-df8168b55f04') } |
| static get RX_CHARACTERISTIC_UUID(){ return BluetoothUUID.getCharacteristic('00000003-abcd-42d0-bc79-df8168b55f04') } |
| static get TX_CHARACTERISTIC_UUID(){ return BluetoothUUID.getCharacteristic('00000004-abcd-42d0-bc79-df8168b55f04') } |
| |
| static buf2hex(buffer){ |
| return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('') |
| } |
| |
| static parseActor(data){ |
| const dataArr = new Uint8Array(data.buffer) |
| const actorTypeId = parseInt(dataArr.slice(0, 2)) |
| const idStr = BLEPeerClient.buf2hex(dataArr.slice(1, dataArr.length)) |
| |
| const ACTORS = [undefined, 'user', 'team', 'org', 'device', 'app'] |
| |
| return { |
| type: ACTORS[actorTypeId] || undefined, |
| id: idStr |
| } |
| } |
| |
| handleGATTDisconnected(){ |
| debug('GATT disconnected', new Date()) |
| |
| if (this.autoReconnect){ |
| this.resume() |
| } |
| } |
| |
| async resume(){ |
| |
| debug('resuming', new Date()) |
| return new Promise((resolve, reject) => { |
| this.start() |
| .then(resolve) |
| .catch((err) => { |
| this.reconnectFails += 1 |
| |
| if (this.reconnectFails >= this.reconnectMaxTries){ |
| debug('max-reconnect', err) |
| return reject(new Error('max-reconnect')) |
| } |
| |
| const delay = this.reconnectDelay * Math.pow(2, this.reconnectFails) |
| debug('start failed (error,', err, ') retying in', delay, 'ms') |
| |
| this.reconnectTimer = setTimeout(() => { |
| this.resume().then(resolve).catch(reject) |
| }, delay) |
| }) |
| |
| }) |
| } |
| |
| onRx(event){ |
| debug('onRx', event.target.value.buffer) |
| |
| debug('raw value', event.target.value) |
| const input = new Uint8Array(event.target.value.buffer) |
| |
| debug('input', input) |
| |
| const header = BLEMessage.parseHeader(input) |
| |
| let buffer = this.rxBuffer[ header.seq ] |
| if (!buffer){ |
| |
| buffer = BLEMessage.fromPacket(input) |
| this.rxBuffer[ header.seq ] = buffer |
| |
| } else { |
| |
| buffer.parsePacket(input) |
| |
| } |
| |
| if (buffer.rxComplete){ |
| debug('onRx complete ', header.seq) |
| debug('buffer', buffer) |
| this.emit('message', buffer) |
| this.emit('message:' + header.seq, buffer) |
| } |
| |
| } |
| |
| async transmit(arr){ |
| return this.characteristics.tx.writeValue(arr) |
| } |
| |
| async send(obj){ |
| debug('sending', obj) |
| const msg = new BLEMessage({msg: obj}) |
| |
| debug(msg) |
| |
| const packets = [] |
| |
| for (let i = 0; i < msg.packetCount; i++){ |
| await this.transmit(msg.getPacket(i)) |
| } |
| return msg |
| } |
| |
| async run(obj){ |
| const op = new BLEOp(obj, this) |
| |
| return op.run().then(response => { |
| debug('op got respone') |
| return response |
| }) |
| } |
| |
| async start(autoreconnect = true){ |
| this.autoReconnect = autoreconnect |
| |
| try { |
| await this.bleDevice.gatt.connect() |
| |
| if (!this.bleDevice.gatt.connected){ |
| debug('warning GATT not connected') |
| } |
| |
| debug('GATT connected', new Date()) |
| const primaryService = await this.bleDevice.gatt.getPrimaryService(BLEPeerClient.DATAPARTY_SERVICE_UUID) |
| |
| const primaryCharacteristics = await primaryService.getCharacteristics() |
| debug('PRIMARY SERVICE', primaryCharacteristics.length, primaryCharacteristics) |
| |
| debug('\t found primary service') |
| this.characteristics.owner = await primaryService.getCharacteristic(BLEPeerClient.OWNER_ID_CHARACTERISTIC_UUID) |
| debug('\t found owner', this.characteristics.owner) |
| |
| this.characteristics.actor = await primaryService.getCharacteristic(BLEPeerClient.ACTOR_ID_CHARACTERISTIC_UUID) |
| debug('\t found actor') |
| |
| this.characteristics.tx = await primaryService.getCharacteristic(BLEPeerClient.TX_CHARACTERISTIC_UUID) |
| debug('\t found tx') |
| |
| this.characteristics.rx = await primaryService.getCharacteristic(BLEPeerClient.RX_CHARACTERISTIC_UUID) |
| debug('\t found rx') |
| |
| this.characteristics.rx.addEventListener('characteristicvaluechanged', this.onRx.bind(this)) |
| this.characteristics.rx.startNotifications() |
| debug('\t started rx notifications') |
| |
| const owner = await this.characteristics.owner.readValue() |
| debug('\t read owner') |
| |
| const actor = await this.characteristics.actor.readValue() |
| debug('\t read actor') |
| |
| |
| |
| this.info = { |
| owner: BLEPeerClient.parseActor(owner), |
| actor: BLEPeerClient.parseActor(actor) |
| } |
| |
| debug(this.info) |
| this.reconnectFails = 0 |
| return this |
| } catch (error){ |
| debug('exception in bluetooth :(', error) |
| debug(error) |
| debug(error.stack) |
| if(this.reconnectFails == 0){ |
| return this.resume() |
| } |
| return Promise.reject(error) |
| } |
| } |
| |
| stop(){ |
| this.autoReconnect = false |
| |
| if (!this.bleDevice.gatt.connected){ |
| debug('gatt already disconnected') |
| return |
| } |
| |
| debug('disconnecting gatt') |
| this.bleDevice.gatt.disconnect() |
| } |
| |
| static requestDevice(shortId){ |
| |
| const filters = [] |
| |
| if (!shortId){ |
| |
| filters.push({ |
| services: [BLEPeerClient.DATAPARTY_SERVICE_UUID], |
| namePrefix: 'DataParty' |
| }) |
| |
| } else { |
| |
| filters.push({ |
| services: [BLEPeerClient.DATAPARTY_SERVICE_UUID], |
| name: 'DataParty-' + shortId |
| }) |
| |
| } |
| |
| return navigator.bluetooth.requestDevice({ filters: filters }).then(device => { |
| debug('requested device') |
| debug(device) |
| return new BLEPeerClient(device) |
| }) |
| } |
| } |
| |
| module.exports = BLEPeerClient |