network_connector.js

/*
 * Copyright 2017-2019 Tom Swindell
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
const os = require('os')
const net = require('net')
const dgram = require('dgram')

const MessageChannel = require('./channel')

/**
 *   Manages connecting to remote device via IP networking, handshaking and
 * MessageChannel creation.
 * 
 * @class
 * @public
 * 
 * @param {string} [remoteAddr='192.168.53.1'] IP address of target network device
 * @param {uint16} [remotePort=44444] ARSDK TCP discovery port
 * @param {Object} [opts]
 * @param {boolean} [opts.debug=false] Enable debugging output
 * 
 * @example
 * 
 * const connector = new NetworkConnector('192.168.53.1', 44444, { debug: true })
 * connector.connect()
 *   .then(ch => { ... })
 *   .catch(error => { ... })
 */
function NetworkConnector (remoteAddr, remotePort, __opts) {
  if (arguments.length === 1 && typeof(remoteAddr) === 'object') {
    __opts = remoteAddr
    remoteAddr = undefined
  }

  remoteAddr = remoteAddr || '192.168.53.1'
  remotePort = remotePort || 44444
  __opts = Object.assign({ debug: false }, __opts || {})

  const host = `${remoteAddr}:${remotePort}`
  const uid = `arnet://${host}`

  this.uid = () => uid

  /**
   * Initiate connection to remote network end-point. Resolves on
   * successful connection.
   * 
   * @method NetworkConnector#connect
   * @public
   * 
   * @param {string} [controller_type='js-arsdk']
   * @param {string} [controller_name=HOSTNAME]
   * 
   * @returns {MessageChannel}
   */
  const connect = async (controllerType, controllerName) => {
    controllerType = controllerType || 'js-arsdk'
    controllerName = controllerName || os.hostname()

    if (__opts.debug) console.info(`Attempting to connect to: ${host}`)
    const adv_sock = new net.Socket()

    await new Promise((resolve, reject) => {
      adv_sock.on('error', (error) => reject(error))
      adv_sock.connect(remotePort, remoteAddr, resolve)
    })

    if (__opts.debug) console.info(`Connection established to: ${host}`)

    if (__opts.debug) console.info('Creating UDP control channel port...')
    const d2c_sock = dgram.createSocket('udp4')
    await new Promise(resolve => d2c_sock.bind(resolve))

    const d2c_addr = d2c_sock.address()
    if (__opts.debug) console.info(`UDP control channel listening on: ${d2c_addr.address}:${d2c_addr.port}`)

    if (__opts.debug) console.info('Performing handshake with:', host)
    adv_sock.write(JSON.stringify({
      controllerType,
      controllerName,
      d2c_port: d2c_addr.port,
    }))

    const data = await new Promise(resolve => adv_sock.once('data', resolve))
    try {
      const source = (data[data.length-1] === 0x00 ? data.slice(0, data.length-1) : data).toString('utf8')
      const params = JSON.parse(source)
      if (params.status !== 0) {
        console.error("Handshake failure...", params)
        throw 'HANDSHAKE_FAILURE'
      }

      if (__opts.debug) console.info('Handshake successful, creating device control channel.')
      const channel = new MessageChannel(remoteAddr, params, d2c_sock, __opts)

      adv_sock.end()
      return Promise.resolve(channel)
    } catch (e) {
      d2c_sock.close()
      adv_sock.end()
      return Promise.reject(e)
    }
  }

  // Export Public Interface
  this.connect = connect.bind(this)
}

module.exports = NetworkConnector