Devices

An actor refers to an entity that participates in an activity. Typically, these refer to devices; however, there are two other types of actors: groups which combine actors accordingly to a logical relationship (e.g., 'and' or 'first/next') and pseudo actors which are software-only constructs (e.g., the place).

Architecture

Support for devices is added by creating a module with a path and name that conform to the Device Taxonomy. This module is detected and loaded by the steward during startup. (Note that whenever you add a module to a running steward, you must restart the steward.)

When the module is loaded, the start() function is invoked which does two things:

Module Structure

A module consists of several parts. In reading this section, it is probably useful to also glance at one of the existing modules. For the examples that follow, let's say we're going to the define a module for a macguffin presence device manufactured by Yoyodyne Propulsion Systems.

The first step is to select the name of the device prototype. In this case, it's probably going to be:

/device/presence/yoyodyne/macguffin

The corresponding file name would be:

devices/device-presence/presence-yoyodyne-macguffin.js

The file should have six parts:

First, is the require section where external modules are loaded. By convention, system and third-party modules are loaded first, followed by any steward-provided modules, e.g.,

var util        = require('util')
  , devices     = require('./../../core/device')
  , steward     = require('./../../core/steward')
  , utility     = require('./../../core/utility')
  , presence    = require('./../device-presence')
  ;

Second, is the logger section, which is usually just one line:

var logger = presence.logger;

Logging is done syslog-style, which means these functions are available

logger.crit
logger.error
logger.warning
logger.notice
logger.info
logger.debug

These functions take two arguments: a string and a property-list object, e.g.,

try {
  ...
} catch(ex) {
  logger.error('device/' + self.deviceID,
               { event: 'perform', diagnostic: ex.message });
}

Third, is the prototype function that is invoked by the steward whenever an instance of this device is (re-)discovered:

Fourth, comes the optional observe section, that implements asynchronous observation of events.

Fifth, comes the optional perform section, that implements the performance of tasks.

Sixth, comes the start() function.

The Prototype function

var Macguffin = exports.Device = function(deviceID, deviceUID, info) {
  // begin boilerpate...
  var self = this;

  self.whatami = info.deviceType;
  self.deviceID = deviceID.toString();
  self.deviceUID = deviceUID;
  self.name = info.device.name;

  self.info = utility.clone(info);
  delete(self.info.id);
  delete(self.info.device);
  delete(self.info.deviceType);
  // end boilerplate...

  self.status = '...';
  self.changed();

  // perform initialization here

  utility.broker.subscribe('actors', function(request, taskID, actor, observe, parameter) {
    if (request === 'ping') {
      logger.info('device/' + self.deviceID, { status: self.status });
      return;
    }

         if (actor !== ('device/' + self.deviceID)) return;
    else if (request === 'observe') self.observer(self, taskID, observe, parameter);
    else if (request === 'perform') self.perform(self, taskID, observe, parameter);
  });
};
util.inherits(Macguffin, indicator.Device);

The Observe section

The prototype function invokes this whenever the steward module publishes a request to the actor asking that a particular event be monitored:

Macguffin.prototype.observe = function(self, eventID, observe, parameter) {
  var params;

  try { params = JSON.parse(parameter); } catch(ex) { params = {}; }

  switch (observe) {
    case '...':
      // create a data structure to monitor for this observe/params pair

      // whenever an event occurs, invoke
      //     steward.observed(eventID);

      // now tell the steward that monitoring has started
      steward.report(eventID);
      break;

    default:
      break;
  }
}

The Perform section

The prototype function invokes this whenever the steward module publishes a request to the actor asking that a particular task be performed:

Macguffin.prototype.perform = function(self, taskID, perform, parameter) {
  var params;

  try { params = JSON.parse(parameter); } catch(ex) { params = {}; }

  if (perform === 'set') {
    if (!!params.name) self.setName(params.name);

    // other state variables may be set here
    if (!!params.whatever) {
      // may wish to range-check params.whatever here...

      self.info.whatever = params.whatever;
      self.setInfo();
    }

    return steward.performed(taskID);
  }

 // any other tasks allowed?
 if (perform === '...') {
 }

 return false;
};

