Home Reference Source Repository

lib/index.js


const Console = require('console').Console;
const Writable = require('stream').Writable;
const crypto = require('crypto');

const { version:sdkVersion } = require('../package.json');

class SkygearWritable extends Writable {
  constructor(skygear) {
    super();
    this.skygear = skygear;
  }
  _write(chunk, encoding, callback) {
    this.skygear.lambda(
      'iot:log', chunk.toString()
    ).then(
      _ => callback(),
      e => callback(JSON.stringify(e,null,2))
    );
  }
}

/**
 * Skygear IoT container
 */
class SkygearIoT {
  /**
   * @param {SkygearContainer} skygear
   */
  constructor(skygear) {
    this.skygear = skygear;
    this._device = {
      id: null,
      platform: null,
      pubsubChannel: null,
    };
    this._console = new Console(
      new SkygearWritable(skygear)
    );
  }

  /**
   * Device-specific data
   * @type {Object}
   * @property {string} device.id
   * @property {Object} device.platform See `initialize` method.
   * @property {string} device.loginID
   * @property {string} device.pubsubChannel PubSub channel name for this device.
   */
  get device() {
    return this._device;
  }

  /**
   * A javascript console that outputs to the skygear server.
   * @type {Console}
   */
  get console() {
    return this._console;
  }

  /**
   * Binds SDK with provided platform, setup event handlers.
   * Register this device with the current user if not registered
   *
   * Note: This function must be called AFTER logging into skygear and you MUST ensure that the current user is not already registered with another device.
   *
   * @param {Object}          platform 
   * @param {Object}          platform.action           Object containing platform specific actions, all actions are optional.
   * @param {async function}  platform.action.*
   * @param {string}          platform.deviceSecret     A string that is unique to the hardware, could be SoC model + serial number.
   * @param {string}          platform.appVersion       Version string of the user application.
   *
   * @return {Promise} Resolves on complete, reject on error.
   */
  async initDevice(platform) {
    const skygear = this.skygear;
    if(!skygear.currentUser) throw Error('[Skygear IoT] ERROR: login required before callng initialize');

    const deviceRecordACL = new skygear.ACL([
      { role: 'iot-device', level: 'write' },
      { role: 'iot-manager', level: 'write' },
    ]);
    const deviceLoginRecordACL = new skygear.ACL([
      { role: 'iot-device', level: 'write' },
      { role: 'iot-manager', level: 'read' },
    ]);

    const deviceID = skygear.currentUser.id;

    // generate pubsub channel hash
    const deviceHash = crypto.createHash('sha256');
    deviceHash.update(platform.deviceSecret);
    const pubsubChannel = `iot-${deviceHash.digest('hex')}`;

    // register device if nessessary
    if(!skygear.currentUser.hasRole(new skygear.Role('iot-device'))) {
      console.log('[Skygear IoT] user does not have role "iot-device", registering device with user...');
      const deviceRecord = new skygear.Record(
        'iot_device', {
          _id:    `iot_device/${deviceID}`,
          secret: platform.deviceSecret,
          active: true,
        }
      );
      deviceRecord.setAccess(deviceRecordACL);
      await skygear.publicDB.save(deviceRecord);
      await skygear.lambda('iot:add-device-role', []);
      await skygear.whoami();
      console.log('OK!');
    }

    // save login record
    const loginRecord = new skygear.Record(
      'iot_device_login', {
        deviceID,
        sdkVersion,
        appVersion: platform.appVersion,
      }
    );
    const deviceRecord = new skygear.Record(
      'iot_device', {
        _id: `iot_device/${deviceID}`,
        login: new skygear.Reference(loginRecord),
      }
    );
    loginRecord.setAccess(deviceLoginRecordACL);
    loginRecord.setAccess(deviceRecordACL);
    await skygear.publicDB.save([loginRecord, deviceRecord]);

    // register pubsub hooks
    skygear.on('iot-request-status', _ => {
      this.reportStatus()
    });
    skygear.on(pubsubChannel, ({action}) => {
      if(!action) return console.error('[Skygear IoT] ERROR: missing key "action" in pubsub message'); 
      const match = action.match(/^iot-(.+)$/);
      if(match) {
        if(platform.action.hasOwnProperty(match[1])) {
          platform.action[match[1]]();
        } else {
          console.warn(`[Skygear IoT] WARNING: Sever requested unsupported action: ${match[1]}`);
        }
      }
    });

    // set SDK state
    this.device.id            = deviceID;
    this.device.platform      = platform;
    this.device.pubsubChannel = pubsubChannel

    // report status
    await this.reportStatus();

    console.log('[Skygear IoT] Device initialized.');
  }

  /**
   * Reports device status to the skygear server.
   * This function is called automatically as requested by the server.
   *
   * @param {Object} [payloadOverride] Override fields in the default payload
   * @param {string} [payloadOverride.status] Device status
   * @param {Object} [payloadOverride.metadata] Additional status metadata
   * @return {Promise}
   */
  reportStatus(payloadOverride) {
    return this.skygear.lambda(
      'iot:report-status',
      Object.assign(
        {
          deviceID: this.device.id,
          status:   'online',
          metadata: null,
        },
        payloadOverride
      )
    );
  }

};


/**
 * Return a new Skygear IoT container for the supplied Skygear container.
 *
 * @param {SkygearContainer} skygear Skygear container
 * @return {SkygearIoT} Skygear IoT container
 *
 * @example
 * const skygear = require('skygear');
 * const skygearIoT = require(skygear-iot)(skygear);
 */
module.exports = function(skygear) {
  return new SkygearIoT(skygear);
}