comms_ble-socket.js

'use strict'

const debug = require('debug')('dataparty.comms.ble-peer')
const EventEmitter = require('eventemitter3')

const BLEMessage = require('./ble/BLEMessage')
const BLEOp = require('./ble/BLEOp')
// const bluetooth = (navigator) navigator.bluetooth

/**
 * A simple BLE socket
 * ⚠️ Warning: This class maybe significantly refactored in future releases
 * 
 * @class module:Comms.BLEPeerClient
 * @extends EventEmitter
 * @link module:Comms
 * @see https://webbluetoothcg.github.io/web-bluetooth
 * @param {BleDevice} bleDevice A connected BLE device. See BLEPeerClient.requestDevice()
 */

class BLEPeerClient extends EventEmitter {
  constructor(bleDevice){
    super()
    this.autoReconnect = true
    this.reconnectFails = 0
    this.reconnectDelay = 500
    this.reconnectMaxTries = 5
    this.reconnectTimer = undefined

    this.rxBuffer = {} // indexed on command sequence -> Message

    /* */
    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')

      // let decoder = new TextDecoder('utf-8')

      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