The start() function: Linkage

As noted earlier, this function performs two tasks. The first task is to link the device prototype into the steward:

exports.start = function() {
  steward.actors.device.presence.yoyodyne =
      steward.actors.device.presence.yoyodyne ||
      { $info     : { type: '/device/presence/yoyodyne' } };

  steward.actors.device.presence.yoyodyne.macguffin =
      { $info     : { type       : '/device/presence/yoyodyne/macguffin'
                    , observe    : [ ... ]
                    , perform    : [ ... ]
                    , properties : { name   : true
                                   , status : [ ... ]
                                   ...
                                   }
                    }
      , $observe  : {  observe   : validate_observe }
      , $validate : {  perform   : validate_perform }
      };
  devices.makers['/device/presence/yoyodyne/macguffin'] = MacGuffin;
...

The first assignment:

  steward.actors.device.presence.yoyodyne =
      steward.actors.device.presence.yoyodyne ||
      { $info     : { type: '/device/presence/yoyodyne' } };

is there simply to make sure the naming tree already has a node for the parent of the device prototype. (The steward automatically creates naming nodes for the top-level categories).

The next assignment is the tricky one:

  steward.actors.device.presence.yoyodyne.macguffin =
      { $info     : { type       : '/device/presence/yoyodyne/macguffin'
                    , observe    : [ ... ]
                    , perform    : [ ... ]
                    , properties : { name   : true
                                   , status : [ ... ]
                                   ...
                                   }
                    }
      , $list     : function()   { ... }
      , $lookup   : function(id) { ... }
      , $validate : { create     : validate_create
                    , observe    : validate_observe
                    , perform    : validate_perform
                    }
      };

The $info field is mandatory, as are its four sub-fields:

The $list and $lookup fields are for "special" kinds of actors, and are described later.

The $validate field contains an object, with these sub-fields:

The start() function: Discovery

The second task performed by the start() function is to register with the appropriate discovery module. There are presently five modules:

For the first two discovery modules, the steward will automatically (re-)create device instances as appropriate. For the others, the module may need to do some additional processing before it decides that a device instance should (re-)created. Accordingly, the module calls device.discovery() directly.

Discovery via SSDP occurs when the steward encounters a local host with an SSDP server that advertises a particular "friendlyName" or "deviceType":

devices.makers['Yoyodye Propulsion Systems MacGuffin'] = Macguffin;

Discovery via BLE occurs when the steward identifies a BLE device from a particular manufacturer, and find a matching characteristic value:

devices.makers['/device/presence/yoyodyne/macguffing'] = Macguffin;
require('./../../discovery/discovery-ble').register(
  { 'Yoyodyne Propulsion' :
      { '2a24' :
          { 'MacGuffin 1.1' :
              { type : '/device/presence/yoyodyne/macguffin' }
          }
      }
  });

Discovery via TCP port occurs when the steward is able to connect to a particular TCP port on a local host:

require('./../../discovery/discovery-portscan').pairing([ 1234 ],
function(socket, ipaddr, portno, macaddr, tag) {
  var info = { };

  ...
  info.deviceType = '/device/presence/yoyodyne/macguffin';
  info.id = info.device.unit.udn;
  if (devices.devices[info.id]) return socket.destroy();

  utility.logger('discovery').info(tag, { ... });
  devices.discover(info);
});

Discovery via MAC OUI occurs when the steward encounters a local host with a MAC address whose first 3 octets match a particular prefix:

require('./../../discovery/discovery-mac').pairing([ '01:23:45' ],
function(ipaddr, macaddr, tag) {
  var info = { };

  ...
  info.deviceType = '/device/presence/yoyodyne/macguffin';
  info.id = info.device.unit.udn;
  if (devices.devices[info.id]) return;

  utility.logger('discovery').info(tag, { ... });
  devices.discover(info);
});

Discovery via TSRP occurs when the steward receives a multicast message on the TSRP port.

Design Patterns

There are three design patterns currently in use for device actors.

Standalone Actor

A standalone actor refers to a device that is discovered by the steward and doesn't discover anything on its own.

Examples of standalone actors include:

Gateway and Subordinate Actors

Typically the steward is able to discover a gateway for a particular technology, and the module for that gateway then discovers "interesting" devices. Examples of these kinds of actors include things like:

When a gateway actor discovers an "interesting" device, it calls devices.discover() to tell the steward to (re-)create it.

Creatable Actors

These kind of actors aren't discoverable, so a client must make a specific API call to the steward in order to create an instance. Examples of creatable actors include:

In general, these actors refer to software-only constructs: while it's the steward's job to discovery devices, only a user can decide whether they want sensor readings uploaded somewhere.

API calls

Devices are managed by authorized clients using the

/manage/api/v1/device/

path prefix, e.g.,

{ path      : '/api/v1/actor/list'
, requestID : '1'
, options   : { depth: all }
}

Create Device

To create a device, an authorized client sends:

{ path      : '/api/v1/actor/create/UUID'
, requestID : 'X'
, name      : 'NAME'
, whatami   : 'TAXONOMY'
, info      : { PARAMS }
, comments  : 'COMMENTS'
}

where UUID corresponds to an unpredictable string generated by the client, X is any non-empty string, NAME is a user-friendly name for this instance, PARAMS are any parameters associated with the device, TAXONOMY is a taxonomy for the device, and COMMENTS (if present) are textual, e.g.,

{ path      : '/api/v1/actor/create/YPI'
, requestID : '1'
, name      : 'OO'
, whatami   : '/device/presence/yoyodune/macguffin'
, info      : { beep: 'annoying' }
}

List Device(s)

To list the properties of a single device, an authorized client sends:

{ path      : '/api/v1/actor/list/ID'
, requestID : 'X'
, options   : { depth: DEPTH }
}

where ID corresponds to the deviceID of the device to be defined, X is any non-empty string, and DEPTH is either 'flat', 'tree', or 'all'

If the ID is omitted, then all devices are listed, e.g., to find out anything about everything, an authorized client sends:

{ path      : '/api/v1/actor/list'
, requestID : '2'
, options   : { depth: 'all' }
}

Perform Task

To have a device perform a task, an authorized client sends:

{ path      : '/api/v1/actor/perform/ID'
, requestID : 'X'
, perform   : 'TASK'
, parameter : 'PARAM'
}

where ID corresponds to the deviceID of the device to perform the task, X is any non-empty string, TASK identifies a task to be performed, and PARAM (if present) provides parameters for the task, e.g.,

{ path      : '/api/v1/actor/perform/7'
, requestID : '3'
, perform   : 'on'
, parameter : '{"color":{"model":"cie1931","cie1931":{"x":0.5771,"y":0.3830}},"brightness":50}'
}

Delete Device

To define a device, an authorized client sends:

{ path      : '/api/v1/actor/device/ID'
, requestID : 'X'
}

where ID corresponds to the deviceID of the device to be deleted, and X is any non-empty string, e.g.,

{ path      : '/api/v1/actor/device/7'
, requestID : '4'
}

Choosing a technology to integrate

There are a large number of technologies available for integration. The steward's architecture is agnostic with respect to the choice of communication and application protocols. However, many of these technologies compete (in the loose sense of the word). Here is the algorithm that the steward's developers use to determine whether something should go into the development queue.

Finally, you may have a choice of devices to integrate with, and you may even have the opportunity to build your own. If you go the "off-the-shelf" route, please consider what is going to be easiest for others to connect to: