device.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 EventEmitter = require('events')

const CommandDict = require('./commands')

const {
  FEATURE_CLASSES,
  ARDrone3,
  SkyController,
} = require('./features')

/**
 * This class handles sending and recieving commands from ARNet based
 * devices. It serves as a nice abstraction layer removing the underlying
 * protocol architecture, and providing an easy to use interface for
 * communicating with Parrot devices.
 * 
 * @class Device
 * @public
 * 
 * @hideconstructor
 * 
 * @param {string} uid 
 * @param {MessageChannel} channel 
 */
function Device (__uid, __channel) {
  EventEmitter.call(this)

  const __self = this

  const __messages = new CommandDict()
  const __features = {}

  let __is_connected = false
  let __is_ready = false

  /**
   * Handle incoming navdata messages, detect connected features, parse
   * messages and notify next layer with annotated messages.
   * 
   * @private
   * @param {*} mesg 
   * @param {*} rinfo 
   */
  const onNavdata = (mesg, rinfo) => {
    // If we're in disconnected state, mark as connected and emit.
    if (!__is_connected) {
      __messages.import(
          'common', 'ardrone3', 'skyctrl', 'wifi', 'rc', 'drone_manager',
          'mapper', 'debug', 'controller_info', 'animation', 'user_storage',
          'rth', 'gimbal', 'battery', 'mediastore', 'precise_home',
        )
        .then(async () => {
          __self.emit('connected')
          __is_ready = true
        })
        .catch(e => __self.emit('error', e))

      __is_connected = true
    }

    // Wait until message dictionary is populated.
    if (!__is_ready) return

    // Start detecting plugin features.
    if (!(mesg.featureId in __features) && mesg.featureId in FEATURE_CLASSES) {
      const feature = new FEATURE_CLASSES[mesg.featureId](__self)
      __features[mesg.featureId] = feature

      console.info('Detected feature:', feature.uid)
      feature.init()
        .then(() => {
          __self.emit('feature:attached', feature)    
        })
        .catch(e => console.error(e))
    }

    // Lookup message information and annotate message if successful.
    const minfo = __messages.resolve(mesg)

    // Ignore messages we can't decode.
    if (!minfo || !minfo.decode) return

    mesg.info = minfo

    mesg.path = minfo.path
    mesg.params = minfo.decode && minfo.decode(mesg.args)

    // Notify navdata received
    __self.emit('message', mesg)
  }

  /**
   * Send an ARNet message / command to remote device.
   * @method Device#command
   * @public
   * 
   * @param {string} message Message type path format: featureId.classId.messageId
   * @param {any[]} [args] Arguments for message
   */
  const message = function (message, args) {
    const minfo = __messages.resolve(message)
    return __channel.sendMessage(minfo, Array.prototype.slice.call(arguments, 1))
      .then(results => results.map(mesg => {
        const minfo = __messages.resolve(mesg)

        mesg.path = minfo.path
        mesg.params = minfo.decode(mesg.args)

        return mesg
      }))
  }

  /**
   * Returns the unique id associated with this device.
   * @member Device#uid
   * @public
   * 
   * @returns {string} Unique Id for this device.
   */
  this.uid = __uid

  /**
   * Returns the MessageChannel associated with this device.
   * @member Device#channel
   * @public
   * 
   * @returns {MessageChannel}
   */
  this.channel = __channel

  // Expose Public Interface
  this.message = message.bind(this)

  this.toJSON = () => ({
    uid: __uid,
    features: Object.keys(__features).map(k => __features[k].toJSON())
  })

  // Listen to MessageChannel events.
  __channel.on('navdata', onNavdata)
}

Device.prototype = Object.create(EventEmitter.prototype)

module.exports = Device